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.
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']
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)
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
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
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:
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.
Existen 2 técnicas básicas para normalizar los datos:
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)
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())
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).
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)
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()
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)
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?