Springfield: im Regex-Labyrinth die zweite gleich rechts

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

2 Kommentare zu „Springfield: im Regex-Labyrinth die zweite gleich rechts

Gib deinen ab

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden /  Ändern )

Google Foto

Du kommentierst mit Deinem Google-Konto. Abmelden /  Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s

Bloggen auf WordPress.com.

Nach oben ↑

%d Bloggern gefällt das: