Esse código apresenta uma resolução para o desafio de regressão proposto durante o curso de Data Science e Machine Learning da Tera. O conjunto de dados descreve a venda de propriedades residenciais individuais de uma cidade americana, entre 2006 e 2010. São 2.930 observações e um npumero considerável de features (23 nominais, 23 ordinais, 14 discretas e 20 contínuas). O dataset tem, ao todo, 80 variáveis explicativas para a determinação do valor do imóvel.
Esse trabalho é dividido em duas partes: Em um primeiro momento é realizada a análise exploratória dos dados, além das feature engineering para organização e manutenção das variáveis. Depois, são apresentadas leituras realizadas a partir de modelos de Machine Learning, usando algorítmos de regressão e emsembles.
Grandes agradecimentos à equipe da Tera, em especial ao expert Marcus Oliveira, que foi de grande ajuda na solução do problema.
#Core
import numpy as np
import pandas as pd
from typing import List, Tuple
import math
#Plot
import matplotlib.pyplot as plt
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
import seaborn as sns
#Estatística - Modelo de interpretação
from statsmodels.stats.outliers_influence import variance_inflation_factor
import statsmodels.api as sm
import statsmodels.formula.api as smf
#Criação do modelo de Machine Learning
from sklearn.impute import SimpleImputer
from category_encoders.target_encoder import TargetEncoder
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_percentage_error, mean_absolute_error
from sklearn.inspection import permutation_importance
from sklearn.model_selection import train_test_split
def plota_cat(variaveis: list, figsize: tuple, dataframe: pd.DataFrame()) -> plt.figure:
'''Dada uma lista de variáveis categóricas, plota uma figura
com os gráficos de violino de todas as variáveis na lista'''
total_plots = len(variaveis)
i = 0
fig = plt.figure(figsize=figsize)
fig.subplots_adjust(hspace=0.8, wspace=0.3)
for var in variaveis:
ax = fig.add_subplot(total_plots, 2, i+1)
sns.violinplot(x=var, y=var_resp, data=df, ax=ax, color='#435058')
ax.set_title(var)
ax.set_ylabel('')
ax.set_xlabel('')
if dataframe[var].nunique() > 6:
ax.xaxis.set_tick_params(rotation=90)
i += 1
def calc_vif(dataframe: pd.DataFrame()) -> pd.DataFrame():
'''Calcula o VIF para identificar multicolinearidade'''
vif = pd.DataFrame()
vif['variables'] = dataframe.columns
vif['VIF'] = [variance_inflation_factor(dataframe.values, i) for i in range(dataframe.shape[1])]
return(vif)
def plot_vif(vif: pd.DataFrame()) -> None:
'''Apresenta a soma do VIF'''
print('vars: ', list(vif.columns))
print('Variable Inflation Factors:', vif.VIF.sum(), '\n')
def train_test_valid_split(df: pd.DataFrame, features: list, target: str, test_size: float, valid_size: float, random_state: int) -> Tuple[np.array, np.array, np.array, np.array, np.array, np.array]:
'''Cria os folds de treino, teste e validação'''
#Treino e teste
(X_train, X_test, y_train, y_test) = train_test_split(
df[features],
df[target],
test_size=test_size,
random_state=random_state,
)
#Validação
(X_test, X_val, y_test, y_val) = train_test_split(
X_test, y_test, test_size=valid_size, random_state=random_state
)
return X_train, X_test, X_val, y_train, y_test, y_val
def metrics_report(y: pd.Series(), y_pred: pd.Series(), dataset_name='valid') -> pd.DataFrame():
'''Cria um dataframe contendo as
principais métricas do modelo de
Machine Learning criado'''
mean_y = np.mean(y)
mean_pred = np.mean(y_pred)
rmse = round(math.sqrt(mean_squared_error(y, y_pred)), 2)
mae = mean_absolute_error(y, y_pred)
r2 = r2_score(y, y_pred)
mape = mean_absolute_percentage_error(y, y_pred)
report = pd.DataFrame({
'metric': ['RMSE', 'MAE', 'R^2', 'MAPE', 'Avg. target', 'Avg. Prediction'],
dataset_name: [rmse, mae, r2, mape, mean_y, mean_pred]
})
report[dataset_name] = report[dataset_name].round(2)
return report
def random_forest_preprocessing(X_train: pd.DataFrame(), X_val: pd.DataFrame(), X_test: pd.DataFrame(), y_train: pd.Series(), categoricas: list) -> pd.DataFrame():
'''Realiza o pré-processamento das variáveis categóricas utilizadas
no algoritmo de RandomForest. O pré-processamento é realizado através
do método de target Encodding'''
target_encoder = TargetEncoder()
X_train[categoricas] = target_encoder.fit_transform(X_train[categoricas], y_train)
X_val[categoricas] = target_encoder.transform(X_val[categoricas])
X_test[categoricas] = target_encoder.transform(X_test[categoricas])
return X_train, X_val, X_test
#Carregamento da base original
df = pd.read_csv('base.csv', skipinitialspace=True)
#Verificando as informações sobre nulos e tipo de cada coluna no dataframe
df.info(verbose=True)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1460 entries, 0 to 1459
Data columns (total 81 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Id 1460 non-null int64
1 ClasseImovel 1460 non-null int64
2 ClasseZona 1460 non-null object
3 Fachada 1201 non-null float64
4 TamanhoLote 1460 non-null int64
5 Rua 1460 non-null object
6 Beco 91 non-null object
7 FormaProp 1460 non-null object
8 PlanoProp 1460 non-null object
9 Servicos 1460 non-null object
10 ConfigLote 1460 non-null object
11 InclinacaoLote 1460 non-null object
12 Bairro 1460 non-null object
13 Estrada1 1460 non-null object
14 Estrada2 1460 non-null object
15 TipoHabitacao 1460 non-null object
16 EstiloHabitacao 1460 non-null object
17 Qualidade 1460 non-null int64
18 Condicao 1460 non-null int64
19 AnoConstrucao 1460 non-null int64
20 AnoReforma 1460 non-null int64
21 TipoTelhado 1460 non-null object
22 MaterialTelhado 1460 non-null object
23 Exterior1 1460 non-null object
24 Exterior2 1460 non-null object
25 TipoAlvenaria 588 non-null object
26 AreaAlvenaria 1452 non-null float64
27 QualidadeCobertura 1460 non-null object
28 CondicaoExterna 1460 non-null object
29 TipoFundacao 1460 non-null object
30 AlturaPorao 1423 non-null object
31 CondicaoPorao 1423 non-null object
32 ParedePorao 1422 non-null object
33 TipoAcabPorao1 1423 non-null object
34 AreaAcabPorao1 1460 non-null int64
35 TipoAcabPorao2 1422 non-null object
36 AreaAcabPorao2 1460 non-null int64
37 AreaInacabPorao 1460 non-null int64
38 AreaPorao 1460 non-null int64
39 Aquecimento 1460 non-null object
40 QualidadeAquecimento 1460 non-null object
41 ArCentral 1460 non-null object
42 InstalacaoEletrica 1459 non-null object
43 AreaTerreo 1460 non-null int64
44 Area2Andar 1460 non-null int64
45 BaixaQualiAreaAcab 1460 non-null int64
46 AreaConstruida 1460 non-null int64
47 BanheiroPorao 1460 non-null int64
48 LavaboPorao 1460 non-null int64
49 Banheiro 1460 non-null int64
50 Lavabo 1460 non-null int64
51 BedroomAbvGr 1460 non-null int64
52 KitchenAbvGr 1460 non-null int64
53 QualidadeCozinha 1460 non-null object
54 TotalQuartos 1460 non-null int64
55 Funcionalidade 1460 non-null object
56 Lareira 1460 non-null int64
57 QualdiadeLareira 770 non-null object
58 LocalGaragem 1379 non-null object
59 AnoGaragem 1379 non-null float64
60 AcabamentoGaragem 1379 non-null object
61 CarrosGaragem 1460 non-null int64
62 AreaGaragem 1460 non-null int64
63 QualidadeGaragem 1379 non-null object
64 CondicaoGaragem 1379 non-null object
65 EntradaPavimentada 1460 non-null object
66 AreaDeck 1460 non-null int64
67 AreaVarandaAberta 1460 non-null int64
68 AreaVarandaFechada 1460 non-null int64
69 AreaVaranda3Estacoes 1460 non-null int64
70 AreaAlpendre 1460 non-null int64
71 AreaPiscina 1460 non-null int64
72 QualidadePiscina 7 non-null object
73 QualidadeCerca 281 non-null object
74 Outros 54 non-null object
75 ValorOutros 1460 non-null int64
76 MesVenda 1460 non-null int64
77 AnoVenda 1460 non-null int64
78 TipoVenda 1460 non-null object
79 CondicaoVenda 1460 non-null object
80 PrecoVenda 1460 non-null int64
dtypes: float64(3), int64(35), object(43)
memory usage: 924.0+ KB
#Variável resposta
var_resp = 'PrecoVenda'
#Lista das variáveis numéricas
var_num = df.select_dtypes(include=["int", "float"]).columns.to_list()
var_num.remove(var_resp)
var_num.remove('ClasseImovel') #Removida pois é categórica
#Lista das variáveis categóricas
v_cat = df.select_dtypes(exclude=["int", "float"]).columns.to_list()
v_cat.append('ClasseImovel')
#Distinção das variáveis categóricas, a diferenciação entre cardinais e ordinais nesse caso foi realizada para facilitar a visualização dos gráficos
v_cat_ord = ['FormaProp', 'Servicos', 'InclinacaoLote', 'Qualidade', 'Condicao', 'QualidadeCobertura', 'CondicaoExterna', 'AlturaPorao', 'CondicaoPorao', 'ParedePorao', 'TipoAcabPorao1', 'TipoAcabPorao2', 'QualidadeAquecimento', 'QualidadeCozinha', 'QualdiadeLareira', 'AcabamentoGaragem', 'QualidadeGaragem', 'CondicaoGaragem', 'EntradaPavimentada']
v_cat_card = [x for x in v_cat if x not in v_cat_ord]
Para efeito de comparação, são feitos os gráficos de distribuição da variável reposta nas formas original e logarítmica. Como há grande diferença entre as duas bases, uma vez que variável em base logaritmica possui distribuição mais próxima à normal, é a mais correta para o trabalho no modelo de interpretação por regressão linear. A escolha procura diminuir os possíveis erros causados pelos outliers no valor do imóvel.
#Criando uma variável com o log dos preços para verificar diferenças entre a distribuição da variável em forma comum e logarítimica
df['logPrecoVenda'] = np.log(df[var_resp])
#Verificando graficamente a distribuição das variáveis
fig = make_subplots(rows= 2, cols=2, subplot_titles=["Distribuição da variável PrecoVenda", "Distribuição da variável logPrecoVenda", "Area Contruida x Preço de Venda", "Área construída x log Preço de Venda"])\
.add_trace(go.Histogram(x = df[var_resp]), row=1,col=1)\
.add_trace(go.Histogram(x = df['logPrecoVenda']), row=1, col=2)\
.add_trace(go.Scatter(x=df[var_resp], y=df['AreaConstruida'], mode='markers', showlegend=True), row=2, col=1)\
.add_trace(go.Scatter(x=df['logPrecoVenda'], y=df['AreaConstruida'], mode='markers'), row=2, col=2)\
.update_layout(width=1200, height=600)\
.update_yaxes(title_text=('Teste1', 'Teste1', 'Teste1', 'Teste1'), row=(1,1,2,2), col=(1,2,1,2))\
.show()
São criados gráficos violinos de todas as variáveis para entender quais dessas têm diferenciações mais significativas no valor do imóvel. As variáveis com maior impacto no valor podem ser as mais interessantes para utilização no modelo.
plota_cat(variaveis=v_cat_card, figsize=(10,90), dataframe=df)

#Verificando o grau de cardinalidade das variáveis categóricas
df[v_cat].nunique().sort_values(ascending=False)
Bairro 25
Exterior2 16
ClasseImovel 15
Exterior1 15
Estrada1 9
TipoVenda 9
MaterialTelhado 8
EstiloHabitacao 8
Estrada2 8
Funcionalidade 7
TipoAcabPorao1 6
LocalGaragem 6
TipoTelhado 6
CondicaoVenda 6
Aquecimento 6
TipoFundacao 6
TipoAcabPorao2 6
QualdiadeLareira 5
QualidadeGaragem 5
CondicaoGaragem 5
InstalacaoEletrica 5
QualidadeAquecimento 5
ClasseZona 5
ConfigLote 5
TipoHabitacao 5
CondicaoExterna 5
FormaProp 4
Outros 4
QualidadeCerca 4
PlanoProp 4
ParedePorao 4
QualidadeCozinha 4
QualidadeCobertura 4
AlturaPorao 4
CondicaoPorao 4
InclinacaoLote 3
AcabamentoGaragem 3
TipoAlvenaria 3
EntradaPavimentada 3
QualidadePiscina 3
Rua 2
Servicos 2
ArCentral 2
Beco 2
dtype: int64
São removidas variáveis com valores muito altos de inputs nulos. Os dados nulos de outras variáveis são inputados a partir da mediana do conjunto para valores numéricos ou do input mais frequente para valores categóricos
df = df.drop(['QualidadePiscina', 'Outros', 'Beco', 'QualidadeCerca', 'QualdiadeLareira'], axis=1)
nulos_categoricas = []
nulos_numericas = []
for col in df:
if df[col].isna().sum() != 0:
print('{}: {} nulos'.format(col,df[col].isna().sum()))
nulos_numericas.append(col) if df[col].dtype == 'float64' else nulos_categoricas.append(col) #Cria listas com as colunas que tem inputs nulos, é importante para o mascaramento
Fachada: 259 nulos
TipoAlvenaria: 872 nulos
AreaAlvenaria: 8 nulos
AlturaPorao: 37 nulos
CondicaoPorao: 37 nulos
ParedePorao: 38 nulos
TipoAcabPorao1: 37 nulos
TipoAcabPorao2: 38 nulos
InstalacaoEletrica: 1 nulos
LocalGaragem: 81 nulos
AnoGaragem: 81 nulos
AcabamentoGaragem: 81 nulos
QualidadeGaragem: 81 nulos
CondicaoGaragem: 81 nulos
Como alguns modelos de Machine Learning não aceitam colunas com valores nulos, é realizado um mascaramento em todas as colunas que tem valores vazios. O mascaramento é feito com o método “SimpleImputer” e preenche as colunas com os valores medianos (no caso de colunas numéricas) ou com os valores mais frequentes (para o caso de colunas categóricas)
#Criando os recortes
num_com_nulos = df[nulos_numericas]
cat_com_nulos = df[nulos_categoricas]
#Criando os inputers
mascara1 = SimpleImputer(missing_values=np.nan, strategy='median').fit(num_com_nulos)
mascara2 = SimpleImputer(missing_values=np.nan, strategy='most_frequent').fit(cat_com_nulos)
#Substituindo os valores nulos do recorte pelos inputers
df[nulos_numericas] = mascara1.transform(num_com_nulos)
df[nulos_categoricas] = mascara2.transform(cat_com_nulos)
Optei pela remoção das colunas com alta cardinalidade ou sem interpretação interessante apenas para simplificação da leitura dos modelos posteriores. Outras situações podem pedir o agrupamento dessas variáveis ou outro tipo de trabalho.
df = df.drop(['Bairro','Exterior1','Exterior2','ClasseImovel','Funcionalidade', 'Estrada1', 'Estrada2', 'MesVenda', 'ValorOutros'], axis=1)
Nesse notebook trabalho com imóveis vendidos sob condição normal, uma vez que são os mais recorrentes e interessante para a análise. Além disso, imóveis vendidos sob outras condições podem distorcer a análise significativamente.
df = df.query("CondicaoVenda == 'Normal'")
df.drop('CondicaoVenda', axis=1)
| Id | ClasseZona | Fachada | TamanhoLote | Rua | FormaProp | PlanoProp | Servicos | ConfigLote | InclinacaoLote | ... | AreaDeck | AreaVarandaAberta | AreaVarandaFechada | AreaVaranda3Estacoes | AreaAlpendre | AreaPiscina | AnoVenda | TipoVenda | PrecoVenda | logPrecoVenda | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | RL | 65.0 | 8450 | Pave | Reg | Lvl | AllPub | Inside | Gtl | ... | 0 | 61 | 0 | 0 | 0 | 0 | 2008 | WD | 208500 | 12.247694 |
| 1 | 2 | RL | 80.0 | 9600 | Pave | Reg | Lvl | AllPub | FR2 | Gtl | ... | 298 | 0 | 0 | 0 | 0 | 0 | 2007 | WD | 181500 | 12.109011 |
| 2 | 3 | RL | 68.0 | 11250 | Pave | IR1 | Lvl | AllPub | Inside | Gtl | ... | 0 | 42 | 0 | 0 | 0 | 0 | 2008 | WD | 223500 | 12.317167 |
| 4 | 5 | RL | 84.0 | 14260 | Pave | IR1 | Lvl | AllPub | FR2 | Gtl | ... | 192 | 84 | 0 | 0 | 0 | 0 | 2008 | WD | 250000 | 12.429216 |
| 5 | 6 | RL | 85.0 | 14115 | Pave | IR1 | Lvl | AllPub | Inside | Gtl | ... | 40 | 30 | 0 | 320 | 0 | 0 | 2009 | WD | 143000 | 11.870600 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 1455 | 1456 | RL | 62.0 | 7917 | Pave | Reg | Lvl | AllPub | Inside | Gtl | ... | 0 | 40 | 0 | 0 | 0 | 0 | 2007 | WD | 175000 | 12.072541 |
| 1456 | 1457 | RL | 85.0 | 13175 | Pave | Reg | Lvl | AllPub | Inside | Gtl | ... | 349 | 0 | 0 | 0 | 0 | 0 | 2010 | WD | 210000 | 12.254863 |
| 1457 | 1458 | RL | 66.0 | 9042 | Pave | Reg | Lvl | AllPub | Inside | Gtl | ... | 0 | 60 | 0 | 0 | 0 | 0 | 2010 | WD | 266500 | 12.493130 |
| 1458 | 1459 | RL | 68.0 | 9717 | Pave | Reg | Lvl | AllPub | Inside | Gtl | ... | 366 | 0 | 112 | 0 | 0 | 0 | 2010 | WD | 142125 | 11.864462 |
| 1459 | 1460 | RL | 75.0 | 9937 | Pave | Reg | Lvl | AllPub | Inside | Gtl | ... | 736 | 68 | 0 | 0 | 0 | 0 | 2008 | WD | 147500 | 11.901583 |
1198 rows × 67 columns
As variáveis de área estão em pés quadrados, uma notação mais difícil de entender. Para facilitar a visualização dos dados, converti as áreas para m².
area_features = [var for var in var_num if 'area' in var.lower()]
converte_m2 = lambda x: x*0.0929
df[area_features] = df[area_features].apply(converte_m2)
df[['Fachada', 'TamanhoLote']] = df[['Fachada', 'TamanhoLote']].apply(converte_m2)
Criei novas variáveis a partir da manipulação de colunas existentes.
df['Taxa_Ocupacao_Lote'] = df['AreaTerreo']/df['TamanhoLote']
df['Coeficiente_Aproveitamento'] = df['AreaConstruida']/df['TamanhoLote']
df['Ultima_Reforma'] = df['AnoReforma'] - df['AnoConstrucao']
df['Banheiros_por_Quarto'] = df['Banheiro']/df['TotalQuartos']
O desafio proposto pela Tera era o de criar um modelo de regressão linear que explicasse ao menos 85% da variação do preço, considerando um máximos de 6 variáveis.
Primeiro é feito o encoding de variáveis categóricas para a utilização nos modelos de regressão, essas variáveis são utilizadas na fórmula para obtenção da regressão a partir da biblioteca statsmodel. A escolha da statsmodel ocorreu pela completude da informação da regressão.
O modelo responde 85,6% da variação dos preços. Todas as variáveis rejeitam a hipótese nula e são estatísticamente significantes.
Foram testadas algumas combinações para a regressão linear, mas para a melhor legibilidade do código só é apresentado o conjunto final das variáveis escolhidas.
#Realizando enconding de algumas variáveis categóricas que são testadas no modelo de regressão
df['QualidadeCozinha'] = df['QualidadeCozinha'].map({'Po':0, 'Fa':1, 'TA':2, 'Gd':3, 'Ex':4})
df['AlturaPorao'] = df['AlturaPorao'].map({'Po':0, 'Fa':1, 'TA':2, 'Gd':3, 'Ex':4})
df[['QualidadeCozinha', 'AlturaPorao']] = df[['QualidadeCozinha', 'AlturaPorao']].astype(int)
df_dummies = pd.get_dummies(df, columns=['EstiloHabitacao', 'TipoHabitacao'], drop_first=True)
vars_reg = ["AreaConstruida","AreaPorao","Ultima_Reforma","CarrosGaragem","Condicao","Qualidade"]
formula ="logPrecoVenda ~ " + ' + '.join(vars_reg)
model = smf.ols(formula = formula, data = df_dummies).fit()
print(model.summary())
OLS Regression Results
==============================================================================
Dep. Variable: logPrecoVenda R-squared: 0.856
Model: OLS Adj. R-squared: 0.856
Method: Least Squares F-statistic: 1185.
Date: Thu, 31 Aug 2023 Prob (F-statistic): 0.00
Time: 14:21:24 Log-Likelihood: 664.68
No. Observations: 1198 AIC: -1315.
Df Residuals: 1191 BIC: -1280.
Df Model: 6
Covariance Type: nonrobust
==================================================================================
coef std err t P>|t| [0.025 0.975]
----------------------------------------------------------------------------------
Intercept 10.3111 0.030 348.106 0.000 10.253 10.369
AreaConstruida 0.0029 0.000 25.474 0.000 0.003 0.003
AreaPorao 0.0022 0.000 17.272 0.000 0.002 0.002
Ultima_Reforma -0.0022 0.000 -10.681 0.000 -0.003 -0.002
CarrosGaragem 0.0965 0.007 13.167 0.000 0.082 0.111
Condicao 0.0602 0.004 13.998 0.000 0.052 0.069
Qualidade 0.1007 0.004 23.191 0.000 0.092 0.109
==============================================================================
Omnibus: 109.052 Durbin-Watson: 2.029
Prob(Omnibus): 0.000 Jarque-Bera (JB): 221.516
Skew: -0.574 Prob(JB): 7.91e-49
Kurtosis: 4.767 Cond. No. 1.31e+03
==============================================================================
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 1.31e+03. This might indicate that there are
strong multicollinearity or other numerical problems.
A multicolinearidade entre as variáveis é verificada através do heatmap das variáveis escolhidas e do Variance Inflation Factor (VIF) entre as variáveis. É importante notar que o VIF foi de 97, muito acima do admitido em testes de maior rigor estatístico ou artigos, mas aceitável para o trabalho proposto.
#Criando heatmap para verificação da colinearidade
sns.heatmap(df_dummies[["AreaConstruida","AreaPorao","Ultima_Reforma","CarrosGaragem","Condicao","Qualidade"]].corr(), annot=True)
<AxesSubplot: >

#Variance Inflation Factor para verificar multicolinearidade entre as variáveis
vif = calc_vif(df_dummies[vars_reg])
print('vars: ', vars_reg)
print('Variable Inflation Factors:', vif.VIF.sum(), '\n')
vif
vars: ['AreaConstruida', 'AreaPorao', 'Ultima_Reforma', 'CarrosGaragem', 'Condicao', 'Qualidade']
Variable Inflation Factors: 97.83204531842381
| variables | VIF | |
|---|---|---|
| 0 | AreaConstruida | 16.931763 |
| 1 | AreaPorao | 10.528213 |
| 2 | Ultima_Reforma | 2.113803 |
| 3 | CarrosGaragem | 11.510602 |
| 4 | Condicao | 17.279173 |
| 5 | Qualidade | 39.468492 |
A distribuição dos erros é relativamente homoscedástica e os resíduos têm distribuição com alto grau de normalidade
fig, axs = plt.subplots(1, 3, figsize=(20,5), facecolor='white')
sns.regplot(x=df_dummies.AreaConstruida, y=model.resid, ax=axs[0])
sm.qqplot(model.resid, line="s", ax=axs[1])
sns.histplot(model.resid, kde=True, ax=axs[2])
axs[0].set_title('Distribuição dos erros (Checando Heteroscedasticidade)')
axs[1].set_title('Distribuição dos resíduos (QQPlot)')
axs[2].set_title('Distribuição dos resíduos')
fig.suptitle('Condições de aceitação da regressão linear')
fig.text(s='(VIF: {})'.format(vif.VIF.sum()), y=0.95, x=0.61)
Text(0.61, 0.95, '(VIF: 97.83204531842381)')

round((np.exp(model.params[1:]) - 1)*100, 2)
AreaConstruida 0.29
AreaPorao 0.22
Ultima_Reforma -0.22
CarrosGaragem 10.13
Condicao 6.21
Qualidade 10.59
dtype: float64
O aumento de 1m² na área construída aumenta o preço da residência em 0.29%
O aumento de 1m² na área do porão aumenta o preço da residência em 0.22%
O aumento de 1 ano desde a última reforma diminui o preço da residência em 0.22%
O aumento de 1 carro no espaço da garagem aumenta o preço da residência em 10.13%
A melhoria de 1 grau de condição aumenta o preço da residência em 6.21%
A melhoria de 1 grau de qualidade aumenta o preço da residência em 10.59%
O algoritmo de regressão linear dá boa explicabilidade sobre o modelo, mas o RandomForest pode alcançar melhores índices na predição, com um erro quadrático menor. Por ser um algoritmo não linear, o RF lida muito melhor com desbalancemantos nas variáveis numéricas e outros problemas que travam o modelo de regressão linear. Apesar da ótima performance do modelo de RF, sua interpretação é difícil, isso justifica o uso anterior do algoritmo de regressão linear para entendimento da constituição do valor do imóvel.
#Obtendo novamente as variáveis numéricas e categóricas do modelo, dessa vez a partir da base mais trabalhada
var_resp = 'PrecoVenda'
#Lista das variáveis numéricas
var_num = df.select_dtypes(include=["int", "float"]).columns.to_list()
var_num.remove(var_resp)
var_num.remove('logPrecoVenda')
#Lista das variáveis categóricas
var_cat = df.select_dtypes(exclude=["int", "float"]).columns.to_list()
features = var_cat + var_num
(X_train, X_test, X_val,
y_train, y_test, y_val) = train_test_valid_split(df, features=features, target=var_resp, test_size=0.3, valid_size=0.5, random_state=31)
X_train, X_val, X_test = random_forest_preprocessing(X_train, X_val, X_test, y_train, categoricas=var_cat)
O baseline do RandomForest retornou um R² de 0.88, com um erro quadrático médio de pouco mais de 22 mil em valor médio da habitação de pouco mais de 175 mil
rf_baseline = RandomForestRegressor(random_state=42)
# Treino
rf_baseline.fit(X_train, y_train)
y_val_baseline_pred = rf_baseline.predict(X_val)
baseline_report = metrics_report(y_val, y_val_baseline_pred, 'validation')
baseline_report
| metric | validation | |
|---|---|---|
| 0 | RMSE | 22948.80 |
| 1 | MAE | 16376.22 |
| 2 | R^2 | 0.87 |
| 3 | MAPE | 0.10 |
| 4 | Avg. target | 175268.56 |
| 5 | Avg. Prediction | 176033.85 |
O baseline do RandomForest já apresenta um resultado relativamente satisfatório, uma vez que o RMSE é tolerável e o valor predito é bem próximo do valor médio. Além disso, o R² encontrado no modelo foi bem alto. Uma última etapa desse trabalho envolve a hiperparametrização a partir dos testes dos hiperparâmetros. Para cada parâmetro são testadas n possibilidades, sendo admitida a possibilidade na qual o valor de RMSE é menor
#Testando o número de estimadores (as "árvores" da floresta)
rmse_final = 0
n_estimators_final = 0
for n_estimator in [5, 10, 20, 40, 80, 120, 160, 200, 240]:
print('número de estimadores: {}'.format(n_estimator))
rf_model = RandomForestRegressor(n_estimators=n_estimator,random_state=42)
rf_model.fit(X_train, y_train)
y_val_model_pred = rf_model.predict(X_val)
rmse = math.sqrt(mean_squared_error(y_val, y_val_model_pred))
if (rmse_final == 0) or (rmse < rmse_final):
rmse_final = rmse
n_estimators_final = n_estimator
print('rmse: {}'.format(rmse_final) + ' melhor número de features: {}'.format(n_estimator))
número de estimadores: 5
número de estimadores: 10
número de estimadores: 20
número de estimadores: 40
número de estimadores: 80
número de estimadores: 120
número de estimadores: 160
número de estimadores: 200
número de estimadores: 240
rmse: 22761.48480355246 melhor número de features: 240
#Testando o número de features maximo (o tanto de variáveis do modelo que as árvores considerarão)
rmse_final = 0
n_features_final = 0
for max_feature in np.arange(0.1, 1, 0.05):
print('número de features contabilizadas: {}'.format(max_feature))
rf_model = RandomForestRegressor(max_features=max_feature,random_state=42)
rf_model.fit(X_train, y_train)
y_val_model_pred = rf_model.predict(X_val)
rmse = math.sqrt(mean_squared_error(y_val, y_val_model_pred))
if (rmse_final == 0) or (rmse < rmse_final):
rmse_final = rmse
n_features_final = max_feature
print('rmse: {}'.format(rmse_final) + ' melhor número de features: {}'.format(n_features_final))
número de features contabilizadas: 0.1
número de features contabilizadas: 0.15000000000000002
número de features contabilizadas: 0.20000000000000004
número de features contabilizadas: 0.25000000000000006
número de features contabilizadas: 0.30000000000000004
número de features contabilizadas: 0.3500000000000001
número de features contabilizadas: 0.40000000000000013
número de features contabilizadas: 0.45000000000000007
número de features contabilizadas: 0.5000000000000001
número de features contabilizadas: 0.5500000000000002
número de features contabilizadas: 0.6000000000000002
número de features contabilizadas: 0.6500000000000001
número de features contabilizadas: 0.7000000000000002
número de features contabilizadas: 0.7500000000000002
número de features contabilizadas: 0.8000000000000002
número de features contabilizadas: 0.8500000000000002
número de features contabilizadas: 0.9000000000000002
número de features contabilizadas: 0.9500000000000003
rmse: 19892.96496276272 melhor número de features: 0.25000000000000006
A partir dos testes realizados anteriormente foram escolhidos 240 estimadores e leitura de 1/3 das colunas da base
#Teste
rf_model_final = RandomForestRegressor(n_estimators=240, max_features=0.3)
rf_model_final.fit(X_train, y_train)
y_test_model_final = rf_model_final.predict(X_test)
baseline_report = metrics_report(y_test, y_test_model_final, 'teste')
baseline_report
| metric | teste | |
|---|---|---|
| 0 | RMSE | 19624.17 |
| 1 | MAE | 13352.12 |
| 2 | R^2 | 0.90 |
| 3 | MAPE | 0.09 |
| 4 | Avg. target | 169394.04 |
| 5 | Avg. Prediction | 170048.73 |
#Validação
rf_model_final = RandomForestRegressor(n_estimators=240, max_features=0.3)
rf_model_final.fit(X_train, y_train)
y_val_model_final = rf_model_final.predict(X_val)
baseline_report = metrics_report(y_val, y_val_model_final, 'validation')
baseline_report
| metric | validation | |
|---|---|---|
| 0 | RMSE | 20306.62 |
| 1 | MAE | 14459.51 |
| 2 | R^2 | 0.90 |
| 3 | MAPE | 0.09 |
| 4 | Avg. target | 175268.56 |
| 5 | Avg. Prediction | 175168.98 |
Foram testados dois modelos de Machine Learning para a predição e interpretação dos valores dos imóveis, baseados nos algorítmos de Regressão Linear e RandomForest. O Algoritmo de regressão linear ajudou na interpretação da constituição do valor das habitações com bom índice de explicabilidade: a partir da verificação do modelo é possível identificar que além do aumento da área, a melhoria da qualidade na habitação, a criação de vagas de garagem e a reforma recente ajudam a aumentar o valor do imóvel. A inserção de mais espaço na garagem e a melhoria na qualidade do edifício têm impacto mais rápido no valor do imóvel. O modelo de RandomForest considerou a variável absoluta do valor do imóvel e chegou a um bom índice de predição após hiperparametrização, com erro quadrático de cerca de 20 mil, e valor do imóvel em 170 mil. O erro quadrático ainda pode ser refinado a partir de uma hiperparametrização mais densa.