Für analysefreudige Datendumpster*innen hält das Web dieser tags an jeder zweiten Ecke einen kleinen Schatz bereit. Davor, sich die Hemdsärmel hochkrempeln und sich die Finger schmutzig machen zu müssen, ist mensch aber auch hier nicht gefeit. Das gilt um so mehr, wenn der Blick an einer übervollen Bio-Text-Tonne hängen bleibt, die das Dumpster*innenherz höher schlagen lässt. Wie der Zufall so will, steht eine dieser Tonnen direkt an der Einfahrt nach Springfield und über Hemdsärmel wissen die Simpsons Bände zu sprechen – genauer gesagt 158.315 Zeilen.
Rückblickend auf eine 27jährige Seriengeschichte und gestützt auf 28 Staffeln mit 616 Folgen, ließ sich im kleinen Städchen Springfield schon die Kulisse der ein oder anderen Analyse finden. Da sich aber auch im winzigsten Provinznest immer noch eine geschlossene Türe öffnen und noch ein unberührter Lichtschalter betätigen lässt, lädt Kaggle seit einigen Monaten dazu ein, sich vor Ort selbst mal genauer umzuschaun. In den bereitgestellten Daten tummeln sich 6.722 Charaktere an 4.459 Orten, die über 158.315 Textzeilen in 28 Staffeln und 600 Folgen miteinander in Interaktion treten.
Von kleinen und von größeren Übeln
Ein Blick auf die Episodenliste zeigt, dass die Daten bis auf die letzten 16 Folgen am aktuellsten Stand sind. Allerdings fehlt zu einigen Folgen das passende Skript. Betroffen sind davon die Episoden 424, 441, 447, 550 und alle ab 569. Angesichts der sonstigen Datenfülle stellen die vereinzelten Lücken aber kein Problem dar. Viel eher drehen da schon die verworrenen Datenwürste aus nicht-geschlossenen Anführungszeichen jeder weiteren Analyse den Strick. Auf diese Weise in sich verknotet, bleiben von den 600 Episoden-Datenzeilen nur mehr 238 übrig. Von den erwartbaren 158.315 Skriptzeilen, verlieren sich sogar 57.004 in diesem Geflecht und von ‚wohl strukturiert‘ kann dabei mit keinem Wort mehr die Rede sein.
Schelmische Verursacher dieses Sauhaufens sind – wie so oft – Rohtext-Spalten mit ihrer Hassliebe zum csv-Format: ungeschlossene Ketten von Anführungszeichen in Kombination mit Rohtext, der das Spaltentrennzeichen enthält. Damit finden sich zum Einen teils mehrere Datenzeilen in einer Zelle komprimiert, zum Anderen verschieben sich die Inhalte.
Was mensch sonntags halt so macht
Nachdem sich die manuelle Suche und Korrektur bei dieser Datenfülle recht schnell äußerst umständlich gestaltet, heißt es die ursprünglichen Reisepläne erst mal auf Eis zu legen. Als Trostpreis lockt stattdessen ein sonntäglicher Ausflug in die Regex-Welt. Das neue Reiseziel ist bekannt: zwei saubere Datensätze; der Weg dorthin gleicht vermutlich aber ein wenig einem mit Stolpersteinen gepflastertes Labyrinth aus Rohtextranken. Die richtige Regex-Blaupause im Gepäch, sollte aber auch das zielstrebige Durchqueren des Irrgartens kein hoffnungsloses Unterfangen werden. Zumindest spricht so die Zuversicht. Also ab zum Zeichner*innentisch für die freie Definition von Spaltengrenzen.
################################################################################ # EPISODEN EINLESEN # ################################################################################ # Episoden-CSV zeilenweise einlesen dmmy <- readLines("simpsons_episodes.csv")[-1] # header löschen # Spalten definieren match.list<- c("^[0-9]*?", # id ".*?", # title "\\d{4}-\\d{2}-\\d{2}", # original_air_date ".*?", # production_code "[0-9]*?", # season "[0-9]*?", # number_in_season "[0-9]*?", # number_in_series ".*?", # us_viewers_in_millions "[0-9]*?", # views ".*?", # imdb_rating ".*?", # imdb_votes ".*?", # img_url ".*$") # video_url # Ein kurzer Blick auf die ersten Datenzeilen head(dmmy)
## [1] "10,Homer's Night Out,1990-03-25,7G10,1,10,10,30.3,50816,7.4,1511,http://static-media.fxx.com/img/FX_Networks_-_FXX/305/815/Simpsons_01_10.jpg,http://www.simpsonsworld.com/video/275197507879" ## [2] "12,Krusty Gets Busted,1990-04-29,7G12,1,12,12,30.4,62561,8.3,1716,http://static-media.fxx.com/img/FX_Networks_-_FXX/245/843/Simpsons_01_12.jpg,http://www.simpsonsworld.com/video/288019523914" ## [3] "14,Bart Gets an \"F\",1990-10-11,7F03,2,1,14,33.6,59575,8.2,1638,http://static-media.fxx.com/img/FX_Networks_-_FXX/662/811/bart_gets_F.jpg,http://www.simpsonsworld.com/video/260539459671" ## [4] "17,Two Cars in Every Garage and Three Eyes on Every Fish,1990-11-01,7F01,2,4,17,26.1,64959,8.1,1457,http://static-media.fxx.com/img/FX_Networks_-_FXX/660/859/Simpsons_02_01.jpg,http://www.simpsonsworld.com/video/260537411822" ## [5] "19,Dead Putting Society,1990-11-15,7F08,2,6,19,25.4,50691,8,1366,http://static-media.fxx.com/img/FX_Networks_-_FXX/662/811/Simpsons_02_08.jpg,http://www.simpsonsworld.com/video/260539459670" ## [6] "21,Bart the Daredevil,1990-12-06,7F06,2,8,21,26.2,57605,8.4,1522,http://static-media.fxx.com/img/FX_Networks_-_FXX/662/811/Simpsons_02_06.jpg,http://www.simpsonsworld.com/video/260539459702"
################################################################################ # SKRIPT EINLESEN # ################################################################################ # Skript-CSV zeilenweise einlesen dmmy <- readLines("simpsons_script_lines.csv")[-1] # header löschen # Spalten definieren match.list <- c("^[0-9]*?", # id "[0-9]*?", # episode_id "[0-9]*?", # number ".*?", # raw_text "[0-9]*?", # timestamp_in_ms "\\w*", # speaking_line "[0-9]*?", # character_id "[0-9]*?", # location_id ".*?", # raw_character_text ".*?", # raw_location_text ".*", # spoken_words ".*?", # normalized_text "[0-9]*$") # word_count # Ein kurzer Blick auf die ersten Datenzeilen head(dmmy)
## [1] "9549,32,209,\"Miss Hoover: No, actually, it was a little of both. Sometimes when a disease is in all the magazines and all the news shows, it's only natural that you think you have it.\",848000,true,464,3,Miss Hoover,Springfield Elementary School,\"No, actually, it was a little of both. Sometimes when a disease is in all the magazines and all the news shows, it's only natural that you think you have it.\",no actually it was a little of both sometimes when a disease is in all the magazines and all the news shows its only natural that you think you have it,31" ## [2] "9550,32,210,Lisa Simpson: (NEAR TEARS) Where's Mr. Bergstrom?,856000,true,9,3,Lisa Simpson,Springfield Elementary School,Where's Mr. Bergstrom?,wheres mr bergstrom,3" ## [3] "9551,32,211,Miss Hoover: I don't know. Although I'd sure like to talk to him. He didn't touch my lesson plan. What did he teach you?,856000,true,464,3,Miss Hoover,Springfield Elementary School,I don't know. Although I'd sure like to talk to him. He didn't touch my lesson plan. What did he teach you?,i dont know although id sure like to talk to him he didnt touch my lesson plan what did he teach you,22" ## [4] "9552,32,212,Lisa Simpson: That life is worth living.,864000,true,9,3,Lisa Simpson,Springfield Elementary School,That life is worth living.,that life is worth living,5" ## [5] "9553,32,213,\"Edna Krabappel-Flanders: The polls will be open from now until the end of recess. Now, (SOUR) just in case any of you have decided to put any thought into this, we'll have our final statements. Martin?\",864000,true,40,3,Edna Krabappel-Flanders,Springfield Elementary School,\"The polls will be open from now until the end of recess. Now, just in case any of you have decided to put any thought into this, we'll have our final statements. Martin?\",the polls will be open from now until the end of recess now just in case any of you have decided to put any thought into this well have our final statements martin,33" ## [6] "9554,32,214,Martin Prince: (HOARSE WHISPER) I don't think there's anything left to say.,877000,true,38,3,Martin Prince,Springfield Elementary School,I don't think there's anything left to say.,i dont think theres anything left to say,8"
In der Hoffnung, dass mein fehlender Unterricht in geometrischem Zeichnen nicht allzu starken Eingang in meine Blaupause findet, gilt es die Enden der einzelnen Teilstück noch so zu kombinieren, dass – so die eigentlich triviale Aufgabe – für jede Spalte aus den einzelnen Datenzeilen-Strings immer nur genau jener Inhalt kopiert wird, der auch tatsächlich zur jeweiligen Spalte passt. Um als Match durchzugehen, muss ein Textausschnitt dazu von allen zuvor bereits verwendeten sowie von allen noch nachfolgenden Regex-Ausdrücken umschlossen sein. Um nicht in mühsame und fehleranfällige Kleinsarbeit an der Erschaffung von Regex-Monstern zu verfallen, bietet es sich hier geradezu an mit der Linken nach Abstraktion und mit Rechten nach Iteration zu langen und [R] den Rest der Arbeit übernehmen zu lassen.
# data.frame spaltenweise befüllen match <- 1:length(dmmy) for (i in 1:length(match.list)){ start <- paste(match.list[1:(i-1)],collapse=",") end <- paste(match.list[(i+1):length(match.list)],collapse=",") start <- ifelse(i==1,"",paste0("(?:",start,",)")) # non-capturing group cap <- paste0("(",match.list[i],")") # capturing group end <- ifelse(i==length(match.list),"", # non-capturing group paste0("(?:,",end,")")) match <- cbind(match,str_match(dmmy,paste0(start,cap,end))[,2]) } script.df <- dplyr::as_data_frame(match) %>% magrittr::set_colnames(c("row","id","episode_id","number","raw_text", "timestamp_in_ms","speaking_line","character_id", "location_id","raw_character_text", "raw_location_text","spoken_words", "normalized_text","word_count"))
Einem ersten, prüfenden Blick scheinen die Ergebnisse standzuhalten: alle Zeilen sind vorhanden, alle Spalten im korrekten Format und die überwiegende Zahl der Zellen mit sinnhaften Werten befüllt. Kleinere Inkonsistenzen stechen zwar bei den Spalten raw_character_text, raw_location_text und spoken_words (eine Mischung aus raw_text und normalized_text … standardmäßig roh oder roher Standard … wie mensch’s nimmt) ins Auge. Da es sich dabei aber durchwegs um redundante Informationen handelt, sollte auch das kein allzu großer Stolperstein auf dem Weg durchs Labyrinth sein. Stattdessen lassen wir diese Spalten mit geringer Qualität einfach zurück. Sicherheitshalber lässt sich der Word Count auch nochmal anwerfen. Im Vergleich zur word_count-Spalte aus den Rohdaten liefert das weitgehend vergleichbare Ergebnisse und im Bereich von +/- einem Wort Abweichung auch leichte Verbesserungen. In dem Sinn: Springfield, ahoi!
# Episoden.df strukturieren cols <- colnames(episodes.df) episodes.df <- dplyr::as_data_frame(episodes.df) %>% mutate_each_(funs(as.numeric),cols[c(1,2,6:12)]) %>% mutate_each_(funs(as.Date),cols[4]) # Script.df strukturieren/word_count neu berechnen cols <- colnames(script.df) script.df <- dplyr::as_data_frame(script.df) %>% mutate_each_(funs(as.numeric),cols[c(1:4,6,8,9)]) %>% mutate_each_(funs(as.logical),cols[7]) %>% select(row:location_id,normalized_text) %>% mutate(word_count=str_count(normalized_text,"\\S+"))
str(character.df)
## Classes 'tbl_df', 'tbl' and 'data.frame': 6722 obs. of 4 variables: ## $ id : int 7 12 13 16 20 24 26 27 29 30 ... ## $ name : chr "Children" "Mechanical Santa" "Tattoo Man" "DOCTOR ZITSOFSKY" ... ## $ normalized_name: chr "children" "mechanical santa" "tattoo man" "doctor zitsofsky" ... ## $ gender : chr "" "" "" "" ...
str(location.df)
## Classes 'tbl_df', 'tbl' and 'data.frame': 4459 obs. of 3 variables: ## $ id : int 1 2 3 4 5 6 7 8 9 10 ... ## $ name : chr "Street" "Car" "Springfield Elementary School" "Auditorium" ... ## $ normalized_name: chr "street" "car" "springfield elementary school" "auditorium" ...
episodes.df str(episodes.df) summary(episodes.df)
# A tibble: 600 × 14 row id title 1 1 10 Homer's Night Out 2 2 12 Krusty Gets Busted 3 3 14 Bart Gets an "F" 4 4 17 Two Cars in Every Garage and Three Eyes on Every Fish 5 5 19 Dead Putting Society 6 6 21 Bart the Daredevil 7 7 23 Bart Gets Hit by a Car 8 8 26 Homer vs. Lisa and the 8th Commandment 9 9 28 "Oh Brother, Where Art Thou?" 10 10 30 Old Money # ... with 590 more rows, and 11 more variables: original_air_date , # production_code , season , number_in_season , # number_in_series , us_viewers_in_millions , views , # imdb_rating , imdb_votes , img_url , video_url
## Classes 'tbl_df', 'tbl' and 'data.frame': 600 obs. of 14 variables: ## $ row : num 1 2 3 4 5 6 7 8 9 10 ... ## $ id : num 10 12 14 17 19 21 23 26 28 30 ... ## $ title : chr "Homer's Night Out" "Krusty Gets Busted" "Bart Gets an \"F\"" "Two Cars in Every Garage and Three Eyes on Every Fish" ... ## $ original_air_date : Date, format: "1990-03-25" "1990-04-29" ... ## $ production_code : chr "7G10" "7G12" "7F03" "7F01" ... ## $ season : num 1 1 2 2 2 2 2 2 2 2 ... ## $ number_in_season : num 10 12 1 4 6 8 10 13 15 17 ... ## $ number_in_series : num 10 12 14 17 19 21 23 26 28 30 ... ## $ us_viewers_in_millions: num 30.3 30.4 33.6 26.1 25.4 26.2 24.8 26.2 26.8 21.2 ... ## $ views : num 50816 62561 59575 64959 50691 ... ## $ imdb_rating : num 7.4 8.3 8.2 8.1 8 8.4 7.8 8 8.2 7.6 ... ## $ imdb_votes : num 1511 1716 1638 1457 1366 ... ## $ img_url : chr "http://static-media.fxx.com/img/FX_Networks_-_FXX/305/815/Simpsons_01_10.jpg" "http://static-media.fxx.com/img/FX_Networks_-_FXX/245/843/Simpsons_01_12.jpg" "http://static-media.fxx.com/img/FX_Networks_-_FXX/662/811/bart_gets_F.jpg" "http://static-media.fxx.com/img/FX_Networks_-_FXX/660/859/Simpsons_02_01.jpg" ... ## $ video_url : chr "http://www.simpsonsworld.com/video/275197507879" "http://www.simpsonsworld.com/video/288019523914" "http://www.simpsonsworld.com/video/260539459671" "http://www.simpsonsworld.com/video/260537411822" ...
## row id title original_air_date ## Min. : 1.0 Min. : 1.0 Length:600 Min. :1989-12-17 ## 1st Qu.:150.8 1st Qu.:150.8 Class :character 1st Qu.:1996-05-03 ## Median :300.5 Median :300.5 Mode :character Median :2003-02-05 ## Mean :300.5 Mean :300.5 Mean :2003-03-04 ## 3rd Qu.:450.2 3rd Qu.:450.2 3rd Qu.:2010-01-04 ## Max. :600.0 Max. :600.0 Max. :2016-10-16 ## ## production_code season number_in_season number_in_series ## Length:600 Min. : 1.0 Min. : 1.00 Min. : 1.0 ## Class :character 1st Qu.: 7.0 1st Qu.: 6.00 1st Qu.:150.8 ## Mode :character Median :14.0 Median :11.00 Median :300.5 ## Mean :14.1 Mean :11.59 Mean :300.5 ## 3rd Qu.:21.0 3rd Qu.:17.00 3rd Qu.:450.2 ## Max. :28.0 Max. :25.00 Max. :600.0 ## ## us_viewers_in_millions views imdb_rating imdb_votes ## Min. : 2.320 Min. : 144 Min. :4.500 Min. : 104.0 ## 1st Qu.: 7.055 1st Qu.: 41302 1st Qu.:6.900 1st Qu.: 560.0 ## Median :10.300 Median : 46036 Median :7.300 Median : 697.0 ## Mean :11.843 Mean : 48759 Mean :7.386 Mean : 832.4 ## 3rd Qu.:15.250 3rd Qu.: 57594 3rd Qu.:8.000 3rd Qu.:1095.0 ## Max. :33.600 Max. :171408 Max. :9.200 Max. :3734.0 ## NA's :6 NA's :4 NA's :3 NA's :3 ## img_url video_url ## Length:600 Length:600 ## Class :character Class :character ## Mode :character Mode :character
script.df str(script.df) summary(script.df)
## # A tibble: 158,315 × 11 ## row id episode_id number ## ## 1 1 9549 32 209 ## 2 2 9550 32 210 ## 3 3 9551 32 211 ## 4 4 9552 32 212 ## 5 5 9553 32 213 ## 6 6 9554 32 214 ## 7 7 9555 32 215 ## 8 8 9556 32 216 ## 9 9 9557 32 217 ## 10 10 9558 32 218 ## # ... with 158,305 more rows, and 7 more variables: raw_text , ## # timestamp_in_ms , speaking_line , character_id , ## # location_id , normalized_text , word_count
## Classes 'tbl_df', 'tbl' and 'data.frame': 158315 obs. of 11 variables: ## $ row : num 1 2 3 4 5 6 7 8 9 10 ... ## $ id : num 9549 9550 9551 9552 9553 ... ## $ episode_id : num 32 32 32 32 32 32 32 32 32 32 ... ## $ number : num 209 210 211 212 213 214 215 216 217 218 ... ## $ raw_text : chr "\"Miss Hoover: No, actually, it was a little of both. Sometimes when a disease is in all the magazines and all the news shows, "| __truncated__ "Lisa Simpson: (NEAR TEARS) Where's Mr. Bergstrom?" "Miss Hoover: I don't know. Although I'd sure like to talk to him. He didn't touch my lesson plan. What did he teach you?" "Lisa Simpson: That life is worth living." ... ## $ timestamp_in_ms: num 848000 856000 856000 864000 864000 877000 881000 882000 889000 889000 ... ## $ speaking_line : logi TRUE TRUE TRUE TRUE TRUE TRUE ... ## $ character_id : num 464 9 464 9 40 38 40 8 NA 9 ... ## $ location_id : num 3 3 3 3 3 3 3 3 374 374 ... ## $ normalized_text: chr "no actually it was a little of both sometimes when a disease is in all the magazines and all the news shows its only natural th"| __truncated__ "wheres mr bergstrom" "i dont know although id sure like to talk to him he didnt touch my lesson plan what did he teach you" "that life is worth living" ... ## $ word_count : int 31 3 22 5 33 8 1 5 0 4 ...
## row id episode_id number ## Min. : 1 Min. : 1 Min. : 1.0 Min. : 0.0 ## 1st Qu.: 39580 1st Qu.: 39580 1st Qu.:138.0 1st Qu.: 70.0 ## Median : 79158 Median : 79158 Median :274.0 Median :140.0 ## Mean : 79158 Mean : 79158 Mean :278.4 Mean :141.5 ## 3rd Qu.:118737 3rd Qu.:118737 3rd Qu.:418.0 3rd Qu.:210.0 ## Max. :158315 Max. :158315 Max. :568.0 Max. :394.0 ## ## raw_text timestamp_in_ms speaking_line character_id ## Length:158315 Min. : 0 Mode :logical Min. : 1.0 ## Class :character 1st Qu.: 354000 FALSE:26163 1st Qu.: 2.0 ## Mode :character Median : 657000 TRUE :132152 Median : 9.0 ## Mean : 662531 NA's :0 Mean : 667.1 ## 3rd Qu.: 971000 3rd Qu.: 240.0 ## Max. :1447000 Max. :6749.0 ## NA's :17526 ## location_id normalized_text word_count ## Min. : 1.0 Length:158315 Min. : 0.000 ## 1st Qu.: 5.0 Class :character 1st Qu.: 2.000 ## Median : 194.0 Mode :character Median : 6.000 ## Mean : 846.2 Mean : 8.253 ## 3rd Qu.:1358.0 3rd Qu.: 12.000 ## Max. :4459.0 Max. :122.000 ## NA's :407