Explore and understand
Once we have access to our raw data file, we’ll want to explore it and see what we need to do to clean it up. This requires some initial understanding of what our dataset contains.
We’re starting with an excel workbook of bicycle traffic data in Berlin. To learn more about where we found this data and how to download it, see the “Extract” chapter that we skipped over. In the interest of time, we’ll start here and learn:
- how to load an excel file as a tablecloth dataset
- how to explore and work with Excel workbooks that contain multiple sheets
- how to handle Excel’s date format in Clojure
- how to extract metadata from reference sheets
- how to clean, transform, and pivot messy data to make tidy data
Loading xlsx data in Clojure
To load xlsx data we’ll need to require fastexcel:
require '[tech.v3.libs.fastexcel :as xlsx]) (
For consistency, we’ll use the same file name as the one specified in the previous notebook, which means we need to require it, too:
require '[notebooks.3-extract :as extract]) (
Attempting to load the excel file as a dataset directly shows us that there are actually 16 datasets contained in this workbook:
(tc/dataset extract/raw-data-file-name)
_unnamed [1 2]:
:\(value | :\)error | |
---|---|
data/prepared/raw-data.xlsx | Multiple (16) datasets found in file |
So instead we’ll make a seq of datasets, loading each sheet as its own dataset:
def raw-datasets
( (xlsx/workbook->datasets extract/raw-data-file-name))
Exploring the Excel sheets
We can see what our sheets are named like this:
map tc/dataset-name raw-datasets) (
"Hinweise"
("Legende"
"Standortdaten"
"Jahresdatei 2012"
"Jahresdatei 2013"
"Jahresdatei 2014"
"Jahresdatei 2015"
"Jahresdatei 2016"
"Jahresdatei 2017"
"Jahresdatei 2018"
"Jahresdatei 2019"
"Jahresdatei 2020"
"Jahresdatei 2021"
"Jahresdatei 2022"
"Jahresdatei 2023"
"Jahresdatei 2024")
The first three are reference datasets and the rest are a time series. It’s pretty common to see Excel workbooks set up like this, with a few sheets that are reference datasets and the rest containing the actual data. We’ll extract some metadata out of these first three datasets to use in our analysis. The first dataset just contains a big description, formatted for aesthetics rather than machine-readability, so we’ll have to dig in to the one random column containing the text to get the content out.
def info-ds (first raw-datasets)) (
First we’ll drop all the columns where every value is nil, which we can see leaves us with a single column:
fn [col] (every? nil? (info-ds col)))) (tc/drop-columns info-ds (
Hinweise [40 1]:
column-1 |
---|
Datenbereitsteller: |
Senatsverwaltung für Mobilität, Verkehr, Klimaschutz und Umwelt |
Abt. VI - Verkehrsmanagement |
E-Mail: Verkehrsmanagement@SenMVKU.Berlin.de |
Hinweise: |
Auf den folgenden Tabellenblättern finden Sie die Rohdaten der Zählstellen für den Radverkehr in Berlin. Rohdaten sind Daten, die direkt aus den Zählgeräten der Dauerzählstellen ausgelesen und nicht weiter bearbeitet werden. |
Die Daten werden einmal jährlich nach Abschluss des Zähljahres zusammengestellt und veröffentlicht. |
Bei der Auswertung und Interpretation der Daten sind folgende Randbedingungen zu beachten: |
… |
3. in interne und externe Geschäftsprozesse, Produkte und Anwendungen in öffentlichen und nicht öffentlichen elektronischen |
Netzwerken eingebunden werden. |
(2) Die Daten unterliegen der folgenden Lizenz: |
Datenlizenz Deutschland – Zero – Version 2.0 (dl-de-zero-2.0) |
https://www.govdata.de/dl-de/zero-2-0 |
(3) Veränderungen, Bearbeitungen, neue Gestaltungen oder sonstige Abwandlungen sind im Quellenvermerk mit dem Hinweis |
zu versehen, dass die Daten geändert wurden. |
(4) Die Bereitstellung der Daten erfolgt unter Ausschluss jeder Haftung für direkte und indirekte Schäden durch die Datennutzung. Der Eigentümer der Daten bemüht sich, aussagekräftige Informationen zur Qualtität der Daten, z.B. hinsichtlich Aktualität, |
Richtigkeit, Vollständigkeit und Genauigkeit der veröffentlichen Daten bereitzustellen, wobei keine Garantie hinsichtlich der Qualität übernommen wird. Der Datenabnehmer haftet für alle Schäden, die durch die Verarbeitung oder Nutzung der Daten durch den Datenabnehmer entstehen. Der Datenabnehmer stellt den Datenbereitsteller von allen Ansprüchen Dritter frei. |
Then we’ll join all the non-nil values in this column into a single wall of text, and store it as the description for this dataset.
->> (tc/drop-columns info-ds (fn [col] (every? nil? (info-ds col))))
(
tc/rows
flattenremove nil?)
(map str/trim)
("\n")
(str/join "data/prepared/description-de.txt")) (spit
nil
You’ll notice this description is in German. As a quick bonus step, we can translate it to English using a free online translation API. To follow this particular example you’ll need an API key from DeepL. This code expects the key to be GPG encrypted in secrets/translate-api-key.txt.gpg
.
comment
(require '[clj-http.client :as http]
(:as json])
'[clojure.data.json
let [key (u/decrypt-gpg-key "secrets/translate-api-key.txt.gpg")
("https://api-free.deepl.com/v2/translate"
response (http/post :body (json/write-str {:text [(slurp "data/prepared/description-de.txt")]
{:target_lang "EN"})
:headers {"Authorization" (str "DeepL-Auth-Key " key)}
:content-type :json})
-> response :body json/read-str (get-in ["translations" 0 "text"]))]
english ("data/prepared/description-en.txt"
(spit str "NOTE: THIS CONTENT IS A MACHINE TRANSLATION OF THE ORIGINAL IN data/prepared/description-de.txt\n"
("provided only for convenience with no guarantee of accuracy.\n\n"
english))))
Next we’ll look at the “Legende” sheet to see what we can learn about our dataset from it.
second raw-datasets) (
Legende [3 3]:
column-0 | 0.0 | Zeiträume, in denen keine (0) oder unplausible Zähldaten an den Zählstellenaufgetreten sind |
---|---|---|
Zeiträume, in denen keine Zähldaten an den Zählstellen vorhanden sind | ||
123.0 | Zeiträume, in denen Datenlücken/fehlerhafte Daten durch Rekonstruktion aufgefüllt wurden |
This does not appear to be very useful. We’ll come back to it if it seems like it might be interesting later on.
Next is the “Standortdaten” sheet:
nth raw-datasets 2) (
Standortdaten [35 5]:
Zählstelle | Beschreibung - Fahrtrichtung | Breitengrad | Längengrad | Installationsdatum |
---|---|---|---|---|
12-PA-SCH | Schwedter Steg | 52.54907100 | 13.40036684 | 40969.0 |
02-MI-JAN-N | Jannowitzbrücke Nord | 52.51393216 | 13.41783461 | 42095.0 |
02-MI-JAN-S | Jannowitzbrücke Süd | 52.51394299 | 13.41761107 | 42095.0 |
13-CW-PRI | Prinzregentenstraße | 52.48813558 | 13.33311991 | 42095.0 |
18-TS-YOR-O | Yorckstraße Ost | 52.49193796 | 13.37347227 | 42095.0 |
18-TS-YOR-W | Yorkstraße West | 52.49228000 | 13.37321000 | 42095.0 |
19-TS-MON | Monumentenstraße | 52.48811700 | 13.36978500 | 42125.0 |
27-RE-MAR | Markstraße | 52.55819000 | 13.36494350 | 42125.0 |
03-MI-SAN-O | Invalidenstraße Ost | 52.52717690 | 13.37201589 | 42156.0 |
03-MI-SAN-W | Invalidenstraße West | 52.52768602 | 13.37310486 | 42156.0 |
… | … | … | … | … |
06-FK-FRA-O | Frankfurter Allee Ost | 52.51358400 | 13.47424200 | 42522.0 |
06-FK-FRA-W | Frankfurter Allee West | 52.51379000 | 13.47441400 | 42522.0 |
14-CW-JU-W | Straße des 17. Juni West | 52.51313259 | 13.32684517 | 44404.0 |
14-CW-JU-O | Straße des 17. Juni Ost | 52.51259000 | 13.32663000 | 44404.0 |
01-MI-AL-W | Karl-Marx-Allee | 52.52190716 | 13.41753602 | 44546.0 |
11-PA-SE-N | Senefelder Platz Nord | 52.53137962 | 13.41235473 | 44840.0 |
04-MI-NO | Nordufer | 52.51393300 | 13.41786400 | 45215.0 |
16-SP-NO-O | Nonnendammallee Ost | 52.53790100 | 13.24791400 | 45252.0 |
16-SP-NO-W | Nonnendammallee West | 52.53841400 | 13.24699600 | 45252.0 |
07-FK-ST | Strausberger Platz | 52.51894100 | 13.42613900 | 45404.0 |
09-PA-SA-S | Schönhauser Allee | 52.54304400 | 13.41206700 | 45404.0 |
This is useful information about the different tracker locations. It also reveals our first encounter with one very annoying thing about working with Excel files – dates. Very annoyingly, dates in excel are stored as the number of days since 1900-1-1. We’ll fix this and do some other basic transformations to this dataset, then store it as a CSV file.
def location-info-file-name "data/prepared/location-info.csv") (
-> raw-datasets
(nth 2)
(;; It's idiomatic to use keywords as column names in Clojure, so we'll translate these to English and make them keywords
"Zählstelle" :station-id
(tc/rename-columns {"Beschreibung - Fahrtrichtung" :direction
"Breitengrad" :lat
"Längengrad" :long
"Installationsdatum" :installed})
;; We'll parse the installation date values as dates based on Excel's idiosyncratic date handling
:installed (partial map dates/parse-excel-cell-as-date))
(tc/update-columns ;; Also update the lat/long precision to be realistic (https://xkcd.com/2170/)
:lat :long] #(-> % (tcc/* 10000.0) tcc/round (tcc// 10000.0)))
(tc/update-columns [;; Save this transformed data
(tc/write-csv! location-info-file-name))
36
We can also quickly plot this to visualize the locations of the stations as our first introduction to tableplot, the visualization library we’ll be using throughout this workshop. To overlay the points on a map of Berlin, we’ll need a Mapbox token.
Here’s how we can see the locations of the stations on a map, starting from the cleaned up dataset we just saved:
Some basic visualization
let [token (u/decrypt-gpg-key "secrets/mapbox-api-token.txt.gpg")]
(-> location-info-file-name
(:key-fn keyword}) ;; Note this is how we convert the column names to keywords on load
(tc/dataset {:geo
(plotly/base {:=coordinates :lat
:=lat :long
:=lon 1000
:=width 700})
:=height "Bike traffic counting stations"
(plotly/layer-point {:=name "orange"
:=mark-color 10})
:=mark-size
plotly/plotassoc-in [:data 0 :type] "scattermapbox")
(assoc-in [:layout :mapbox]
(:style "carto-positron" ;; Choose a Mapbox style
{:center {:lat 52.51, :lon 13.37} ;; Center the map on Berlin
:zoom 10.5 ;; Adjust zoom level
:accesstoken token})) ;; Use your Mapbox token
)
Cleaning up the timeseries data
Now we get into the interesting work. Inspecting one of the annual data files we can see that this dataset is stored in a very wide format with bad column names
def data-2012 (nth raw-datasets 3)) (
-2012 data
Jahresdatei 2012 [17544 27]:
Zählstelle Inbetriebnahme | 02-MI-JAN-N 01.04.2015 | 02-MI-JAN-S 01.04.2015 | 03-MI-SAN-O 01.06.2015 | 03-MI-SAN-W 01.06.2015 | 05-FK-OBB-O 01.06.2015 | 05-FK-OBB-W 01.06.2015 | 06-FK-FRA-O 01.06.2016 | 06-FK-FRA-W 01.06.2016 | 10-PA-BER-N 01.05.2016 | 10-PA-BER-S 01.05.2016 | 12-PA-SCH 01.03.2012 | 13-CW-PRI 01.04.2015 | 15-SP-KLO-N 01.06.2016 | 15-SP-KLO-S 01.06.2016 | 17-SZ-BRE-O 01.05.2016 | 17-SZ-BRE-W 01.05.2016 | 18-TS-YOR-O 01.04.2015 | 18-TS-YOR-W 01.04.2015 | 19-TS-MON 01.05.2015 | 20-TS-MAR-N 01.05.2016 | 20-TS-MAR-S 01.05.2016 | 21-NK-MAY 01.05.2016 | 23-TK-KAI 01.05.2016 | 24-MH-ALB 01.07.2015 | 26-LI-PUP 01.06.2015 | 27-RE-MAR 01.05.2015 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
40909.00000000 | ||||||||||||||||||||||||||
40909.04166667 | ||||||||||||||||||||||||||
40909.08333333 | ||||||||||||||||||||||||||
40909.12500000 | ||||||||||||||||||||||||||
40909.16666667 | ||||||||||||||||||||||||||
40909.20833333 | ||||||||||||||||||||||||||
40909.25000000 | ||||||||||||||||||||||||||
40909.29166667 | ||||||||||||||||||||||||||
40909.33333333 | ||||||||||||||||||||||||||
40909.37500000 | ||||||||||||||||||||||||||
… | … | … | … | … | … | … | … | … | … | … | … | … | … | … | … | … | … | … | … | … | … | … | … | … | … | … |
41639.54166667 | 108.0 | |||||||||||||||||||||||||
41639.58333333 | 110.0 | |||||||||||||||||||||||||
41639.62500000 | 83.0 | |||||||||||||||||||||||||
41639.66666667 | 65.0 | |||||||||||||||||||||||||
41639.70833333 | 26.0 | |||||||||||||||||||||||||
41639.75000000 | 22.0 | |||||||||||||||||||||||||
41639.79166667 | 28.0 | |||||||||||||||||||||||||
41639.83333333 | 26.0 | |||||||||||||||||||||||||
41639.87500000 | 12.0 | |||||||||||||||||||||||||
41639.91666667 | 18.0 | |||||||||||||||||||||||||
41639.95833333 | 20.0 |
This data is a mess. The one saving grace is that all years of data are messy in the same way. We can see all of the timeseries datasets have the same column names:
map tc/column-names raw-datasets) (
"column-0" "column-1" "column-2" "column-3")
(("column-0"
(0.0
"Zeiträume, in denen keine (0) oder unplausible Zähldaten an den Zählstellenaufgetreten sind")
"Zählstelle"
("Beschreibung - Fahrtrichtung"
"Breitengrad"
"Längengrad"
"Installationsdatum")
"Zählstelle Inbetriebnahme"
("02-MI-JAN-N 01.04.2015"
"02-MI-JAN-S 01.04.2015"
"03-MI-SAN-O 01.06.2015"
"03-MI-SAN-W 01.06.2015"
"05-FK-OBB-O 01.06.2015"
"05-FK-OBB-W 01.06.2015"
"06-FK-FRA-O 01.06.2016"
"06-FK-FRA-W 01.06.2016"
"10-PA-BER-N 01.05.2016"
"10-PA-BER-S 01.05.2016"
"12-PA-SCH 01.03.2012"
"13-CW-PRI 01.04.2015"
"15-SP-KLO-N 01.06.2016"
"15-SP-KLO-S 01.06.2016"
"17-SZ-BRE-O 01.05.2016"
"17-SZ-BRE-W 01.05.2016"
"18-TS-YOR-O 01.04.2015"
"18-TS-YOR-W 01.04.2015"
"19-TS-MON 01.05.2015"
"20-TS-MAR-N 01.05.2016"
"20-TS-MAR-S 01.05.2016"
"21-NK-MAY 01.05.2016"
"23-TK-KAI 01.05.2016"
"24-MH-ALB 01.07.2015"
"26-LI-PUP 01.06.2015"
"27-RE-MAR 01.05.2015")
"Zählstelle Inbetriebnahme"
("02-MI-JAN-N 01.04.2015"
"02-MI-JAN-S 01.04.2015"
"03-MI-SAN-O 01.06.2015"
"03-MI-SAN-W 01.06.2015"
"05-FK-OBB-O 01.06.2015"
"05-FK-OBB-W 01.06.2015"
"06-FK-FRA-O 01.06.2016"
"06-FK-FRA-W 01.06.2016"
"10-PA-BER-N 01.05.2016"
"10-PA-BER-S 01.05.2016"
"12-PA-SCH 01.03.2012"
"13-CW-PRI 01.04.2015"
"15-SP-KLO-N 01.06.2016"
"15-SP-KLO-S 01.06.2016"
"17-SZ-BRE-O 01.05.2016"
"17-SZ-BRE-W 01.05.2016"
"18-TS-YOR-O 01.04.2015"
"18-TS-YOR-W 01.04.2015"
"19-TS-MON 01.05.2015"
"20-TS-MAR-N 01.05.2016"
"20-TS-MAR-S 01.05.2016"
"21-NK-MAY 01.05.2016"
"23-TK-KAI 01.05.2016"
"24-MH-ALB 01.07.2015"
"26-LI-PUP 01.06.2015"
"27-RE-MAR 01.05.2015")
"Zählstelle Inbetriebnahme"
("02-MI-JAN-N 01.04.2015"
"02-MI-JAN-S 01.04.2015"
"03-MI-SAN-O 01.06.2015"
"03-MI-SAN-W 01.06.2015"
"05-FK-OBB-O 01.06.2015"
"05-FK-OBB-W 01.06.2015"
"06-FK-FRA-O 01.06.2016"
"06-FK-FRA-W 01.06.2016"
"10-PA-BER-N 01.05.2016"
"10-PA-BER-S 01.05.2016"
"12-PA-SCH 01.03.2012"
"13-CW-PRI 01.04.2015"
"15-SP-KLO-N 01.06.2016"
"15-SP-KLO-S 01.06.2016"
"17-SZ-BRE-O 01.05.2016"
"17-SZ-BRE-W 01.05.2016"
"18-TS-YOR-O 01.04.2015"
"18-TS-YOR-W 01.04.2015"
"19-TS-MON 01.05.2015"
"20-TS-MAR-N 01.05.2016"
"20-TS-MAR-S 01.05.2016"
"21-NK-MAY 01.05.2016"
"23-TK-KAI 01.05.2016"
"24-MH-ALB 01.07.2015"
"26-LI-PUP 01.06.2015"
"27-RE-MAR 01.05.2015")
"Zählstelle Inbetriebnahme"
("02-MI-JAN-N 01.04.2015"
"02-MI-JAN-S 01.04.2015"
"03-MI-SAN-O 01.06.2015"
"03-MI-SAN-W 01.06.2015"
"05-FK-OBB-O 01.06.2015"
"05-FK-OBB-W 01.06.2015"
"06-FK-FRA-O 01.06.2016"
"06-FK-FRA-W 01.06.2016"
"10-PA-BER-N 01.05.2016"
"10-PA-BER-S 01.05.2016"
"12-PA-SCH 01.03.2012"
"13-CW-PRI 01.04.2015"
"15-SP-KLO-N 01.06.2016"
"15-SP-KLO-S 01.06.2016"
"17-SZ-BRE-O 01.05.2016"
"17-SZ-BRE-W 01.05.2016"
"18-TS-YOR-O 01.04.2015"
"18-TS-YOR-W 01.04.2015"
"19-TS-MON 01.05.2015"
"20-TS-MAR-N 01.05.2016"
"20-TS-MAR-S 01.05.2016"
"21-NK-MAY 01.05.2016"
"23-TK-KAI 01.05.2016"
"24-MH-ALB 01.07.2015"
"26-LI-PUP 01.06.2015"
"27-RE-MAR 01.05.2015")
"Zählstelle Inbetriebnahme"
("02-MI-JAN-N 01.04.2015"
"02-MI-JAN-S 01.04.2015"
"03-MI-SAN-O 01.06.2015"
"03-MI-SAN-W 01.06.2015"
"05-FK-OBB-O 01.06.2015"
"05-FK-OBB-W 01.06.2015"
"06-FK-FRA-O 01.06.2016"
"06-FK-FRA-W 01.06.2016"
"10-PA-BER-N 01.05.2016"
"10-PA-BER-S 01.05.2016"
"12-PA-SCH 01.03.2012"
"13-CW-PRI 01.04.2015"
"15-SP-KLO-N 01.06.2016"
"15-SP-KLO-S 01.06.2016"
"17-SZ-BRE-O 01.05.2016"
"17-SZ-BRE-W 01.05.2016"
"18-TS-YOR-O 01.04.2015"
"18-TS-YOR-W 01.04.2015"
"19-TS-MON 01.05.2015"
"20-TS-MAR-N 01.05.2016"
"20-TS-MAR-S 01.05.2016"
"21-NK-MAY 01.05.2016"
"23-TK-KAI 01.05.2016"
"24-MH-ALB 01.07.2015"
"26-LI-PUP 01.06.2015"
"27-RE-MAR 01.05.2015")
"Zählstelle Inbetriebnahme"
("02-MI-JAN-N 01.04.2015"
"02-MI-JAN-S 01.04.2015"
"03-MI-SAN-O 01.06.2015"
"03-MI-SAN-W 01.06.2015"
"05-FK-OBB-O 01.06.2015"
"05-FK-OBB-W 01.06.2015"
"06-FK-FRA-O 01.06.2016"
"06-FK-FRA-W 01.06.2016"
"10-PA-BER-N 01.05.2016"
"10-PA-BER-S 01.05.2016"
"12-PA-SCH 01.03.2012"
"13-CW-PRI 01.04.2015"
"15-SP-KLO-N 01.06.2016"
"15-SP-KLO-S 01.06.2016"
"17-SZ-BRE-O 01.05.2016"
"17-SZ-BRE-W 01.05.2016"
"18-TS-YOR-O 01.04.2015"
"18-TS-YOR-W 01.04.2015"
"19-TS-MON 01.05.2015"
"20-TS-MAR-N 01.05.2016"
"20-TS-MAR-S 01.05.2016"
"21-NK-MAY 01.05.2016"
"23-TK-KAI 01.05.2016"
"24-MH-ALB 01.07.2015"
"26-LI-PUP 01.06.2015"
"27-RE-MAR 01.05.2015")
"Zählstelle Inbetriebnahme"
("02-MI-JAN-N 01.04.2015"
"02-MI-JAN-S 01.04.2015"
"03-MI-SAN-O 01.06.2015"
"03-MI-SAN-W 01.06.2015"
"05-FK-OBB-O 01.06.2015"
"05-FK-OBB-W 01.06.2015"
"06-FK-FRA-O 01.06.2016"
"06-FK-FRA-W 01.06.2016"
"10-PA-BER-N 01.05.2016"
"10-PA-BER-S 01.05.2016"
"12-PA-SCH 01.03.2012"
"13-CW-PRI 01.04.2015"
"15-SP-KLO-N 01.06.2016"
"15-SP-KLO-S 01.06.2016"
"17-SZ-BRE-O 01.05.2016"
"17-SZ-BRE-W 01.05.2016"
"18-TS-YOR-O 01.04.2015"
"18-TS-YOR-W 01.04.2015"
"19-TS-MON 01.05.2015"
"20-TS-MAR-N 01.05.2016"
"20-TS-MAR-S 01.05.2016"
"21-NK-MAY 01.05.2016"
"23-TK-KAI 01.05.2016"
"24-MH-ALB 01.07.2015"
"26-LI-PUP 01.06.2015"
"27-RE-MAR 01.05.2015")
"Zählstelle Inbetriebnahme"
("02-MI-JAN-N 01.04.2015"
"02-MI-JAN-S 01.04.2015"
"03-MI-SAN-O 01.06.2015"
"03-MI-SAN-W 01.06.2015"
"05-FK-OBB-O 01.06.2015"
"05-FK-OBB-W 01.06.2015"
"06-FK-FRA-O 01.06.2016"
"06-FK-FRA-W 01.06.2016"
"10-PA-BER-N 01.05.2016"
"10-PA-BER-S 01.05.2016"
"12-PA-SCH 01.03.2012"
"13-CW-PRI 01.04.2015"
"15-SP-KLO-N 01.06.2016"
"15-SP-KLO-S 01.06.2016"
"17-SZ-BRE-O 01.05.2016"
"17-SZ-BRE-W 01.05.2016"
"18-TS-YOR-O 01.04.2015"
"18-TS-YOR-W 01.04.2015"
"19-TS-MON 01.05.2015"
"20-TS-MAR-N 01.05.2016"
"20-TS-MAR-S 01.05.2016"
"21-NK-MAY 01.05.2016"
"23-TK-KAI 01.05.2016"
"24-MH-ALB 01.07.2015"
"26-LI-PUP 01.06.2015"
"27-RE-MAR 01.05.2015")
"Zählstelle Inbetriebnahme"
("02-MI-JAN-N 01.04.2015"
"02-MI-JAN-S 01.04.2015"
"03-MI-SAN-O 01.06.2015"
"03-MI-SAN-W 01.06.2015"
"05-FK-OBB-O 01.06.2015"
"05-FK-OBB-W 01.06.2015"
"06-FK-FRA-O 01.06.2016"
"06-FK-FRA-W 01.06.2016"
"10-PA-BER-N 01.05.2016"
"10-PA-BER-S 01.05.2016"
"12-PA-SCH 01.03.2012"
"13-CW-PRI 01.04.2015"
"15-SP-KLO-N 01.06.2016"
"15-SP-KLO-S 01.06.2016"
"17-SZ-BRE-O 01.05.2016"
"17-SZ-BRE-W 01.05.2016"
"18-TS-YOR-O 01.04.2015"
"18-TS-YOR-W 01.04.2015"
"19-TS-MON 01.05.2015"
"20-TS-MAR-N 01.05.2016"
"20-TS-MAR-S 01.05.2016"
"21-NK-MAY 01.05.2016"
"23-TK-KAI 01.05.2016"
"24-MH-ALB 01.07.2015"
"26-LI-PUP 01.06.2015"
"27-RE-MAR 01.05.2015")
"Zählstelle Inbetriebnahme"
("02-MI-JAN-N 01.04.2015"
"02-MI-JAN-S 01.04.2015"
"03-MI-SAN-O 01.06.2015"
"03-MI-SAN-W 01.06.2015"
"05-FK-OBB-O 01.06.2015"
"05-FK-OBB-W 01.06.2015"
"06-FK-FRA-O 01.06.2016"
"06-FK-FRA-W 01.06.2016"
"10-PA-BER-N 01.05.2016"
"10-PA-BER-S 01.05.2016"
"12-PA-SCH 01.03.2012"
"13-CW-PRI 01.04.2015"
"15-SP-KLO-N 01.06.2016"
"15-SP-KLO-S 01.06.2016"
"17-SZ-BRE-O 01.05.2016"
"17-SZ-BRE-W 01.05.2016"
"18-TS-YOR-O 01.04.2015"
"18-TS-YOR-W 01.04.2015"
"19-TS-MON 01.05.2015"
"20-TS-MAR-N 01.05.2016"
"20-TS-MAR-S 01.05.2016"
"21-NK-MAY 01.05.2016"
"23-TK-KAI 01.05.2016"
"24-MH-ALB 01.07.2015"
"26-LI-PUP 01.06.2015"
"27-RE-MAR 01.05.2015"
"14-CW-JU-O 27.07.2021"
"14-CW-JU-W 27.07.2021"
"02-MI-AL-W 16.12.2021"
"column-30"
"column-31")
"Zählstelle Inbetriebnahme"
("02-MI-JAN-N 01.04.2015"
"02-MI-JAN-S 01.04.2015"
"03-MI-SAN-O 01.06.2015"
"03-MI-SAN-W 01.06.2015"
"05-FK-OBB-O 01.06.2015"
"05-FK-OBB-W 01.06.2015"
"06-FK-FRA-O 01.06.2016"
"06-FK-FRA-W 01.06.2016"
"10-PA-BER-N 01.05.2016"
"10-PA-BER-S 01.05.2016"
"12-PA-SCH 01.03.2012"
"13-CW-PRI 01.04.2015"
"15-SP-KLO-N 01.06.2016"
"15-SP-KLO-S 01.06.2016"
"17-SZ-BRE-O 01.05.2016"
"17-SZ-BRE-W 01.05.2016"
"18-TS-YOR-O 01.04.2015"
"18-TS-YOR-W 01.04.2015"
"19-TS-MON 01.05.2015"
"20-TS-MAR-N 01.05.2016"
"20-TS-MAR-S 01.05.2016"
"21-NK-MAY 01.05.2016"
"23-TK-KAI 01.05.2016"
"24-MH-ALB 01.07.2015"
"26-LI-PUP 01.06.2015"
"27-RE-MAR 01.05.2015"
"14-CW-JU-O 27.07.2021"
"14-CW-JU-W 27.07.2021"
"02-MI-AL-W 16.12.2021"
"02-PA-SE-N\n06.10.2022")
"Zählstelle Inbetriebnahme"
("01-MI-AL-W 16.12.2021"
"02-MI-JAN-N 01.04.2015"
"02-MI-JAN-S 01.04.2015"
"03-MI-SAN-O 01.06.2015"
"03-MI-SAN-W 01.06.2015"
"03-SP-NO-O 22.11.2023"
"03-SP-NO-W 22.11.2023"
"04-MI-NO\n16.10.2023"
"05-FK-OBB-O 01.06.2015"
"05-FK-OBB-W 01.06.2015"
"06-FK-FRA-O 01.06.2016"
"06-FK-FRA-W 01.06.2016"
"10-PA-BER-N 01.05.2016"
"10-PA-BER-S 01.05.2016"
"11-PA-SE-N\n06.10.2022"
"12-PA-SCH 01.03.2012"
"13-CW-PRI 01.04.2015"
"14-CW-JU-O 27.07.2021"
"14-CW-JU-W 27.07.2021"
"15-SP-KLO-N 01.06.2016"
"15-SP-KLO-S 01.06.2016"
"17-SZ-BRE-O 01.05.2016"
"17-SZ-BRE-W 01.05.2016"
"18-TS-YOR-O 01.04.2015"
"18-TS-YOR-W 01.04.2015"
"19-TS-MON 01.05.2015"
"20-TS-MAR-N 01.05.2016"
"20-TS-MAR-S 01.05.2016"
"21-NK-MAY 01.05.2016"
"23-TK-KAI 01.05.2016"
"24-MH-ALB 01.07.2015"
"26-LI-PUP 01.06.2015"
"27-RE-MAR 01.05.2015"
"column-34"
"column-35"
"column-36"
"column-37")
"Zählstelle Inbetriebnahme"
("01-MI-AL-W\n16.12.2021"
"02-MI-JAN-N\n01.04.2015"
"02-MI-JAN-S\n01.04.2015"
"03-MI-SAN-O\n01.06.2015"
"03-MI-SAN-W\n01.06.2015"
"04-MI-NO\n16.10.2023"
"05-FK-OBB-O\n01.06.2015"
"05-FK-OBB-W\n01.06.2015"
"06-FK-FRA-O\n01.06.2016"
"06-FK-FRA-W\n01.06.2016"
"07-FK-ST\n22.04.2024"
"09-PA-SA-S\n22.04.2024"
"10-PA-BER-N\n01.05.2016"
"10-PA-BER-S\n01.05.2016"
"11-PA-SE-N\n06.10.2022"
"12-PA-SCH\n01.03.2012"
"13-CW-PRI\n01.04.2015"
"14-CW-JU-O\n27.07.2021"
"14-CW-JU-W\n27.07.2021"
"15-SP-KLO-N\n01.06.2016"
"15-SP-KLO-S\n01.06.2016"
"16-SP-NO-O\n22.11.2023"
"16-SP-NO-W\n22.11.2023"
"17-SZ-BRE-O\n01.05.2016"
"17-SZ-BRE-W\n01.05.2016"
"18-TS-YOR-O\n01.04.2015"
"18-TS-YOR-W\n01.04.2015"
"19-TS-MON\n01.05.2015"
"20-TS-MAR-N\n01.05.2016"
"20-TS-MAR-S\n01.05.2016"
"21-NK-MAY\n01.05.2016"
"23-TK-KAI\n01.05.2016"
"24-MH-ALB\n01.07.2015"
"26-LI-PUP\n01.06.2015"
"27-RE-MAR\n01.05.2015"))
We’ll figure out how to parse a single dataset first here, then make it more robust and apply the transformations to every year.
We can see the data is very sparse, there are columns for every station in every year, even though most stations were not installed in the early years.
We’ll clean up the data by dropping the empty columns, renaming the columns, and making it tidy (i.e. one variable per column and one observation per row):
let [;; Get the names of all empty columns that are not empty
(-> data-2012
non-empty-columns (
tc/infocomp pos? :n-valid))
(tc/select-rows (:col-name)]
-> data-2012
(;; Select only those
set non-empty-columns))
(tc/select-columns (;; Now fix the column names
;; First column is always the date
"Zählstelle Inbetriebnahme" :datetime})
(tc/rename-columns {;; Now we'll deal with the excel dates
:datetime (partial map dates/parse-excel-cell-as-date))
(tc/update-columns ;; Then we'll strip the date part off the end of the rest of the column names. Regex isn't super efficient, but that's fine we're only ever working with a small number of columns. Since these are domain-specific IDs, we'll leave them as-is, rather than converting them to keywords or something else.
complement #{:datetime})
(tc/rename-columns (->> % (re-matches #"^(.+)\d{2}\.\d{2}\.\d{4}") second str/trim))
#(
;; Now we'll make the data "tidy". It's not super obvious why this is necessary with a small dataset like this, but it will be for the ones that have more columns
complement #{:datetime}) {:target-columns :station-id
(tc/pivot->longer (:value-column-name :count})))
Jahresdatei 2012 [16102 3]:
:datetime | :station-id | :count |
---|---|---|
2012-03-01T00:00 | 12-PA-SCH | 11.0 |
2012-03-01T01:00 | 12-PA-SCH | 10.0 |
2012-03-01T02:00 | 12-PA-SCH | 2.0 |
2012-03-01T03:00 | 12-PA-SCH | 2.0 |
2012-03-01T04:00 | 12-PA-SCH | 0.0 |
2012-03-01T05:00 | 12-PA-SCH | 6.0 |
2012-03-01T06:00 | 12-PA-SCH | 20.0 |
2012-03-01T07:00 | 12-PA-SCH | 82.0 |
2012-03-01T08:00 | 12-PA-SCH | 108.0 |
2012-03-01T09:00 | 12-PA-SCH | 81.0 |
… | … | … |
2013-12-31T13:00 | 12-PA-SCH | 108.0 |
2013-12-31T14:00 | 12-PA-SCH | 110.0 |
2013-12-31T15:00 | 12-PA-SCH | 83.0 |
2013-12-31T16:00 | 12-PA-SCH | 65.0 |
2013-12-31T17:00 | 12-PA-SCH | 26.0 |
2013-12-31T18:00 | 12-PA-SCH | 22.0 |
2013-12-31T19:00 | 12-PA-SCH | 28.0 |
2013-12-31T20:00 | 12-PA-SCH | 26.0 |
2013-12-31T21:00 | 12-PA-SCH | 12.0 |
2013-12-31T22:00 | 12-PA-SCH | 18.0 |
2013-12-31T23:00 | 12-PA-SCH | 20.0 |
Now that we’ve seen what kind of data we’re working with, we’ll look at how to apply these transformations systematically to the rest of the data.