Studenten in München haben es nicht leicht. Die Mietpreise sind der Wahnsinn, es gab großen Stress um das relativ teuere Semesterticket – und dann sind die Bibliotheken noch so voll. Das war zumindest der Anlass, weshalb die Universitätsbibliothek der LMU Ende 2016 ein neues Tool gestartet hat: Den Platzfinder. Zusammen mit einer Parkscheibe, die Studierende bekommen, wenn sie die Bibliothek betreten, ermitteln die Bibliotheksmitarbeiter, wie voll ihre Bibliothek gerade ist – und tragen das in ein Onlineformular ein (Was natürlich auch sehr fehlerbehaftet ist – aber die Daten sind das Beste, was wir haben).
Auf der Webseite der Uni-Bibliothek können Studierende dann checken, wie voll ihre „Lieblingsbib“ ist.
Für M94.5 wollte ich herausfinden, welche Bib am vollsten ist – und wie sich das im Tagesverlauf verändert.
Die Seite der LMU gibt SVGs mit Prozentwerten für die Füllung aus, die in den Balkendiagrammen angezeigt werden. Mit Python und der Bibliothek BeautifulSoup konnte ich also easy die Daten auslesen und in ein CSV speichern:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
#!/usr/bin/python # -*- coding: utf-8 -*- from bs4 import BeautifulSoup import urllib import re import csv from datetime import datetime r = urllib.urlopen('http://www.ub.uni-muenchen.de/arbeiten/platzfinder/index.html').read() soup = BeautifulSoup(r, "lxml") iframes = soup.find_all("iframe") csvdata = [] print "Sammle Daten" for i in iframes: r1 = urllib.urlopen(i["src"]).read() iframe = BeautifulSoup(r1, "lxml") name = iframe.find("div", {"id": "chart_text1"}).get_text().encode('utf-8') data = iframe.find_all("script")[1].string data = data.replace("'", '"') p = re.compile('var data = google.visualization.arrayToDataTable\(\[\[(.*?)\],\[.+"\,(.*?)\]\]\)') m = p.search(data) try: lst = str(m.group(2).encode('utf-8')).split(",") except: lst = str(m.group(0).encode('utf-8')).split(",") if lst[0] == "1": name = name.strip() fill = "Geschlossen" empty = 0 else: name = name.strip() fill = lst[0] empty = lst[1] csvdata.append((name, fill, empty)) print "Schreibe: ", i["src"] # open a csv file with append, so old data will not be erased with open('data.csv', 'a') as csv_file: writer = csv.writer(csv_file, delimiter=';', lineterminator='\n') # The for loop print "Schreibe CSV" for name, fill, empty in csvdata: writer.writerow([name, fill, empty, datetime.now()]) print "Fertig" |
Damit das Ganze alle 15 Minuten laufen kann – in den Zeiten, in denen die Bibliotheken geöffnet haben – habe ich zu einem Trick gegriffen. Das Pythonskript lag auf meinem Webspace, der Python vorinstalliert hat. Daneben lag ein einfaches Shell-Startskript für das Python. Dieses Skript rufe ich über über einen Cronjob auf. In meinem Fall habe ich Cronjob.de benutzt (ein Skript ist kostenlos, weitere kosten ab 99 Cent pro Monat) – es gibt Alternativen.
Dann hieß es: Warten. 1,5 Monate lang etwa. Das CSV füllte sich immer weiter, ohne, dass ich irgendwas tun musste.
Dann hieß es: Auswerten. Dafür habe ich das CSV gedownloaded und in R eingelesen.
1 2 3 4 5 6 7 8 |
library(dplyr) library(ggplot2) library(lubridate) library(extrafont) library(grid) colN <- c("Bibliothek", "Belegt", "Leer", "Datum") d <- read.csv2("data.csv", stringsAsFactors = TRUE, col.names = colN, colClasses = c("factor", "numeric", "numeric", "POSIXct"), header = FALSE, encoding = "UTF-8", na.strings = "Geschlossen") |
Schon beim ersten Rumspielen hat sich gezeigt: Weihnachten ist ne blöde Zeit für den Datensatz. Im Vergleich zu den anderen Zeiten lag die Belegungsquote ziemlich weit unten. Das hieß für mich: Weihnachten raus.
1 2 3 4 5 6 7 |
#Remove Test-Data and create new columns, filter out Christmas-Time d %>% filter(Datum > "2016-12-17 23:45:29") %>% filter(Datum <= "2016-12-23 23:59:59" | Datum >= "2017-01-02 00:00:00") %>% mutate(weekday = weekdays.POSIXt(Datum), hour = hour(Datum)) -> w w$weekday <- factor(w$weekday, levels = c("Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag")) |
Dann folgten die einzelnen Analysen. Zunächst wollte ich wissen (und natürlich auch plotten), wie groß der Unterschied zwischen Wochentag und Wochenende in den Bibliotheken ist.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# create tbl_df for weekend and weekday with mean per hour w %>% select(Bibliothek, Belegt, weekday, hour, Datum) %>% filter(complete.cases(.)) %>% filter(weekday != "Samstag" & weekday != "Sonntag") %>% group_by(hour) %>% summarise(mean(Belegt)) -> week w %>% select(Bibliothek, Belegt, weekday, hour, Datum) %>% filter(complete.cases(.)) %>% filter(weekday == "Samstag" | weekday == "Sonntag") %>% group_by(hour) %>% summarise(mean(Belegt)) -> weekend ## Plotting Weekday vs Weekend svg("week_vs_weekend.svg", pointsize = 28, width = 11.78, height = 8.39) ggplot() + geom_line(data = weekend, aes(hour, `mean(Belegt)`), color = "#590086", size = 1.3) + geom_area(data = weekend, aes(hour, `mean(Belegt)`), color = "#dd9aff", alpha = 0.2) + geom_line(data = week, aes(hour, `mean(Belegt)`), color = "#865900", size = 1.3) + geom_area(data = week, aes(hour, `mean(Belegt)`), fill = "#ffd686", alpha = 0.6) + labs(title = "Durchschnitts-Belegung im Tagesverlauf", subtitle = "Braun: Werktag, Blau: Wochenende") + ylab("Belegung in Prozent") + xlab("Uhrzeit") + theme_bw() + theme(text = element_text(size = 12), plot.margin = unit(c(1, 1, 4, 1), "lines"), panel.grid.major.x = element_blank(), panel.grid.minor.x = element_blank(), panel.border = element_blank()) dev.off() |
Dann wollte ich wissen, wie die unterschiedlichen Wochentage sich einzeln unterscheiden. Das könnte man in ggplot mit Facets lösen. Ich wollte das ganze aber in einem Plot. Das hieß: Rumspielen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# Plotting Every day by hour w %>% select(Bibliothek, Belegt, weekday, hour, Datum) %>% filter(Datum <= "2016-12-23 23:59:59" | Datum >= "2017-01-02 00:00:00") %>% filter(complete.cases(.)) %>% group_by(weekday, hour) %>% summarise(mean = mean(Belegt)) -> weekday_by_hour svg("weekday_per_hour.svg", pointsize = 28, width = 11.78, height = 8.39) #png("weekday_per_hour.png", pointsize = 28, width = 500) g1 <- ggplot(weekday_by_hour, aes(x = interaction(weekday, hour, lex.order = TRUE), y = mean, group = 1)) + geom_line(colour = "#008659", size = 1.3) + geom_area(fill = "#86ffd6", alpha = 0.5) + coord_cartesian(ylim = c(0, 65), expand = FALSE) + annotate(geom = "text", x = seq_len(nrow(weekday_by_hour)), y = -1, label = weekday_by_hour$hour, size = 2) + annotate(geom = "text", x = 9 + 16 * (0:6), y = -4, label = unique(weekday_by_hour$weekday), size = 5) + theme_bw() + theme(text = element_text(size = 12), plot.margin = unit(c(1, 1, 4, 1), "lines"), axis.title.x = element_blank(), axis.text.x = element_blank(), panel.grid.major.x = element_blank(), panel.grid.minor.x = element_blank(), panel.border = element_blank()) + geom_hline(yintercept = mean(weekday_by_hour$mean), linetype = 3) + ylab("Durchschnittsbelegung in Prozent") g2 <- ggplot_gtable(ggplot_build(g1)) g2$layout$clip[g2$layout$name == "panel"] <- "off" grid::grid.draw(g2) dev.off() |
Und dann das Highlight. Ich dachte mir, ein bisschen Bewegung schadet nicht. Deswegen wollte ich ein GIF erstellen, dass für jede Stunde des Tages für jede Bibliothek die Durchschnittsbelegung angibt. Das Skript sollte mir die einzelnen Stunden automatisch ausgeben, damit ich daraus nur noch ein GIF bauen muss. (Das würde auch direkt in R gehen, mit diesem Package)
Ich habe meinen w-Dataframe nochmal kopiert, weil ich die Rohdaten-NAs durch 0 ersetzt habe. Das wollte ich mir im Original-Datensatz nicht zerschießen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
## # Create PNG for each Hour of the Day ### #Make copy of w-df w_nas <- w #replace NAs with 0 w_nas[is.na(w_nas$Belegt),]$Belegt <- 0 #Debugging needed for the hjust of plot.title for (i in 8:23){ w_help = NULL w_nas %>% filter(hour==i) %>% group_by(Bibliothek) %>% summarise(mean = mean(Belegt)) -> w_help print(head(w_help)) png(paste0(i,".png"), width = 500, units = "px") p <- ggplot(w_help, aes(Bibliothek, mean)) + geom_bar(stat="identity", fill = "#008659") + coord_flip() + theme(text = element_text(family = "Arial"), axis.title = element_blank(), plot.title = element_text(face = "bold", size = 18)) + labs(title = paste("Besetzte UB-Plätze um", i, "Uhr")) + scale_y_continuous(limits = c(0, 100)) print(p) dev.off() } |
Fertig.
Das Ergebnis gibt es hier.