Machine Learning para sua caça plana. Parte 1

Você já procurou um apartamento? Deseja adicionar algum aprendizado de máquina e tornar um processo mais interessante?


Apartamentos em Ecaterimburgo

Hoje, consideraremos a aplicação do Machine Learning para encontrar um plano ideal.


1. Introdução


Antes de tudo, quero esclarecer esse momento e explicar o que significa "um apartamento ideal". É um apartamento com um conjunto de características diferentes como "área", "distrito", "número de varandas" e assim por diante. E para esses recursos do apartamento, esperamos um preço específico. Parece uma função que usa vários parâmetros e retorna um número. Ou talvez uma caixa preta que ofereça alguma mágica.


Mas ... há um grande "mas", às vezes você pode enfrentar um apartamento muito caro por causa de um conjunto de razões, como uma boa posição geográfica. Além disso, existem distritos de maior prestígio no centro de uma cidade e distritos fora da cidade. Ou ... às vezes as pessoas querem vender seus apartamentos porque se mudam para outro ponto da Terra. Em outras palavras, existem muitos fatores que podem afetar o preço. Parece familiar?


Pequeno passo de lado


Antes de continuar, deixe-me fazer uma pequena digressão lírica.
Morei em Yekaterinburg (a cidade entre a Europa e a Ásia, uma das cidades que realizou o Campeonato Mundial de Futebol em 2018) por 5 anos.


Eu estava apaixonado por essas selvas de concreto. E eu odiava aquela cidade no inverno e nos transportes públicos. É uma cidade em crescimento e todos os meses há milhares e milhares de apartamentos a serem vendidos.


Sim, é uma cidade superlotada e poluída. Ao mesmo tempo - é um bom lugar para analisar um mercado imobiliário. Recebi muitos anúncios de apartamentos da Internet. E usarei essas informações para uma extensão adicional.


Além disso, tentei visualizar diferentes ofertas no mapa de Ecaterimburgo. Sim, é a imagem do habracut, feita no Kepler.gl


imagem


Existem mais de 2 mil apartamentos de 1 quarto que foram vendidos em julho de 2019 em Ecaterimburgo. Eles tinham um preço diferente, de menos de um milhão a quase 14 milhões de rublos.


Esses pontos se referem à sua posição geográfica. A cor dos pontos no mapa representa o preço, quanto menor o preço próximo à cor azul, maior o preço próximo ao vermelho. Você pode considerar isso como uma analogia com cores frias e quentes, a cor mais quente é o maior preço.
Por favor, lembre-se daquele momento, quanto mais vermelha é a cor, maior é o valor de alguma coisa. A mesma idéia funciona para o azul, mas na direção do preço mais baixo.


Agora você está tendo uma visão geral da imagem e o tempo para analisar está chegando.


Objetivo


O que eu queria quando morei em Ecaterimburgo? Procurei um apartamento suficientemente bom ou, se falarmos em termos de ML - queria construir um modelo que me recomendasse a compra.


Por um lado, se um flat for muito caro, o modelo deve recomendar a espera pela diminuição do preço, mostrando o preço esperado para esse flat.
Por outro lado - se um preço é bom o suficiente, de acordo com o estado do mercado - talvez eu deva considerar essa oferta.


Claro, não há nada ideal e eu estava pronto para aceitar um erro nos cálculos. Normalmente, para esse tipo de tarefa, use erro médio de previsão e eu estava pronto para um erro de 10%. Por exemplo, se você tiver 2 a 3 milhões de rublos russos, poderá ignorar erros entre 200 e 300 mil, poderá pagar. Como me pareceu.


Preparar


Como mencionei antes, havia muitos apartamentos, vamos examiná-los de perto.
importar pandas como pd


df = pd.read_csv('flats.csv') df.shape 

imagem


2310 apartamentos por um mês, podemos extrair algo útil disso. Que tal uma visão geral geral dos dados?


 df.describe() 

imagem
Não há algo extraordinário - longitude, latitude, preço de um apartamento (o rótulo " custo ") e assim por diante. Sim, naquele momento usei " custo " em vez de " preço ", espero que não leve a mal-entendidos, considere-os como iguais.


Limpeza


Todo registro tem o mesmo significado? Alguns deles são apartamentos representados como um cubículo, você pode trabalhar lá, mas não gostaria de morar lá. São pequenos quartos apertados, não um apartamento de verdade. Vamos removê-los.


 df = df[df.total_area >= 20] 

O preço previsto do apartamento vem das questões mais antigas da economia e de áreas afins. Não havia nada relacionado ao termo "ML" e as pessoas tentavam adivinhar o preço com base em metros quadrados / pés.
Então, olhamos para essas colunas / etiquetas e tentamos obter a distribuição delas.


 numerical_fields = ['total_area','cost'] for col in numerical_fields: mask = ~np.isnan(df[col]) sns.distplot(df[col][mask], color="r",label=col) plot.show() 

imagem


Bem ... não há nada de especial, parece uma distribuição normal. Talvez nós precisamos ir mais fundo?


 sns.pairplot(df[numerical_fields]) 

imagem


Opa ... Algo errado está aí. Limpe os valores discrepantes nesses campos e tente analisar nossos dados novamente.


 #Remove outliers df = df[abs(df.total_area - df.total_area.mean()) <= (3 * df.total_area.std())] df = df[abs(df.cost - df.cost.mean()) <= (3 * df.cost.std())] #Redraw our data sns.pairplot(df[numerical_fields]) 

imagem


Os outliers desapareceram e agora parece melhor.


Transformação


O rótulo "ano", que é apontado para um ano de construção, deve ser transformado em algo mais informativo. Que seja a era da construção, em outras palavras, como uma casa específica é antiga.


 df['age'] = 2019 -df['year'] 

Vamos dar uma olhada no resultado.


 df.head() 

imagem


Existem todos os tipos de dados, categóricos, valores nanométricos, descrição de texto e algumas informações geográficas (longitude e latitude). Deixemos de lado os últimos, porque nesse estágio eles são inúteis. Voltaremos a eles mais tarde.


 df.drop(columns=["lon","lat","description"],inplace=True) 

Dados categóricos


Geralmente, para dados categóricos, as pessoas usam diferentes tipos de codificação ou coisas como o CatBoost, que oferecem uma oportunidade de trabalhar com eles, como nas variáveis ​​numéricas.
Mas, poderíamos usar algo mais lógico e mais intuitivo? Agora é hora de tornar nossos dados mais compreensíveis sem perder o significado deles.


Distritos


Bem, existem mais de vinte distritos possíveis, podemos adicionar mais de 20 variáveis ​​adicionais em nosso modelo? Claro que poderíamos, mas ... deveríamos? Somos pessoas e poderíamos comparar as coisas, não é?
Primeiro de tudo - nem todo distrito é equivalente a outro. No centro da cidade, os preços de um metro quadrado são mais altos, mais longe do centro da cidade - ela passa a diminuir. Parece lógico? Podemos usar isso?
Sim, definitivamente poderíamos combinar qualquer distrito com um coeficiente específico e, quanto mais distrito, mais baratos os apartamentos.


Depois de corresponder a cidade e usar outro mapa de serviço da web (ArcGIS Online), mudou e tem uma visão semelhante
imagem


Eu usei a mesma ideia que para a visualização de flat. O distrito mais "prestigioso" e "caro" colorido em vermelho e menos azul. A temperatura da cor, você se lembra disso?
Além disso, devemos fazer alguma manipulação sobre nosso quadro de dados.


 district_map = {'alpha': 2, 'beta': 4, ... 'delta':3, ... 'epsilon': 1} df.district = df.district.str.lower() df.replace({"district": district_map}, inplace=True) 

A mesma abordagem será usada para descrever a qualidade interna do apartamento. Às vezes, precisa de algum reparo, às vezes plana é muito bem e pronta para viver. E em outros casos, você deve gastar dinheiro adicional para melhorar a aparência (trocar torneiras, pintar paredes). Também pode haver coeficientes de uso.


 repair = {'A': 1, 'B': 0.6, 'C': 0.7, 'D': 0.8} df.repair.fillna('D', inplace=True) df.replace({"repair": repair}, inplace=True) 

A propósito, sobre paredes. Claro, também influencia no preço do apartamento. O material moderno é melhor que o antigo, o tijolo é melhor que o concreto. Paredes de madeira é um momento bastante controverso, talvez seja uma boa escolha para o campo, mas não tão bom para a vida urbana.


Usamos a mesma abordagem de antes, além de sugerir linhas que não sabemos de nada. Sim, às vezes as pessoas não fornecem todas as informações sobre seu apartamento. Além disso, com base na história, podemos tentar adivinhar o material das paredes. Em um período específico de tempo (por exemplo, o período de liderança de Khrushchev) - conhecemos o material típico para a construção.


 walls_map = {'brick': 1.0, ... 'concrete': 0.8, 'block': 0.8, ... 'monolith': 0.9, 'wood': 0.4} mask = df[df['walls'].isna()][df.year >= 2010].index df.loc[mask, 'walls'] = 'monolith' mask = df[df['walls'].isna()][df.year >= 2000].index df.loc[mask, 'walls'] = 'concrete' mask = df[df['walls'].isna()][df.year >= 1990].index df.loc[mask, 'walls'] = 'block' mask = df[df['walls'].isna()].index df.loc[mask, 'walls'] = 'block' df.replace({"walls": walls_map}, inplace=True) df.drop(columns=['year'],inplace=True) 

Além disso, há informações sobre a varanda. Na minha humilde opinião - a varanda é uma coisa realmente útil, então eu não pude evitar pensar nisso.
Infelizmente, existem alguns valores nulos. Se o autor de um anúncio tivesse verificado informações sobre ele, teríamos informações mais realistas.
Bem, se não houver informações, isso significa "não há varanda".


 df.balcony.fillna(0,inplace=True) 

Depois disso, lançamos colunas com informações sobre o ano de construção (temos uma boa alternativa para isso). Além disso, removemos a coluna com informações sobre o tipo de construção, pois possui muitos valores de NaN e não encontrei nenhuma oportunidade de preencher essas lacunas. E eliminamos todas as linhas com NaN que temos.


 df.drop(columns=['type_house'],inplace=True) df = df.astype(np.float64) df.dropna(inplace=True) 

Verificando


Então ... usamos uma abordagem não padrão e substituímos os valores categóricos pela representação numérica. E agora terminamos com uma transformação de nossos dados.
Uma parte dos dados foi descartada, mas, em geral, é um conjunto de dados bastante bom. Vamos olhar para a correlação entre variáveis ​​independentes.


 def show_correlation(df): sns.set(style="whitegrid") corr = df.corr() * 100 # Select upper triangle of correlation matrix mask = np.zeros_like(corr, dtype=np.bool) mask[np.triu_indices_from(mask)] = True # Set up the matplotlib figure f, ax = plt.subplots(figsize=(15, 11)) # Generate a custom diverging colormap cmap = sns.diverging_palette(220, 10) # Draw the heatmap with the mask and correct aspect ratio sns.heatmap(corr, mask=mask, cmap=cmap, center=0, linewidths=1, cbar_kws={"shrink": .7}, annot=True, fmt=".2f") plot.show() # df[columns] = scale(df[columns]) return df df1 = show_correlation(df.drop(columns=['cost'])) 

imagem


Erm ... ficou muito interessante.
Correlação positiva
Área total - varandas . Porque não Se o nosso apartamento for grande, haverá uma varanda.
Correlação negativa
Área total - idade . Quanto mais novo é plano, maior é a área para morar. Parece lógico, os novos são mais espaçosos e planos do que os mais antigos.
Idade - varanda . Quanto mais velho for plano, menos varandas terá. Parece uma correlação através de outra variável. Talvez seja um triângulo Idade-Varanda-Área em que uma variável exerce influência implícita em outra. Coloque isso em espera por um tempo.
Idade - distrito. O apartamento mais antigo é a grande probabilidade que será colocada nos distritos de maior prestígio. Poderia estar relacionado a preços mais altos perto do centro?


Além disso, pudemos ver a correlação com a variável dependente


 plt.figure(figsize=(6,6)) corr = df.corr()*100.0 sns.heatmap(corr[['cost']], cmap= sns.diverging_palette(220, 10), center=0, linewidths=1, cbar_kws={"shrink": .7}, annot=True, fmt=".2f") 

imagem


Aqui vamos nós ...


A correlação muito forte entre a área de apartamento e preço. Se você quer ter um lugar maior para morar, isso exigirá mais dinheiro.
Existe uma correlação negativa entre os pares " idade / custo " e " distrito / custo ". Um apartamento em uma casa mais nova, menos acessível que a antiga. E no campo os apartamentos são mais baratos.
De qualquer forma, parece claro e compreensível, então decidi seguir em frente.


Modelo


Para tarefas relacionadas ao preço do plano de previsão normalmente, use regressão linear. De acordo com uma correlação significativa de um estágio anterior, poderíamos tentar usá-lo também. É um cavalo de trabalho que é adequado para muitas tarefas.
Prepare nossos dados para as próximas ações


 from sklearn.model_selection import train_test_split y = df.cost X = df.drop(columns=['cost']) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) 

Além disso, criamos algumas funções simples para previsão e avaliação do resultado. Vamos fazer a nossa primeira tentativa de prever o preço!


 def predict(X, y_test, model): y = model.predict(X) score = round((r2_score(y_test, y) * 100), 2) print(f'Score on {model.__class__.__name__} is {score}') return score def train_model(X, y, regressor): model = regressor.fit(X, y) return model 

 from sklearn.linear_model import LinearRegression regressor = LinearRegression() model = train_model(X_train, y_train, regressor) predict(X_test, y_test, model) 

imagem


Bem ... 76,67% de precisão. É um número grande ou não? De acordo com o meu ponto de vista, não é ruim. Além disso, é um bom ponto de partida. Obviamente, não é o ideal e existe um potencial de melhoria.


Ao mesmo tempo - tentamos prever apenas uma parte dos dados. Que tal aplicar a mesma estratégia para outros dados? Sim, hora da validação cruzada.


 def do_cross_validation(X, y, model): from sklearn.model_selection import KFold, cross_val_score regressor_name = model.__class__.__name__ fold = KFold(n_splits=10, shuffle=True, random_state=0) scores_on_this_split = cross_val_score(estimator=model, X=X, y=y, cv=fold, scoring='r2') scores_on_this_split = np.round(scores_on_this_split * 100, 2) mean_accuracy = scores_on_this_split.mean() print(f'Crossvaladaion accuracy on {model.__class__.__name__} is {mean_accuracy}') return mean_accuracy do_cross_validation(X, y, model) 

imagem


O resultado da validação cruzadaAgora, levamos outro resultado. 73 é menor que 76. Mas também é um bom candidato até o momento em que teremos um melhor. Além disso, significa que uma regressão linear funciona bastante estável em nosso conjunto de dados.


E agora é a hora do último passo.


Veremos a melhor característica da regressão linear - interpretabilidade .
Essa família de modelos, ao contrário dos modelos mais complexos, tem maior capacidade de compreensão. Existem apenas alguns números com coeficientes e você pode colocar seus números na equação, fazer algumas contas simples e obter um resultado.


Vamos tentar interpretar nosso modelo


 def estimate_model(model): sns.set(style="white", context="talk") f, ax = plot.subplots(1, 1, figsize=(10, 10), sharex=True) sns.barplot(x=model.coef_, y=X.columns, palette="vlag", ax=ax) for i, v in enumerate(model.coef_.astype(int)): ax.text(v + 3, i + .25, str(v), color='black') ax.set_title(f"Coefficients") estimate_model(regressor) 

Os coeficientes do nosso modelo


A imagem parece bastante lógica. Varanda / Paredes / Área / Reparação dão uma contribuição positiva a um preço fixo.
Quanto mais plano, maior a contribuição negativa . Também aplicável para a idade. O apartamento mais antigo é o preço mais baixo será.


Então, foi uma jornada fascinante.
Começamos do zero, usamos a abordagem atípica para transformação de dados com base no ponto de vista humano (números em vez de variáveis ​​fictícias), variáveis ​​verificadas e sua relação entre si. Depois disso, construímos nosso modelo simples, usado a validação cruzada para testar seus. E como a cereja no topo do bolo - observe as características internas do modelo, o que nos dá confiança sobre o nosso caminho.


Mas! Não é o fim de nossa jornada, mas apenas uma pausa. Vamos tentar mudar nosso modelo no futuro e talvez (apenas talvez) aumente a precisão da previsão.


Obrigado pela leitura!


A segunda parte está

PS Os dados de origem e o notebook Ipython estão localizados

Source: https://habr.com/ru/post/pt468053/


All Articles