Datenanalyse vor Gebrauchtwagenkauf?

By Philipp Leppert in R Finanzen

February 9, 2021

Das Internet bringt uns im Großen und Ganzen eine ganze Menge Erleichterungen. Nahezu alles kann man sich online bestellen oder, bei größeren Anschaffungen, zumindest schon mal von zu Hause aus in aller Ruhe begutachten. Da unser Auto über die Weihnachtsfeiertage zum wiederholten Male gestreikt hat, haben wir uns entschieden, dass ein neues (gebrauchtes) Auto angeschafft werden muss. Die beiden größten Internetportale für die Automobilsuche autoscout24.de und mobile.de haben sehr detaillierte Suchmasken, sodass man sich das gewünschte Auto ziemlich genau konfigurieren kann.

Doch genau hier beginnt auch ein Problem. Selbst wenn man sich auf einen Hersteller und ein Modell festgelegt hat, wird die Suche aufgrund der Vielzahl von Anzeigen schnell unübersichtlich. Zwar bieten die beiden Portale auch eine unabhängige Preisbewertung der Inserate an, also ob das Auto relativ zu vergleichbaren Inseraten im Segment eher teuer oder günstig ist, jedoch werden dabei viele Faktoren nicht berücksicht. Auf Bagatellschäden wird in der Anzeige meistens nicht hingewiesen und die Nachfrage nach der Unfallfreiheit des PKW, sofern diese nicht explizit im Inserat bestätigt ist, ergibt häufig eine negative Antwort bei vermeintlich besonders günstigen Angeboten. Auch ist selten erkennbar, ob der PKW privat oder gewerblich genutzt wurde. Ein neuer Anbieter names Autohero, bei dem man sich den Gebrauchtwagen direkt nach Hause liefern lassen kann, ist hier transparenter und führt zumindest die Eigenschaft Gewerbliche Nutzung auf.

Insgesamt erscheint mir als Laie der Gebrauchtwagenmarkt ziemlich schwer zu durchblicken. Für ein bestimmtes Modell eines Herstellers gibt es oft zahlreiche Ausstattungsvarianten, die einen objektiven Vergleich ziemlich erschweren. Am besten man informiert sich hier bereits vorab anhand von ADAC Testberichten oder Ähnlichem, um herauszufinden, was man eigentlich braucht. Aber es heisst ja auch nicht umsonst Gebrauchtwagen. Doch nun weiter zur Datenanalyse.

Datenbeschaffung

Die Suchergebnisseiten der oben erwähnten Gebrauchtwagenportale bieten eine gut strukturierte Oberfläche, um mittels Webscraping automatisiert die Eckdaten eines Inserats zu extrahieren. Dies funktioniert (Stand Februar 2021) bei mir nur auf der Webseite von autoscout24.de. Generell sind die Suchergebnisse auf den beiden Webseiten sehr ähnlich und insbesondere Händler schalten ihre Inserate meist auf beiden Portalen.

Unten findet sich eine Funktion, mit welcher ich die relevanten Inseratsinformationen von einer Seite der Suchergebnisse gewinne. Ich extrahiere den preis, den Inseratstitel, welcher aus zwei Elementen (titel1, titel2) besteht, die Laufleistung (km), das Datum der Erstzulassung (ez), die Anzahl der bisherigen Fahrzeughalter (halter), die Motorleistung (ps) und den Standort des Gebrauchtwagens (ort). Jedes Element wird dabei so aufbereitet, dass es für datenanalytische Zwecke sinnvoll verwendet werden kann. Die Funktion gibt zum Schluss einen Data Frame mit 8 Spalten zurück, der in der Regeln 20 Zeilen lang ist. Eine Suchseite auf autoscout24.de umfasst nämlich genau 20 Inserate.

library(tidyverse)
library(rvest)

scraper_autoscout24 <- function(page){
  
  preis <- page %>%
    html_nodes(".sc-font-xl") %>%
    html_text() %>%
    str_match_all("€ *.*") %>%
    unlist() %>%
    str_remove_all("\\.|,-|€") %>%
    as.numeric() %>%
    `length<-` (20) %>%
    as_tibble_col("preis")
  
  titel1 <- page %>%
    html_nodes("#cldt-ot-summary .sc-font-bold") %>%
    html_text() %>%
    `length<-` (20) %>%
    as_tibble_col("titel1")
  
  titel2 <- page %>%
    html_nodes(".cldt-summary-version") %>%
    html_text() %>%
    `length<-` (20) %>%
    as_tibble_col("titel2") %>%
    mutate(id = 1:20)
  
  km <- page %>%
    html_nodes("li:nth-child(1)") %>%
    html_text() %>%
    str_match_all("[0-9].+ km") %>%
    unlist() %>%
    str_remove_all("\\.| km") %>%
    as.numeric() %>%
    `length<-` (20) %>%
    as_tibble_col("km")
  
  ez <- page %>%
    html_nodes("li:nth-child(2)") %>%
    html_text() %>%
    str_match_all(".*[0-9]/[0-9]*|.*Erstzulassung") %>%
    unlist() %>%
    str_remove_all("-/- \\(Erstzulassung") %>%
    `length<-` (20) %>%
    as_tibble_col("ez") %>%
    separate(col = ez, into = c("monat", "jahr"), sep = "/", remove = FALSE)
  
  halter <- page %>%
    html_nodes("li:nth-child(5)") %>%
    html_text() %>% 
    str_match_all(".*Fahrzeughalter") %>%
    unlist() %>%
    str_remove_all(" Fahrzeughalter|-/- \\(Fahrzeughalter") %>%
    as.numeric() %>%
    `length<-` (20) %>%
    as_tibble_col("halter")
  
  ps <- page %>%
    html_nodes("li:nth-child(3)") %>%
    html_text() %>% 
    str_match_all(".[0-9]* PS") %>%
    unlist() %>%
    str_remove_all(" PS|\\(") %>%
    as.numeric() %>%
    `length<-` (20) %>%
    as_tibble_col("ps")
  
  ort <- page %>%
    html_nodes(".cldf-summary-seller-contact-zip-city") %>%
    html_text() %>%
    `length<-` (20) %>%
    as_tibble_col("ort") %>%
    separate(col = ort, into = c("plz", "stadt"), sep = " ", remove = FALSE)
  
  scraped_data <- tibble(
    titel1, titel2, 
    preis, km, ez, 
    halter, ps, ort
  )
  
  return(scraped_data)
  
}

Nun habe ich die Qual der Wahl und muss meine Suche für ein bestimmtes Auto spezifizieren. Wir haben uns vorab für einen Opel Adam entschieden und für meine Auswertung verwende ich die folgenden Spezifikationen:

  • Erstzulassung zwischen Anfang 2013 und Ende 2018
  • keine Beschränkung der Laufleistung (km)
  • ab 90 PS (Benziner)
  • maximal 2 Fahrzeughalter
  • scheckheftgepflegt
  • keine Privatangebote

Am 09.02.2021 wurden dabei 439 Inserate gefunden. Die Webseite autoscout24.de begrenzt die Anzahl abrufbarer Suchseiten automatisch auf 20 (= 400 Inserate). D.h. wenn mehr Inserate gefunden werden, kann man diese sich weder manuell anzeigen lassen noch automatisiert abrufen. Dies kann umgangen werden, indem man die Angebote nach ihrem Preis filtert. D.h. wenn ich die Suchergebnisse aufsteigend nach dem Preis sortiere und die ersten 400 Inserate extrahiere, kann ich nach dem letzten Inserat auf Seite 20 (Inserat 400) den Preis als Mindestpreis für die nächste Suche festlegen und erhalte so die verbleibenden Inserate.

Technisch muss man zunächst eine leere Liste (pages) erstellen, in welcher für jede Seite der Suchergebnisse der Webseiteninhalt, ausgelesen mit der Funktion read_html(), gespeichert wird. Die for-Schleife läuft dann von Seite 1 bis maximal Seite 20. Der Link der Webseite wird am URL-Bestandteil page= geteilt. Danach verwende ich die Funktion map_dfr() aus dem R-Paket purrr und wende die oben codierte Funktion auf jedes Listenelement von pages an. Fertig ist ein Datensatz mit Gebrauchtwageninseraten!

# Seiten
pages <- list()

# Suchseiten 1:X
for (i in 1:20) {
  
  url <- paste0("https://www.autoscout24.de/lst/opel/adam?sort=price&desc=0&custtype=D&prevownersid=2&eq=49&fuel=B&ustate=N%2CU&size=20&page="
                ,i,
                "&powerfrom=66&powertype=hp&cy=D&fregto=2018&fregfrom=2013&atype=C&fc=25&qry=&") 
  pages[[i]] <- read_html(url)
}

# Mappen und NA entfernen
inserate <- map_dfr(.x = pages, .f = ~scraper_autoscout24(page = .x)) %>%
  filter(ez != "") 

Die Daten sehen so aus:


titel1 titel2 id preis km ez monat jahr halter ps ort plz stadt
Opel Adam 1,4 SLAM ecoFLEX S/S Panorama 1 4900 150000 07/2013 07 2013 1 101 71679 Asperg 71679 Asperg
Opel Adam Slam ecoFlex 2 5950 125897 07/2013 07 2013 2 101 84088 Neufahrn 84088 Neufahrn
Opel Adam 1.4 Slam *101PS*TEILLEDER*SOUNDSYSTEM*1.HD* 3 5990 159750 04/2014 04 2014 1 101 31275 Lehrte 31275 Lehrte
Opel Adam 1.4 Jam*2Hd*Panorama*Garantie*SH*LH*Eu5*TOP 4 6490 133900 04/2013 04 2013 2 101 67549 Worms 67549 Worms
Opel Adam Glam~Lradhzg~Parkaut~Klimaaut.~Sihzg~Tempom 5 6699 101700 03/2014 03 2014 1 101 07629 Hermsdorf 07629 Hermsdorf
Opel Adam GLAM 1.4 SHZ/PANO/UNFALLFREI! 6 6890 86934 05/2013 05 2013 2 101 99310 Arnstadt 99310 Arnstadt
Opel Adam 1.4 Jam Panoramadach+Scheckheft+Sitzheizung 7 6950 90154 11/2014 11 2014 2 101 41063 Mönchengladbach 41063 Mönchengladbach
Opel Adam Glam Panoramadach Sitzheizung PDC 8 6980 94900 06/2013 06 2013 2 101 91522 Ansbach 91522 Ansbach
Opel Adam Glam ecoFlex *Panorama/Lenkradheizung* 9 6990 65000 05/2013 05 2013 2 101 45881 Gelsenkirchen 45881 Gelsenkirchen
Opel Adam Slam ecoFlex 10 6990 95200 03/2015 03 2015 2 101 93104 Sünching 93104 Sünching
Opel Adam Slam 1,4 ecoFlex 1.Hand *Panoramadach* 11 6999 96000 04/2013 04 2013 1 101 55252 Mainz-Kastel 55252 Mainz-Kastel
Opel Adam Slam ecoFlex Sportpaket NAVI~SHZ~KLIMA~USB 12 6999 91000 10/2013 10 2013 1 101 70173 Stuttgart 70173 Stuttgart
Opel Adam 1.4 Slam,Alu 17Zoll,Sportfahrwerk,Top Zustand 13 6999 73900 11/2013 11 2013 1 101 94469 Deggendorf 94469 Deggendorf
Opel Adam 1.4 Jam 8 fach Bereift 14 7400 97144 09/2014 09 2014 1 101 31234 Edemissen / Abbensen 31234 Edemissen
Opel Adam 1.4 Glam 15 7500 51667 07/2013 07 2013 1 101 01723 Wilsdruff 01723 Wilsdruff
Opel Adam Jam'2.Hand'93.602KM'Klima'8-fach' 16 7500 93602 08/2013 08 2013 2 101 45772 Marl 45772 Marl
Opel Adam 1.4 Glam *Pano*Tempo*IntelliLink*PDC*Klima* 17 7560 48357 07/2015 07 2015 1 101 65936 Frankfurt am Main 65936 Frankfurt
Opel Adam Jam 18 7690 51400 05/2013 05 2013 2 101 42327 Wuppertal 42327 Wuppertal
Opel Adam 1.4 Jam 1.Hand unfallfrei 17''LM Bluetooth AUX 19 7700 74000 04/2013 04 2013 1 101 61350 Bad Homburg 61350 Bad
Opel Adam 1.4 Jam IntelliLink/Klimaauto./SHZ/LHZ/PDC 20 7790 62000 12/2014 12 2014 1 101 91154 Roth 91154 Roth
Opel Adam Slam ecoFlex 1 7850 95007 03/2016 03 2016 2 101 84088 Neufahrn 84088 Neufahrn
Opel Adam Jam 2 7900 53000 04/2014 04 2014 1 101 74743 Seckach-Großeicholzheim 74743 Seckach-Großeicholzheim
Opel Adam Slam ( Schnäppchen / unbedingt lesen ) 3 7950 47200 04/2015 04 2015 2 90 26655 Westerstede 26655 Westerstede
Opel Adam 1.4 Slam / DAB / SHZ / PDC / NAVI / SPORT 4 7950 99720 10/2013 10 2013 2 101 84032 Altdorf 84032 Altdorf
Opel Adam Rocks ecoFlex S +Schiebdach+Alufelgen 5 7990 82900 06/2015 06 2015 2 90 88499 Riedlingen 88499 Riedlingen
Opel Adam 1.4 Glam Pano PDC Lenkradh. Sportsitze App 6 7990 61625 01/2014 01 2014 2 101 36251 Bad Hersfeld 36251 Bad
Opel Adam Jam ecoFlex*Klima*Leder*PDC*SHZ*SternLED* 7 7990 52725 04/2013 04 2013 2 101 63165 Mühlheim / Main 63165 Mühlheim
Opel Adam Jam 8 7990 89900 08/2016 08 2016 2 101 38302 Wolfenbüttel 38302 Wolfenbüttel
Opel Adam ADAM JAM ECOFLEX "BILDHÜBSCH UND NEUWERTIG" 9 7999 55000 04/2015 04 2015 1 90 55129 Mainz 55129 Mainz
Opel Adam 1.0 Start/Stop - Sondermodell 10 8280 86985 03/2015 03 2015 1 116 78315 Radolfzell-Böhringen 78315 Radolfzell-Böhringen

Datenaufbereitung

Die Daten sind zwar durch die Scraping-Funktion schon relativ sauber, allerdings möchte ich noch einige zusätzliche Merkmale für die folgende Analyse erstellen. Zunächst nutze ich das Merkmal ez (Datum der Erstzulassung), um das Alter des Gebrauchtwagens in Tagen/Monaten/Jahren zu berechnen. Hierzu verwende ich die Funktionen today() und mdy() aus dem R-Paket lubridate. Vom Opel Adam gibt es (leider) eine Vielzahl von Ausstattungsvarianten, die sich in unterschiedlichster Form auf den Preis niederschlagen könnten. Um dies in der Analyse berücksichtigen zu können, erstelle ich das Merkmal linie, welches durch Textabgleich mit titel2 erstellt wird. Hierbei suche ich den Inseratstitel nach den Ausstattungsvarianten ab (Slam, Jam, Glam, usw.). Bei einigen Inseraten lässt der Titel keine eindeutige Zuweisung einer Ausstattungsvariante zu, sodass ich eine Restkategorie ("8 Sonstige") erstellen musste.

library(lubridate)

cars <- inserate %>%
  mutate(alter_d = today() - mdy(ez),
         alter_m = round(as.numeric(alter_d)/30, digits = 0),
         alter_y = round(as.numeric(alter_d)/365, digits = 1),
         titel2 = tolower(titel2),
         linie = case_when(
           str_detect(titel2, "jam") == TRUE ~ "1 Jam",
           str_detect(titel2, "slam") == TRUE ~ "2 Slam",
           str_detect(titel2, "glam") == TRUE ~ "3 Glam",
           str_detect(titel2, "rocks|open air") == TRUE ~ "4 Rocks",
           str_detect(titel2, "unlimited") == TRUE ~ "5 Unlimited",
           str_detect(titel2, "s 1.4|1.4 turbo s") == TRUE & ps == 150 ~ "6 Adam S",
           str_detect(titel2, "s") == TRUE & ps == 150 ~ "6 Adam S",
           str_detect(titel2, "120") == TRUE ~ "7 120 Jahre",
           TRUE ~ "8 Sonstige"))

Die Standortangabe des Gebrauchtwagens (PLZ und Stadt) nutze ich, um das Inserat einem Bundesland zuzuordnen. Dafür verwende ich einen Schlüssel aus dem Internet und verknüpfe die beiden Dateien.

plz_data<- readr::read_delim("data/plz_zuordnung.csv", delim = ";",
                              locale = locale(encoding = "latin1")) %>%
  mutate(plz = ifelse(nchar(plz) == 4, paste0("0", plz),plz))

cars <- left_join(cars, plz_data, by = "plz")

Die Daten sehen nun so aus und sind bereit für die Auswertung.


titel1 titel2 id preis km ez monat jahr halter ps ort plz stadt alter_d alter_m alter_y linie bundesland kreis typ
Opel Adam 1,4 slam ecoflex s/s panorama 1 4900 150000 07/2013 07 2013 1 101 71679 Asperg 71679 Asperg 3753 days 125 10.3 2 Slam Baden-Württemberg Ludwigsburg Kreis
Opel Adam slam ecoflex 2 5950 125897 07/2013 07 2013 2 101 84088 Neufahrn 84088 Neufahrn 3753 days 125 10.3 2 Slam Bayern Landshut Kreis
Opel Adam 1.4 slam *101ps*teilleder*soundsystem*1.hd* 3 5990 159750 04/2014 04 2014 1 101 31275 Lehrte 31275 Lehrte 3479 days 116 9.5 2 Slam Niedersachsen Hannover Kreis
Opel Adam 1.4 jam*2hd*panorama*garantie*sh*lh*eu5*top 4 6490 133900 04/2013 04 2013 2 101 67549 Worms 67549 Worms 3844 days 128 10.5 1 Jam Rheinland-Pfalz Worms Stadt
Opel Adam glam~lradhzg~parkaut~klimaaut.~sihzg~tempom 5 6699 101700 03/2014 03 2014 1 101 07629 Hermsdorf 07629 Hermsdorf 3510 days 117 9.6 3 Glam Thüringen Saale-Holzland-Kreis Kreis
Opel Adam glam 1.4 shz/pano/unfallfrei! 6 6890 86934 05/2013 05 2013 2 101 99310 Arnstadt 99310 Arnstadt 3814 days 127 10.4 3 Glam Thüringen Ilm-Kreis Kreis
Opel Adam 1.4 jam panoramadach+scheckheft+sitzheizung 7 6950 90154 11/2014 11 2014 2 101 41063 Mönchengladbach 41063 Mönchengladbach 3265 days 109 8.9 1 Jam Nordrhein-Westfalen Mönchengladbach Stadt
Opel Adam glam panoramadach sitzheizung pdc 8 6980 94900 06/2013 06 2013 2 101 91522 Ansbach 91522 Ansbach 3783 days 126 10.4 3 Glam Bayern Ansbach Stadt
Opel Adam glam ecoflex *panorama/lenkradheizung* 9 6990 65000 05/2013 05 2013 2 101 45881 Gelsenkirchen 45881 Gelsenkirchen 3814 days 127 10.4 3 Glam Nordrhein-Westfalen Gelsenkirchen Stadt
Opel Adam slam ecoflex 10 6990 95200 03/2015 03 2015 2 101 93104 Sünching 93104 Sünching 3145 days 105 8.6 2 Slam Bayern Regensburg Kreis
Opel Adam slam 1,4 ecoflex 1.hand *panoramadach* 11 6999 96000 04/2013 04 2013 1 101 55252 Mainz-Kastel 55252 Mainz-Kastel 3844 days 128 10.5 2 Slam Hessen Wiesbaden Stadt
Opel Adam slam ecoflex sportpaket navi~shz~klima~usb 12 6999 91000 10/2013 10 2013 1 101 70173 Stuttgart 70173 Stuttgart 3661 days 122 10.0 2 Slam Baden-Württemberg Stuttgart Stadt
Opel Adam 1.4 slam,alu 17zoll,sportfahrwerk,top zustand 13 6999 73900 11/2013 11 2013 1 101 94469 Deggendorf 94469 Deggendorf 3630 days 121 9.9 2 Slam Bayern Deggendorf Kreis
Opel Adam 1.4 jam 8 fach bereift 14 7400 97144 09/2014 09 2014 1 101 31234 Edemissen / Abbensen 31234 Edemissen 3326 days 111 9.1 1 Jam Niedersachsen Peine Kreis
Opel Adam 1.4 glam 15 7500 51667 07/2013 07 2013 1 101 01723 Wilsdruff 01723 Wilsdruff 3753 days 125 10.3 3 Glam Sachsen Sächsische Schweiz-Osterzgebirge Kreis
Opel Adam jam'2.hand'93.602km'klima'8-fach' 16 7500 93602 08/2013 08 2013 2 101 45772 Marl 45772 Marl 3722 days 124 10.2 1 Jam Nordrhein-Westfalen Recklinghausen Kreis
Opel Adam 1.4 glam *pano*tempo*intellilink*pdc*klima* 17 7560 48357 07/2015 07 2015 1 101 65936 Frankfurt am Main 65936 Frankfurt 3023 days 101 8.3 3 Glam Hessen Frankfurt am Main Stadt
Opel Adam jam 18 7690 51400 05/2013 05 2013 2 101 42327 Wuppertal 42327 Wuppertal 3814 days 127 10.4 1 Jam Nordrhein-Westfalen Wuppertal Stadt
Opel Adam 1.4 jam 1.hand unfallfrei 17''lm bluetooth aux 19 7700 74000 04/2013 04 2013 1 101 61350 Bad Homburg 61350 Bad 3844 days 128 10.5 1 Jam Hessen Hochtaunuskreis Kreis
Opel Adam 1.4 jam intellilink/klimaauto./shz/lhz/pdc 20 7790 62000 12/2014 12 2014 1 101 91154 Roth 91154 Roth 3235 days 108 8.9 1 Jam Bayern Roth Kreis
Opel Adam slam ecoflex 1 7850 95007 03/2016 03 2016 2 101 84088 Neufahrn 84088 Neufahrn 2779 days 93 7.6 2 Slam Bayern Landshut Kreis
Opel Adam jam 2 7900 53000 04/2014 04 2014 1 101 74743 Seckach-Großeicholzheim 74743 Seckach-Großeicholzheim 3479 days 116 9.5 1 Jam Baden-Württemberg Neckar-Odenwald-Kreis Kreis
Opel Adam slam ( schnäppchen / unbedingt lesen ) 3 7950 47200 04/2015 04 2015 2 90 26655 Westerstede 26655 Westerstede 3114 days 104 8.5 2 Slam Niedersachsen Ammerland Kreis
Opel Adam 1.4 slam / dab / shz / pdc / navi / sport 4 7950 99720 10/2013 10 2013 2 101 84032 Altdorf 84032 Altdorf 3661 days 122 10.0 2 Slam Bayern Landshut Kreis
Opel Adam rocks ecoflex s +schiebdach+alufelgen 5 7990 82900 06/2015 06 2015 2 90 88499 Riedlingen 88499 Riedlingen 3053 days 102 8.4 4 Rocks Baden-Württemberg Alb-Donau-Kreis Kreis
Opel Adam 1.4 glam pano pdc lenkradh. sportsitze app 6 7990 61625 01/2014 01 2014 2 101 36251 Bad Hersfeld 36251 Bad 3569 days 119 9.8 3 Glam Hessen Hersfeld-Rotenburg Kreis
Opel Adam jam ecoflex*klima*leder*pdc*shz*sternled* 7 7990 52725 04/2013 04 2013 2 101 63165 Mühlheim / Main 63165 Mühlheim 3844 days 128 10.5 1 Jam Hessen Offenbach Kreis
Opel Adam jam 8 7990 89900 08/2016 08 2016 2 101 38302 Wolfenbüttel 38302 Wolfenbüttel 2626 days 88 7.2 1 Jam Niedersachsen Wolfenbüttel Kreis
Opel Adam adam jam ecoflex "bildhübsch und neuwertig" 9 7999 55000 04/2015 04 2015 1 90 55129 Mainz 55129 Mainz 3114 days 104 8.5 1 Jam Rheinland-Pfalz Mainz Stadt
Opel Adam 1.0 start/stop - sondermodell 10 8280 86985 03/2015 03 2015 1 116 78315 Radolfzell-Böhringen 78315 Radolfzell-Böhringen 3145 days 105 8.6 8 Sonstige Baden-Württemberg Konstanz Kreis

Datenanalyse

Im Folgenden zeige ich einige Eckwerte des Datensatzes. Über die Hälfte der Inserate stammen aus dem Jahr 2017 und 2018. Immerhin ein Viertel stammt aus den Jahren 2015 und 2016. Für die Jahre 2013 und 2014 ist die Datenlage eher dürftig - hier gibt es insgesamt nur 33 Inserate.

cars %>%
  group_by(jahr) %>%
  summarise(n = n()) %>%
  mutate(freq = n / sum(n) * 100) %>%
  mutate(ypos = cumsum(freq)- 0.5*freq ) %>%
  ggplot(data = .,
      aes(x= "", y = freq, fill = forcats::fct_rev(jahr))) +
  geom_col(width = 1)  + 
  coord_polar(theta = "y", start = 0) +
  geom_text(aes(y = ypos, label = jahr), color = "white", size = 4, angle = 65) +
  theme_void() +
  theme(legend.position="none")

Im Balkendiagramm unten sieht man, dass bei den Inserate mit einer Erstzulassung in den Jahren 2013 und 2014 nicht alle Motorvarianten des Opel Adam vorliegen. In den folgenden Jahren sind die Anteile relativ ausgeglichen, wodurch ich mich bei der späteren Analyse wohl auf diese 4 Jahre beschränken muss.

ggplot(data = cars,
      aes(x= factor(jahr), fill=factor(ps))) +
  geom_bar(position = "fill", color = "black") +
  scale_y_continuous(labels = scales::percent) +
  labs(x = "Jahr der Erstzulassung", 
       y = "Anteil in %",
       fill = "Leistung (PS)")

Auch bei den Ausstattungslinien gibt es für die Erstzulassungen in 2013 und 2014 nicht alle Varianten. In den folgenden Jahren ist das Verhältnis wieder einigermaßen ausgeglichen, wobei gemessen an der Anzahl verfügbarer Inserate, die absoluten Fallzahlen pro Kategorie relativ niedrig sein werden.

ggplot(data = cars,
      aes(x= factor(jahr), fill=factor(linie))) +
  geom_bar(position = "fill", color = "black") +
  scale_y_continuous(labels = scales::percent) +
  labs(x = "Jahr der Erstzulassung", 
       y = "Anteil in %",
       fill = "Ausstattungslinie")

Die Boxplots zeigen die Verteilung des Verkaufspreises nach dem Jahr der Erstzulassung und der Leistung der Inserate. Der Opel Adam mit 150 PS weist ab EZ 2015 den höchsten Median-Preis auf. Bei der Variante mit 116 PS Variante liegt der Median Preis für EZ in 2018 und 2017 über den beiden Modellen mit dem kleineren Motor. Für Modelle mit EZ 2016 ist der Median-Preis zur 101 PS Variante nahezu identisch. Die Motorausführungen mit 90 PS und 101 PS unterscheiden sich preislich nur für Inserate mit EZ 2016.

ggplot(data = cars,
       aes(x = factor(jahr), y = preis, fill = factor(ps))) +
  geom_boxplot() +
  scale_y_continuous(breaks = seq(2500,20000,2500),
                     labels = scales::number_format()) +
  labs(x = "Jahr der Erstzulassung", 
       y = "Preis in Euro", 
       fill = "Leistung (PS)")

Hinsichtlich der Ausstattungsvarianten gibt es ebenfalls preisliche Unterschiede. Allerdings muss berücksichtigt werden, dass manche Ausstattungslinien wie der Adam S stellvertretend für die Motorvariante mit 150 PS stehen und nicht für die anderen Motorvarianten verfügbar sind. Die Linie 120 Jahre scheint es laut den Daten nur für die Motorisierung mit 101 PS und 116 PS zu geben.

ggplot(data = cars,
       aes(x=factor(linie), y =preis)) +
  geom_boxplot() +
  coord_flip() +
  labs(x = "Ausstattungslinie", y = "Preis in €")

Berücksichtigt man die Motorisierung, sind die preislichen Unterschiede zwischen den Ausstattungslinien deutlich geringer.

ggplot(data = cars,
       aes(x=factor(linie), y =preis)) +
  geom_boxplot() +
  coord_flip() +
  facet_grid(.~ps) +
  scale_y_continuous(breaks = c(7500, 15000)) +
  labs(x = "Ausstattungslinie", y = "Preis in €")

In der nächsten Grafik ist der Zusammenhang zwischen dem Verkaufspreis und der Laufleistung in Kilometern dargstellt. Die 150 PS und 116 PS Motorvarianten weisen in den ersten 25.000 Kilometern den stärksten Wertverfall auf. Die 101 PS Motorvariante ist in diesem Intervall wertstabiler, wobei nach 25.000 Kilometern der Preis stärker sinkt.

ggplot(data = cars,
       aes(x=km, y =preis, col = factor(ps))) +
  geom_point(alpha = 0.5) +
  geom_smooth(method = "loess",se = FALSE) +
  scale_x_continuous(breaks = seq(0,175000,25000),labels = scales::number_format()) +
  scale_y_continuous(breaks = seq(2500,20000,2500),labels = scales::number_format()) +
  labs(x = "Laufleistung in Kilometer",
       y = "Preis in €",
       col = "Leistung (PS")

Da die Laufleistung der Fahrzeuge deutlich rechtsschief verteilt ist, erstelle ich die Grafik erneut mit der logarithmierten Laufleistung und entferne aus Gründen der Übersichtlickeit zwei Inserate mit einer Laufleistung von unter 1000 Kilometern. In dieser Darstellung wirkt sich jeder zusätzlich gefahrene Kilometer besonders negativ auf den Verkaufspreis der 150 PS und 101 PS Variante aus.

cars %>%
  filter(km > 1000) %>%
ggplot(data = .,
         aes(x=log(km), y =preis, col = factor(ps))) +
  geom_point(alpha = 0.5) +
  geom_smooth(method = "lm",se = F) +
  scale_x_continuous(limits = c(7,12),
                     breaks=c(7:12), 
                     labels=c(round(exp(7:12)))) +
  scale_y_continuous(breaks = seq(2500,20000,2500),labels = scales::number_format()) +
  labs(x = "Log(Laufleistung in km)",
       y = "Preis in €",
       col = "Leistung (PS)")

Die vorherigen beiden Grafiken berücksichtigen die Laufleistung, jedoch nicht das Alter der Gebrauchtwagen. Unten ist daher der Zusammenhang von Verkaufspreis und dem Alter des Gebrauchtwagen in Monaten abgebildet. Je größer ein Punkt im Steudiagramm ist, um so höher die Laufleistung des Autos. Auch das Alter wirkt sich negativ auf den Verkaufspreis aus und man erkennt ähnliche Muster wie in der vorherigen Darstellung.

ggplot(data = cars,
         aes(x=alter_y, y =preis, col = factor(ps))) +
  geom_point(aes(size = km), alpha = 0.5) +
  geom_smooth(se = F) +
  scale_x_continuous(limits = c(2, 8), breaks = c(seq(2, 8, 0.5))) +
  scale_y_continuous(breaks = seq(2500,20000,2500),labels = scales::number_format()) +
  labs(x = "Alter des Fahrzeugs (in Jahren)",
       y = "Preis in €",
       col = "Leistung (PS)", size = "Laufleistung (km)")

Num zum spannenden Teil dieses Artikels: mittels linearer Regression (OLS) möchte den Einfluss verschiedener Charakteristiken des Autos auf seinen Verkaufspreis schätzen. Bspw. um herauszufinden, welche der 4 Motorvarianten den stärksten Wertverlust mit steigendem Alter und stegender Laufleistung aufweist. Ich beschränke diese Analyse auf Modelle mit Erstzulassungen in den Jahren 2015 bis 2018.

Basis Modell: Als Regressoren verwende ich die Laufleistung pro 10.000 gefahrene Kilometer, sowie das Jahr der Erstzulassung und die Motorvariante als Kontrollvariablen.

model1 <- lm(preis ~  I(km/10000) + factor(jahr) + factor(ps), 
            data = cars, subset = jahr %in% c(2015:2018))
## # A tibble: 8 x 5
##   term             estimate std.error statistic   p.value
##   <chr>               <dbl>     <dbl>     <dbl>     <dbl>
## 1 (Intercept)       10885.      277.     39.2   7.94e-139
## 2 I(km/10000)        -360.       32.4   -11.1   4.10e- 25
## 3 factor(jahr)2016    800.      211.      3.80  1.70e-  4
## 4 factor(jahr)2017   1388.      194.      7.15  4.29e- 12
## 5 factor(jahr)2018   2003.      219.      9.14  3.33e- 18
## 6 factor(ps)101        60.1     185.      0.324 7.46e-  1
## 7 factor(ps)116       743.      185.      4.02  7.06e-  5
## 8 factor(ps)150      2797.      189.     14.8   7.41e- 40

Der Koeffizient von I(km/10000) zeigt, dass der Verkaufspreis pro 10.000 gefahrene Kilometer im Durchschnitt um 360 Euro sinkt. Die Koeffizienten der Faktorvariablen Jahr der Erstzulassung geben an, dass relativ zum Basisjahr 2015, Modelle zugelassen in 2016, 2017, 2018 im Durchschnitt einen 800, 1388, 2000 Euro höheren Verkaufspreis aufweisen (unabhängig von der Motorvariante). Der Preisunterschied der Motorvariante 101 PS im Vergleich zur Basis 90 PS liegt bei nur durchschnittlich 60 Euro. Der Preisunterschied bzgl. der 116 PS und 150 PS Variante fällt höher aus - im Durchschnitt 743 bzw. 2797 Euro.


Erweitertes Modell: Nun füge ich weitere Merkmale wie die Anzahl der Fahrzeughalter, die Ausstattungslinie und den Standort des Wagen (Bundesland) als Kontrollvariablen hinzu.

model2 <- lm(preis ~ I(km/10000) + factor(jahr) + factor(ps) + 
               factor(halter) + factor(linie) + factor(bundesland), 
            data = cars, subset = jahr %in% c(2015:2018))
## # A tibble: 30 x 5
##    term                estimate std.error statistic   p.value
##    <chr>                  <dbl>     <dbl>     <dbl>     <dbl>
##  1 (Intercept)         10204.       284.    36.0    1.09e-123
##  2 I(km/10000)          -367.        29.4  -12.5    3.76e- 30
##  3 factor(jahr)2016      733.       191.     3.85   1.39e-  4
##  4 factor(jahr)2017     1263.       176.     7.19   3.53e- 12
##  5 factor(jahr)2018     2033.       198.    10.3    5.60e- 22
##  6 factor(ps)101          -2.30     171.    -0.0135 9.89e-  1
##  7 factor(ps)116         649.       166.     3.91   1.08e-  4
##  8 factor(ps)150        2088.       274.     7.62   2.15e- 13
##  9 factor(halter)2      -335.       106.    -3.15   1.76e-  3
## 10 factor(linie)2 Slam  1054.       196.     5.38   1.31e-  7
## # ... with 20 more rows

Der Einfluss der Laufleistung auf den Verkaufspreis hat sich nur geringfügig verändert - von 360 auf 367 Euro Wertverlust pro 10.000 gefahrene Kilometer. Der Einfluss dieser zusätzlichen Kontrollvariablen scheint also nur von geringer Bedeutung zu sein.


Interaktions-Modell 1:

Nicht untersucht wurde bisher, ob der Einfluss der Laufleistung auf den Verkaufspreis von der Motorvariante abhängt. Dies ist eine sog. Interkation der beiden Regressoren, Ich logarithmiere zudem nun die Laufleistung. Um die Schätzergebnisse zu veranschaulichen verwende ich einen Conditional Effects Plot (CEP).

model_cep1 <- lm(preis ~ log(km/10000)*factor(ps), 
                 data = cars, subset = jahr %in% c(2015:2018))
## # A tibble: 8 x 5
##   term                        estimate std.error statistic   p.value
##   <chr>                          <dbl>     <dbl>     <dbl>     <dbl>
## 1 (Intercept)                  12391.       306.   40.5    3.53e-143
## 2 log(km/10000)                -1503.       240.   -6.27   9.25e- 10
## 3 factor(ps)101                   24.1      358.    0.0675 9.46e-  1
## 4 factor(ps)116                  638.       355.    1.80   7.28e-  2
## 5 factor(ps)150                 2997.       359.    8.35   1.16e- 15
## 6 log(km/10000):factor(ps)101    251.       284.    0.884  3.77e-  1
## 7 log(km/10000):factor(ps)116    115.       282.    0.407  6.84e-  1
## 8 log(km/10000):factor(ps)150   -162.       274.   -0.592  5.54e-  1

Unten findet man den CEP für dieses Modell.

tibble(
  km = rep(seq(from = 20000, to = 80000, by = 5000), 4),
  ps = factor(rep(c("90", "101","116","150"), each = 13))
  ) %>%
  mutate(fit = predict(model_cep1, 
                       newdata = ., 
                       type = "response"),
         se = predict(model_cep1, 
                      newdata = ., 
                      type = "response",
                      se = TRUE)$se.fit,
         ll = fit - (1.96 * se),
         ul = fit + (1.96 * se)) %>%
  ggplot(data = ., 
         aes(x = km, y = fit)) + 
  geom_ribbon(aes(ymin = ll,
                  ymax = ul, fill = ps), alpha = 0.2) + 
  geom_line(aes(colour = ps),
            size = 1) +
  scale_x_continuous(limits = c(20000,80000),
                     breaks = seq(20000,80000,10000), labels =scales::number_format()) +
  scale_y_continuous(limits = c(8000,16000),
                     breaks = seq(8000,16000,2000),labels = scales::number_format()) +
  labs(x = "Laufleistung in km",
       y = "Vorhergesagter Preis in €", 
       fill = "Laufleistung (PS)", col = "Laufleistung (PS)")

Die Koeffizienten der Interaktionsterme und der CEP zeigen, dass es nur geringe, aber signifikante Unterschiede zwischen dem Einfluss der Laufleistung auf den Verkaufspreis in Abhängigkeit der Motorvariante gibt. Verdoppelt man die Laufleistung, so ist der Verkaufspreis für die 101 PS Variante höher als bei der 90 PS oder 150 PS Variante. Die 116 PS Variante liegt ebenfalls hinter der 101 PS Variante zurück, schlägt aber ebenso die 90 PS oder 150 PS Variante.


Interaktions-Modell 2:

Für das nächste Modell interagiere ich analog zum vorherigen Modell das Alter des PKWs in Monaten und die Motorvariante. Zusätzlich nehme ich als Regressor das quadrierte Alter auf, um zu überprüfen, ob sich der Wertverfall mit steigendem Alter des Gebrauchtwagens beschleunigt oder nicht.

model_cep2 <- lm(preis ~ alter_m*factor(ps) + I(alter_m^2),
                data = cars, subset = jahr %in% c(2015:2018))
## # A tibble: 9 x 5
##   term                   estimate std.error statistic  p.value
##   <chr>                     <dbl>     <dbl>     <dbl>    <dbl>
## 1 (Intercept)           19483.     2212.       8.81   4.00e-17
## 2 alter_m                -141.       53.1     -2.65   8.47e- 3
## 3 factor(ps)101          -189.     1093.      -0.173  8.63e- 1
## 4 factor(ps)116           876.     1035.       0.846  3.98e- 1
## 5 factor(ps)150          4769.     1103.       4.32   1.94e- 5
## 6 I(alter_m^2)              0.368     0.319    1.15   2.51e- 1
## 7 alter_m:factor(ps)101     3.62     14.0      0.258  7.96e- 1
## 8 alter_m:factor(ps)116    -0.861    13.0     -0.0660 9.47e- 1
## 9 alter_m:factor(ps)150   -26.7      13.9     -1.92   5.60e- 2

Der Koeffizient des Alters ist negativ und der Koeffizient des quadrierten Alters ist positiv. Somit ist der Wertverfall des PKW in den ersten Jahren besonders hoch und wird mit zunehmendem Alter geringer. Dies erkennt man auch im CEP anhand den abflachenden Kurven. Für die 150 PS Variante ist der Wertverfall über das Alter hinweg erneut am höchsten - relativ zu den anderen 3 Motorvarianten.

tibble(
  alter_m = rep(seq(from = min(cars$alter_m)+6, to = max(cars$alter_m)-24, length.out = 7), 4),
  ps = factor(rep(c("90", "101","116","150"), each = 7))
  ) %>%
  mutate(fit = predict(model_cep2, 
                       newdata = ., 
                       type = "response"),
         se = predict(model_cep2, 
                      newdata = ., 
                      type = "response",
                      se = TRUE)$se.fit,
         ll = fit - (1.96 * se),
         ul = fit + (1.96 * se)) %>%
  ggplot(data = ., 
         aes(x = alter_m, y = fit)) + 
  geom_ribbon(aes(ymin = ll,
                  ymax = ul, fill = ps), alpha = 0.2) + 
  geom_line(aes(colour = ps),
            size = 1)  +
  scale_y_continuous(limits = c(6000,16000),
                     breaks = seq(6000,16000,2000),labels = scales::number_format()) +
  labs(x = "Alter in Monaten",
       y = "Vorhergesagter Preis in €", 
       fill = "Laufleistung (PS)", col = "Laufleistung (PS)")

Vorhersage:

Zum Schluss möchte ich für ein gegebenes Baujahr und Modell die zukünftige Verkaufspreisentwicklung vorhersagen. Ich habe einen Opel Adam mit 116 PS, erstmalig zugelassen im September 2017 und einer Laufleistung von aktuell 33.000 Kilometern in die engere Auswahl genommen. Ich möchte das Auto nun 4 Jahre lang halten und im Jahr etwa 14.000 Kilometer fahren. Das zukünftige Alter des Autos (in Monaten) ergibt sich somit aus dem aktuellen Alter und den 4 von mir genutzten Jahren (41 + 48 = 89). Die zukünftige Laufleistung ergibt sich aus dem aktuellen Kilometerstand und den in 4 Jahren von mir gefahrenen Kilometern (33.000 + 56.000 = 89.000).

model_vorhersage <- lm(preis ~ alter_m + I(alter_m^2) + log(km),
                       data = cars, subset = ps == 116)

vorhersagen <- tibble(
  alter_m = c(41, 89),
  km = c(33000, 89000)
  ) %>%
  mutate(fit = predict(model_vorhersage, 
                       newdata = ., 
                       type = "response"),
         se = predict(model_vorhersage, 
                      newdata = ., 
                      type = "response",
                      se = TRUE)$se.fit,
         ll = fit - (1.96 * se),
         ul = fit + (1.96 * se))

Unten findet man die Ergebnisse des Modells. Der aktuelle Verkaufspreis liegt bei durchschnittlich 11.812 Euro, wobei hier die Ausstattungslinie keine Berücksichtigung findet. In 4 Jahren und 56.000 gefahrenen Kilometern würde laut dem Modell der durchschnittliche Verkaufspreis noch bei 8275 Euro liegen. Dies entspricht einem durchschnittlichen Wertverlust von rund 884 Euro pro Jahr.


alter_m km fit se ll ul
41 33000 13825.23 876.6929 12106.91 15543.54
89 89000 10391.43 198.1450 10003.06 10779.79
Posted on:
February 9, 2021
Length:
23 minute read, 4760 words
Categories:
R Finanzen
See Also: