Herramientas de usuario

Herramientas del sitio


clase:ia:saa:2eval:analisis_normalidad

07 - Análisis de la normalidad y normalización de datos

Análisis de la normalidad

En muchos casos, necesitamos comprobar que los datos recabados proceden de una población con distribución con una distribución normal. Para comprobarlo, podemos varias estrategias (a menudo usadas conjuntamente).

Vamos a usar un dataset con datos del pueblo !Kung San, que vive en el desierto de Kalahari entre Botsuana, Namibia y Angola.

kung_san.zip

df = pd.read_csv('./kung_san.csv')
df.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 544 entries, 0 to 543
Data columns (total 5 columns):
 #   Column      Non-Null Count  Dtype  
---  ------      --------------  -----  
 0   Unnamed: 0  544 non-null    int64  
 1   height      544 non-null    float64
 2   weight      544 non-null    float64
 3   age         544 non-null    float64
 4   male        544 non-null    int64  
dtypes: float64(3), int64(2)
memory usage: 21.4 KB

Seleccionamos sólo los pesos de las mujeres mayores de 15 años:

datos = df[(df.age > 15) & (df.male ==0)]
peso = datos['weight']

Métodos gráficos

Podemos mostrar la forma de la distribución con seaborn:

figure = plt.figure(figsize=(15, 5))
axes = figure.add_subplot()

axes.set_title('Distribución peso mujeres mayores de 15 años')
axes.set_xlabel('peso')
axes.set_ylabel('Densidad de probabilidad')
_ = sns.histplot(x = peso, axes = axes, kde = True, stat='density', bins=30)

Además, podemos sobreponer en el gráfico una distribución normal con la misma media y desviación estándar de la población estudiada:

# Histograma + curva normal teórica
# ==============================================================================

# Valores de la media (mu) y desviación típica (sigma) de los datos
mu, sigma = stats.norm.fit(peso)

# Valores teóricos de la normal en el rango observado
x_hat = np.linspace(min(peso), max(peso), num=100)
y_hat = stats.norm.pdf(x_hat, mu, sigma)

# Gráfico
figure = plt.figure(figsize=(15, 5))
axes = figure.add_subplot()

axes.set_title('Distribución peso mujeres mayores de 15 años')
axes.set_xlabel('peso')
axes.set_ylabel('Densidad de probabilidad')
_ =axes.plot(x_hat, y_hat, linewidth=2, label='normal')
_ = sns.histplot(x = peso, axes = axes, kde = True, stat='density', bins=30)
_ = axes.legend()

Para simplificar, podemos mostrar únicamente las dos funciones de distribución (la teórica y la prática):

# Comparar funciones de distribución
# ==============================================================================

# Valores de la media (mu) y desviación típica (sigma) de los datos
mu, sigma = stats.norm.fit(peso)

# Valores teóricos de la normal en el rango observado
x_hat = np.linspace(min(peso), max(peso), num=100)
y_hat = stats.norm.pdf(x_hat, mu, sigma)

# Gráfico
figure = plt.figure(figsize=(15, 5))
axes = figure.add_subplot()

axes.set_title('Distribución peso mujeres mayores de 15 años')
axes.set_xlabel('peso')
axes.set_ylabel('Densidad de probabilidad')
_ =axes.plot(x_hat, y_hat, linewidth=2, label='normal')
_ = sns.kdeplot(x = peso, axes = axes)
_ = axes.legend()

Otro gráfico bastante usado son los gráficos de cuantiles teóricos, que comparan los cuantiles de la distribución observada con los cuantiles teóricos de una distribución normal con la misma media y desviación estándar que los datos. Cuanto más se aproximen los datos a una normal, más alineados están los puntos entorno a la recta.

figure = plt.figure(figsize=(10, 5))
axes = figure.add_subplot()
#fit = True para estandarizar los datos, line = q para ajustar la línea a los cuartiles
sm.qqplot(peso, fit   = True, line  = "q", alpha = 0.4, ax    = axes)
axes.set_title('Gráfico Q-Q del peso mujeres mayores de 15 años', fontsize = 10,
             fontweight = "bold")
axes.tick_params(labelsize = 7)

Métodos analíticos

Los estadísticos de asimetría (Skewness) y curtosis pueden emplearse para detectar desviaciones de la normalidad. Un valor de curtosis y/o coeficiente de asimetría entre -1 y 1, es generalmente considerada una ligera desviación de la normalidad. Entre -2 y 2 hay una evidente desviación de la normal pero no extrema.

Podemos utilizar la librería scipy para calcular los coeficientes de asimetría:

from scipy import stats

print('Kursotis:', stats.kurtosis(peso))
print('Skewness:', stats.skew(peso))

Kursotis: 0.05524614843093856
Skewness: 0.032122514283202334

Contraste de hipótesis

Los test Shapiro-Wilk test y D'Agostino's K-squared test son dos de los test de hipótesis más empleados para analizar la normalidad. En ambos, se considera como hipótesis nula que los datos proceden de una distribución normal.

El test de Shapiro-Wilk se desaconseja cuando se dispone de muchos datos (más de 50) por su elevada sensibilidad a pequeñas desviaciones de la normal.

# Shapiro-Wilk test
# ==============================================================================
shapiro_test = stats.shapiro(peso)
shapiro_test

ShapiroResult(statistic=0.9963726997375488, pvalue=0.9239704012870789)

# D'Agostino's K-squared test
# ==============================================================================
k2, p_value = stats.normaltest(peso)
print(f"Estadístico = {k2}, p-value = {p_value}")

Estadístico = 0.19896549779904893, p-value = 0.9053055672511008

Cuando estos test se emplean con la finalidad de verificar las condiciones de métodos paramétricos, por ejemplo un t-test o un ANOVA, es importante tener en cuenta que, al tratarse de p-values, cuanto mayor sea el tamaño de la muestra más poder estadístico tienen y más fácil es encontrar evidencias en contra de la hipótesis nula de normalidad. Al mismo tiempo, cuanto mayor sea el tamaño de la muestra, menos sensibles son los métodos paramétricos a la falta de normalidad. Por esta razón, es importante no basar las conclusiones únicamente en el p-value del test, sino también considerar la representación gráfica y el tamaño de la muestra.

Consecuencias de la falta de normalidad

El hecho de no poder asumir la normalidad influye principalmente en los test de hipótesis paramétricos (t-test, anova,…) y en los modelos de regresión. Las principales consecuencias de la falta de normalidad son:

  • Los estimadores mínimo-cuadráticos no son eficientes (de mínima varianza).
  • Los intervalos de confianza de los parámetros del modelo y los contrastes de significancia son solamente aproximados y no exactos.
Los test estadísticos expuestos requieren que la población de la que procede la muestra tenga una distribución normal, no la muestra en sí. Si la muestra se distribuye de forma normal, se puede aceptar que así lo hace la población de origen. En el caso de que la muestra no se distribuya de forma normal pero se tenga certeza de que la población de origen sí lo hace, entonces, puede estar justificado aceptar los resultados obtenidos por los contrastes paramétricos como válidos.
La utilización de transformaciones para lograr que los datos se ajusten a una distribución normal es en muchas ocasiones la solución más natural a la falta de normalidad, ya que existen gran cantidad de parámetros biológicos que tienen una distribución asimétrica, y que se convierten en aproximadamente simétricas al transformarlas mediante el logaritmo.
Otra solución cuando los datos no siguen una distribución normal es utilizar pruebas no paramétricas, que son aquellas que no presuponen una distribución de probabilidad para los datos.

Normalización de datos

Cuando tratamos con características en ML, el rango de éstas es diferente. Por ejemplo, podemos tener una variable A cuyos valores vayan del 0 - 1.000.000 y otra variable B con valores comprendidos entre 0 - 1. En este caso, nuestro modelo le dará más peso a la variable A que a la B, con lo que podría causar un problema de sesgo.

La normalización es el proceso de igualar los rangos de todas las variables numéricas.

Aunque la normalización es un proceso bastante habitual (incluso recomendable según modelos), no siempre es aconsejable, ya que constituye una pérdida de información inmediata y puede ser perjudicial en algunos casos.

Existen 2 técnicas básicas para normalizar los datos:

  • Normalización
  • Normalización estandarizada (estandarización)

Para los ejemplos que veremos a continuación vamos a utilizar el dataset wine de sklearn. Este dataset clasifica los vinos en 3 clases según una serie de características.

import pandas as pd

from sklearn.datasets import load_wine

data = load_wine()
df = pd.DataFrame(data = data.data, columns = data.feature_names)
df['class'] = data.target

Como siempre, antes de hacer cualquier transformación con los datos, vamos a separar los conjuntos de entrenamiento y test.

X = df.drop(columns = 'class', axis = 1)
y = df['class']

X_train, X_test, y_train, y_test = train_test_split(X, y, train_size   = 0.7)

Normalización

La fórmula para normalizar los datos es bastante sencilla:

$\displaystyle X_{norm} = \frac{x - x_{min}}{x_{max} - x_{min}} $

Vamos a normalizar la columna alcohol de nuestro dataset.

Primero veamos el rango original de nuestros datos:

X_train['alcohol'].describe()

count    124.000000
mean      12.942339
std        0.829526
min       11.030000
25%       12.290000
50%       12.975000
75%       13.622500
max       14.830000
Name: alcohol, dtype: float64

Como vemos, nuestros datos tienen el rango [11.03, 14.83].

Aplicamos la fórmula anterior para normalizar nuestros datos:

max = X_train['alcohol'].max()
min = X_train['alcohol'].min()

normalized_alcohol=(X_train['alcohol']-min)/(max-min)

normalized_alcohol.describe()

count    124.000000
mean       0.506176
std        0.220841
min        0.000000
25%        0.342105
50%        0.500000
75%        0.682237
max        1.000000
Name: alcohol, dtype: float64

El rango de la nueva columna normalizada es [0, 1].

Si queremos normalizar todo el dataset:

normalized_X_train = (X_train - X_train.min()) / (X_train.max() - X_train.min())

Usando librerías de Python

Podemos utilizar el transformador MinMaxScaler de sklearn.

from sklearn import preprocessing

min_max_scaler = preprocessing.MinMaxScaler()

Vamos a normalizar la misma columna que antes (alcohol).

Hay que tener en cuenta que el método fit_transform() (o el método fit()) de MinMaxScaler espera como parámetro un array del tipo (n_samples, n_features) y devuelve un array con la misma dimensión

normalized_alcohol = min_max_scaler.fit_transform(X_train[['alcohol']])

X_train['normalized_alcohol'] = normalized_alcohol
X_train['normalized_alcohol'].describe()

count    124.000000
mean       0.503247
std        0.218296
min        0.000000
25%        0.331579
50%        0.511842
75%        0.682237
max        1.000000
Name: normalized_alcohol, dtype: float64

Si queremos normalizar todo el dataset:

X_train_scaled = min_max_scaler.fit_transform(X_train)
# Tenemos que volver a crear el dataframe, ya que MinMaxScaler.fit_transform devuelve numpy array
normalized_X_train = pd.DataFrame(X_train_scaled)

Estandarización

En este caso, aplicamos la fórmula:

$\displaystyle X_{standard} = \frac{X - \mu}{\sigma} $

Aplicado sobre la columna alcohol:

alcohol_mean = X_train['alcohol'].mean()
alcohol_std = X_train['alcohol'].std()

normalized_alcohol=(X_train['alcohol']-alcohol_mean)/alcohol_std

normalized_alcohol.describe()

count    1.240000e+02
mean     1.200294e-14
std      1.000000e+00
min     -2.506927e+00
25%     -8.174763e-01
50%      1.155041e-01
75%      8.373031e-01
max      1.729308e+00
Name: alcohol, dtype: float64

En este caso, el rango no es [0, 1]. Al estandarizar, lo que hacemos es hacer que nuestra distribución tenga media 0 (o lo más cercano posible a 0) y desviación estándar 1.

Si queremos estandarizar todo el dataset:

normalized_X_train = (X_train - X_train.mean()) / X_train.std()

Usando librerías de Python

Igual que con la normalización, vamos a utilizar una clase de sklearn: StandardScaler.

from sklearn import preprocessing

standard_scaler = preprocessing.StandardScaler()

Normalizando la columna alcohol:

standarized_alcohol = standard_scaler.fit_transform(X_train[['alcohol']])

X_train['normalized_alcohol'] = standarized_alcohol
X_train['normalized_alcohol'].describe()

count    1.240000e+02
mean     8.810157e-16
std      1.004057e+00
min     -2.517097e+00
25%     -8.207926e-01
50%      1.159727e-01
75%      8.406999e-01
max      1.736323e+00
Name: normalized_alcohol, dtype: float64

Normalizar todo el dataset:

X_train_scaled = standard_scaler.fit_transform(X_train)

# Tenemos que volver a crear el dataframe, ya que MinMaxScaler.fit_transform devuelve numpy array
normalized_X_train = pd.DataFrame(X_train_scaled)

Referencias

Ejercicios

Ejercicio 1

Carga el dataset wine de sklearn. Este dataset contiene una serie de registros de tipos de vino (0, 1 o 2) según varias características (alcohol, magnesio, intensidad del color…).

Utiliza la función train_test_split para separar tus datos en entrenamiento y test (30%).

Ejercicio 2

Crea un clasificador KNN con los siguientes parámetros (no te preocupes ahora por el significado de los mismos, ya los veremos más adelante):

model_knn = KNeighborsClassifier(n_neighbors=5, weights='uniform')

Entrena el modelo (con su método fit()), y muestra su exactitud (accuracy) ejecutando lo siguiente:

y_pred_rf = model_knn.predict(X_test)
 
print(model_knn.__class__.__name__, float("{0:.4f}".format(accuracy_score(y_test, y_pred_rf))))

Ejercicio 3

Normaliza todas las columnas del dataset y comprueba ahora la exactitud del modelo.

Ejericicio 4

Haz lo mismo que en el ejercicio anterior, pero estandarizando ahora las columnas en lugar de normalizándolas. ¿Qué técnica ha resultado mejor de las tres?

clase/ia/saa/2eval/analisis_normalidad.txt · Última modificación: 2023/02/20 10:25 por cesguiro