Análise Exploratória dos Dados (AED) - Parte 1
Nesta parte inicial exploraremos os dados disponíveis, buscando alguns insights que podem ser úteis para a etapa de modelagem. Desenvolvemos, nessa e nas demais etapas, notebooks no próprio kaggle, sob a competição House Prices: Advanced Regression Techniques, que serão detalhados ao longo dessa questão.
Setup e checagem inicial
Carregamos todas as bibliotecas que serão utilizadas ao longo dessa etapa, bem como setamos o padrão dos gráficos para o default do seaborn:
import os
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import warnings
from scipy.stats import norm
sns.set()
warnings.filterwarnings('ignore')
single_plot_w, single_plot_h = 1.5 * 3.8, 3.8
ylim = (0, 10 ** 6)
"Espiamos" o diretório da base de dados para verificar o que há disponível:
for dirname, _, filenames in os.walk('/kaggle/input'):
for filename in filenames:
print(os.path.join(dirname, filename))
Obtendo:
/kaggle/input/house-prices-advanced-regression-techniques/sample_submission.csv
/kaggle/input/house-prices-advanced-regression-techniques/data_description.txt
/kaggle/input/house-prices-advanced-regression-techniques/train.csv
/kaggle/input/house-prices-advanced-regression-techniques/test.csv
Conforme descrição na própria página da base de dados, em train.csv temos os dados nominados, mas não há preços listados em test.csv, já que é o arquivo para submissão de previsões para quem participar da competição. Vamos então nos ater a train.csv, que será considerada como nossa única base neste exercício. Outro arquivo importante é data_description.txt, que contém a descrição de todos os features, como veremos a seguir. Carregamos então train.csv em um DataFrame, listamos suas colunas, e damos uma breve espiada em parte dos dados e em suas dimensões:
df = pd.read_csv("/kaggle/input/house-prices-advanced-regression-techniques/train.csv")
print("colunas:", df.columns)
print(df.head())
Que nos retorna:
colunas: Index(['Id', 'MSSubClass', 'MSZoning', 'LotFrontage', 'LotArea', 'Street',
'Alley', 'LotShape', 'LandContour', 'Utilities', 'LotConfig',
'LandSlope', 'Neighborhood', 'Condition1', 'Condition2', 'BldgType',
'HouseStyle', 'OverallQual', 'OverallCond', 'YearBuilt', 'YearRemodAdd',
'RoofStyle', 'RoofMatl', 'Exterior1st', 'Exterior2nd', 'MasVnrType',
'MasVnrArea', 'ExterQual', 'ExterCond', 'Foundation', 'BsmtQual',
'BsmtCond', 'BsmtExposure', 'BsmtFinType1', 'BsmtFinSF1',
'BsmtFinType2', 'BsmtFinSF2', 'BsmtUnfSF', 'TotalBsmtSF', 'Heating',
'HeatingQC', 'CentralAir', 'Electrical', '1stFlrSF', '2ndFlrSF',
'LowQualFinSF', 'GrLivArea', 'BsmtFullBath', 'BsmtHalfBath', 'FullBath',
'HalfBath', 'BedroomAbvGr', 'KitchenAbvGr', 'KitchenQual',
'TotRmsAbvGrd', 'Functional', 'Fireplaces', 'FireplaceQu', 'GarageType',
'GarageYrBlt', 'GarageFinish', 'GarageCars', 'GarageArea', 'GarageQual',
'GarageCond', 'PavedDrive', 'WoodDeckSF', 'OpenPorchSF',
'EnclosedPorch', '3SsnPorch', 'ScreenPorch', 'PoolArea', 'PoolQC',
'Fence', 'MiscFeature', 'MiscVal', 'MoSold', 'YrSold', 'SaleType',
'SaleCondition', 'SalePrice'],
dtype='object')
Id MSSubClass MSZoning LotFrontage LotArea Street Alley LotShape \
0 1 60 RL 65.0 8450 Pave NaN Reg
1 2 20 RL 80.0 9600 Pave NaN Reg
2 3 60 RL 68.0 11250 Pave NaN IR1
3 4 70 RL 60.0 9550 Pave NaN IR1
4 5 60 RL 84.0 14260 Pave NaN IR1
LandContour Utilities ... PoolArea PoolQC Fence MiscFeature MiscVal MoSold \
0 Lvl AllPub ... 0 NaN NaN NaN 0 2
1 Lvl AllPub ... 0 NaN NaN NaN 0 5
2 Lvl AllPub ... 0 NaN NaN NaN 0 9
3 Lvl AllPub ... 0 NaN NaN NaN 0 2
4 Lvl AllPub ... 0 NaN NaN NaN 0 12
YrSold SaleType SaleCondition SalePrice
0 2008 WD Normal 208500
1 2007 WD Normal 181500
2 2008 WD Normal 223500
3 2006 WD Abnorml 140000
4 2008 WD Normal 250000
[5 rows x 81 columns]
Podemos perceber que as colunas do dataframe seguem uma estrutura típica deste tipo de base de dados: a primeira coluna traz o 'Id' da observação, a última o nosso target, e entre elas 79 colunas de features, tanto numéricos quanto categóricos, que vamos explorar em seguida.
Já aproveitamos para verificar quais colunas possuem valores faltantes:
df.columns[df.isna().any()]
Obtendo:
Index(['LotFrontage', 'Alley', 'MasVnrType', 'MasVnrArea', 'BsmtQual',
'BsmtCond', 'BsmtExposure', 'BsmtFinType1', 'BsmtFinType2',
'Electrical', 'FireplaceQu', 'GarageType', 'GarageYrBlt',
'GarageFinish', 'GarageQual', 'GarageCond', 'PoolQC', 'Fence',
'MiscFeature'],
dtype='object')
Ou seja, há uma série de features com valores faltantes, para os quais buscaremos um tratamento durante a etapa de modelagem. Partimos então para avaliação do target e das features.
Avaliação do target
Plotamos a distribuição do preço de venda das casas e verificamos medidas de assimetria e de curtose:
fig, (ax_box, ax_hist) = plt.subplots(2, sharex=True, gridspec_kw={"height_ratios": (.15, .85)}, figsize=(10, 6))
_ = sns.boxplot(df['SalePrice'], ax=ax_box)
_ = sns.distplot(df['SalePrice'], ax=ax_hist)
ax_box.set(xlabel='')
print("{} {:f}{} {} {:f}".format("Medida de assimetria (Skewness):", df['SalePrice'].skew(), ";", "Medida de curtose:", df['SalePrice'].kurt()))
Medida de assimetria (Skewness): 1.882876; Medida de curtose: 6.536282

Assim podemos verificar que nossa distribuição é Leptocúrtica Positivamente Assimétrica (medida de curtose acima de zero é relacionada com o afunilamento no pico em relação a Normal, e a medida de assimetria/skewness positiva indica o que verificamos visualmente: a cauda mais longa a direita). Interessante notar que tal distribuição provavelmente é a que esperaríamos em se tratando de preços de casas em uma cidade típica, em que podemos identificar claramente outliers / preços altos que se destoam dos demais.
Aplicamos então uma transformação logarítmica em nosso target, e verificamos que com isso basicamente a aproximamos para uma distribuição Normal, o que utilizaremos na etapa de modelagem:
fig, (ax_box, ax_hist) = plt.subplots(2, sharex=True, gridspec_kw={"height_ratios": (.15, .85)}, figsize=(10, 6))
logSalePrice = np.log(df['SalePrice'])
_ = sns.boxplot(logSalePrice, ax=ax_box)
_ = sns.distplot(logSalePrice, ax=ax_hist, fit=norm)
ax_box.set(xlabel='')
print("{} {:f}{} {} {:f}".format("Medida de assimetria (Skewness):", logSalePrice.skew(), ";", "Medida de curtose:", logSalePrice.kurt()))
Medida de assimetria (Skewness): 0.121335; Medida de curtose: 0.809532

Avaliação dos Features
Se verificarmos a base de dados apenas pelo tipo em que os dados são codificados:
df.dtypes.value_counts()
Obtemos:
object 43
int64 35
float64 3
dtype: int64
Mas não conseguimos na realidade concluir quais features são numéricas ou categóricas, pois a realidade da base é um pouco mais complexa, o que concluímos ao verificar a descrição das features disponível no arquivo data_description.txt.
Após analisar todas as features listadas, basicamente, podemos dividi-las em 18 features numéricos contínuos que se referem a dimensões / área (em pés / pés quadrados) (18 no total), 1 feature numéricos contínuos de valores monetários, 9 features numéricos discretos de quantidades referentes a alguma característica específica da casa, 5 features numéricos discretos relacionados a datas (com a observação que a feature 'GarageYrBlt' está codificada em float), 22 features categóricos ordinais (sendo que alguns estão codificados em valores inteiros e outros com a apenas a descrição) e 24 features categóricos nominais (sendo que um deles, MSSubClass, está codificado com valores inteiros, mas que não apresenta nenhuma ordinalidade).
Abaixo listamos os features em cada um desses grupos, com uma breve descrição:
Features numéricos contínuos que se referem a dimensões / área (em pés / pés quadrados) (18):
- 'LotFrontage': Pés lineares da rua conectados à propriedade;
- 'LotArea': Área do lote;
- 'MasVnrArea': Área de alvenaria;
- 'BsmtFinSF1': Área acabada do porão (do primeiro tipo);
- 'BsmtFinSF2': Área acabada do porão (do segundo tipo, se mais de um);
- 'BsmtUnfSF': Área não acabada do porão;
- 'TotalBsmtSF': Área total do porão;
- '1stFlrSF': Área do primeiro andar;
- '2ndFlrSF': Área do segundo andar;
- 'LowQualFinSF': Área com acabamento de baixa qualidade (todos os andares);
- 'GrLivArea': Área útil acima do nível solo;
- 'GarageArea': Área da garagem;
- 'WoodDeckSF': Área do deck de madeira;
- 'OpenPorchSF': Área de varanda aberta;
- 'EnclosedPorch': Área de varanda fechada;
- '3SsnPorch': Área da varanda de três temporadas;
- 'ScreenPorch': Área da tela da varanda;
- 'PoolArea': Área da piscina;
Features numéricos contínuos de valores monetários (1):
- 'MiscVal': Valor monetário de características diversas não incluídas em outras categorias;
Features numéricos discretos de quantidades de alguma característica da casa (9):
- 'BsmtFullBath': Banheiros completos no porão;
- 'BsmtHalfBath': Lavabos no porão;
- 'FullBath': Banheiros completos acima do nível do solo;
- 'HalfBath': Lavabos completos acima do nível do solo;
- 'BedroomAbvGr': Quartos acima do nível do solo (não inclui quartos no subsolo);
- 'KitchenAbvGr': Cozinhas acima do nível do solo;
- 'TotRmsAbvGrd': Total de cômodos acima do nível do solo (não inclui banheiros);
- 'Fireplaces': Número de lareiras;
- 'GarageCars': Capacidade da garagem (número de carros);
Features numéricos discretos relacionados a datas (5) (exceção para 'GarageYrBlt', que possui valores contínuos):
- 'YearBuilt': Ano de construção da casa;
- 'YearRemodAdd': Ano da reforma da casa (mesma do ano da construção de não houver);
- 'GarageYrBlt': Ano de construção da garagem;
- 'MoSold': Mês de venda;
- 'YrSold': Ano de venda;
Features categóricos ordinais (22):
- 'LotShape': Formato geral do lote (regular, ligeiramente irregular, moderadamente irregular, irregular);
- 'Utilities': Utilidades disponíveis (de apenas eletricidade a todas);
- 'LandSlope': Inclinação do terreno (3 níveis);
- 'OverallQual': Qualidade geral da casa (de 0 a 10);
- 'OverallCond': Condições gerais da cada (de 0 a 10);
- 'ExterQual': Qualidade do material no exterior (5 níveis);
- 'ExterCond': Avaliação a condição atual do material no exterior (5 níveis);
- 'BsmtQual': Avaliação a altura do porão (5 níveis);
- 'BsmtCond': Avaliação do estado geral do porão (5 níveis);
- 'BsmtFinType1': Classificação da área acabada do porão (6 níveis + NA);
- 'BsmtFinType2': Classificação da área acabada do porão (para o segundo tipo, se mais de um) (6 níveis + NA);
- 'BsmtExposure': Avaliação da exposição externa do porão;
- 'HeatingQC': Qualidade e condição do aquecimento (5 níveis);
- 'KitchenQual': Qualidade da cozinha (5 níveis);
- 'Functional': Funcionalidade inicial (presuma típica, a menos que as deduções sejam garantidas) (8 níveis);
- 'FireplaceQu': Qualidade da lareira (5 níveis + NA);
- 'GarageFinish': Estado do acabamento interior da garagem (acabado, mal acabado, não acabado, sem garagem, NA);
- 'GarageQual': Qualidade da garagem (5 níveis + NA);
- 'GarageCond': Condição da garagem (5 níveis + NA);
- 'PavedDrive': Pavimento da pista até a garagem (pavimentado, parcialmente pavimentado, sem pavimento/cascalho);
- 'PoolQC': Qualidade da piscina (4 níveis + NA);
- 'Fence': Qualidade da cerca (4 níveis + NA);
Features categóricos nominais (24):
- 'Street': Tipo de estrada de acesso à propriedade (pavimentada ou de cascalho);
- 'Alley': Tipo de acesso à propriedade (pavimentada, de cascalho ou NA);
- 'MSSubClass': Classificação do tipo de imóvel;
- 'MSZoning': Classificação geral do zoneamento (Comercial, Agricultural, Residencial de média densidade, etc);
- 'LandContour': Tipo de nivelamento do terreno da propriedade;
- 'LotConfig': Configuração do lote (ex: esquina, dentro do lote, etc.);
- 'Neighborhood': Bairro;
- 'Condition1': Proximidade a alguma via específica (ex: Adjacente à Ferrovia Norte-Sul);
- 'Condition2': Idem a anterior, se mais de uma;
- 'BldgType': Tipo de moradia (ex: duplex, unidade interna da casa geminada, etc.);
- 'HouseStyle': Estilo de moradia (ex: casa térrea, sobrado, etc.). Semelhante a MSSubClass, mas menos detalhado;
- 'RoofStyle': Tipo de telhado;
- 'RoofMatl': Material do telhado;
- 'Exterior1st': Tipo de acabamento exterior;
- 'Exterior2nd': Idem a anterior, se mais de um;
- 'MasVnrType': Tipo da cobertura da alvenaria;
- 'Foundation': Tipo de fundação;
- 'Heating': Tipo de aquecimento;
- 'CentralAir': Ar condicionado central (sim ou não);
- 'Electrical': Sistema elétrico;
- 'GarageType': Localização da garagem em relação a casa;
- 'MiscFeature': Características diversas não incluídas em outras categorias;
- 'SaleType': Tipo de venda (tipo de garantia, tipo de contrato);
- 'SaleCondition': Condição de venda (ex: normal, venda entre membros de família, etc.).
Análise dos Features e de sua relação com o target
Nessa etapa plotamos as distribuições de cada features, bem como os gráficos de dispersão, em relação ao preço de venda, para as features numéricas, e bloxplots para as features categóricas, divididos conforme grupos citados anteriormente.
Assim, para os os features numéricos que se referem a dimensões / área:
def plot_numeric(feature_list, ylim=(0, 10 ** 6), single_plot_w=1.5 * 3.8, single_plot_h=3.8):
m, n = len(feature_list), 2
fig, ax = plt.subplots(m, n, figsize=(n * single_plot_w, m * single_plot_h))
for i, feature in enumerate(feature_list):
g = sns.distplot(df[feature], ax=ax[i][0], kde=False)
g = sns.regplot(x=df[feature], y=df['SalePrice'], ax=ax[i][1])
g.set(ylim=ylim)
area_features = ['LotFrontage', 'LotArea', 'MasVnrArea', 'BsmtFinSF1', 'BsmtFinSF2', 'BsmtUnfSF', 'TotalBsmtSF', '1stFlrSF', '2ndFlrSF', 'LowQualFinSF', 'GrLivArea', 'GarageArea', 'WoodDeckSF', 'OpenPorchSF', 'EnclosedPorch', '3SsnPorch', 'ScreenPorch', 'PoolArea']
plot_numeric(area_features)
O que nos gera o seguinte gráfico:
Features numéricos que se referem a dimensões / área
Separamos aqui dois exemplos interessantes:

Podemos traçar algumas conclusões com relação a estes gráficos:
Como esperado, o preço de venda possui relação aproximadamente linear com a área útil total acima do nível do solo (GrLivArea), mas com considerável heteroscedasticidade. Também podemos observar alguns outliers (valores para áreas acima de 4000 pés quadrados), mas que segundo [ 1 ] não representam observações incorretas, e sim valores não usuais de venda;
Outros features de área também possuem relação aproximadamente linear com o preço de venda, por exemplo, área total do porão;
Moradores de Ames parecem não ser muito fãs de piscinas...
Observação: notamos que, se utilizarmos a transformação logarítmica em SalePrices, reduzimos também heteroscedasticidade em relação às medidas de área. Por exemplo:
fig, ax = plt.subplots(figsize=(single_plot_w, single_plot_h))
g = sns.regplot(x=df['GrLivArea'], y=np.log(df['SalePrice']))

Com relação a valores monetários, possuímos apenas um feature, correspondente ao valor de itens adicionais que uma casa possuí, o qual plotamos por:
m, n = 1, 2
fig, (ax1, ax2) = plt.subplots(m, n, figsize=(n * single_plot_w, m * single_plot_h))
g1 = sns.distplot(df['MiscVal'], ax=ax1, kde=False)
g2 = sns.regplot(x=df['MiscVal'], y=df['SalePrice'], ax=ax2)
_ = g2.set(ylim=ylim)

Aparentemente, os valores de itens adicionais, individualmente, parecem pouco informativos sobre o valor do imóvel.
Para os features numéricos discretos de quantidades de itens específicos da casa:
def plot_categorical(feature_list, single_plot_w=1.5 * 3.8, single_plot_h=3.8):
m, n = len(feature_list), 2
fig, ax = plt.subplots(m, n, figsize=(n * single_plot_w, m * single_plot_h))
for i, feature in enumerate(feature_list):
g = sns.countplot(df[feature], ax=ax[i][0])
g = sns.boxplot(x=df[feature], y=df['SalePrice'], ax=ax[i][1])
g.set_yticklabels(['{:,.0f}'.format(y) + 'K' for y in g.get_yticks()/1000])
discrete_features = ['BsmtFullBath', 'BsmtHalfBath', 'FullBath', 'HalfBath', 'BedroomAbvGr', 'KitchenAbvGr', 'TotRmsAbvGrd', 'Fireplaces', 'GarageCars']
plot_categorical(discrete_features)
O que nos gera o seguinte plot:
Features numéricos discretos de quantidades de ítens específicos da casa
Em que separamos dois exemplos interessantes:

Podemos verificar que algumas quantidades possuem boa correlação com o preço do imóvel (ex: número de banheiros, quando diferente de zero), apesar de, em alguns casos, a relação para determinados valores não ser óbvia, possivelmente pela interação com outra variável ou pelo tamanho da amostra. Por exemplo, o número de quartos acima do nível do solo, em média, é correlacionado com o preço do imóvel quando de dois a quatro quartos, mas tal correlação não se mantém para o próximo quarto. Porém, o número de amostras para 1 ou 5 quartos é muito menor em relação às de 2 a 4 quartos. Algo semelhante ocorre para cômodos totais acima do nível do solo: temos uma relação quase linear em média, até chegarmos a 12 ou mais cômodos, quando também o número de amostras é bem menor. Para garagens, o mesmo, até a terceira garagem, mas uma quarta "reduz" o preço do imóvel (mas neste caso quase não há observações). Outro fato interessante é que ter duas cozinhas não parece ser um bom negócio...