sns.scatterplot(df_data,x='GrLivArea',y='SalePrice',alpha=0.5)
plt.show()9 Análisis exploratorio de datos
Objetivo
El objetivo de esta clase es que los estudiantes aprendan a aplicar técnicas de análisis exploratorio de datos en Python, utilizando herramientas como pandas, matplotlib y seaborn, para identificar patrones y relaciones clave en los datos.
El estudio previo de los datos o EDA (Exploratory Data Analysis) es una etapa crítica en la ciencia de datos y sin duda la que consume más tiempo.
Seguiremos los pasos expuestos en el capítulo ‘Examining your data’ de Hair et al. (2019), para realizar un análisis del conjunto de datos Ames Housing Dataset de Kaggle.
Vamos a dividir el análisis en los siguientes apartados:
- Comprender el problema
- Estudio univariable
- Estudio multivariable
- Limpieza básica de los datos
- Comprobación de suposiciones
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style('whitegrid')
sns.set_palette('Set2')
import numpy as np
from scipy.stats import norm
from sklearn.preprocessing import StandardScaler
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
%matplotlib inlinedf_data = pd.read_csv('Datasets/AmesHousing.csv')
df_data.head(10)df_data.columns9.1 El problema
Para entender realmente el conjunto de datos, veamos la descripción de cada variable:
- Order y PID: identificadores de la instancia (los borraremos)
- MSSubClass: clase de construcción
- MSZoning: clasificación de la zona
- LotFrontage: pies lineales de calle de la parcela
- LotArea: tamaño de la parcela en pies cuadrados
- Street: tipo de acceso por carretera
- Alley: tipo de acceso al callejón
- LotShape: forma de la parcela
- LandContour: planitud de la parcela
- Utilities: servicios públicos disponibles
- LotConfig: Configuración de parcela
- LandSlope: pendiente de la parcela
- Neighborhood: ubicación física dentro de los límites de la ciudad de Ames
- Condition1: proximidad a la carretera principal o al ferrocarril
- Condition2: proximidad a la carretera principal o al ferrocarril (si hay un segundo)
- BldgType: tipo de vivienda
- HouseStyle: estilo de vivienda
- OverallQual: calidad general del material y del acabado
- OverallCond: condición general
- YearBuilt: fecha original de construcción
- YearRemodAdd: fecha de remodelación
- RoofStyle: tipo de cubierta
- RoofMatl: material del techo
- Exterior1st: revestimiento exterior de la casa
- Exterior2nd: revestimiento exterior de la casa (si hay más de un material)
- MasVnrType: tipo de revestimiento de mampostería
- MasVnrArea: área de revestimiento de mampostería en pies cuadrados
- ExterQual: calidad del material exterior
- ExterCond: estado del material en el exterior
- Foundation: tipo de cimentación
- BsmtQual: altura del sótano
- BsmtCond: estado general del sótano
- BsmtExposure: paredes del sótano a nivel de calle o de jardín
- BsmtFinType1: calidad del área acabada del sótano
- BsmtFinSF1: pies cuadrados de la superficie acabada tipo 1
- BsmtFinType2: calidad de la segunda superficie acabada (si existe)
- BsmtFinSF2: Pies cuadrados de la superficie acabada tipo 2
- BsmtUnfSF: pies cuadrados del área sin terminar del sótano
- TotalBsmtSF: pies cuadrados totales del sótano
- Heating: tipo de calefacción
- HeatingQC: calidad y estado de la calefacción
- CentralAir: aire acondicionado central
- Electrical: sistema eléctrico
- 1erFlrSF: área en pies cuadrados de la primera planta (o planta baja)
- 2ndFlrSF: área en pies cuadrados de la segunda planta
- LowQualFinSF: pies cuadrados acabados de baja calidad (todos los pisos)
- GrLivArea: superficie habitable por encima del nivel del suelo en pies cuadrados
- BsmtFullBath: cuartos de baño completos en el sótano
- BsmtHalfBath: medio baño del sótano
- FullBath: baños completos sobre el nivel del suelo
- HalfBath: medios baños sobre el nivel del suelo
- Bedroom: número de dormitorios por encima del nivel del sótano
- Kitchen: número de cocinas
- KitchenQual: calidad de la cocina
- TotRmsAbvGrd: total de habitaciones por encima del nivel del suelo (no incluye baños)
- Functional: valoración de la funcionalidad de la vivienda
- Fireplaces: número de chimeneas
- FireplaceQu: calidad de la chimenea
- GarageType: ubicación del garaje
- GarageYrBlt: año de construcción del garaje
- GarageFinish: acabado interior del garaje
- GarageCars: tamaño del garaje en capacidad de coches
- GarageArea: tamaño del garaje en pies cuadrados
- GarageQual: calidad de garaje
- GarageCond: condición de garaje
- PavedDrive: calzada asfaltada
- WoodDeckSF: area de plataforma de madera en pies cuadrados
- OpenPorchSF: área de porche abierto en pies cuadrados
- EnclosedPorch: área de porche cerrada en pies cuadrados
- 3SsnPorch: área de porche de tres estaciones en pies cuadrados
- ScreenPorch: superficie acristalada del porche en pies cuadrados
- PoolArea: área de la piscina en pies cuadrados
- PoolQC: calidad de la piscina
- Fence: calidad de la valla
- MiscFeature: característica miscelánea no cubierta en otras categorías
- MiscVal: valor en dólares de la característica miscelánea
- MoSold: mes de venta
- YrSold: año de venta
- SaleType: tipo de venta
- SaleCondition: Condiciones de venta
9.2 Análisis univariable: SalePrice
La variable ‘SalePrice’ es la variable objetivo de este conjunto de datos. En pasos posteriores a este análisis exploratorio de datos se realizaría una predicción del valor de esta variable, por lo que vamos a estudiarla con mayor detenimiento:
df_data['SalePrice'].describe()sns.distplot(df_data['SalePrice'])
plt.show()A simple vista se pueden apreciar:
- Una desviación con respecto a la distribución normal.
- Una asimetría positiva.
- Algunos picos.
print('Skewness: %f' % df_data['SalePrice'].skew())
print('Kurtosis: %f' % df_data['SalePrice'].kurt())9.2.1 Relación con variables numéricas
sns.scatterplot(df_data,x='TotalBsmtSF',y='SalePrice',alpha=0.5)
plt.show()9.2.2 Relación con variables categóricas
f, ax = plt.subplots(figsize=(10, 6))
fig = sns.boxplot(df_data, x='OverallQual', y='SalePrice', hue='OverallQual')
fig.axis(ymin=0, ymax=800000)
plt.show()f, ax = plt.subplots(figsize=(18, 6))
fig = sns.boxplot(df_data, x='YearBuilt', y='SalePrice', hue='YearBuilt')
fig.axis(ymin=0, ymax=800000)
ax.xaxis.set_major_locator(plt.MaxNLocator(15))
plt.show()Resumiendo:
- ‘GrLivArea’ y ‘TotalBsmtSF’ mantienen una relación lineal positiva con ‘SalePrice’, aumentando en el mismo sentido. En el caso de ‘TotalBsmtSF’, la pendiente de esta relación es muy acentuada.
- ‘OverallQual’ y ‘YearBuilt’ también parecen relacionadas con ‘SalePrice’ (más fuerte en el primer caso), tal y como se puede observar en los diagramas de cajas.
Sólo hemos explorado cuatro variables, pero hay muchas otras a analizar.
9.3 Análisis multivariable
Hasta ahora sólo me hemos usado la intuición para el análisis de las variables consideradas como importantes. Es hora de un análisis más objetivo.
Para ello realizaremos las siguientes pruebas de correlación: * Matriz de correlación general. * Matriz de correlación centrada en la variable ‘SalePrice’. * Diagramas de dispersión entre las variables más correlacionadas.
9.3.1 Matriz de correlación (en forma de mapa de calor)
corrmat = df_data.corr(numeric_only=True)
f, ax = plt.subplots(figsize=(15, 12))
sns.heatmap(corrmat, vmax=1, vmin=-1, square=True, cmap='RdBu')
plt.show()El mapa de calor es una forma visual muy útil para para conocer las variables y sus relaciones.
9.3.2 Matriz de correlación de ‘SalePrice’
k = 10 # Número de variables
cols = corrmat.nlargest(k, 'SalePrice')['SalePrice'].index
cm = df_data[cols].corr(numeric_only=True)
sns.set(font_scale = 0.9)
hm = sns.heatmap(cm, cbar = True, annot = True, square = True, fmt = '.2f', annot_kws = {'size': 10},
yticklabels = cols.values, xticklabels = cols.values, cmap='RdBu')
plt.show()corr = df_data.corr(numeric_only=True)
corr[['SalePrice']].sort_values(by = 'SalePrice',ascending = False).style.background_gradient(cmap='RdBu',
vmin=-1, vmax=1)9.3.3 Diagramas de dispersión entre ‘SalePrice’ y sus seis variables más correlacionadas
sns.set()
cols = ['SalePrice','OverallQual','GrLivArea','GarageCars','GarageArea','TotalBsmtSF','1stFlrSF']
sns.pairplot(df_data[cols], size = 2.5)
plt.show()Aunque ya habíamos visto algunas de las figuras, este diagrama nos facilita una comprensión general sobre las relaciones entre las variables.
9.4. Limpieza de datos
9.4.1 Datos faltantes
Antes de tratar los datos faltantes, es importante determinar su prevalencia y su aleatoriedad, ya que pueden implicar una reducción del tamaño de la muestra. También hay que asegurarse que su gestión no esté sesgada .
total = df_data.isnull().sum().sort_values(ascending = False)
percent = (df_data.isnull().sum() / df_data.isnull().count()).sort_values(ascending = False)
missing_data = pd.concat([total, percent], axis = 1, keys = ['Total', 'Percent'])
missing_data.head(28)Por razones prácticas vamos a eliminar las variables con más de un 15% de datos faltantes. Con respecto a las variables ‘GarageX’, se observa el mismo número de datos desaparecidos, hecho que quizás habría que estudiar con más detenimiento. Pero, dado que la información más relevante en cuanto al garaje ya está recogida por la variable ‘GarageCars’, y que sólo se trata de un 5% de datos faltantes, borraremos las citadas variables ‘GarageX’, además de las ‘BsmtX’ bajo la misma lógica.
En cuanto a las variables ‘MasVnrArea’ y ‘MasVnrType’, se puede decir que no son esenciales y que, incluso, tienen una fuerte correlación con ‘YearBuilt’ y ‘OverallQual’. No parece que se vaya a perder mucha información si las eliminamos.
Para finalizar, se encuentran muy pocos datos faltante (1 o 2) en algunas variables, por lo que solo se borrarán aquellas instancias y a mantener la variable.
En resumen, borraremos todas las variables con datos desaparecidos, excepto las que tienen 1 o 2 datos faltantes, en este caso sólo borrararemos la observación con el dato faltante.
cols = list((missing_data[missing_data['Total'] > 2]).index)
df_data = df_data.drop(columns=cols)
df_datacols=['BsmtFullBath','BsmtHalfBath','BsmtFinSF1','GarageCars','Electrical','TotalBsmtSF','BsmtUnfSF',
'BsmtFinSF2','GarageArea']
for col in cols:
df_data = df_data.drop(df_data.loc[df_data[col].isnull()].index)
df_data.isnull().sum().max() # Para comprobar que no hay más datos desaparecidos.9.4.2 Datos atípicos
Los datos atípicos u outliers pueden afectar marcadamente el modelo, además de suponer una fuente de información en sí misma. Su tratamiento es un asunto complejo que requiere más atención; por ahora sólo vamos a hacer un análisis rápido a través de la desviación estándar de la variable ‘SalePrice’ y a realizar un par de diagramas de dispersión.
Análisis univariable
La primera tarea en este caso es establecer un umbral que defina una observación como valor atípico. Para ello vamos a estandarizar los datos, es decir, transformar los valores datos para que tengan una media de 0 y una desviación estándar de 1.
saleprice_scaled = StandardScaler().fit_transform(df_data['SalePrice'].values.reshape(-1, 1))
low_range = saleprice_scaled[saleprice_scaled[:,0].argsort()][:10]
high_range = saleprice_scaled[saleprice_scaled[:,0].argsort()][-10:]
print('Fuera de la distribución (por debajo):')
print(low_range)
print('\nFuera de la distribución (por arriba):')
print(high_range)- Los valores bajos son similares y relativamente no muy alejados del 0.
- Los valores altos están muy alejados del 0. Los valores superiores a 7 están realmente fuera de rango.
Análisis bivariable
sns.scatterplot(df_data, x = 'GrLivArea', y = 'SalePrice', alpha = 0.5)
plt.show()Este diagrama de dispersión muestra un par de cosas interesantes:
- Los tres valores más altos de la variable ‘GrLivArea’ resultan extraños. Se pueden hacer especulaciones, pero podría tratarse de terrenos agrícolas o muy degradados, algo que explicaría su bajo precio. Lo que está claro es que estos dos puntos son atípicos, por lo que procederemos a eliminarlos.
- Las dos observaciones más altas de la variable ‘SalePrice’ se corresponden con las que observamos en el análisis univariable anterior. Son casos especiales, pero parece que siguen la tendencia general, por lo que vamos a mantenerlas.
df_data.sort_values(by = 'GrLivArea', ascending = False)[:3]df_data = df_data.drop(df_data[df_data['Order'] == 1499].index)
df_data = df_data.drop(df_data[df_data['Order'] == 2181].index)
df_data = df_data.drop(df_data[df_data['Order'] == 2182].index)sns.scatterplot(df_data, x = 'GrLivArea', y = 'SalePrice', alpha = 0.5)
plt.show()sns.scatterplot(df_data, x = 'TotalBsmtSF', y = 'SalePrice', alpha = 0.5)
plt.show()Aunque se pueden observar algunos valores bastante extremos (p.ej. TotalBsmtSF > 3000), parece que conservan la tendencia, por lo que vamos a mantenerlos.
9.5 Comprobación de normalidad
Ya hemos realizado cierta limpieza de datos y estudiado la variable ‘SalePrice’. Ahora vamos a comprobar si ‘SalePrice’ cumple los supuestos estadísticos que nos permiten aplicar las técnicas del análisis multivariable.
De acuerdo con Hair et al. (2019), hay que comprobar cuatro suposiciones fundamentales:
Normalidad - Cuando hablamos de normalidad lo que queremos decir es que los datos deben parecerse a una distribución normal. Es importante porque varias pruebas estadísticas se basan en esta suposición. Sólo voy a comprobar la normalidad de la variable ‘SalePrice’, aunque resulte un tanto limitado ya que no asegura la normalidad multivariable. Además, si resolvemos la normalidad evitamos otros problemas, como la homocedasticidad.
Homocedasticidad - La homocedasticidad se refiere a la suposición de que las variables dependientes tienen el mismo nivel de varianza en todo el rango de las variables predictoras. La homocedasticidad es deseable porque queremos que el término de error sea el mismo en todos los valores de las variables independientes.
Linealidad- La forma más común de evaluar la linealidad es examinar los diagramas de dispersión y buscar patrones lineales. Si los patrones no son lineales, valdría la pena explorar las transformaciones de datos. Sin embargo, no vamos a entrar en esto porque la mayoría de los gráficos de dispersión que hemos visto parecen tener relaciones lineales.
Ausencia de errores correlacionados - Esto ocurre a menudo en series temporales, donde algunos patrones están relacionados en el tiempo. Tampoco voy a tocar este asunto.
9.5.1 En búsqueda de la normalidad
El objetivo es estudiar la variable ‘SalePrice’ de forma fácil, comprobando:
- Histograma - Curtosis y asimetría.
- Gráfica de probabilidad normal - La distribución de los datos debe ajustarse a la diagonal que representa la distribución normal.
sns.distplot(df_data['SalePrice'], fit = norm);
fig = plt.figure()
res = stats.probplot(df_data['SalePrice'], plot = plt)
plt.show()De estos gráficos se desprende que ‘SalePrice’ no conforma una distribución normal. Muestra picos, asimetría positiva y no sigue la línea diagonal; aunque una simple transformación de datos puede resolver el problema.
df_data['SalePrice'] = np.log(df_data['SalePrice'])sns.distplot(df_data['SalePrice'], fit = norm);
fig = plt.figure()
res = stats.probplot(df_data['SalePrice'], plot = plt)
plt.show()Terminado el trabajo con ‘SalePrice’, vamos a seguir con ‘GrLivArea’.
sns.distplot(df_data['GrLivArea'], fit = norm);
fig = plt.figure()
res = stats.probplot(df_data['GrLivArea'], plot = plt)
plt.show()La variable ‘GrLivArea’ muestra asimetría.
df_data['GrLivArea'] = np.log(df_data['GrLivArea'])sns.distplot(df_data['GrLivArea'], fit = norm);
fig = plt.figure()
res = stats.probplot(df_data['GrLivArea'], plot = plt)
plt.show()A continuación, la variable ‘TotalBsmtSF’.
sns.distplot(df_data['TotalBsmtSF'], fit = norm);
fig = plt.figure()
res = stats.probplot(df_data['TotalBsmtSF'], plot = plt)
plt.show()Estos gráficos nos muestran que la variable ‘TotalBsmtSF’:
- Presenta asimetrías.
- Hay un número significativo de observaciones con valor cero (casas sin sótano).
- El valor cero no nos permite hacer transformaciones logarítmicas.
Para aplicar una transformación logarítmica, crearemos una variable binaria (tener o no tener sótano). Después, aplicaremos la transformación logarítmica a todas las observaciones que no sean cero, ignorando aquellas con valor cero. De esta manera podremos transformar los datos, sin perder el efecto de tener o no sótano.
df_data['HasBsmt'] = pd.Series(len(df_data['TotalBsmtSF']), index = df_data.index)
df_data['HasBsmt'] = 0
df_data.loc[df_data['TotalBsmtSF']>0,'HasBsmt'] = 1df_data.loc[df_data['HasBsmt'] == 1,'TotalBsmtSF'] = np.log(df_data['TotalBsmtSF'])sns.distplot(df_data[df_data['TotalBsmtSF'] > 0]['TotalBsmtSF'], fit = norm);
fig = plt.figure()
res = stats.probplot(df_data[df_data['TotalBsmtSF']>0]['TotalBsmtSF'], plot = plt)
plt.show()9.5.2 En búsqueda de la homocedasticidad
El mejor método para probar la homocedasticidad para dos variables métricas es de forma gráfica. Las desviaciones de una dispersión uniforme se muestran mediante formas tales como conos (pequeña dispersión a un lado del gráfico, gran dispersión en el lado opuesto) o diamantes (un gran número de puntos en el centro de la distribución).
Empecemos por ‘SalePrice’ y ‘GrLivArea’.
plt.scatter(df_data['GrLivArea'], df_data['SalePrice'], alpha = 0.5)
plt.show()Las anteriores versiones de este gráfico de dispersión (antes de las transformaciones logarítmicas), tenían una forma cónica. Como puede apreciarse, el gráfico actual ya no tiene una forma cónica. Tan solo asegurando la normalidad en algunas variables, hemos resuelto el problema de la homocedasticidad.
Ahora vamos a comprobar ‘SalePrice’ con ‘TotalBsmtSF’.
plt.scatter(df_data[df_data['TotalBsmtSF'] > 0]['TotalBsmtSF'], df_data[df_data['TotalBsmtSF'] > 0]['SalePrice'],
alpha = 0.5)
plt.show()Podemos decir que, en general, la variable ‘SalePrice’ muestra niveles equivalentes de varianza en todo el rango de ‘TotalBsmtSF’.
9.5.3 Variables dummy
Finalmente, las variables categóricas, de las que no nos encargamos a lo largo de la sesión, las convertiremos en variables dummy.
df_data = pd.get_dummies(df_data)
df_data9.6 Ejercicios prácticos
- Cree un nuevo Notebook.
- Guarde el archivo como Ejercicios_practicos_clase_9.ipynb.
- Asigne un título H1 con su nombre.
9.6.1 Ejercicio práctico 1
Busque una base de datos en el UCI Machine Learning Repository y realice análisis exploratorio de los datos.