Haben Sie jemals nach einer Wohnung gesucht? Möchten Sie etwas maschinelles Lernen hinzufügen und einen Prozess interessanter gestalten?
Heute werden wir überlegen, maschinelles Lernen anzuwenden, um eine optimale Wohnung zu finden.
Einführung
Zunächst möchte ich diesen Moment klären und erklären, was "eine optimale Wohnung" bedeutet. Es ist eine Wohnung mit einer Reihe von verschiedenen Merkmalen wie "Fläche", "Bezirk", "Anzahl der Balkone" und so weiter. Und für diese Eigenschaften der Wohnung erwarten wir einen bestimmten Preis. Sieht aus wie eine Funktion, die mehrere Parameter akzeptiert und eine Zahl zurückgibt. Oder vielleicht eine Black Box, die etwas Magie bietet.
Aber ... es gibt ein großes "aber", manchmal kann man sich einer Wohnung stellen, die aus einer Reihe von Gründen wie einer guten Geoposition überteuert ist. Es gibt auch prestigeträchtigere Bezirke im Zentrum einer Stadt und Bezirke außerhalb der Stadt. Oder ... manchmal wollen Menschen ihre Wohnungen verkaufen, weil sie an einen anderen Punkt der Erde ziehen. Mit anderen Worten, es gibt viele Faktoren, die den Preis beeinflussen können. Kommt Ihnen das bekannt vor?
Kleiner Schritt zur Seite
Bevor ich fortfahre, möchte ich einen kleinen lyrischen Exkurs machen.
Ich lebte 5 Jahre in Jekaterinburg (der Stadt zwischen Europa und Asien, einer der Städte, in denen 2018 die Fußball-Weltmeisterschaft ausgetragen wurde).
Ich war in diesen konkreten Dschungel verliebt. Und ich hasste diese Stadt für den Winter und die öffentlichen Verkehrsmittel. Es ist eine wachsende Stadt und jeden Monat gibt es Tausende und Abertausende von Wohnungen zu verkaufen.
Ja, es ist eine überfüllte, verschmutzte Stadt. Gleichzeitig ist es ein guter Ort, um einen Immobilienmarkt zu analysieren. Ich habe viele Anzeigen für Wohnungen aus dem Internet erhalten. Und ich werde diese Informationen in weiterem Umfang nutzen.
Außerdem habe ich versucht, verschiedene Angebote auf Jekaterinburgs Karte zu visualisieren. Ja, es ist das auffällige Bild von Habracut, das auf Kepler.gl gemacht wurde

Es gibt über zweitausend 1-Zimmer-Wohnungen, die im Juli 2019 in Jekaterinburg verkauft wurden. Sie hatten einen anderen Preis, von weniger als einer Million bis fast 14 Millionen Rubel.
Diese Punkte beziehen sich auf ihre geografische Position. Die Farbe der Punkte auf der Karte stellt den Preis dar. Je niedriger der Preis in der Nähe der blauen Farbe ist, desto höher ist der Preis in der Nähe der roten Farbe. Sie können es als Analogie zu kalten und warmen Farben betrachten, je wärmer die Farbe ist, desto größer ist der Preis.
Bitte denken Sie daran, dass je röter die Farbe ist, desto höher ist der Wert von etwas. Die gleiche Idee funktioniert für Blau, aber in Richtung des niedrigsten Preises.
Jetzt haben Sie einen allgemeinen Überblick über das Bild und es wird Zeit für die Analyse.
Ziel
Was wollte ich, als ich in Jekaterinburg lebte? Ich habe nach einer Wohnung gesucht, die gut genug ist, oder wenn wir über ML sprechen - ich wollte ein Modell bauen, das mir eine Kaufempfehlung gibt.
Wenn eine Wohnung zu teuer ist, sollte das Modell einerseits empfehlen, auf eine Preissenkung zu warten, indem der erwartete Preis für diese Wohnung angezeigt wird.
Auf der anderen Seite - wenn ein Preis je nach Marktlage gut genug ist - sollte ich dieses Angebot vielleicht in Betracht ziehen.
Natürlich gibt es nichts Ideales und ich war bereit, einen Fehler in den Berechnungen zu akzeptieren. Normalerweise verwenden Sie für diese Art von Aufgabe einen mittleren Vorhersagefehler und ich war bereit, 10% Fehler zu machen. Wenn Sie zum Beispiel 2-3 Millionen russische Rubel haben, können Sie Fehler in 200-300 Tausend ignorieren, Sie können es sich leisten. Wie es mir schien.
Vorbereiten
Wie ich bereits erwähnt habe, gab es viele Wohnungen. Schauen wir sie uns genauer an.
Pandas als pd importieren
df = pd.read_csv('flats.csv') df.shape

2310 Wohnungen für einen Monat, wir könnten etwas Nützliches daraus extrahieren. Was ist mit einer allgemeinen Datenübersicht?
df.describe()

Es gibt nichts Außergewöhnliches - Längengrad, Breitengrad, Preis einer Wohnung (das Etikett " Kosten ") und so weiter. Ja, für diesen Moment habe ich " Kosten " anstelle von " Preis " verwendet. Ich hoffe, dass dies nicht zu Missverständnissen führt. Bitte betrachten Sie sie als gleich.
Reinigung
Hat jeder Datensatz die gleiche Bedeutung? Einige von ihnen sind Wohnungen wie eine Kabine, Sie können dort arbeiten, aber Sie möchten dort nicht leben. Es sind kleine, beengte Räume, keine echte Wohnung. Lassen Sie sie entfernen.
df = df[df.total_area >= 20]
Der Prognosepreis für eine Wohnung stammt aus den ältesten Fragen der Wirtschaft und verwandten Bereichen. Es gab nichts mit dem Begriff "ML" zu tun und die Leute versuchten, den Preis anhand von Quadratmetern / Fuß zu erraten.
Also schauen wir uns diese Spalten / Beschriftungen an und versuchen, ihre Verteilung zu ermitteln.
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()

Nun ... es gibt nichts Besonderes, sieht aus wie eine Normalverteilung. Vielleicht müssen wir tiefer gehen?
sns.pairplot(df[numerical_fields])

Ups ... da stimmt etwas nicht. Bereinigen Sie Ausreißer in diesen Feldern und versuchen Sie erneut, unsere Daten zu analysieren.

Ausreißer sind gegangen, und jetzt sieht es besser aus.
Das Label "Jahr", das auf ein Baujahr hinweist, sollte in etwas Informativeres umgewandelt werden. Es sei das Zeitalter des Bauens, mit anderen Worten, wie alt ein bestimmtes Haus ist.
df['age'] = 2019 -df['year']
Schauen wir uns das Ergebnis an.
df.head()

Es gibt alle Arten von Daten, kategoriale Daten, Nan-Werte, Textbeschreibungen und einige Geoinformationen (Längen- und Breitengrad). Lassen Sie uns die letzten beiseite legen, denn auf dieser Bühne sind sie nutzlos. Wir werden später darauf zurückkommen.
df.drop(columns=["lon","lat","description"],inplace=True)
Kategoriale Daten
Normalerweise verwenden Benutzer für kategoriale Daten verschiedene Arten der Codierung oder Dinge wie CatBoost, die die Möglichkeit bieten, mit ihnen wie mit numerischen Variablen zu arbeiten.
Aber könnten wir etwas logischeres und intuitiveres verwenden? Jetzt ist es an der Zeit, unsere Daten verständlicher zu machen, ohne ihre Bedeutung zu verlieren.
Bezirke
Nun, es gibt über zwanzig mögliche Bezirke. Können wir unserem Modell über 20 zusätzliche Variablen hinzufügen? Natürlich könnten wir, aber ... sollten wir? Wir sind Menschen und wir könnten Dinge vergleichen, nicht wahr?
Erstens - nicht jeder Bezirk ist einem anderen gleichgestellt. Im Zentrum der Stadt sind die Preise für einen Quadratmeter höher, weiter von der Innenstadt entfernt - es wird sinken. Klingt es logisch? Könnten wir das nutzen?
Ja, definitiv könnten wir jeden Bezirk mit einem bestimmten Koeffizienten abgleichen und der weitere Bezirk sind die billigeren Wohnungen.
Nach dem Abgleichen der Stadt und der Verwendung einer anderen Webdienstkarte (ArcGIS Online) geändert und hat eine ähnliche Ansicht

Ich habe die gleiche Idee wie für die Visualisierung von Flat verwendet. Das "prestigeträchtigste" und "teuerste" Viertel, rot und am wenigsten blau gefärbt. Eine Farbtemperatur, erinnerst du dich daran?
Außerdem sollten wir einige Manipulationen an unserem Datenrahmen vornehmen.
district_map = {'alpha': 2, 'beta': 4, ... 'delta':3, ... 'epsilon': 1} df.district = df.district.str.lower() df.replace({"district": district_map}, inplace=True)
Der gleiche Ansatz wird zur Beschreibung der internen Qualität der Wohnung verwendet. Manchmal muss es repariert werden, manchmal ist die Wohnung recht gut und bereit zum Leben. In anderen Fällen sollten Sie zusätzliches Geld dafür ausgeben, dass es besser aussieht (um die Wasserhähne zu wechseln, Wände zu streichen). Es könnten auch Koeffizienten verwendet werden.
repair = {'A': 1, 'B': 0.6, 'C': 0.7, 'D': 0.8} df.repair.fillna('D', inplace=True) df.replace({"repair": repair}, inplace=True)
Übrigens über Wände. Natürlich beeinflusst es auch den Wohnungspreis. Modernes Material ist besser als älter, Ziegel ist besser als Beton. Wände aus Holz sind ein ziemlich kontroverser Moment, vielleicht ist es eine gute Wahl für die Landschaft, aber nicht so gut für das städtische Leben.
Wir verwenden den gleichen Ansatz wie zuvor und machen einen Vorschlag zu Zeilen, von denen wir nichts wissen. Ja, manchmal geben die Leute nicht alle Informationen über ihre Wohnung an. Darüber hinaus können wir anhand der Geschichte versuchen, das Material von Wänden zu erraten. In einem bestimmten Zeitraum (zum Beispiel Chruschtschows führender Zeitraum) kennen wir typische Baumaterialien.
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)
Es gibt auch Informationen über den Balkon. Meiner bescheidenen Meinung nach ist der Balkon eine wirklich nützliche Sache, daher konnte ich mir nicht helfen, darüber nachzudenken.
Leider gibt es einige Nullwerte. Wenn der Autor einer Anzeige Informationen darüber überprüft hätte, hätten wir realistischere Informationen.
Wenn es keine Informationen gibt, bedeutet dies "es gibt keinen Balkon".
df.balcony.fillna(0,inplace=True)
Danach löschen wir Spalten mit Informationen über das Baujahr (wir haben eine gute Alternative dafür). Außerdem entfernen wir Spalten mit Informationen zum Gebäudetyp, da diese viele NaN-Werte aufweisen und ich keine Möglichkeit gefunden habe, diese Lücken zu schließen. Und wir lassen alle Zeilen mit NaN fallen, die wir haben.
df.drop(columns=['type_house'],inplace=True) df = df.astype(np.float64) df.dropna(inplace=True)
Überprüfen
Also ... haben wir einen nicht standardmäßigen Ansatz verwendet und kategoriale Werte durch ihre numerische Darstellung ersetzt. Und jetzt haben wir eine Transformation unserer Daten abgeschlossen.
Ein Teil der Daten wurde gelöscht, aber im Allgemeinen handelt es sich um einen recht guten Datensatz. Betrachten wir die Korrelation zwischen unabhängigen Variablen.
def show_correlation(df): sns.set(style="whitegrid") corr = df.corr() * 100

Ähm ... es wurde sehr interessant.
Positive Korrelation
Gesamtfläche - Balkone . Warum nicht? Wenn unsere Wohnung groß ist, gibt es einen Balkon.
Negative Korrelation
Gesamtfläche - Alter . Je neuer flach ist, desto größer ist eine Wohnfläche. Klingt logisch, neue sind geräumiger als ältere.
Alter - Balkon . Je älter flach ist, desto weniger Balkone hat es. Scheint wie eine Korrelation durch eine andere Variable. Vielleicht ist es ein Dreieck Age-Balcony-Area, in dem eine Variable einen impliziten Einfluss auf eine andere hat. Halten Sie das für eine Weile auf Eis.
Alter - Bezirk. Die ältere Wohnung ist die große Wahrscheinlichkeit, die in den prestigeträchtigeren Bezirken platziert wird. Könnte es mit einem höheren Preis in der Nähe des Zentrums zusammenhängen?
Wir konnten auch die Korrelation mit der abhängigen Variablen sehen
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")

Los geht's ...
Die sehr starke Korrelation zwischen Wohnungsfläche und Preis. Wenn Sie einen größeren Wohnraum haben möchten, benötigen Sie mehr Geld.
Es besteht eine negative Korrelation zwischen den Paaren " Alter / Kosten " und " Bezirk / Kosten ". Eine Wohnung in einem neueren Haus, das weniger erschwinglich ist als das alte. Und auf dem Land sind Wohnungen billiger.
Wie auch immer, es scheint klar und verständlich zu sein, also habe ich mich dafür entschieden.
Modell
Verwenden Sie für Aufgaben, die sich normalerweise auf den Preis der Vorhersageebene beziehen, die lineare Regression. Aufgrund einer signifikanten Korrelation aus einem früheren Stadium könnten wir versuchen, es auch zu verwenden. Es ist ein Arbeitstier, das für viele Aufgaben geeignet ist.
Bereiten Sie unsere Daten für die nächsten Aktionen vor
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)
Außerdem erstellen wir einige einfache Funktionen zur Vorhersage und Auswertung des Ergebnisses. Lassen Sie uns unseren ersten Versuch machen, den Preis vorherzusagen!
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)

Nun ... 76,67% der Genauigkeit. Ist es eine große Zahl oder nicht? Meiner Meinung nach ist es nicht schlecht. Darüber hinaus ist es ein guter Ausgangspunkt. Natürlich ist es nicht ideal und es gibt Verbesserungspotential.
Gleichzeitig haben wir versucht, nur einen Teil der Daten vorherzusagen. Was ist mit der Anwendung derselben Strategie auf andere Daten? Ja, Zeit für eine Kreuzvalidierung.
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)

Das Ergebnis der Kreuzvalidierung Jetzt nehmen wir ein anderes Ergebnis. 73 ist weniger als 76. Aber es ist auch ein guter Kandidat, bis wir einen besseren haben werden. Dies bedeutet auch, dass eine lineare Regression in unserem Datensatz ziemlich stabil funktioniert.
Und jetzt ist eine Zeit für den letzten Schritt.
Wir werden das beste Merkmal der linearen Regression betrachten - die Interpretierbarkeit .
Diese Modellfamilie hat im Gegensatz zu komplexeren eine bessere Fähigkeit zum Verständnis. Es gibt nur einige Zahlen mit Koeffizienten, und Sie können Ihre Zahlen in die Gleichung einfügen, einfache Berechnungen anstellen und ein Ergebnis erzielen.
Versuchen wir, unser Modell zu interpretieren
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)

Das Bild sieht ziemlich logisch aus. Balkon / Wände / Fläche / Reparatur leisten einen positiven Beitrag zu einem Pauschalpreis.
Je weiter flach, desto größer ist ein negativer Beitrag . Gilt auch für das Alter. Die ältere Wohnung ist der niedrigere Preis.
Es war also eine faszinierende Reise.
Wir haben von Grund auf begonnen und den untypischen Ansatz für die Datentransformation verwendet, der auf der menschlichen Sichtweise (Zahlen statt Dummy-Variablen), überprüften Variablen und ihrer Beziehung zueinander basiert. Danach erstellen wir unser einfaches Modell, das durch Kreuzvalidierung getestet wird. Und als Kirsche auf dem Kuchen - schauen Sie sich die Einbauten des Modells an, was uns Vertrauen in unseren Weg gibt.
Aber! Es ist nicht das Ende unserer Reise, sondern nur eine Pause. Wir werden versuchen, unser Modell in Zukunft zu ändern, und vielleicht (nur vielleicht) erhöht es die Genauigkeit der Vorhersage.
Danke fürs Lesen!
Der zweite Teil ist da
PS Dort befinden sich die Quelldaten und das Ipython-Notebook