#!/usr/bin/env python # coding: utf-8 #

RELATÓRIO DOS EXPERIMENTOS E RESULTADOS DO TCC DE GUILHERME PASSOS

#

INTRODUÇÃO

# # >
Este trabalho tem como objetivo realizar uma análise de dados dos preços de apartamentos da cidade de Belo Horizonte e desenvolver um modelo computacional preditivo capaz de prever tais preços. Para tal, obteve-se um conjunto de dados composto por aproximadamente 57 mil anúncios de imóveis entre os meses agosto e outubro de 2018.
# # >
Alguns kernels do problema House Prices: Advanced Regression Techniques do portal Kaggle foram utilizados como referência dos experimentos mostrados a seguir. Em especial os kernels:
# # >
    #
  1. Stacked Regressions : Top 4% on LeaderBoard de Serigne.
  2. #
  3. Comprehensive data exploration with Python de Pedro Marcelino.
# # # >
A análise de dados realizada neste trabalho se restringiu apenas à preços de apartamentos devido à grande quantidade de anúncios e pela dificuldade em se criar um modelo genérico para diferentes tipos de imóveis. Os resultados finais obtidos foram satisfatoriamente validados e demonstraram que a modelagem do problema realizada neste trabalho pode ser útil ao setor imobiliário da cidade.
# # >
Este relatório está dividido nas seguintes etapas: # >
# #

1. OBTENÇÃO DOS DADOS

# # >
Os dados foram obtidos utilizando o script contido neste repositório.
# # >
Inicialmente, foram realizadas seis extrações no portal da Rede NetImóveis nos dias 05/08/2018, 12/08/2018, 21/08/2018, 25/08/2018 e 29/08/2018 a fim de se obter dados para o compor conjunto de treino dos experimentos. Posteriormente, foi realizada uma extração no dia 12/10/2018 para ser utilizada como conjunto de teste dos experimentos. Os arquvivos csv contendo os dados podem ser visualizados neste link.
# # >
  • Dados obtidos em 05/08/2018:
  • # In[1]: # Dados obtidos em 05/08/2018: import pandas as pd data05_08= pd.read_csv('https://raw.githubusercontent.com/gpass0s/Graduation_Project/master/okData05-08-18.csv', sep=',',header=0, encoding='utf-8') data05_08[['Preço','Bairro','Regiao','Area','Qtde_Quartos','Qtde_Banheiros','Qtde_Suites', 'Vagas_Garagem']].sample(20) # In[2]: # Quantidade de dados obtidos na extração do dia 05/08/2018: len(data05_08) # >
  • Dados obtidos em 12/08/2018:
  • # In[3]: # Quantidade de dados obtidos na extração do dia 12/08/2018: data12_08= pd.read_csv('https://raw.githubusercontent.com/gpass0s/Graduation_Project/master/okData12-08-18.csv', sep=',',encoding='utf-8') len(data12_08) # >
  • Dados obtidos em 21/08/2018:
  • # In[4]: data21_08= pd.read_csv('https://raw.githubusercontent.com/gpass0s/Graduation_Project/master/okData21-08-18.csv', sep=',',encoding='utf-8') # Quantidade de dados obtidos na extração do dia 12/08/2018: len(data21_08) # >
  • Dados obtidos em 25/08/2018:
  • # In[5]: data25_08= pd.read_csv('https://raw.githubusercontent.com/gpass0s/Graduation_Project/master/okData25-08-18.csv', sep=',',encoding='utf-8') # Quantidade de dados obtidos na extração do dia 25/08/2018: len(data25_08) # >
  • Dados obtidos em 29/08/2018:
  • # In[6]: data29_08= pd.read_csv('https://raw.githubusercontent.com/gpass0s/Graduation_Project/master/okData29-08-18.csv', sep=',',encoding='utf-8') # Quantidade de dados obtidos na extração do dia 29/08/2018: len(data29_08) # >
  • Dados obtidos em 21/08/2018:
  • # In[7]: data21_09= pd.read_csv('https://raw.githubusercontent.com/gpass0s/Graduation_Project/master/okData21-09-18.csv', sep=',',encoding='utf-8') # Quantidade de dados obtidos na extração do dia 29/08/2018: len(data21_09) # >
  • Dados obtidos em 12/10/2018:
  • # In[8]: data12_10= pd.read_csv('https://raw.githubusercontent.com/gpass0s/Graduation_Project/master/okData12-10-18.csv', sep=',',encoding='utf-8') # Quantidade de dados obtidos na extração do dia 29/08/2018: len(data12_10) #

    2. ORGANIZAÇÃO DOS DADOS

    # # >

    2.1 EXCLUSÃO DOS DADOS QUE POSSUEM INFORMAÇÕES INCONSISTENTES

    # # >
    Por meio de uma análise empírica, foi possivel observar que as coordenadas geográficas com latitudes superiores a -19.788 e inferiores a -20.023 bem como aquelas com longitudes superiores a -43.878 e inferiores a -44.055 não estão dentro dos limites da cidade de Belo Horizonte.
    # # In[9]: # Dados da extração do dia 05/08 que possuem informações de localização inconsistente data0508_Err = data05_08.loc[(data05_08['Latitude'] > -19.788) | (data05_08['Latitude'] < -20.023) | (data05_08['Longitude'] > -43.878) | (data05_08['Longitude'] < -44.055)] # Dados inconsistentes: data0508_Err.loc[:,['ImovelID','Latitude','Longitude','Bairro','Regiao']] # >
    Pode se verificar no google maps que as coordenadas mostradas acima não correspodem ao bairro e região que se referem.
    # # >
    Além de inconsistência nas informações de localização, alguns dados possuem outras informações incoerentes como quantidade de quartos e banheiros igual a zero ou área inferior a 10m². A função "Drop_MisInformation" a seguir é responsável por excluir todas essas inconsistências.
    # In[10]: # Dados da extração do dia 05/08 que possuem quantidade de quartos igual a 0: dataErr = data12_08.loc[(data12_08['Qtde_Quartos'] ==0 )] # Dados inconsistentes: dataErr.loc[:,['ImovelID','Preço','Area','Qtde_Quartos','Qtde_Banheiros','Bairro']].sample(5) # In[11]: # A função Drop_Mislocation exclui os dados que possui informações de localização # fora do território de Belo Horizonte def Drop_MisInformation(df): df = df.drop(df.loc[(df['Latitude'] > -19.788) | (df['Latitude'] < -20.023) | (df['Longitude'] > -43.878) | (df['Longitude'] < -44.055)].index) df = df.drop(df.loc[(df['Qtde_Quartos']==0)].index) df = df.drop(df.loc[(df['Qtde_Banheiros']==0)].index) df = df.drop(df.loc[(df['Area']<10)].index) return df # Excluindo dados inconsistentes da extração de 05/08/2018: data05_08 = Drop_MisInformation(data05_08) # Excluindo dados inconsistentes da extração de 12/08/2018: data12_08 = Drop_MisInformation(data12_08) # Excluindo dados inconsistentes da extração de 21/08/2018: data21_08 = Drop_MisInformation(data21_08) # Excluindo dados inconsistentes da extração de 25/08/2018: data25_08 = Drop_MisInformation(data25_08) # Excluindo dados inconsistentes da extração de 29/08/2018: data29_08 = Drop_MisInformation(data29_08) # Excluindo dados inconsistentes da extração de 29/08/2018: data21_09 = Drop_MisInformation(data21_09) # Excluindo dados inconsistentes da extração de 29/08/2018: data12_10 = Drop_MisInformation(data12_10) # >

    2.2 PREPARAÇÃO DO CONJUNTO DE TREINAMENTO

    # # >
    A seguir irá se agrupar os dados obtidos em todas as extrações.
    # In[12]: # Quantidade de dados obtidos na consolidadação das extrações do dia 05/08/2018 e 12/08/2018: base_consolidada = pd.concat([data05_08, data12_08]).groupby('ImovelID', as_index=False, sort=False).first() len(base_consolidada) # In[13]: # Quantidade de dados novos nas extrações que foram consolidadas: len(base_consolidada) - len(data05_08) # In[14]: # Quantidade de dados duplicados nas extrações que foram consolidadas: len(pd.concat([data05_08, data12_08])) - len(base_consolidada) # >
    Os resultados anteriores mostram que a extração do dia 12/08/2018 acrescenta 9191 dados novos à extração do dia 05/08/2018. Além disso, são produzidos 6220 valores repetidos na junção dessas extrações. Analisando mais minuciosamente os dados duplicados, por meio da função "CheckingDuplicateValues", observa-se que 130 desses 6220 (ou 2% dos duplicados) apresentaram variação no preço; o que parece aceitável. A variação percentual média dos preços que sofreram alteração entre as extrações foi de -1.93%.
    # # In[15]: import numpy as np # Verifica dentre os dados duplicados, quais ocorreram variação no preço de uma extração para outra def Checking_Duplicate_Values(df1,df2): result= pd.merge(df1[['ImovelID','Preço']],df2[['ImovelID','Preço']], on='ImovelID', how='inner') return pd.DataFrame({"ImovelID":result.loc[(result.iloc[:,1]!=result.iloc[:,2])]['ImovelID'], "Preco_1":result.loc[(result.iloc[:,1]!=result.iloc[:,2])]['Preço_x'], "Preco_2":result.loc[(result.iloc[:,1]!=result.iloc[:,2])]['Preço_y']}) # Dados duplicados que apresentaram variação no preço entre as extrações do dia 05/08 e 12/08/2018: df_duplicates_01 = Checking_Duplicate_Values(data05_08, data12_08) df_duplicates_01.sample(5) # In[16]: # Quantidade de dados duplicados que sofreram variação no preço entre uma extração e outra: len(df_duplicates_01) # In[17]: # Percentual de dados duplicados que apresentaram variação de preço entre uma extração e outra: per_duplicates = len(df_duplicates_01)*100 / (len(pd.concat([data05_08, data12_08])) - len(base_consolidada)) str(round(per_duplicates,2)) + "%" # In[18]: # Variação percentual média dos preços duplicados: variação_01 = ((df_duplicates_01['Preco_2'] - df_duplicates_01['Preco_1'])/df_duplicates_01['Preco_1'])*100 str(round(variação_01.mean(),2)) + "%" # >
    Ao se prosseguir com a junção das extrações da maneira que foi realizado anteriormente, observou-se que a variação média desses preço foi de -1.9%. Prosseguindo com a junção das extrações.
    # In[19]: # Quantidade de dados não duplicados na consolidação das extrações dos dias 05/08, 12/08 e 21/08: base_consolidada_01 = pd.concat([base_consolidada, data21_08]).groupby('ImovelID', as_index=False, sort=False).first() len(base_consolidada_01) # In[20]: # Quantidade de dados não duplicados na consolidação das extrações dos dias 05/08, 12/08, 21/08 e 25/08: base_consolidada_02 = pd.concat([base_consolidada_01, data25_08]).groupby('ImovelID', as_index=False, sort=False).first() len(base_consolidada_02) # In[21]: # Quantidade de dados não duplicados na consolidação das extrações dos dias 05/08, 12/08, 21/08 e 29/08: base_consolidada_03 = pd.concat([base_consolidada_02, data29_08]).groupby('ImovelID', as_index=False, sort=False).first() len(base_consolidada_03) # In[22]: # Quantidade de dados não duplicados na consolidação das extrações dos dias 05/08, 12/08, 21/08, 29/08 # e 21/09: base_consolidada_04 = pd.concat([base_consolidada_03, data21_09]).groupby('ImovelID', as_index=False, sort=False).first() len(base_consolidada_04) # In[23]: # Quantidade de dados não duplicados na consolidação das extrações dos dias 05/08, 12/08, 21/08, 29/08, # 21/09 e 12/10: base_consolidada_05 = pd.concat([base_consolidada_04, data12_10]).groupby('ImovelID', as_index=False, sort=False).first() len(base_consolidada_05) # >
    Ir-se-á urilziar os 21234 dados da "baseconsolidada05" como o conjunto de treino dos experimentos que serão apresentados nos tópicos seguintes.
    # In[24]: # Conjunto de treino: columns = ['ImovelID','Preço','Area','Latitude','Longitude','Qtde_Quartos', 'Qtde_Banheiros','Qtde_Suites','Vagas_Garagem','Valor_IPTU', 'Valor_Cond','Bairro','Regiao','DataExtração','Rua','Numero'] train = base_consolidada_05[columns] #

    3. ANÁLISE E TRANSFORMAÇÃO DOS DADOS

    # # >

    3.1 OUTLIERS

    # # >
    Em estatística, outlier, valor aberrante ou valor atípico, é uma observação que apresenta um grande afastamento das demais da série (que está "fora" dela), ou que é inconsistente. A existência de outliers implica, tipicamente, em prejuízos a interpretação dos resultados dos testes estatísticos aplicados às amostras. Existem vários métodos de identificação de outliers. Examinar-se-á o conjunto de treino a fim de se identificar tais ocorrências.
    # In[25]: # Otbtendo algumas biblotecas importantes para esta etapa do relatório get_ipython().run_line_magic('matplotlib', 'inline') import matplotlib.pyplot as plt # Matlab-style plotting from matplotlib import pyplot from matplotlib import colors as mcolors import seaborn as sns color = sns.color_palette() sns.set_style('darkgrid') import random from random import shuffle from scipy import stats from scipy.stats import norm, skew #for some statistics from scipy.special import boxcox1p import warnings def ignore_warn(*args, **kwargs): pass warnings.warn = ignore_warn # ignora avisos das bilotecas sklearn e seaborn pd.set_option('display.float_format', lambda x: '{:.4f}'.format(x)) # Limita a exibição de floats em 4 casa decimais # >
    A função "PlotPriceVsArea" definida abaixo traça o gráfico da distribuição de preços versus a área do imóvel.
    # In[26]: # Define os melhores limites para os eixos do gráfico def Define_Axis_Limits(limit,up): axis_limit = 1; while((axis_limit * 10) <= limit): axis_limit *= 10; if up: axis_limit *= 10 return axis_limit # Traça o gráfico da distribuição de preços versus a área do imóvel. def PlotPriceVsArea (df): # Obtem a lista de regiões da base de dados regions = df['Regiao'].loc[~df['Regiao'].isnull()].unique() data = [] # Separa os dados por região for region in regions: data.append((df['Area'].loc[df['Regiao']==region] , df['Preço'].loc[df['Regiao']==region])) # Define as cores da legenda colors = ('gray','green','brown','blue','black','purple','olive','gold','red') # Formata a figura de exibição do gráfico fig = plt.figure(figsize=(8*1.5, 6*1.5)) ax = fig.add_subplot(1, 1, 1, facecolor="1.0") # Sepera os dados por região e cor for data, color, region in zip(data, colors, regions): x, y = data ax.scatter(x,y, alpha=0.8, c=color, edgecolors='none', s=30, label=region) # Configura e plota o gráfico plt.title('Distribuição dos Preços de Apartamentos por Área e Região') plt.xlim(0,800,10) y_lower = Define_Axis_Limits(df['Preço'].min(),False) y_upper = Define_Axis_Limits(df['Preço'].max(),True) plt.ylim(y_lower,y_upper) plt.semilogy() plt.xlabel('Área(m²)', fontsize=12) plt.ylabel('Preço em R$', fontsize=12) plt.legend(loc='lower right') plt.grid(True) plt.show() # Visualizando os dados: PlotPriceVsArea(train) # >
    O gráfico acima permite identificar alguns outliers de maneira direta, como o ponto roxo na parte inferior direita e o ponto preço na parte inferior esquerda, outros porém merecem uma análise mais detalhada. Todovia a remoção de todos eles pode afetar negativamente o desempenho dos modelos, uma vez que pode haver outros outliers no conjunto de teste também. Assim, ao invés de removê-los, ir-se-á tentar criar modelos robustos o suficiente a esses dados.
    # In[27]: # Outlier mostrado na parte inferior esquerda do gráfico: train.loc[(train['Preço']<10000)][['Preço','Area','Regiao','Bairro','Qtde_Quartos','Qtde_Banheiros', 'Qtde_Suites','Vagas_Garagem','Valor_IPTU']] # In[28]: # Outliers mostrado na parte inferiror direita do gráfico: train.loc[(train['Regiao'] == 'noroeste') & (train['Area'] > 400)][['Preço','Area','Regiao','Bairro','Qtde_Quartos','Qtde_Banheiros', 'Qtde_Suites','Vagas_Garagem','Valor_IPTU']] # In[29]: # Outros outliers train.loc[(train['Area']>520)][['Preço','Area','Regiao','Bairro','Qtde_Quartos','Qtde_Banheiros', 'Qtde_Suites','Vagas_Garagem','Valor_IPTU']] # In[30]: # Exclusão do apartamento que possui área igual a 1450m²: train = train.drop(train.loc[(train['Area']>1000)].index) # Exclusão do outlier mostrado na parter inferior esquerda do gráfico: train = train.drop(train.loc[(train['Preço']<10000)].index) # Exclusão do outlier mostrado inferior direita do gráfico: train = train.drop(train.loc[(train['Regiao'] == 'noroeste') & (train['Area'] > 400)].index) columns = ['ImovelID','Preço','Area','Latitude','Longitude','Qtde_Quartos', 'Qtde_Banheiros','Qtde_Suites','Vagas_Garagem','Valor_IPTU', 'Valor_Cond','Bairro','Regiao','DataExtração','Rua','Numero'] train.to_csv("/home/gpassos/Documents/tcc/data/train.csv", encoding="utf-8", sep=",") # >

    3.4 CORRELAÇÃO ENTRE AS VARIÁVEIS DO PROBLEMA

    # # >
    Ir-se-á observar agora a correlação entre as variáveis independentes do problema com a variável de interesse, o preço. A partir deste ponto, ir-se-á desconsiderar as informações de região, bairro e endereço; pois acredita-se com muita convicção que a coordenas geográficas por si só serão capazes de modelar a localidade dos imóveis.
    # In[31]: # Características que serão usadas para se modelar os preços dos apartamentos train.iloc[:,1:11].sample(3) # In[32]: #Mapa de coorelação entre as variáveis do problema: corrmat = train.iloc[:,1:12].corr() plt.subplots(figsize=(12,9)) sns.heatmap(corrmat, vmax=0.9,annot=True, square=True) # >
    O gráfico acima indica uma baixa coorelação das variáveis Valor_IPTU, Valor_Cond, Latitude e Longitude com a variável de interesse, Preço. As duas primeiras (Valor_IPTU e Valor_Cond) podem ser tranquilamente excluídas da análise; de maneira contrária, sabe-se que a latitude e a longitude carregam informações relevantes sobre os preços e não podem ser desconsideradas. Então por que essas variáveis não apresentam correlação signiticativa como se espera?
    # # >
    Uma possível explicação para a baixa correlação das duas primeiras variáveis citadas é o fato de vários imóveis serem isentos de IPTU, não terem taxa de condomínio, ou até mesmo não possuírem essas informações cadastradas no banco de dados da Rede NetImóveis. A resposta a seguir revela que 7.634 ou aproximadamente 36% dos dados está em uma dessas situações mencionadas anteriormente, um valor bem expressivo.
    # # In[33]: # Quantidade de imóveis que são isentos de IPTU ou não possuem taxa de condomínio len(train.loc[(train['Valor_IPTU']==0) | (train['Valor_Cond']==0)]) # >
    A razão da baixa correlação entre os preços dos imóveis e as coordenadas geográficas dos mesmos se deve ao fato da latitude e longitude não possuirem nenhuma propiedade ordinária, como a idade de uma pessoa. Por exemplo, os dois pontos (-19.9385153, -43.9558656) e (-19.9285010, -43.9558656) não possuem nenhum significado ou ordem de grandeza. Eles são apenas dois pontos no espaço como qualquer outro. Desde modo, a latitude e a longitude não podem ser aplicadas diretamente nos modelos de predição.
    # # >
    Uma alternativa é utilizar um algoritmo de clausterização para criar zonas (cluster) de dados que estão geograficamente próximos. Ir-se-á utilizar o algoritmo K-means.
    # >
    CLAUSTERIZAÇÃO DAS POSIÇÕES GEOGRÁFICAS DOS DADOS
    # # >
    Ao se analisar a distribuição geográfica dos dados do conjunto de treino, observa-se várias zonas de clausterização e também alguns outliers (dados com informações geográficas não consistente com a região classificada). Para se determinar o número ideal de clusters na distribuição dos dados, pode-se utilizar o método do cotovelo (Elbow Method) que consiste em encontrar visualmente o ponto de maior inflexão da curva números de clusters vs. erro do proceso de clausterização.
    # In[34]: # Plota a distribuição geográfica dos dados def Remove_Bright_Colors(colors): bright_colors = ['white','snow','w','whitesmoke', 'blanchedalmond','ghostwhite', 'azure','aliceblue','mintcream','ivory','floralwhite','oldlace', 'cornsilk','papayawhip','lightgoldenrodyellow','antiquewhite', 'lavenderblush','linen','lemonchiffon','honeydew','mistyrose', 'lightgray','lightgrey','peachpuff'] for color in bright_colors: colors.remove(color) return colors def PlotLocations(df,clusters = False, title = None): # Sepera todas as regiões regions = df['Regiao'].loc[~df['Regiao'].isnull()].unique() # Lista contendo os dados seperados por região data = [] # Loop para separar os dados por região for region in regions: data.append((df['Latitude'].loc[df['Regiao']==region] , df['Longitude'].loc[df['Regiao']==region])) # Cores que aperação no gráfico colors = ('gray','green','brown','blue','black','purple','olive','gold','red') # Cores que aperação no gráfico de clusters if clusters: colors = list(dict(mcolors.BASE_COLORS, **mcolors.CSS4_COLORS).keys()) colors = Remove_Bright_Colors(colors) random.shuffle(colors) colors = colors # Dimensona o tamanho da figura fig = plt.figure(figsize=(8*1.5, 6*1.5)) ax = fig.add_subplot(1, 1, 1, facecolor="1.0") # Posiciona o pontos for data, color, region in zip(data, colors, regions): x, y = data ax.scatter(x,y, alpha=0.8, c=color, edgecolors='none', s=30, label=region) # Configura o gráfico plt.title(title) #---------------------------- x_lower = df['Latitude'].min() x_upper = df['Latitude'].max() y_lower = df['Longitude'].min() y_upper = df['Longitude'].max() x_resolution = abs(x_upper-x_lower)/len(df) y_resolution = abs(y_upper-y_lower)/len(df) x_lower = x_lower - 100*x_resolution x_upper = x_upper + 100*x_resolution y_lower = y_lower - 100*y_resolution y_upper = y_upper + 100*y_resolution plt.xlim(x_lower,x_upper,x_resolution) plt.ylim(y_upper,y_lower,y_resolution) #------------------------------------- plt.xlabel('Latitude', fontsize=12) plt.ylabel('Longitude', fontsize=12) if not clusters: plt.legend(loc='lower left') plt.grid(True) plt.show() # Visualizando os dados: PlotLocations(train,clusters=False,title ='Distribuição Geográfica dos Dados por Região') # >
    Contudo, sabe-se que dividir a distribuição de preços dos imóveis no território da cidade nas noves regiões mostradas é uma aproximação bastante grosseira. Vamos, portanto, definir uma quantidade de centroides tal que permita cada cluster ter no mínimo 0,5% dos dados (82 amostras) do conjunto de treino. Após uma série de tentativas, verificou-se que 60 centroides é a quantidade limiar da regra estabelecida. Acima desse valor, os dados ficam melhores subdivididos no espaço mas algumas regiões apresentam uma quantidade ínfima de amostras.
    # In[35]: import sklearn from sklearn.cluster import KMeans # Encontra a distribuição de clusters que atende a regra estabelicida (cada cluster deve ter no mínimo 82 dados) while True: # Define o número de centroides clusters = KMeans(50) # Determina os clusters clus = clusters.fit_predict(train[['Latitude', 'Longitude']]) centros = np.unique(clus) clus = list(clus) # Verifica se a regra foi atendida data = [] for centro in centros: if clus.count(centro) < 107: data.append((clus.count(centro))) if len(data)==0: break # Define o número de centroides clusters = KMeans(60) # Determina os clusters clus = clusters.fit_predict(train[['Latitude', 'Longitude']]) centros = np.unique(clus) clus = list(clus) # Organiza os clusters e os dados clusters_df = pd.DataFrame({"ImovelID":train['ImovelID'],"Latitude":train['Latitude'], "Longitude":train['Longitude'],"Regiao":clus}) # Exibe o gráfico: PlotLocations(clusters_df,clusters=True, title = 'Distribuição Geográfica dos Dados por Zonas Clausterizadas') # >
    O resultado mostrado na imagem anterior indica que a aproximação da distribuição geográfica dos preços por 50 zonas distintas é razoável. Ao se computar a média e o desvio padrão de cada umas regiões mostradas na Figura 22, # cria-se mais duas variáveis, Mean_PreçoZone e Std_PreçoZone, no conjunto de dados. A # matriz de correlação mostrada a seguir revela que essas duas novas variáveis possuem uma # relação significativa com os preços e ajudam agregar valor quantitativo à localidade dos imóveis.
    # In[36]: def transform_prices (train, clusters): # Determina o cluster de cada amostra clus = clusters.predict(train[['Latitude', 'Longitude']]) # Associa os clusters às colunas ImovelID e Preço zonas = pd.DataFrame({"ImovelID":train['ImovelID'],"Zona":clus,"Preço":train['Preço']}) # Cria um Dataframe Zonas_Preço que associa a média e o desvio padrão de cada cluster centros = np.unique(clus) zonas_preço = pd.DataFrame({"Zona":centros}) zonas_preço['Mean_PreçoZone'] = 0 zonas_preço['Std_PreçoZone'] = 0 # Computa a média e o desvio padrão dos preços de cada zona for centro in centros: preço_metro = zonas.loc[zonas['Zona']==centro]['Preço'] zonas_preço.iloc[zonas_preço.loc[zonas_preço['Zona']==centro].index,1] = preço_metro.mean() zonas_preço.iloc[zonas_preço.loc[zonas_preço['Zona']==centro].index,2] = preço_metro.std() # Insere no dataframe train a média e o desvio padrão de cada zona train = pd.merge(train, pd.merge(zonas,zonas_preço,on="Zona",how='left')[["ImovelID",'Zona', 'Mean_PreçoZone','Std_PreçoZone']], on="ImovelID", how= 'inner') columns = ['ImovelID','Preço','Area','Qtde_Quartos','Qtde_Banheiros','Qtde_Suites', 'Vagas_Garagem','Valor_Cond','Valor_IPTU','Latitude','Longitude','Mean_PreçoZone', 'Std_PreçoZone'] train = train[columns] return train, zonas_preço train, zonas_preço = transform_prices (train.iloc[:,0:11], clusters) # Mapa de coorelação entre as variáveis do problema: corrmat = train.iloc[:,1:13].corr() plt.subplots(figsize=(12,9)) sns.heatmap(corrmat, vmax=0.9, annot=True, square=True) # >
    Finalmente, o mapa de correlação mostrado acima indica que a transformação de variáveis realizada enriqueceu bastante as informações do problema. Portanto, as características que serão utilizadas para modelar os preços dos apartamentos são as seguintes: # #
    #
          #
        1. Área;
        2. #
        3. Quantidade de suítes;
        4. #
        5. Quantidade de vagas de garagem;
        6. #
        7. Quantidade de banheiros;
        8. #
        9. Quantidade de quartos;
        10. #
        11. Valor do condomínio;
        12. #
        13. Valor do IPTU;
        14. #
        15. Latitude;
        16. #
        17. Longitude;
        18. #
        19. Preço médio dos imóveis da região;
        20. #
        21. Desvio padrão dos preços dos imóveis da região.
    # >

    3.3 VARIÁVEIS COM DISTRIBUIÇÕES ASSIMÉTRICAS

    # # >
    NORMALIZANDO AS DISTRIBUIÇÕES
    # # >
    A distribuição normal, ou distribuição gaussiana, é a distribuição preferida dos estatísticos e dos cientistas de dados por serem capazes de modelar uma vasta quantidade de fenômenos naturais e por tornarem as operações matemáticas de análise relativamente simples, se comparadas à de outras distribuições. Ademais, o teorema central do limite 9 afirma que quando se aumenta a quantidade de amostras de um evento, a distribuição amostral da sua média aproxima-se cada vez mais de uma distribuição normal. Como foi visto no Capítulo 3, os algoritmos de aprendizado de máquina são fundamentalmente baseados em modelos matemáticos; portanto, # esses algoritmos tendem a performar melhor quando os dados estão distribuídos conforme uma # distribuição normal.
    # In[37]: # Gerando o SeaBorn gráfico que mostra a distribuição dos preços def IsDataNormDistribued(df): # Gera o Seaborn gráfico que mostra a distrubição dos dados a4_dims = (8*1.5, 6*1.5) fig, ax = pyplot.subplots(figsize=a4_dims) fig.set_size_inches(8*1.5, 6*1.5) sns.distplot(df['Preço'] , fit=norm, ax=ax); # Calcula a distribuição normal que mais se aproxima dos daos (mu, sigma) = norm.fit(df['Preço']) print( '\n mu = {:.2f} and sigma = {:.2f}\n'.format(mu, sigma)) # Plota o gráfico de distribuição plt.legend(['Normal dist. ($\mu=$ {:.2f} and $\sigma=$ {:.2f} )'.format(mu, sigma)], loc='best') plt.ylabel('Frequency') plt.title('Distribuição dos Preços') # Plota o gráfico de probabilidades fig = plt.figure(figsize=(8*1.5, 6*1.5)) y_lower = Define_Axis_Limits(df['Preço'].min(),False) Define_Axis_Limits(90,False) res = stats.probplot(df['Preço'], plot=plt) plt.title('Gráfico de Probabilidades') plt.show() IsDataNormDistribued(train) # >
    O gráficos acima sugerem que a distribuição dos preços não segue uma distribuição normal, algo que não é desejável. Realizar-se-á uma transformação logarítma (log(1+x)) nos preços dos apartamentos na esperança de tornar a distribuição desses dados mais próxima de uma distribuição "normal".
    # In[38]: # Utilziando a função log1p da bibloteca tnumpy # que aplica a transformação log(1+x) em todos elementos de uma coluna train['Preço'] = np.log1p(train['Preço']) IsDataNormDistribued(train) # >
    Pode se observar nos gráficos acima que a distribuição dos preços está bem mais próxima de uma distribuição normal.
    # # # # >
    Uma outra análise a ser realizada é com relação a assimetria das distribuições das variáveis independentes. A assimetria (em inglês: skewness) mede o quanto a cauda lado esquerdo de uma distribuição é maior do que a cauda do lado direito ou vice-versa. Uma assimetria positiva indica que a cauda do lado direito é maior que a do lado esquerdo e uma assimetria negativa, obviamente, indica o contrário.
    # # >
    A bibloteca pandas oferece uma função chamada "skew", viés em português, que determina o quão assimétrica a distribuição de um conjunto de dados está em relação à sua distribuição normal. Neste link há mais detalhes sobre o que essa função faz. Ir-se-á, portanto, utilizar a função skew para identificar variáveis no conjunto de treino que apresentam uma assimetria elevada.
    # # # In[39]: # Quantidade de variáveis do conjunto de treino com distribuição assimétrica numeric_feats = train.iloc[:,2:14].dtypes[train.iloc[:,2:14].dtypes != "object"].index # Verificando a assimetria das variaveis do problemaa skewed_feats = train.iloc[:,2:14][numeric_feats].apply(lambda x: skew(x.dropna())).sort_values(ascending=False) skewness_train = pd.DataFrame({'Assimetria' :skewed_feats}) # Variaveis com distribuição assimétrica no conjunto de treino skewness_train.head(11) # >
    TRANSFORMAÇÃO BOX COX NAS VARIÁVEIS ALTAMENTE ENVIESADAS
    # # >
    Este artigo explica resumidamente no que se consiste a transformação Box Cox. Vamos utilizar a função "boxcox1p" que computa a tranformação Box Cox de 1 + x. Oberseve que ao definir lambda=0 é o mesmo que usar a tranformação logaratma feita na variável preço anteriormente.
    # In[40]: from scipy.special import boxcox1p # Seleciona as variáveis que possuem coefieciente de assimétria maior do 1 skewness = skewness_train[abs(skewness_train.Assimetria)>1] # Realiza a tranformação Box Cox utilizando lambda = 0.5 skewed_features = skewness.index lam = 0.5 for feat in skewed_features: #all_data[feat] += 1 train[feat] = boxcox1p(train[feat], lam) numeric_feats = train.iloc[:,2:14].dtypes[train.iloc[:,2:14].dtypes != "object"].index # Verificando a assimetria das variaveis do problema skewed_feats = train.iloc[:,2:14][numeric_feats].apply(lambda x: skew(x.dropna())).sort_values(ascending=False) skewness_train = pd.DataFrame({'Assimetria' :skewed_feats}) # Variaveis com distribuição assimétrica no conjunto de treino skewness_train.head(11) train.to_csv("/home/gpassos/Documents/tcc/data/train_transformed.csv", encoding="utf-8", sep=",") #

    4. MODELAGEM E PREDIÇÃO DOS PREÇOS DO CONJUNTO DE TESTE

    # # >
    Nesta etapa, avaliar-se-á a perfomance dos principais modelos de regressão da biblioteca scikit-learn na predição dos preços do conjunto de teste.
    # In[41]: # Obtendo algumas biblotecas importantes para essa etapa. from sklearn.linear_model import ElasticNet, Lasso, BayesianRidge, LassoLarsIC from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor from sklearn.kernel_ridge import KernelRidge from sklearn.svm import SVR from sklearn.pipeline import make_pipeline from sklearn.preprocessing import RobustScaler from sklearn.base import BaseEstimator, TransformerMixin, RegressorMixin, clone from sklearn.model_selection import KFold, cross_val_score, train_test_split from sklearn.metrics import mean_squared_error from sklearn.neural_network import MLPRegressor from sklearn import metrics from sklearn.linear_model import LinearRegression from sklearn.base import clone from sklearn.model_selection import KFold import time import xgboost as xgb import lightgbm as lgb # >

    4.1 DEFININDO UMA ESTRATÉGIA DE VALIDAÇÃO CRUZADA

    # # >
    O principal objetivo de se utilizar algoritmos de aprendizado de máquina em problemas da vida real é obter um modelo computacional que seja capaz de produzir respostas eficazes para situações novas de um mesmo problema. Em outras palavras, a relevância dos resultados dos modelos está atrelada ao desempenho dos mesmos na predição das amostras do conjunto de teste, pois não faz sentido prático predizer os resultados das amostras de treino. Assim, faz-se necessário definir uma estratégia de validação da predição dos preços das 21.232 amostras.
    # # >
    Uma maneira de se resolver tal problema é usar a técnica denominada k-fold Cross Validation (JAMES et al., 2014), popularmente conhecida entre os cientistas de dados. Basicamente, essa técnica consiste em dividir as amostras aleatoriamente em k grupos, ou dobras (em inglês: folds), de tamanho aproximado. Então, o primeiro grupo k1 de amostras é usado como conjunto de validação e os k − 1 grupos restantes são usados como conjunto de treino. O erro quadrático médio (EQM) (em inglês: mean squared error ou MSE) do experimento é computado e armazenado. Esse processo é repetido k vezes e, em cada vez, um grupo diferente de amostras ki é tratado como conjunto de validação e os restantes ki − 1 grupos são usados como conjunto de treino. Esse procedimento produz então k estimativas de erros de testes, MSE1, MSE2 , . . . ,MSEk . Desse modo, a estimativa de erro do procedimento k-fold Cross Validation é definida pela média dos MSE k erros computados em cada experimento
    # In[42]: # Função para associação da média e do desvio padrão dos preços ao dados de teste def transform_test_set(test,clusters,zonas_preço): clus = clusters.predict(test[['Latitude', 'Longitude']]) # Concatena a média e o desvio padrão do preços no conjunto de treino zonas = pd.DataFrame({"ImovelID":test['ImovelID'],"Zona":clus}) test = pd.merge(test, pd.merge(zonas,zonas_preço,on="Zona",how='left')[["ImovelID",'Mean_PreçoZone', 'Std_PreçoZone','Zona']], on="ImovelID", how= 'inner') columns = ['ImovelID','Preço','Area','Qtde_Quartos','Qtde_Banheiros','Qtde_Suites', 'Vagas_Garagem','Valor_Cond','Valor_IPTU','Latitude','Longitude','Mean_PreçoZone', 'Std_PreçoZone'] test=test[columns] return test # Função de validação cruzada def cross_validation_score(estimator,X,clusters,n_folds=5): # Shuffling o conjunto de dados X = X.sample(frac=1).reset_index(drop=True) kf = KFold(n_splits = n_folds, shuffle = True, random_state = 2) # Coluna auxiliares X['Real_PreçoT'] = 0 X['Pred_Preço'] = 0 # Listas auciliares MAPE = [] less_10per = [] less_20per = [] time_lst = [] # Filtros de colunas column = X.columns.get_loc('Pred_Preço') columns = [x for x in X.columns if x not in ['Preço','ImovelID','Real_PreçoT','Pred_Preço']] columns.append('Mean_PreçoZone') columns.append('Std_PreçoZone') for train_index, test_index in kf.split(X): # inicia a contagem de tempo start = time.time() # Calcula a média e o desvio padrão dos preços dos dados treino train_set, zonas_preço = transform_prices(X.iloc[train_index],clusters) # Treina o modelo estimator.fit(train_set.loc[:,columns],train_set.Preço) # Associa a média e os preços dos dados de treino ao conjunto de teste test_set = transform_test_set(X.iloc[test_index],clusters,zonas_preço) # Prediz o preços dos dados de treino X.iloc[test_index,column]=np.expm1(estimator.predict(test_set.loc[:,columns])) # Encerra contagem end = time.time() APE = abs(np.expm1(X.loc[test_index].Preço)- X.loc[test_index].Pred_Preço)/np.expm1(X.loc[test_index].Preço) MAPE.append(APE.mean()) less_10per.append(len(APE.loc[APE<0.1])/len(APE)) less_20per.append(len(APE.loc[APE<0.2])/len(APE)) time_lst.append(end-start) # Computa o Erro Absoluto Percentual X['APE'] = abs(np.expm1(X.Preço)- X.Pred_Preço)*100/np.expm1(X.Preço) # Preços reais X['Real_PreçoT'] = np.expm1(X['Preço']) # Calcula o Erro Médio Absoluto Perceutal MAPE = np.array(MAPE).mean()*100 less_10per = np.array(less_10per).mean()*100 less_20per = np.array(less_20per).mean()*100 time_ = np.array(time_lst).mean() m, s = divmod(time_, 60) columns = ['MAPE','Dentro de ± 10%','Dentro de ± 20%', 'Tempo Médio Pred.'] # Resposta return X,pd.DataFrame({'MAPE':'{:.3f}'.format(MAPE) + "%", 'Dentro de ± 10%':'{:.3f}'.format(less_10per) + "%", 'Dentro de ± 20%':'{:.3f}'.format(less_20per) + "%", 'Tempo Médio Pred.':"%02dmin %02dsec" %(m, s)}, index=[0])[columns] # >

    4.1 PRINCIPAIS MODELOS BASE DA BIBLIOTECA DO SCKIT-LEARN

    # # In[43]: from sklearn.linear_model import LinearRegression linear_regression = make_pipeline(RobustScaler(), LinearRegression(fit_intercept=True, normalize=False, copy_X=True, n_jobs=None)) # >
    LASSO Regression
    # # >
    Este modelo é bem sensível a outliers. Ir-se-á torná-lo mais robusto utilizando o método Robustscaler().
    # In[44]: lasso = make_pipeline(RobustScaler(), Lasso(alpha =0.01, random_state=1)) # >
    Elastic Net Regression
    # # >
    Igualmente ao anterior, esse modelo é também bem sensível a outliers. Ir-se-á torná-lo mais robusto utilizando o método Robustscaler().
    # In[45]: ENet = make_pipeline(RobustScaler(), ElasticNet(alpha=0.001, l1_ratio=.9, random_state=3)) # >
    Support Vector Regression:
    # # In[46]: KRR = KernelRidge(alpha=0.01, kernel='polynomial', degree=2, coef0=2.5) # >
    Multi-layer Perceptron
    # # >
    Definiu-se o número de camadas escondidas igual a 1000 e a função a unidade linear retificada como função de ativação dos neurônios. # Ademais, definiu-se o método de otimização estocástico gradient-based para ajustes dos pesos w dos neurônios.
    # In[47]: mlp = MLPRegressor(hidden_layer_sizes=(1000, ), activation='relu', solver='adam', alpha=0.0001, batch_size='auto', learning_rate='constant', learning_rate_init=0.001, power_t=0.5, max_iter=200, shuffle=True, random_state=None, tol=0.0001, verbose=False, warm_start=False, momentum=0.9, nesterovs_momentum=True, early_stopping=False, validation_fraction=0.1, beta_1=0.9, beta_2=0.999, epsilon=1e-08, n_iter_no_change=10) # >
    Gradient Boosting:
    # In[48]: GBoost = GradientBoostingRegressor(n_estimators=3000, learning_rate=0.1, max_depth=6, max_features='sqrt', min_samples_leaf=15, min_samples_split=10, loss='huber', random_state =5) # >
    Xtreme GBoost:
    # In[49]: model_xgb = xgb.XGBRegressor(colsample_bytree=0.4603, gamma=0.0468, learning_rate=0.1, max_depth=6, min_child_weight=1.7817, n_estimators=3000, reg_alpha=0.4640, reg_lambda=0.8571, subsample=0.5213, silent=1, random_state =7, nthread = -1) # >
    LightGBM:
    # In[50]: model_lgb = lgb.LGBMRegressor(objective='regression',num_leaves=6, learning_rate=0.05, n_estimators=3000, max_bin = 55, bagging_fraction = 0.8, bagging_freq = 5, feature_fraction = 0.2319, feature_fraction_seed=9, bagging_seed=9, min_data_in_leaf =6, min_sum_hessian_in_leaf = 11) # >

    4.2 RESULTADOS

    # # >
    Abaixo são apresentados os resultados dos experimentos.
    # In[51]: # Resultado LASSO df_lreg,df_lreg_resume = cross_validation_score(linear_regression,train.iloc[:,0:11],clusters) df_lreg_resume # In[52]: # Resultado Regressão Linear df_lasso,df_lasso_resume = cross_validation_score(lasso,train.iloc[:,0:11],clusters) df_lasso_resume # In[53]: # Resultado GBoost df_gboost,df_gboost_resume =cross_validation_score(GBoost,train.iloc[:,0:11],clusters) df_gboost_resume # In[54]: # Resultado XGBoost df_xgb,df_xgb_resume = cross_validation_score(model_xgb,train.iloc[:,0:11],clusters) df_xgb_resume # In[55]: # Resultado Light Gboost df_lgb,df_lgb_resume = cross_validation_score(model_lgb,train.iloc[:,0:11],clusters) df_lgb_resume # In[56]: # Resultado da Rede de Percepetrons de Multicamadas from sklearn import preprocessing columns = ['Area','Qtde_Quartos','Qtde_Banheiros','Qtde_Suites', 'Vagas_Garagem','Valor_Cond','Valor_IPTU','Latitude','Longitude'] x = train.iloc[:,2:11].values #returns a numpy array min_max_scaler = preprocessing.MinMaxScaler() x_scaled = min_max_scaler.fit_transform(x) df = pd.DataFrame(x_scaled,columns=columns) df = pd.concat([train.iloc[:,0:2],df],axis=1) df_mlp,df_mlp_resume = cross_validation_score(mlp,df,clusters) df_mlp_resume