/ TUTORIALES, PYTHON, EDA

Introducción al análisis exploratorio de datos

En este post les voy a dar una pequeñísima introducción al análisis exploratorio de datos o EDA por sus siglas en inglés (Exploratory Data Analysis).

Como lo mencioné en mi video de YouTube sobre el tema, El AED una forma de realizar análisis de datos, es usualmente uno de los primeros pasos que uno debe realizar antes de aventurarse utilizar el scikit learn o tensoflow para hacer machine learning. Usualmente este proceso de análisis involucra muchas ayudas visuales, es decir gráficas, que nos ayudan a encontrar información que de otra forma sería difícil de conocer acerca de nuestros datos; mediante este proceso estamos tratando de encontrar:

  • La estructura y distribución de nuestros datos,
  • Encontrar las relaciones entre las variables explicatorias,
  • Encontrar la relación que tienen las variables explicatorias con la variable respuesta,
  • Encontrar posibles errores, puntos extremos y anomalías en los datos,
  • Refinar nuestras hipótesis, o generar nuevas preguntas sobre los datos que tenemos

No existe una técnica formal sobre cómo llevar a cabo este tipo de análisis, sino que más dependen de lo que vayamos encontrando en los datos, así como de la experiencia y conocimiento específico del problema con el que contemos.

Para este post usaré algunas de las librerías más comunes para el análisis de datos en el ecosistema de Python, todas las puedes instalar desde PyPI:

pip install numpy pandas matplotlib seaborn scikit-learn

Y sobre los datos que vamos a analizar, serán los datos de Red Wine Quality. Por cierto, puedes ver el notebook completo en Kaggle, si accedes al notebook ahí, no tienes que preocuparte por instalar nada ni descargar nada, todo está listo. Ahora si, comenzamos con nuestros imports de regla:

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.style as style
from contextlib import contextmanager
from sklearn.model_selection import train_test_split

style.use('ggplot')

Ahora sí, leemos nuestros a un DataFrame datos usando pandas, una vez leídos mostramos qué tan grande es nuestro DataFrame, así como una muestra de los datos:

wine_quality = pd.read_csv("/kaggle/input/red-wine-quality-cortez-et-al-2009/winequality-red.csv")
print("Dataset length", len(wine_quality))
wine_quality.head()

Dataset length 1599

fixed acidity volatile acidity citric acid residual sugar chlorides free sulfur dioxide total sulfur dioxide density pH sulphates alcohol quality
0 7.4 0.70 0.00 1.9 0.076 11.0 34.0 0.9978 3.51 0.56 9.4 5
1 7.8 0.88 0.00 2.6 0.098 25.0 67.0 0.9968 3.20 0.68 9.8 5
2 7.8 0.76 0.04 2.3 0.092 15.0 54.0 0.9970 3.26 0.65 9.8 5
3 11.2 0.28 0.56 1.9 0.075 17.0 60.0 0.9980 3.16 0.58 9.8 6
4 7.4 0.70 0.00 1.9 0.076 11.0 34.0 0.9978 3.51 0.56 9.4 5

De a poco vamos conociendo nuestros datos… ahora la siguiente pregunta ¿Qué variables tenemos?.

Lo primero que hay que hacer es tratar de identificar el tipo de variables que tenemos a la mano, en algunos datasets esto es posible con tan solo leer los nombres de las columnas (como es el caso de el dataset con el que estamos trabajando). Sin embargo, hay algunos casos en los que los nombres no son provistos (o están ofuscados) por diversas razones.

Aprovechando que nuestro dataset sí tiene nombres, podemos verlos con:

wine_quality.columns

Index([‘fixed acidity’, ‘volatile acidity’, ‘citric acid’, ‘residual sugar’, ‘chlorides’, ‘free sulfur dioxide’, ‘total sulfur dioxide’, ‘density’, ‘pH’, ‘sulphates’, ‘alcohol’, ‘quality’], dtype=’object’)

Probablemente si no eres un conocedor de vinos, las variables tengan poco o nada de sentido para ti. Idealmente nosotros deberíamos tener conocimiento del tema sobre el que vamos a trabajar (o podemos conseguir un experto que nos guíe)… pero por ahora pretendamos que sabemos lo que hacemos. Visita este link si quieres una ligera introducción a los componentes del vino.

Antes de continuar ⚠️

Si lo que estás esperando hacer con la información es crear un modelo predictivo, lo primero que hay que hacer es separar los datos en conjuntos de prueba, entrenamiento y, si puedes, validación. El análisis exploratorio de datos se debe conducir únicamente sobre los datos de entrenamiento, ya que realizar el análisis en todo el conjunto de datos nos llevaría a tomar decisiones teniendo en cuenta datos a los que, en teoría, tu modelo no tendría acceso en producción. Es decir, este es un problema de filtración de datos.

Teniendo esto en mente, vamos a separar nuestros datos con:

wine_train, wine_test = train_test_split(wine_quality)

Completitud en los datos

Antes de comenzar cualquier análisis, es bueno revisar los datos para buscar información faltante; y en caso de que la haya, es nuestra tarea decidir qué es lo que podemos hacer con esos registros faltantes. Con los dataframes de pandas podemos usar info para encontrar los datos faltantes: También podríamos haber usado missingno para tener una representación visual de esta información.

wine_train.info()
wine_test.info()

Y así es cómo se ve a la salida:

<class 'pandas.core.frame.DataFrame'>
Int64Index: 1199 entries, 1060 to 418
Data columns (total 12 columns):
fixed acidity           1199 non-null float64
volatile acidity        1199 non-null float64
citric acid             1199 non-null float64
residual sugar          1199 non-null float64
chlorides               1199 non-null float64
free sulfur dioxide     1199 non-null float64
total sulfur dioxide    1199 non-null float64
density                 1199 non-null float64
pH                      1199 non-null float64
sulphates               1199 non-null float64
alcohol                 1199 non-null float64
quality                 1199 non-null int64
dtypes: float64(11), int64(1)
memory usage: 121.8 KB
<class 'pandas.core.frame.DataFrame'>
Int64Index: 400 entries, 1186 to 1163
Data columns (total 12 columns):
fixed acidity           400 non-null float64
volatile acidity        400 non-null float64
citric acid             400 non-null float64
residual sugar          400 non-null float64
chlorides               400 non-null float64
free sulfur dioxide     400 non-null float64
total sulfur dioxide    400 non-null float64
density                 400 non-null float64
pH                      400 non-null float64
sulphates               400 non-null float64
alcohol                 400 non-null float64
quality                 400 non-null int64
dtypes: float64(11), int64(1)
memory usage: 40.6 KB

Y pues no, no hay datos faltantes… sin embargo si faltaran, debes saber que existe toda una metodología para decidir cómo actuar ante datos faltantes en nuestro dataset. Pero de eso podemos hablar en otro momento.

Estadísticas descriptivas

El segundo paso a dar, es ver las estadísticas descriptivas de nuestra información, esto nos ayudará a darnos una idea de los posibles valores de nuestro dataset. El paquete pandas ofrece el método describe para obtener una vista detallada y completa de algunas de las estadísticas más comunes:

wine_train.describe()
fixed acidity volatile acidity citric acid residual sugar chlorides free sulfur dioxide total sulfur dioxide density pH sulphates alcohol quality
count 1199.000000 1199.000000 1199.000000 1199.000000 1199.000000 1199.000000 1199.000000 1199.000000 1199.000000 1199.000000 1199.000000 1199.000000
mean 8.309174 0.525847 0.269867 2.574604 0.088136 15.820267 46.194329 0.996755 3.312035 0.664120 10.436614 5.637198
std 1.758086 0.177889 0.194103 1.448308 0.049179 10.559120 32.648521 0.001879 0.156249 0.177114 1.075123 0.802324
min 4.600000 0.120000 0.000000 0.900000 0.012000 1.000000 6.000000 0.990070 2.740000 0.330000 8.400000 3.000000
25% 7.100000 0.390000 0.090000 1.900000 0.070000 7.000000 22.000000 0.995600 3.210000 0.560000 9.500000 5.000000
50% 7.900000 0.520000 0.260000 2.200000 0.079000 13.000000 37.000000 0.996800 3.310000 0.620000 10.200000 6.000000
75% 9.200000 0.637500 0.420000 2.600000 0.091000 22.000000 61.500000 0.997820 3.400000 0.735000 11.100000 6.000000
max 15.900000 1.330000 1.000000 15.500000 0.611000 72.000000 278.000000 1.003690 4.010000 2.000000 14.900000 8.000000

De este resumen estadístico, una de las primeras cosas que podrían resultar extrañas es que la variable quality únicamente toma valores entre $3$ y $8$ (a pesar de que en la descripción original dice que los valores van de $0$ a $10$. Lo cual representa un problema puesto que nuestro modelo predictivo no tendrá ejemplos de vinos con una calidad de $0$ o de $10$, por ejemplo. Pero por el momento, no nos vamos a preocupar mucho por eso.

También tengo que decir que algun conocedor del tema podría tener opiniones acerca de los rangos de valores que cubren ciertas variables… pero nosotros vamos a pasar al análisis gráfico.

@contextmanager
def plot(title=None, xlabel=None, ylabel=None, figsize=(9,5)):
    fig = plt.figure(figsize=figsize)
    ax = fig.gca()
    yield ax
    if title:
        ax.set_title(title)
    ax.set_xlabel(xlabel, size=15)
    ax.set_ylabel(ylabel, size=15)

Histogramas

Primero podemos echarle un ojo a la distribución de la variable quality, que como ya sabemos que es una variable discreta y que los valores van, en teoría de 0 a 10, podemos simplemente usar countplot del módulo seaborn:

with plot(title="Counts of `qualuty`", xlabel="Quality", ylabel="Count") as ax:
    sns.countplot(x="quality", palette=("Accent"), data=wine_train, ax=ax)

Como puedes ver, nuestro conjunto de datos está desbalanceado, con muchos mas $5$ y $6$ que cualquier otro valor.

Ahora lo que podemos hacer es revisar algunas otras distribuciones, ahora sí, usando histogramas:

with plot(title="Fixed Acidity distribution", xlabel="Acidity") as ax:
    sns.distplot(wine_train["volatile acidity"], ax=ax)

Podemos usar un poco de código para visualizar más de una variable a la vez:

variables = ['fixed acidity', 'volatile acidity', 'citric acid', 'residual sugar',
       'chlorides', 'free sulfur dioxide', 'total sulfur dioxide', 'density',
       'pH', 'sulphates', 'alcohol', 'quality']

columns = 4

fig, axes = plt.subplots(len(variables) //columns, columns, figsize=(15,8))

for current_idx, variable in enumerate(variables):
    i = current_idx // columns
    j = current_idx % columns
    sns.distplot(wine_train[variable], ax=axes[i][j])
    axes[i][j].set_title(variable)
    axes[i][j].set_xlabel("")
    
plt.tight_layout()

De esta gráfica podemos ver que muchas de las variables tienen una distribución asimétrica (fixed acidity, residual sugar, chlorides, por ejemplo), además de que al parecer algunos valores tienen valores extremos (residual sugar, sulphates, total sulfur dioxide). Tal vez merezcan más exploración…

Boxplots

Como mencioné anteriormente, existen algunas variables que merecen un poco más de exploración ya que parecen tener valores extremos, las boxplots nos permiten encontrar precisamente estos valores extremos. Es fácil graficar boxplots con seaborn.

variables = ['fixed acidity', 'residual sugar',
       'chlorides', 'free sulfur dioxide', 
             'total sulfur dioxide','sulphates', 'alcohol']

fig, axes = plt.subplots(1, len(variables), figsize=(15,6))

for ax, variable in zip(axes, variables):
    ax = sns.boxplot( y=variable, data=wine_train, ax=ax)
plt.tight_layout()

Como ya sabemos, los puntos fuera de las líneas horizontales son los famosos outliers o “valores atípicos”, dependiendo de la aplicación podemos reaccionar de diversas maneras frente a ellos… a veces los outliers se eliminan, a veces se transforman, o a veces se dejan porque tienen alto valor predictivo.

Scatterplots

El siguiente paso es tratar de identificar relaciones entre variables, podríamos por ejemplo usar un scatterplot para ver qué tipo de relación existe entre la cantidad de alcohol y la calidad de un vino:

with plot(title="Alcohol - Quality scatterplot", xlabel="Alcohol", ylabel="Quality") as ax:
    sns.scatterplot(x="alcohol", y="quality", data=wine_train, ax=ax) 

Tal vez esta gráfica no sea tan reveladora, ya que nuestra variable quality es más bien del tipo categórico y es difícil identificar una tendencia. Otra cosa a notar es que las correlaciones también se pueden y, en la mayoría de los casos, se deben identificar entre las variables independientes también, no solo entre una de ellas y la variable dependiente. Por ejemplo, entre free sulfur dioxide y total sulfur dioxide:

with plot(title="Free Sulfur Dioxide - Total Sulfur Dioxide scatterplot", xlabel="Free Sulfur Dioxide", ylabel="Total Sulfur Dioxide") as ax:
    sns.scatterplot(x="free sulfur dioxide", y="total sulfur dioxide", data=wine_train, ax=ax) 

En esta gráfica si se puede observar una clara relación entre las variables, ¿cierto?

Si quieres ver solamente el grado de correlación (sin necesidad de tratar de encontrarlo tu mismo desde una scatterplot) también podemos hacer uso de las matrices de correlación.

Matrices de correlación

Una matriz de correlación no es más que una matriz de números (cada número va de -1 a 1) que nos indican qué tan relacionadas están una variable con otra. Existen 3 métodos para calcular esta correlación. Para calcularla en nuestro dataframe de vinos, podemos simplemente usar el método corr de un dataframe:

correlation = wine_train.corr(method="pearson")
correlation.head()
fixed acidity volatile acidity citric acid residual sugar chlorides free sulfur dioxide total sulfur dioxide density pH sulphates alcohol quality
fixed acidity 1.000000 -0.237044 0.675747 0.164493 0.091213 -0.167011 -0.102002 0.681653 -0.692635 0.152724 -0.075869 0.128048
volatile acidity -0.237044 1.000000 -0.543960 -0.011115 0.063904 -0.015815 0.088772 0.031415 0.220483 -0.248910 -0.209193 -0.389654
citric acid 0.675747 -0.543960 1.000000 0.149428 0.225910 -0.078811 0.004797 0.376603 -0.545874 0.314076 0.109625 0.241375
residual sugar 0.164493 -0.011115 0.149428 1.000000 0.059603 0.170785 0.162121 0.390897 -0.114692 0.021992 0.037932 0.026566
chlorides 0.091213 0.063904 0.225910 0.059603 1.000000 0.003828 0.061718 0.192486 -0.284091 0.391324 -0.222979 -0.133884

Luego, para graficar estos números podemos usar un heatmap de seaborn:

with plot(title="Free Sulfur Dioxide - Total Sulfur Dioxide scatterplot") as ax:
    sns.heatmap(correlation, vmin=-1,cmap= 'coolwarm', annot=True, ax=ax) 

Los colores más intensos corresponden con números cercanos a -1 o 1, que indican que tienen mayor relación entre ellas, por ejemplo, la variable pH con fixed acidity (que tienen una relación inversa) o density con fixed acidity que de igual manera, presenta una relación, pero en este caso, directa.

Y pues, eso es todo por este post, sí, es bastante sencillo y aún hay mucho qué discutir sobre el análisis exploratorio de datos, aquí hay algunos de los recursos que yo consulté para preparar este video, y aún así hay mucho más por explorar:

No olvides seguirme en twitter en @io_exception.