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.
Subscríbete a 🌮 tacos de datos | Aprende visualización de datos en español.
Recibe las mejores publicaciones directamente a tu caja de entrada