Hauspreise schätzen mit Machine Learning – Vorbereitung
Nachdem wir die Daten von online angebotenen Häusern gesammelt haben, begeben wir uns auf eine Mission. Und diese lautet: Mit Machine Learning Hauspreise anhand der Eigenschaften der Immobilien möglichst genau vorherzusagen. Dazu müssen wir die Daten so gut es geht vorbereiten. Denn auch das ausgefeilteste Modell kann mit „schmutzigen“ Daten ohne Aussagekraft nichts anfangen. Also werden wir einen Blick auf die Daten werfen, Eigenschaften visualisieren, Spalten säubern, externe Daten dazu spielen und versuchen, den Algorithmen die bestmöglichen Daten zu vorzusetzen. Los geht’s!
Datensatz importieren und Datentypen checken
Wie immer importieren wir die Daten erst mal in die Programmierumgebung.
df = pd.ExcelFile("...\daten.xlsx").parse(0)
Anmerkung: Die Daten lagen zwar ursprünglich als CSV vor, allerdings musste ich eine Excel daraus machen, um ein paar Einträge, die fälschlicherweise als Datum formatiert waren, wieder ins richtige Format zu bringen. Passiert…
Dimensionen anschauen.
df.shape
Die Daten bestehen aus 61.225 Häusern mit je 86 Eigenschaften. Welche sind das?
df.columns
Die markierte Spalte ist die Variable, welche wir später prognostizieren wollen – der Kaufpreis. Schauen wir uns die Datentypen an. Alle Daten, die wir für statistische Modelle nutzen, sollten numerisch sein. Nur ist das wirklich der Fall?
df.dtypes
Viele wichtige Daten sind noch nicht als numerische Werte formatiert. Zum Beispiel bestehen manche Spalten mit binären Eigenschaften aus „y“ und „n“, statt aus 1 und 0. Solche Spalten sind u.a. „obj_cellar“ (Haus hat einen Keller) oder „obj_newlyConst“ (Haus ist ein Neubau). Andere Spalten bestehen aus den Wahrheitswerten True und False. Auch damit können viele Algorithmen nicht arbeiten. Deshalb wandeln wir diese Spalten in Nullen und Einsen um.
df.obj_barrierFree = df.obj_barrierFree.replace("n",0).replace("y",1) df.obj_cellar = df.obj_cellar.replace("n",0).replace("y",1) df.obj_courtage = df.obj_courtage.replace("n",0).replace("y",1) df.obj_international = df.obj_international.replace("n",0).replace("y",1) df.obj_newlyConst = df.obj_newlyConst.replace("n",0).replace("y",1) df.obj_rented = df.obj_rented.replace("n",0).replace("y",1) df.obj_ExclusiveExpose = df.obj_ExclusiveExpose*1 df.ga_cd_cxp_historicallisting = df.ga_cd_cxp_historicallisting*1
Auf der anderen Seite gibt es Daten, die zwar numerisch sind, aber eigentlich gar nicht – zumindest nicht direkt – in das Modell einfließen sollen. Ein Beispiel dafür sind Postleitzahlen. In den wenigsten Fällen gibt es einen linearen oder polynomischen Zusammenhang zwischen ihr und der Zielvariable. Wie man die Postleitzahl dennoch für sich nutzen kann, werde ich später zeigen. Jetzt wandeln wir diese – sowie die eindeutigen Haus-IDs – erst mal in Strings um.
df.obj_zipCode = df.obj_zipCode.astype(str) df.geo_plz = df.geo_plz.astype(str) df.cId = df.cId.astype(str) df.scoutId = df.scoutId.astype(str)
Kategoriale Variablen
Als nächstes widmen wir uns den kategorialen Variablen. Davon gibt es in diesem Datensatz nicht wenige. Kategoriale Variablen sind mitunter solche Eigenschaften wie „Zustand des Hauses“.
df.obj_condition.value_counts(dropna=False).plot(kind="bar") plt.show()
Oben siehst du, dass die Häuser im Datensatz 11 unterschiedliche Zustände haben können. Diese Information ist hilfreich für die Prognose des Kaufpreises. Ein modernisiertes Haus wird – unter sonst gleichen Bedingungen – einen höheres Preis erzielen als ein Haus, das renovierungsbedürftig ist. Allerdings kann ein Machine Learning Modell mit Text nichts anfangen. Deshalb machen wir aus diesen kategorialen Merkmalen Dummy-Variablen. Dafür gibt es in Pandas die Funktion get_dummies(). Statt einer Spalte mit 11 Merkmalen haben wir dann 11 Spalten mit jeweiles einem Merkmal, welches aus Nullen und Einsen besteht. Es gibt dann zum Beispiel eine Spalte „modernized“, in der die Werte bei allen modernisierten Häusern 1 betragen und ansonsten 0. Die Funktion wenden wir jetzt auf alle betreffenden Spalten an und heften die Ergebnisspalten im selben Abwasch an den ursprünglichen DataFrame.
df = pd.concat([df, pd.get_dummies(df.obj_buildingType,prefix="obj_type")], axis=1) df = pd.concat([df, pd.get_dummies(df.obj_condition,prefix="obj_cond")], axis=1) df = pd.concat([df, pd.get_dummies(df.obj_constructionPhase,prefix="obj_const_phase")], axis=1) df = pd.concat([df, pd.get_dummies(df.ga_cd_via,prefix="ga_cd")], axis=1) df = pd.concat([df, pd.get_dummies(df.obj_interiorQual,prefix="obj_int_qual")], axis=1) df = pd.concat([df, pd.get_dummies(df.obj_energyType,prefix="obj_energy_",dummy_na=True)], axis=1) df = pd.concat([df, pd.get_dummies(df.geo_bln,prefix="geo_bln_",dummy_na=True)], axis=1)
Der Übersichtlichkeit halber können wir innerhalb der Funktion noch ein Präfix angeben, welches vor die Spaltennamen der generierten Dummy-Variablen geschrieben wird. Ein Ausschnitt davon sieht dann so aus.
Zum Haus in der ersten Zeile gibt es bezüglich des Zustands keine Information, während das zweite Haut renovierungsbedürftig ist.
Bei manchen kategorialen Variablen lohnt es sich jedoch nur, einen Teil der Merkmal zu Dummy-Variablen zu machen. Dies ist zum Beispiel der Fall, wenn eine Spalte aus sehr vielen unterschiedlichen Kategorien besteht, jedoch nur ein kleiner Teil dieser Kategorien schon einen überwiegenden Teil aller Daten ausmacht. Hier ist die Spalte „obj_heatingType“ solch ein Kandidat.
df.obj_heatingType.value_counts(normalize=True)
Im Datensatz gibt es also 14 verschiedene Arten der Heizung, doch einige Arten sind so schwach vertreten, dass sie wahrscheinlich nur geringe Aussagekraft im Bezug auf eine Schätzung des Kaufpreises haben. Mit 0,1649% werden nur 86 der über 61.000 Häuser mit Solarenergie beheizt. Aus diesen Gründen mache ich nur aus den ersten 9 Kategorien einzelne Dummy-Variablen und fasse den Rest mit „other“ zusammen.
df["heating_central"] = (df.obj_heatingType=="central_heating")*1 df["heating_NaN"] = (df.obj_heatingType.isnull())*1 df["heating_gas"] = (df.obj_heatingType=="gas_heating")*1 df["heating_floor"] = (df.obj_heatingType=="floor_heating")*1 df["heating_no_info"] = (df.obj_heatingType=="no_information")*1 df["heating_oil"] = (df.obj_heatingType=="oil_heating")*1 df["heating_heat_pump"] = (df.obj_heatingType=="heat_pump")*1 df["heating_stove"] = (df.obj_heatingType=="stove_heating")*1 df["heating_self_cont"] = (df.obj_heatingType=="self_contained_central_heating")*1 df["heating_other"] = 1-(df["heating_central"] + df["heating_NaN"] + df["heating_gas"] + df["heating_floor"] + df["heating_no_info"] + df["heating_oil"] + df["heating_heat_pump"] + df["heating_stove"] + df["heating_self_cont"])
Ein weiterer Kandidat ist die Variable „obj_firingTypes“.
df["firing_gas"] = (df.obj_firingTypes=="gas")*1 df["firing_oil"] = (df.obj_firingTypes=="oil")*1 df["firing_no_info"] = (df.obj_firingTypes=="no_information")*1 df["firing_NaN"] = (df.obj_firingTypes.isnull())*1 df["firing_electr"] = (df.obj_firingTypes=="electricity")*1 df["firing_nat_gas"] = (df.obj_firingTypes=="natural_gas_light")*1 df["firing_other"] = 1-(df["firing_gas"]+ df["firing_oil"]+ df["firing_no_info"]+ df["firing_NaN"]+ df["firing_electr"]+ df["firing_nat_gas"])
Wo du letztendlich den Cut machen oder ob du sogar alle Variablen in eigenen Spalten aufführen solltest, ist dir überlassen. Dafür gibt es keinen einzigen besten Weg.
Postleitzahlen brauchbar machen
Wie oben erwähnt kann man Variablen wie Postleitzahlen nicht einfach in ein Machine Learning Modell werfen und hoffen, dass sie signifikant zur Performance eines Schätzers beitragen. Doch es wäre schade, wenn man sie ganz außer Acht lässt. Gerade Postleitzahlen geben Aufschluss über regionale Unterschiede im Bezug auf durchschnittliche Wohnfläche, Grundstücksfläche, Kaufpreise und andere Eigenschaften. Ein Merkmal, welches sich auf den Preis auswirken könnte und sich gleichzeitig von der Postleitzahl ableiten lässt, ist die Anzahl der Einwohner in einer Region. Genauer gesagt die Bevölkerungsdichte. Zum Glück lassen sich Einwohnerzahlen und Grundflächen für alle Postleitzahlen in Deutschland kostenlos herunterladen. Die benötigte Datei ist die Excel-Tabelle „plz-5stellig-daten.xlsx“ (auf der Seite rechts). Um die Tabelle mit unseren Häuserdaten zu joinen, machen wir folgendes.
plz = pd.ExcelFile("...\plz-5stellig-daten.xlsx").parse(0) plz.plz = plz.plz.astype(int) plz.plz = plz.plz.astype(str) df = df.merge(plz,left_on="plz",right_on="plz",how="left") df["bev_pro_km2"] = df.einwohner/df.qkm
Somit haben wir nun für jedes Haus die Einwohnerzahl, Grundfläche und Bevölkerungsdichte der Postleitzahl, in der sich die jeweilige Immobilie befindet.
Doch es gibt noch weitere Variablen, die sich von der Postleitzahl ableiten lassen und mit Hilfe derer sich etwaige Nord-Süd- oder West-Ost-Gefälle in ein statistisches Modell einpflegen lassen. Sie heißen: Längengrad und Breitengrad. Natürlich gibt es keine 1:1-Beziehung zwischen Koordinaten und Postleitzahl, doch für jede PLZ lassen sich der durchschnittliche Längen- und Breitengrad berechnen. Und selbst das haben zum Glück schon andere vor uns getan. Die Datei „DE.tab“ kannst du herunterladen und dann den unten stehenden Code ausführen. Leider ist die Datei etwas unhandlich formatiert. Pro Ort gibt es unabhängig von der Anzahl der Postleitzahlen eine Zeile. So geschieht es, dass zum Beispiel in der Zeile von Berlin in der Zelle „plz“ 191 durch Kommata getrennte Postleitzahlen enthalten sind. Deshalb müssen die Zellen gesplittet und pivotiert werden.
geo = pd.read_csv("...\DE.tab",sep="\t") geo_split = pd.concat([geo.lat,geo.lon,geo.plz.str.split(",",expand=True)],axis=1) geo_split_melt = geo_split.melt(id_vars=["lat","lon"],value_name="plz").drop("variable",axis=1) geo_split_melt = geo_split_melt.loc[geo_split_melt.plz.isnull()==0] geo_split_melt = geo_split_melt.loc[geo_split_melt.plz!=""] geo_split_melt.plz = geo_split_melt.plz.astype(int) geo_split_melt.plz = geo_split_melt.plz.astype(str) geo_split_melt_grouped = geo_split_melt.groupby("plz").agg({"lat":["mean"],"lon":["mean"]}).reset_index() geo_split_melt_grouped.columns = geo_split_melt_grouped.columns.get_level_values(0) df = df.merge(geo_split_melt_grouped[["plz","lat","lon"]],left_on="plz",right_on="plz",how="left")
Somit haben wir nun auch für jedes Haus die ungefähren Koordinaten im Datensatz.
Noch ein paar kleine Sachen
Dann gibt es noch ein paar kleine Dinge, dir mir aufgefallen sind und die ich noch einbringen bzw. anpassen wollte. Einmal sind das die umständlichen Präfixe vor den Spaltennamen – weg damit.
df.columns = [x.replace("obj_","").replace("ga_","").replace("geo_","") for x in df.columns]
Dann wäre da noch die Spalte „Beschreibung“, die – wie der Name schon sagt – die Beschreibung der Immobilie enthält. Zwar können wir diese nicht direkt in ein Machine Learning Modell einbauen, ihre Länge aber schon.
df["len_beschreibung"] = df.beschreibung.str.len()
Ob die Länge der Beschreibung wirklich eine Rolle für den Preis eines Hauses spielt, werden wir später sehen. Genau wie die Eigenschaften eines potentiellen Internet- oder Telefonanschlusses. Hier enthalten die Daten jedoch im Moment noch nervige Strings („Mbit/s“) oder bestehen aus True und False statt 1 und 0. Ändern wir das.
df.ExclusiveExpose = df.ExclusiveExpose.astype(str) df.telekomDownloadSpeed = df.telekomDownloadSpeed.str.replace(" MBit/s","").astype(float) df.telekomHybridDownloadSpeed = df.telekomHybridDownloadSpeed.str.replace(" MBit/s","").astype(float) df.telekomHybridUploadSpeed = df.telekomHybridUploadSpeed.str.replace(" MBit/s","").astype(float) df.telekomUploadSpeed = df.telekomUploadSpeed.str.replace(" MBit/s","").str.replace(",",".").astype(float) df.telekomInternet = df.telekomInternet.str.replace(" MBit/s","").str.replace(" kBit/s","") df.telekomInternet = df.telekomInternet.astype(float) df.telekomHdTelephone = (df.telekomHdTelephone*1).fillna(0) df.telekomInternetProductAvailable = (df.telekomInternetProductAvailable*1).fillna(0)
Fertig
Wir haben die Daten importiert, grundlegend bereinigt, einige Variablen hinzugefügt und den Datensatz so vorbereitet, dass wir als nächstes mit einem einfache Machine Learning Modell den ersten Schritt in Richtung Hauspreisschätzung wagen können. Wir werden als Baseline eine lineare Regression anwenden und überprüfen, wie sehr es sich im Bezug auf die echten Hauspreise irrt. Bei Fragen oder Anmerkungen kannst du gern die Kommentarfunktion nutzen. Viel Spaß beim programmieren!