Apprentissage automatique pour votre chasse à plat. Partie 1

Avez-vous déjà cherché un appartement? Souhaitez-vous ajouter un apprentissage automatique et rendre un processus plus intéressant?


Appartements à Iekaterinbourg

Aujourd'hui, nous envisagerons d'appliquer le Machine Learning pour trouver un appartement optimal.


Présentation


Tout d'abord, je veux clarifier ce moment et expliquer ce que signifie "un appartement optimal". C'est un appartement avec un ensemble de caractéristiques différentes comme "zone", "quartier", "nombre de balcons" et ainsi de suite. Et pour ces caractéristiques de l'appartement, nous attendons un prix spécifique. Ressemble à une fonction qui prend plusieurs paramètres et renvoie un nombre. Ou peut-être une boîte noire qui apporte de la magie.


Mais ... il y a un grand "mais", parfois vous pouvez faire face à un appartement qui est trop cher pour une série de raisons comme une bonne position géographique. En outre, il existe des quartiers plus prestigieux au centre d'une ville et des quartiers à l'extérieur de la ville. Ou ... parfois, les gens veulent vendre leurs appartements parce qu'ils se déplacent vers un autre point de la Terre. En d'autres termes, de nombreux facteurs peuvent affecter le prix. Cela vous semble familier?


Petit pas de côté


Avant de continuer, permettez-moi de faire une petite digression lyrique.
J'ai vécu à Ekaterinbourg (la ville entre l'Europe et l'Asie, l'une des villes qui avaient organisé le Championnat du monde de football en 2018) pendant 5 ans.


J'étais amoureux de ces jungles en béton. Et je détestais cette ville pour l'hiver et les transports en commun. C'est une ville en pleine croissance et chaque mois il y a des milliers et des milliers d'appartements à vendre.


Oui, c'est une ville surpeuplée et polluée. Dans le même temps - c'est un bon endroit pour analyser un marché immobilier. J'ai reçu beaucoup de publicités pour des appartements, sur Internet. Et j'utiliserai ces informations dans une plus large mesure.


J'ai également essayé de visualiser différentes offres sur la carte d'Ekaterinbourg. Oui, c'est l'image accrocheuse de habracut, elle a été faite sur Kepler.gl


image


Il y a plus de 2000 appartements d'une chambre qui ont été vendus en juillet 2019 à Iekaterinbourg. Ils avaient un prix différent, de moins d'un million à près de 14 millions de roubles.


Ces points se réfèrent à leur géo-position. La couleur des points sur la carte représente le prix, plus le prix est proche de la couleur bleue, plus le prix est proche du rouge. Vous pouvez le considérer comme une analogie avec des couleurs froides et chaudes, la couleur plus chaude est la plus grande est le prix.
S'il vous plaît, souvenez-vous de ce moment, plus la couleur est rouge, plus la valeur de quelque chose est élevée. La même idée fonctionne pour le bleu mais dans le sens du prix le plus bas.


Vous avez maintenant un aperçu général de l'image et le moment de l'analyse arrive.


Objectif


Qu'est-ce que je voulais quand j'habitais à Iekaterinbourg? J'ai cherché un appartement assez bon, ou si nous parlons de ML - je voulais construire un modèle qui me donnera une recommandation d'achat.


D'une part, si un appartement est trop cher, le modèle devrait recommander d'attendre une baisse du prix en affichant le prix attendu de cet appartement.
D'un autre côté - si un prix est assez bon, selon l'état du marché - je devrais peut-être envisager cette offre.


Bien sûr, il n'y a rien d'idéal et j'étais prêt à accepter une erreur de calcul. Habituellement, pour ce type de tâche, utiliser une erreur de prédiction moyenne et j'étais prêt à une erreur de 10%. Par exemple, si vous avez 2-3 millions de roubles russes, vous pouvez ignorer l'erreur dans 200-300 mille, vous pouvez vous le permettre. Comme il me semblait.


Préparez


Comme je l'ai mentionné précédemment, il y avait beaucoup d'appartements, regardons-les attentivement.
importer des pandas en tant que pd


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

image


2310 appartements pendant un mois, on pourrait en tirer quelque chose d'utile. Qu'en est-il d'un aperçu général des données?


 df.describe() 

image
Il n'y a pas quelque chose d'extraordinaire - longitude, latitude, prix d'un appartement (l'étiquette " coût "), etc. Oui, pour ce moment j'ai utilisé " coût " au lieu de " prix ", j'espère que cela ne conduira pas à des malentendus, veuillez les considérer comme les mêmes.


Le nettoyage


Est-ce que chaque enregistrement a la même signification? Certains d'entre eux sont des appartements représentés comme une cabine, vous pouvez y travailler, mais vous ne souhaitez pas y vivre. Ce sont de petites pièces exiguës, pas un vrai appartement. Laissez-les retirer.


 df = df[df.total_area >= 20] 

Le prix de prédiction de l'appartement provient des problèmes les plus anciens de l'économie et des domaines connexes. Il n'y avait rien de lié au terme "ML" et les gens essayaient de deviner le prix en fonction des mètres carrés / pieds.
Donc, nous regardons ces colonnes / étiquettes et essayons d'en obtenir la distribution.


 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() 

image


Eh bien ... il n'y a rien de spécial, ça ressemble à une distribution normale. Peut-être que nous devons aller plus loin?


 sns.pairplot(df[numerical_fields]) 

image


Oups ... quelque chose de mal est là. Nettoyez les valeurs aberrantes dans ces domaines et essayez d'analyser à nouveau nos données.


 #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]) 

image


Les valeurs aberrantes ont disparu, et maintenant ça a l'air mieux.


Transformation


Le label "année", qui désigne une année de construction devrait être transformé en quelque chose de plus informatif. Que ce soit l'âge de la construction, c'est-à-dire la façon dont une maison spécifique est ancienne.


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

Jetons un œil au résultat.


 df.head() 

image


Il existe toutes sortes de données, catégorielles, valeurs nanométriques, description textuelle et quelques informations géographiques (longitude et latitude). Mettons de côté les derniers parce qu'à ce stade ils sont inutiles. Nous y reviendrons plus tard.


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

Données catégoriques


Habituellement, pour les données catégorielles, les gens utilisent différents types d'encodage ou des choses comme CatBoost qui offrent la possibilité de travailler avec eux comme avec des variables numériques.
Mais, pourrions-nous utiliser quelque chose de plus logique et plus intuitif? Il est maintenant temps de rendre nos données plus compréhensibles sans en perdre le sens.


Districts


Eh bien, il y a plus de vingt districts possibles, pourrions-nous ajouter plus de 20 variables supplémentaires dans notre modèle? Bien sûr, nous pourrions, mais ... devrions-nous? Nous sommes des gens et nous pourrions comparer les choses, n'est-ce pas?
Tout d'abord, tous les districts ne sont pas équivalents à un autre. Dans le centre de la ville, les prix d'un mètre carré sont plus élevés, plus éloignés du centre-ville - ils diminuent. Cela semble-t-il logique? Pouvons-nous l'utiliser?
Oui, nous pourrions certainement faire correspondre n'importe quel district avec un coefficient spécifique et le district le plus éloigné est celui des appartements les moins chers.


Après avoir fait correspondre la ville et utilisé une autre carte de service Web (ArcGIS Online) a changé et a une vue similaire
image


J'ai utilisé la même idée que pour la visualisation à plat. Le quartier le plus "prestigieux" et "cher" coloré en rouge et le moins - bleu. Une température de couleur, vous vous en souvenez?
De plus, nous devons manipuler notre trame de données.


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

La même approche sera utilisée pour décrire la qualité interne de l'appartement. Parfois, il a besoin d'une réparation, parfois plat est assez bien et prêt à vivre. Et dans d'autres cas, vous devriez dépenser de l'argent supplémentaire pour le rendre plus beau (pour changer les robinets, pour peindre les murs). Il pourrait également y avoir des coefficients d'utilisation.


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

Soit dit en passant, sur les murs. Bien sûr, cela influence également le prix de l'appartement. Le matériau moderne est meilleur que l'ancien, la brique est meilleure que le béton. Les murs en bois sont un moment assez controversé, c'est peut-être un bon choix pour la campagne, mais pas si bon pour la vie urbaine.


Nous utilisons la même approche que précédemment, et faisons une suggestion sur les lignes dont nous ne savons rien. Oui, parfois les gens ne fournissent pas toutes les informations sur leur appartement. En outre, sur la base de l'histoire, nous pouvons essayer de deviner le matériau des murs. Dans une période de temps spécifique (par exemple la période la plus importante de Khrouchtchev) - nous connaissons les matériaux typiques pour la construction.


 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) 

En outre, il y a des informations sur le balcon. À mon humble avis - le balcon est une chose vraiment utile, donc je ne pouvais pas m'empêcher de le considérer.
Malheureusement, il existe des valeurs nulles. Si l'auteur d'une publicité avait vérifié les informations à son sujet, nous aurions des informations plus réalistes.
Eh bien, s'il n'y a aucune information, cela signifiera "il n'y a pas de balcon".


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

Après cela, nous déposons des colonnes avec des informations sur l'année de construction (nous avons une bonne alternative pour elle). De plus, nous supprimons la colonne contenant des informations sur le type de bâtiment car il contient beaucoup de valeurs NaN et je n'ai trouvé aucune possibilité de combler ces lacunes. Et nous supprimons toutes les lignes avec NaN que nous avons.


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

Vérification


Donc ... nous avons utilisé une approche non standard et remplacer les valeurs catégorielles par leur représentation numérique. Et maintenant, nous avons terminé avec une transformation de nos données.
Une partie des données a été supprimée, mais en général, il s'agit d'un assez bon ensemble de données. Regardons la corrélation entre les variables indépendantes.


 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'])) 

image


Euh ... c'est devenu très intéressant.
Corrélation positive
Superficie totale - balcons . Pourquoi pas? Si notre appartement est grand, il y aura un balcon.
Corrélation négative
Superficie totale - âge . Le plus récent est plat, le plus grand est un espace de vie. Sonne logique, les nouveaux sont plus spacieux que les anciens.
Âge - balcon . Plus l'ancien est plat, moins il y a de balcons. On dirait une corrélation à travers une autre variable. C'est peut-être un triangle Age-Balcony-Area où une variable a une influence implicite sur une autre. Mettez cela en attente pendant un certain temps.
Âge - district. L'appartement le plus ancien est la grande probabilité qui sera placée dans les quartiers les plus prestigieux. Cela pourrait-il être lié à un prix plus élevé près du centre?


En outre, nous avons pu voir la corrélation avec la variable dépendante


 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") 

image


C'est parti ...


La très forte corrélation entre le domaine du flat et le prix. Si vous voulez avoir un plus grand logement, il vous faudra plus d'argent.
Il existe une corrélation négative entre les paires « âge / coût » et « district / coût ». Un appartement dans une maison neuve moins abordable que l'ancienne. Et à la campagne, les appartements sont moins chers.
Quoi qu'il en soit, cela semble clair et compréhensible, alors j'ai décidé d'y aller.


Modèle


Pour les tâches liées au prix forfaitaire de prédiction, utilisez généralement la régression linéaire. Selon une corrélation significative d'une étape précédente, nous pourrions essayer de l'utiliser également. C'est un cheval de bataille qui convient à de nombreuses tâches.
Préparez nos données pour les prochaines actions


 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) 

De plus, nous créons des fonctions simples pour la prédiction et l'évaluation du résultat. Faisons notre premier essai pour prédire le prix!


 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) 

image


Eh bien ... 76,67% de précision. Est-ce un grand nombre ou non? Selon mon point de vue, ce n'est pas mal. De plus, c'est un bon point de départ. Bien sûr, ce n'est pas idéal et il y a un potentiel d'amélioration.


Dans le même temps - nous avons essayé de prédire une seule partie des données. Qu'en est-il de l'application de la même stratégie pour d'autres données? Oui, le temps de la validation croisée.


 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) 

image


Le résultat de la validation croiséeNous prenons maintenant un autre résultat. 73 est inférieur à 76. Mais, c'est aussi un bon candidat jusqu'à un moment où nous en aurons un meilleur. Cela signifie également qu'une régression linéaire fonctionne assez stable sur notre ensemble de données.


Et c'est maintenant le moment de la dernière étape.


Nous examinerons la meilleure caractéristique de la régression linéaire - l' interprétabilité .
Cette famille de modèles, contrairement aux modèles plus complexes, a une meilleure capacité de compréhension. Il y a juste quelques nombres avec des coefficients et vous pouvez mettre vos nombres dans l'équation, faire des calculs simples et avoir un résultat.


Essayons d'interpréter notre modèle


 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) 

Les coefficients de notre modèle


L'image semble assez logique. Balcon / Murs / Surface / Réparation contribuent positivement à un prix forfaitaire.
Plus le plat est élevé, plus la contribution négative est importante . S'applique également à l'âge. Le plus ancien appartement est le prix le plus bas.


C'était donc un voyage fascinant.
Nous sommes partis du terrain, utilisons l'approche atypique pour la transformation des données basée sur le point de vue humain (les nombres au lieu des variables factices), les variables vérifiées et leur relation les unes avec les autres. Après cela, nous construisons notre modèle simple, utilisé la validation croisée pour tester son. Et comme la cerise sur le gâteau - regardez les internes du modèle, ce qui nous donne confiance en notre chemin.


Mais! Ce n'est pas la fin de notre voyage mais seulement une pause. Nous allons essayer de changer notre modèle à l'avenir et peut-être (juste peut-être) cela augmentera la précision de la prédiction.


Merci d'avoir lu!


La deuxième partie est

PS Les données sources et le bloc-notes Ipython s'y trouvent

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


All Articles