data > opinion

Tom Alby

Spieglein, Spieglein an der Wand, ist JSON oder CSV das schönste Dateiformat im Land?

2021-12-22


Hintergrund dieses Posts ist, dass ich sowohl in meinen Seminaren gemerkt habe, dass nicht jeder Studierende schon mal mit CSV-Dateien zu tun hatte (woher auch?), ich dann aber bei JSON gemerkt habe, dass dies auch nicht unbedingt das einfachste Dateiformat ist :)

CSV-Grundlagen

Eine CSV, für Comma Separated Values, ist ein Dateiformat, das einer Tabelle entspricht. Die Spalten sind mit Kommata getrennt. Das könnte ungefähr so aussehen:

Name,Vorname,Lieblingsband
"Mueller", "Max", "Go-Betweens"
"Mustermann", "Michael", "Kraftwerk"

Genau so wie wir es gelernt haben, Beobachtungen in die Zeilen, Variablen oder Merkmale in die Spalten. Natürlich wäre es aber zu einfach, wenn es immer so wäre. Manchmal ist das Trennzeichen kein Komma, sondern ein Semikolon. Oder ein Tab (dann müsste es eigentlich TSV heißen). Oder ein Leerzeichen. RStudio kommt damit super klar, man kann über den Import einfach rumspielen, wenn es nicht passt.

CSV versus JSON

CSV ist ein sehr praktisches Dateiformat, denn man kann sich eine Datei ansehen und sie sofort verstehen. Es ist halt eine Tabelle, das ist der große Vorteil einer CSV-Datei. Aber die ist nicht immmer praktisch. Stellen wir uns mal vor, wie fragen nicht nach der Lieblingsband, sondern nach den Bands, die man live im Konzert gesehen hat. Und dann wirds kompliziert. Klar, wir könnten einfach eine Zeile für jede Band hinzufügen:

name,vorname,Livebands
"Mueller", "Max", "Go-Betweens"
"Mueller", "Max", "Buggles"
"Mueller", "Max", "Police"
"Mustermann", "Michael", "Kraftwerk"
"Mustermann", "Michael", "Air"

Effizient ist das nicht. Wir könnten auch die Bands alle in die eine Zelle schreiben, in der vorher eine Band stand. Das ist häufig eine kleine Herausforderung, wenn man damit arbeiten will (dazu unten mehr):

name,vorname,Livebands
"Mueller", "Max", '"Go-Betweens", "Buggles", "Police"'
"Mustermann", "Michael", '"Kraftwerk", "Air"'

Und genau hier kommt das JSON-Format ins Spiel. Denn hier können wir das sehr elegant ausdrücken:

[
  {
    "name":"Mueller",
    "vorname":"Max",
    "Livebands":["Go-Betweens","Buggles","Police"]
  },
  {
    "name":"Mustermann",
    "vorname":"Michael",
    "Livebands":["Kraftwerk","Air"]
  }
]

Offensichtlich ist das etwas effizienter. Aber für das CSV-gewohnte Auge entsteht ein Störgefühl. Wie kommen wir mit dem Störgefühl klar?

Über JSON findet man interessanterweise sehr wenig in den üblichen R-Quellen. Das Tidyverse kennt JSON zwar, indem für readr auch das jsonlite Package installiert wird, aber in keinem Buch von Hadley Wickham zum Beispiel wird JSON genauer erklärt. Es entspricht auch nicht dem “Rectangular Data”-Prinzip, das in fast jedem Tidyverse-Tutorial genutzt wird. Denn in R spielt sich fast jede Funktion in Dataframes ab (abgesehen von Vektoren, Matritzen oder Listen).

Schaut man aber auf Kaggle und Co, dann findet man sehr viele JSON-Dateien. Nicht immer ist die Wahl sinnvoll, aber das ist eine andere Diskussion. Anders gesagt, wir kommen um das JSON-Format nicht herum.

Wenn Du Dich für Data Science interessierst, hier gehts zu meinem neuen Buch!

JSON einlesen

Das jsonlite-Package erlaubt das Einlesen in einen Dataframe. Genau das probieren wir jetzt einmal mit der Datei, die wir uns zuvor angesehen hatten:

library(jsonlite)
test <- fromJSON("example.json")
test
##         name vorname                    Livebands
## 1    Mueller     Max Go-Betweens, Buggles, Police
## 2 Mustermann Michael               Kraftwerk, Air

jsonlite hat aus den verschachtelten Daten einen Vektor in die Zelle unseres Dataframes gebaut, genau das, was wir oben schon gesehen hatten und eigentlich nicht wollten. Mit unnest aus dem tidyr-Package kann man das auseinanderfriemeln:

library(tidyverse)
## ── Attaching packages ─────────────────────────────────────── tidyverse 1.3.1 ──
## ✓ ggplot2 3.3.5     ✓ purrr   0.3.4
## ✓ tibble  3.1.6     ✓ dplyr   1.0.7
## ✓ tidyr   1.1.4     ✓ stringr 1.4.0
## ✓ readr   2.1.0     ✓ forcats 0.5.1
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## x dplyr::filter()  masks stats::filter()
## x purrr::flatten() masks jsonlite::flatten()
## x dplyr::lag()     masks stats::lag()
unnest(test,Livebands)
## # A tibble: 5 × 3
##   name       vorname Livebands  
##   <chr>      <chr>   <chr>      
## 1 Mueller    Max     Go-Betweens
## 2 Mueller    Max     Buggles    
## 3 Mueller    Max     Police     
## 4 Mustermann Michael Kraftwerk  
## 5 Mustermann Michael Air

Das funktioniert allerdings nicht immer. Schauen wir uns die folgende JSON-Datei an:

Parsen wir diese Datei, so ergibt dies einen Dataframe im Dataframe:

spotify <- fromJSON("test.json")
glimpse(spotify)
## List of 2
##  $ info     :List of 3
##   ..$ generated_on: chr "2017-12-03 08:41:42.057563"
##   ..$ slice       : chr "0-999"
##   ..$ version     : chr "v1"
##  $ playlists:'data.frame':   22 obs. of  11 variables:
##   ..$ name         : chr [1:22] "Throwbacks" "Awesome Playlist" "korean " "mat" ...
##   ..$ collaborative: chr [1:22] "false" "false" "false" "false" ...
##   ..$ pid          : int [1:22] 0 1 2 3 4 5 6 7 8 9 ...
##   ..$ modified_at  : int [1:22] 1493424000 1506556800 1505692800 1501027200 1401667200 1430956800 1477094400 1509321600 1508976000 1501804800 ...
##   ..$ num_tracks   : int [1:22] 52 39 64 126 17 80 16 53 46 21 ...
##   ..$ num_albums   : int [1:22] 47 23 51 107 16 71 15 52 37 20 ...
##   ..$ num_followers: int [1:22] 1 1 1 1 2 1 1 1 2 1 ...
##   ..$ tracks       :List of 22
##   .. ..$ :'data.frame':  52 obs. of  8 variables:
##   .. ..$ :'data.frame':  39 obs. of  8 variables:
##   .. ..$ :'data.frame':  64 obs. of  8 variables:
##   .. ..$ :'data.frame':  126 obs. of  8 variables:
##   .. ..$ :'data.frame':  17 obs. of  8 variables:
##   .. ..$ :'data.frame':  80 obs. of  8 variables:
##   .. ..$ :'data.frame':  16 obs. of  8 variables:
##   .. ..$ :'data.frame':  53 obs. of  8 variables:
##   .. ..$ :'data.frame':  46 obs. of  8 variables:
##   .. ..$ :'data.frame':  21 obs. of  8 variables:
##   .. ..$ :'data.frame':  72 obs. of  8 variables:
##   .. ..$ :'data.frame':  134 obs. of  8 variables:
##   .. ..$ :'data.frame':  9 obs. of  8 variables:
##   .. ..$ :'data.frame':  13 obs. of  8 variables:
##   .. ..$ :'data.frame':  103 obs. of  8 variables:
##   .. ..$ :'data.frame':  7 obs. of  8 variables:
##   .. ..$ :'data.frame':  105 obs. of  8 variables:
##   .. ..$ :'data.frame':  79 obs. of  8 variables:
##   .. ..$ :'data.frame':  68 obs. of  8 variables:
##   .. ..$ :'data.frame':  85 obs. of  8 variables:
##   .. ..$ :'data.frame':  14 obs. of  8 variables:
##   .. ..$ :'data.frame':  15 obs. of  8 variables:
##   ..$ num_edits    : int [1:22] 6 5 18 4 7 3 2 38 21 10 ...
##   ..$ duration_ms  : int [1:22] 11532414 11656470 14039958 28926058 4335282 19156557 3408479 12674796 9948921 4297488 ...
##   ..$ num_artists  : int [1:22] 37 21 31 86 16 56 13 48 23 18 ...

In diesem Fall können wir mit dem $ tiefer in die verschachtelte Struktur rein:

spotify.flattened <- spotify$playlists
spotify.flattened %>%
  unnest(tracks, names_repair = "unique") 
## New names:
## * duration_ms -> duration_ms...14
## * duration_ms -> duration_ms...17
## # A tibble: 1,218 × 18
##    name      collaborative   pid modified_at num_tracks num_albums num_followers
##    <chr>     <chr>         <int>       <int>      <int>      <int>         <int>
##  1 Throwbac… false             0  1493424000         52         47             1
##  2 Throwbac… false             0  1493424000         52         47             1
##  3 Throwbac… false             0  1493424000         52         47             1
##  4 Throwbac… false             0  1493424000         52         47             1
##  5 Throwbac… false             0  1493424000         52         47             1
##  6 Throwbac… false             0  1493424000         52         47             1
##  7 Throwbac… false             0  1493424000         52         47             1
##  8 Throwbac… false             0  1493424000         52         47             1
##  9 Throwbac… false             0  1493424000         52         47             1
## 10 Throwbac… false             0  1493424000         52         47             1
## # … with 1,208 more rows, and 11 more variables: pos <int>, artist_name <chr>,
## #   track_uri <chr>, artist_uri <chr>, track_name <chr>, album_uri <chr>,
## #   duration_ms...14 <int>, album_name <chr>, num_edits <int>,
## #   duration_ms...17 <int>, num_artists <int>

Ohne das name_repair gibt es eine Fehlermeldung, da es leider zwei Mal “duration_ms” gibt, einmal für jeden Track, einmal für die Playlist. Sind die JSON-Dateien noch tiefer verschachtelt (“nested”), dann muss unnest mehrmals aufgerufen werden, wichtig ist, dass die richtige Reihenfolge eingehalten wird. Wir sehen schon, JSON-Dateien können einem schon beim Import ganz schön Kopfschmerzen bereiten.

Interessant ist hier, dass eine JSON-Datei, obwohl sie vom Prinzip her eigentlich effizienter sein müsste, nicht unbedingt kleiner ist! Beispiel: Ich habe oben nur ein Sample aus dem Spotify-Datensatz genommen, jede Datei hat eigentlich 1.000 Playlisten. Die JSON-Datei hat 689.058 Zeilen und benötigt 34.119.368 Byte auf der Platte. Die daraus gewonnene CSV-Datei hat 67.504 Zeilen und benötigt 15.344.085 Byte, also weniger als die Hälfte.

Tags: