Beschleunigte Preiszyklen in 2019

Tankstellen erhöhen und senken die Kraftstoffpreise mehrmals täglich. Für die Verbraucher kann es entsprechend erheblich günstiger sein, zu Zeiten zu tanken, welche im Tagesverlauf zu den Tiefpunkten gehören. Dank der Preismeldepflicht seit 2014 ist es möglich umfassend das Preissetzungsverhalten der Tankstellen zu analysieren.

In 2019 haben sich die Preiszyklen an den Tankstellen erneut geändert, dabei hat sich der seit Beginn der Meldepflicht zu beobachtende Trend hin zu mehr Preiszyklen pro Tag weiter fortgesetzt.


Preiszyklen Super E5 nach Stunden in 2019: Durchschnittlicher Super E5 Preis in Euro nach Stunden und Monaten, bereinigt um den monatlichen Mittelwert. Datenbasis sind sämtliche Preisänderungen im Beobachtungszeitraum 01.01.2019 bis 31.12.2019. Durchschnittliche Preise im 15-Miunten-Takt, der 10 Uhr Preis bezieht sich auf alle gemeldeten Preisänderungen zwischen 10:00:00 Uhr und 10:14:59 Uhr.

Im ersten Quartal 2019 konnte noch das aus 2018 bekannte Muster beobachtet werden: Erster Preishochpunkt um 6 Uhr gefolgt von drei weiteren Hochpunkten um 12 Uhr, 17 Uhr und 22 Uhr. Seit April 2019 hingegen ist der erste tägliche Preisanstieg bereits um 5 Uhr zu verzeichnen. Dies stellt ebenfalls den (durchschnittlich) den Tageshöchstpreis dar und liegt im Mittel rund 13 Cent über dem Tagestief. Weitere Tageshochs sind um 10 Uhr, 13 Uhr, 19 Uhr und 22 Uhr zu verzeichnen. Am günstigsten ist das Tanken durchschnittlich zwischen 20 und 22 Uhr (vor der abendlichen Preiserhöhung). Im Tagesverlauf ist tanken in einem 15 Minuten Fenster nach 9:45 Uhr, 12:45 Uhr, 18:45 Uhr und 21:45 Uhr am günstigsten.

Dieses Preissetzungsverhalten zeigt sich über die Monate hinweg äußerst konsistent, es sind kaum saisonale Einflüsse festzustellen nachdem für den jeweiligen Monatsmittelwert korrigiert wurde.

Während die Kraftstoffpreise im Tagesverlauf deutlich schwanken und es somit günstige und teure Zeitpunkte zum tanken gibt, zeigt eine Betrachtung nach Wochentagen kaum Unterschiede zwischen den Preisen. Dieses Muster konnte bereits in den Vorjahren beobachtet werden und hat sich nicht geändert.


Preiszyklen Super E5 nach Tagen in 2019: Durchschnittlicher Super E5 Preis in Euro nach Tagen und Monaten, bereinigt um den monatlichen Mittelwert. Datenbasis sind sämtliche Preisänderungen im Beobachtungszeitraum 01.01.2019 bis 31.12.2019. Durchschnittliche Preise im 1-Stunden-Takt, der 10 Uhr Preis bezieht sich auf alle gemeldeten Preisänderungen zwischen 10:00:00 Uhr und 10:59:59 Uhr.

In Bezug auf die Schwankungsbreite im Verlauf eines Tages oder einer Woche ist ebenfalls keine signifikante Änderung zu den Vorjahren festzustellen.

Abbildungen und der zugehörige R-Code zu den Preiszyklen in den vorherigen Jahren und für 2019 finden sich in den jeweiligen Jahresübersichten: 2014, 2015, 2016, 2017, 2018, 2019.

PostgreSQL Tabelle mit Tankstellendaten

Um die Kraftstoffpreise nicht nur im zeitlichen Verlauf zu analysieren, sondern auch nach verschiedenen Regionen oder Marken sind die Tankstelleninformationen notwendig. Tankerkönig stellt auch diese Daten in seinem git-Repository zur Verfügung.

Wie schon die Kraftstoffpreise liegen auch die Tankstellendaten als csv-Datei vor. Täglich wird eine neue csv-Datei veröffentlicht, welche alle aktuellen Tankstellen Daten enthält. Diese Daten können ebenfalls mit einem git pull täglich abgerufen werden.

Tabelle erstellen

Die csv-Dateien enthalten nicht nur die Namen der Tankstellen, sondern auch die Adresse (Straße, Hausnummer, Stadt, Postleitzahl), die geographische Position (Längen- und Breitengrad), Marke und das Datum der Eröffnung. Weiter werden auch die aktuellen Öffnungszeiten im JSON-Format hinterlegt, diese überspringe ich allerdings. Die Adressdaten sind leider nicht einheitlich formatiert und enthalten Tippfehler, dies gilt ebenso für Name und Marke. Die Längen- und Breitengrad Informationen sind hier am zuverlässigsten.

Um in späteren Analysen die Tankstellen Bundesländern, Kreisen und Gemeinden zuzuordnen ergänze ich die Tankstellendaten um Bundesland-, Kreis- und Gemeindekennziffer sowie Name der jeweiligen Region. Aus dieser Datenstruktur ergibt sich folgende PostgreSQL Tabelle:

CREATE TABLE stations (
  uuid			uuid					NOT NULL,	
  name			character varying(250)	NOT NULL,
  brand			character varying(150),				
  street		character varying(250),			
  house_number	character varying(150),			
  post_code		char(5),								
  city			character varying(150),	
  latitude		numeric(15,12)			NOT NULL,	
  longitude		numeric(15,12)			NOT NULL,	
  first_active	timestamptz,
  bl_kz			char(2),
  bl_name		character varying(150),
  kreis_kz      char(5),
  kreis_name    character varying(250),
  gemeinde_kz   char(12),
  gemeinde_name character varying(250)
);

Einlesen der Tankstellendaten mit R

Zur Verarbeitung der Daten benutze ich folgende R-Pakete

  • RPostgreSQL, um eine Verbindung zur Datenbank herzustellen
  • readr, zum einlesen der csv-Dateien
  • sp und rgdal, zur Verarbeitung der Geoinformationen
# Packages
library(RPostgreSQL)
library(readr)
library(sp)
library(rgdal)

Die aktuellsten Tankstelleninformationen können am einfachsten in Abhängigkeit des aktuellen Datums eingelesen werden, sofern man den letzten Datenstand heruntergeladen hat. Der Name der csv-Datei sowie der genaue Dateipfad lässt sich auf Basis des gestrigen Datums erstellen.

# File name and path
yesterday <- Sys.Date()-1
year <- format(yesterday,"%Y")
month <- format(yesterday,"%m")
day <- format(yesterday,"%d")
pathtoserver <- file.path("//192.168.1.10", "dbdata", "tankerkoenig", "stations", year, month)
filename <- paste(pathtoserver, "/", paste(year, month, day, sep = "-"), "-stations.csv", sep = "")

Anschließend kann die csv-Datei geladen werden und die Daten in einen Dataframe gespeichert werden.

# Stations data
stations <- read_csv(filename, 
                     col_types = cols(brand = col_character(), 
                                      city = col_character(), 
                                      first_active = col_character(),
                                      house_number = col_character(), 
                                      latitude = col_number(), 
                                      longitude = col_number(), 
                                      name = col_character(), 
                                      openingtimes_json = col_skip(), 
                                      post_code = col_character(), 
                                      street = col_character(),
                                      uuid = col_character()),
                     skip = 0,
                     na = c("nicht","Nicht"))

Die zusätzlichen Geoinformationen entnehme ich dem Shapefile der Verwaltungsgrenzen (WGS84), welcher als Begleitmaterial zum Zensus 2011 veröffentlicht wurde. Aktuellere Daten können beim Bundesamt für Kartographie und Geodäsie heruntergeladen werden.

Die Daten für Bundesländer, Kreise und Gemeinden werden einzeln in R geladen.

# Geo data
kreise <- readOGR(dsn="data/map_zensus2011", layer="VG250_Kreise", encoding = "UTF-8")
laender <- readOGR(dsn="data/map_zensus2011", layer="VG250_Bundeslaender", encoding = "UTF-8")
gemeinde <- readOGR(dsn="data/map_zensus2011", layer="VG250_Gemeinden", encoding = "UTF-8")

Zum abrufen der zusätzlichen Geoinformationen für jede Tankstelle nutze ich die kurze Funktion getinfo(), welche die benötigten Daten aus den Geodaten ausliest.

getinfo <- function(in.spat){
  out = vector(mode="character", length=6)
  out[1] = as.character(over(in.spat, laender)$RS)
  out[2] = as.character(over(in.spat, laender)$GEN)
  out[3] = as.character(over(in.spat, kreise)$RS)
  out[4] = as.character(over(in.spat, kreise)$GEN)
  out[5] = as.character(over(in.spat, gemeinde)$RS)
  out[6] = as.character(over(in.spat, gemeinde)$GEN)
  return(out)
}

Die zusätzlichen Informationen speichere ich in einzelnen Vektoren, welche anschließend mittels cbind() den bestehenden Tankstellendaten hinzugefügt werden.

# Initialize vectors
bl_kz <- vector(mode = "character", length = dim(stations)[1])
bl_name <- vector(mode = "character", length = dim(stations)[1])
kreis_kz <- vector(mode = "character", length = dim(stations)[1])
kreis_name <- vector(mode = "character", length = dim(stations)[1])
gemeinde_kz <- vector(mode = "character", length = dim(stations)[1])
gemeinde_name <- vector(mode = "character", length = dim(stations)[1])

Um das auslesen der Geoinformationen zu beschleunigen extrahiere ich zunächst die Längen- und Breitengrade der Tankstellen, um nicht bei jedem Zugriff den ganzen Dataframe mit allen Daten öffnen zu müssen. Die extrahierten Längen- und Breitengrade transformieren ich dann in ein Spatial-Objekt mit dem geodätischen Referenzsystem WGS84.

# Geodetic data
lat <- stations$latitude
lon <- stations$longitude
coords <- as.data.frame(cbind(lon,lat))
coords.spat <- SpatialPoints(coords)
proj4string(coords.spat) <- proj4string(kreise)

Anschließend kann für jede Tankstelle die benötigten Zusatzinformationen (Kennziffer und Name der Region) abgerufen und gespeichert werden. Hierbei nutze ich zudem einen Fortschrittsbalken, da der Prozess einige Minuten dauern kann.

# Get additonal geoinformation
progress <- txtProgressBar(min=1, max=dim(stations)[1], style = 3, width = 100)

for(i in 1:dim(stations)[1]){
  info <- getinfo(coords.spat[i,])
  bl_kz[i] <- info[1]
  bl_name[i] <- info[2]
  kreis_kz[i] <- info[3]
  kreis_name[i] <- info[4]
  gemeinde_kz[i] <- info[5]
  gemeinde_name[i] <- info[6]
  setTxtProgressBar(progress, i)
}

Mittels cbind() werden dann die Geoinformationen zu den bestehenden Tankstelleninformationen hinzugefügt.

stations <- cbind(stations, bl_kz, bl_name, kreis_kz, kreis_name, gemeinde_kz, gemeinde_name)

Abschließend können die Daten in die PostgreSQL Tabelle überführt werden, hierzu stellt man zunächst eine Verbindung zur Datenbank her und überträgt anschließend die Tankstelleninformationen.

# Connect to database
drv <- dbDriver("PostgreSQL") # DBDriver
dbn <- "fuelprice" # Database name
dbh <- "192.168.1.10" # DB Host
dbp <- 5432 # DB Port
dbu <- "dbuser" # DB User
dbpw <- "dbpassword" # DB Password

# Connection
dbcon <- dbConnect(drv = drv, dbname=dbn, host=dbh ,port=dbp, user=dbu, password=dbpw)

# Write to db
dbWriteTable(dbcon, c("stations"), value = stations, append = T, row.names = F)

# Disconnect
dbDisconnect(dbcon)

# Clear Working Space
rm(list=ls())

Der komplette Code

# Packages
library(RPostgreSQL)
library(readr)
library(sp)
library(rgdal)

# File name and path
yesterday <- Sys.Date()-1
year <- format(yesterday,"%Y")
month <- format(yesterday,"%m")
day <- format(yesterday,"%d")
pathtoserver <- file.path("//192.168.1.10", "dbdata", "tankerkoenig", "stations", year, month)
filename <- paste(pathtoserver, "/", paste(year, month, day, sep = "-"), "-stations.csv", sep = "")

# Stations data
stations <- read_csv(filename, 
                     col_types = cols(brand = col_character(), 
                                      city = col_character(), 
                                      first_active = col_character(),
                                      house_number = col_character(), 
                                      latitude = col_number(), 
                                      longitude = col_number(), 
                                      name = col_character(), 
                                      openingtimes_json = col_skip(), 
                                      post_code = col_character(), 
                                      street = col_character(),
                                      uuid = col_character()),
                     skip = 0,
                     na = c("nicht","Nicht"))

# Geo data
kreise <- readOGR(dsn="data/map_zensus2011", layer="VG250_Kreise", encoding = "UTF-8")
laender <- readOGR(dsn="data/map_zensus2011", layer="VG250_Bundeslaender", encoding = "UTF-8")
gemeinde <- readOGR(dsn="data/map_zensus2011", layer="VG250_Gemeinden", encoding = "UTF-8")

getinfo <- function(in.spat){
  out = vector(mode="character", length=6)
  out[1] = as.character(over(in.spat, laender)$RS)
  out[2] = as.character(over(in.spat, laender)$GEN)
  out[3] = as.character(over(in.spat, kreise)$RS)
  out[4] = as.character(over(in.spat, kreise)$GEN)
  out[5] = as.character(over(in.spat, gemeinde)$RS)
  out[6] = as.character(over(in.spat, gemeinde)$GEN)
  return(out)
}

# Initialize vectors
bl_kz <- vector(mode = "character", length = dim(stations)[1])
bl_name <- vector(mode = "character", length = dim(stations)[1])
kreis_kz <- vector(mode = "character", length = dim(stations)[1])
kreis_name <- vector(mode = "character", length = dim(stations)[1])
gemeinde_kz <- vector(mode = "character", length = dim(stations)[1])
gemeinde_name <- vector(mode = "character", length = dim(stations)[1])

# Geodetic data
lat <- stations$latitude
lon <- stations$longitude
coords <- as.data.frame(cbind(lon,lat))
coords.spat <- SpatialPoints(coords)
proj4string(coords.spat) <- proj4string(kreise)

# Get additonal geoinformation
progress <- txtProgressBar(min=1, max=dim(stations)[1], style = 3, width = 100)

for(i in 1:dim(stations)[1]){
  info <- getinfo(coords.spat[i,])
  bl_kz[i] <- info[1]
  bl_name[i] <- info[2]
  kreis_kz[i] <- info[3]
  kreis_name[i] <- info[4]
  gemeinde_kz[i] <- info[5]
  gemeinde_name[i] <- info[6]
  setTxtProgressBar(progress, i)
}

stations <- cbind(stations, bl_kz, bl_name, kreis_kz, kreis_name, gemeinde_kz, gemeinde_name)

# Connect to database
drv <- dbDriver("PostgreSQL") # DBDriver
dbn <- "fuelprice" # Database name
dbh <- "192.168.1.10" # DB Host
dbp <- 5432 # DB Port
dbu <- "dbuser" # DB User
dbpw <- "dbpassword" # DB Password

# Connection
dbcon <- dbConnect(drv = drv, dbname=dbn, host=dbh ,port=dbp, user=dbu, password=dbpw)

# Write to db
dbWriteTable(dbcon, c("stations"), value = stations, append = T, row.names = F)

# Disconnect
dbDisconnect(dbcon)

# Clear Working Space
rm(list=ls())

PostgreSQL Tabelle mit Kraftstoffpreisen

Seit Juni 2014 melden Tankstellen in Deutschland sämtliche Preisänderungen an das Bundeskartellamt. Über Markttransparenzstellen, wie zum Beispiel Tankerkönig, sind die Kraftstoffpreise der Öffentlichkeit zugänglich. In diesem Beitrag erläutere ich wie die Preisdaten mit R in eine PostgreSQL Datenbank gespeichert werden können.

Datenquelle

Tankerkönig stellt sämtliche Preisinformationen in einem git-Repisotory als csv-Dateien zur Verfügung. Für jeden Tag werden die Preisänderungen in einer csv-Datei hinterlegt, die Daten werden täglich aktualisiert und können mit einem einfachen git pull Befehl täglich abgerufen werden.

Tabelle erstellen

Die csv-Dateien enthalten Informationen zur Uhrzeit der Preisänderungen, der Tankstelle (in Form einer eindeutigen ID), die Preisen von Diesel, Super E5 und Super E10 Kraftstoffen, sowie eine 0/1 Variable, ob der Preis eines Kraftstoffes geändert wurde. Daraus ergibt sich folgende PostgreSQL Tabelle:

CREATE TABLE price (
  time 		timestamptz 		NOT NULL,
  stid 		uuid 				NOT NULL,
  pdi 		numeric(8,3),
  pe5 		numeric(8,3),
  pe10 		numeric(8,3),
  cdi 		integer,
  ce5 		integer,
  ce10 		integer
);

Einlesen der Preisdaten mit R

Um die als csv-Dateien vorliegenden Preisdaten in die Datenbank einzulesen nutze ich ein kurzes R-Skript. Dabei nutze ich zwei R-Pakete:

  • RPostgreSQL, um eine Verbindung mit der Datenbank herzustellen
  • readr, zum einlesen der csv-Dateien
# Packages
library(RPostgreSQL)
library(readr)

Um die csv-Dateien später einlesen zu können benötigen wir zunächst eine Liste aller Dateinamen. In meinem Fall liegen alle Daten auf einem Server im lokalen Netzwerk im Ordner dbdata/tankerkoenig/prices/. Der Pfad lässt sich mit file.path()erstellen. Anschließend speichere ich die kompletten Dateipfade mittels list.files().

# Get file names
pathtoserver <- file.path("//192.168.1.10", "dbdata", "tankerkoenig", "prices")
file_names <- list.files(path = pathtoserver, recursive = T, full.names = T)

Bevor die Daten eingelesen werden können muss eine Verbindung zur Datenbank hergestellt werden, hierbei bietet es sich an zunächst die Verbindungsparameter zu definieren und dann die Verbindung herzustellen

# Database connection parameters
drv <- dbDriver("PostgreSQL") # DBDriver
dbn <- "price" # Database name
dbh <- "192.168.1.10" # DB Host
dbp <- 5432 # DB Port
dbu <- "dbuser" # DB User
dbpw <- "dbpassword" # DB Password

# Connection
dbcon <- dbConnect(drv = drv, dbname=dbn, host=dbh ,port=dbp, user=dbu, password=dbpw)

Nun können die csv-Dateien eingelesen werden. Hierzu verwende ich eine einfache for-Schleife, in der zunächst die Daten aus der csv-Datei in einen Dataframe gespeichert werden und anschließend in die Datenbank Tabelle geschrieben werden. Zusätzlich nutze ich einen Fortschrittsbalken, da der ganze Vorgang ein paar Minuten dauern kann

# Loop over data
progress <- txtProgressBar(min=1, max=length(file_names), style = 3, width = 100)

for(i in 1:length(file_names)){
  df <- read_csv(file_names[i],
                 col_types = cols(time = col_character(),
                                  stid = col_character(),
                                  pdi = col_double(),
                                  pe5 = col_double(),
                                  pe10 = col_double(),
                                  cdi = col_integer(),
                                  ce10 = col_integer(),
                                  ce5 = col_integer()),
                 col_names = c("time", "stid", "pdi", "pe5", "pe10", "cdi", "ce5", "ce10"),
                 skip = 1,
                 na = c("-0.001"))
  
  dbWriteTable(dbcon, c("price"), value = df, append = T, row.names = F)
  
  setTxtProgressBar(progress, i)
}

Nach Abschluss der Schleife bleibt nur noch die Verbindung zur Datenbank wieder zu trennen und falls gewünscht den Workspace zu leeren

# Disconnect
dbDisconnect(dbcon)

# Clear Working Space
rm(list=ls())

Der komplette Code

# Packages
library(RPostgreSQL)
library(readr)

# Get file names
pathtoserver <- file.path("//192.168.1.10", "dbdata", "tankerkoenig", "prices")
file_names <- list.files(path = pathtoserver, recursive = T, full.names = T)

# Database connection parameters
drv <- dbDriver("PostgreSQL") # DBDriver
dbn <- "price" # Database name
dbh <- "192.168.1.10" # DB Host
dbp <- 5432 # DB Port
dbu <- "dbuser" # DB User
dbpw <- "dbpassword" # DB Password

# Connection
dbcon <- dbConnect(drv = drv, dbname=dbn, host=dbh ,port=dbp, user=dbu, password=dbpw)

# Loop over data
progress <- txtProgressBar(min=1, max=length(file_names), style = 3, width = 100)

for(i in 1:length(file_names)){
  df <- read_csv(file_names[i],
                 col_types = cols(time = col_character(),
                                  stid = col_character(),
                                  pdi = col_double(),
                                  pe5 = col_double(),
                                  pe10 = col_double(),
                                  cdi = col_integer(),
                                  ce10 = col_integer(),
                                  ce5 = col_integer()),
                 col_names = c("time", "stid", "pdi", "pe5", "pe10", "cdi", "ce5", "ce10"),
                 skip = 1,
                 na = c("-0.001"))
  
  dbWriteTable(dbcon, c("price"), value = df, append = T, row.names = F)
  
  setTxtProgressBar(progress, i)
}


# Disconnect
dbDisconnect(dbcon)

# Clear Working Space
rm(list=ls())