Tidy Tuesday Aufgaben in Python Pandas gelöst

Dieses Tutorial ist für dich interessant, wenn du mithilfe konkreter Codesbeispiele mehr über die Gemeinsamkeiten des R tidyverse und Pandas erfahren willst. Als beispielshafter Datensatz soll horror movie ratings aus dem dem R Tidy Tuesday-Projekt dienen. Beim Vorgehen orientieren wir uns mit unseren Fragen zu den Daten im Code beispielhaft an David Robinson, der auf Youtube Video veröffentlicht wie er Datensätze bearbeitet, unter anderen auch horror movie ratings.

R library() und Python import X as Y

Im ersten Schritt importieren wir in R das Tidyverse, das wir zum Reinigen und Analysieren des Datensatzes benötigen. Dazu verwenden wir die Funktion library, um das tidyverse in das Notebook zu importieren. Zusätzlich das reticulate-package, um Python Code in diesem Notebook auszuführen zu können.

#install.packages("reticulate")
#install.packages("tidyverse")
library(tidyverse)
## -- Attaching packages --------------------------------------- tidyverse 1.3.0 --
## v ggplot2 3.3.3     v purrr   0.3.4
## v tibble  3.1.0     v dplyr   1.0.5
## v tidyr   1.1.3     v stringr 1.4.0
## v readr   1.4.0     v forcats 0.5.1
## Warning: Paket 'tibble' wurde unter R Version 4.0.4 erstellt
## Warning: Paket 'tidyr' wurde unter R Version 4.0.4 erstellt
## Warning: Paket 'dplyr' wurde unter R Version 4.0.4 erstellt
## Warning: Paket 'forcats' wurde unter R Version 4.0.4 erstellt
## -- Conflicts ------------------------------------------ tidyverse_conflicts() --
## x dplyr::filter() masks stats::filter()
## x dplyr::lag()    masks stats::lag()
library(reticulate)

Im Gegensatz zu R werden in Python Module über die sogenannte import Funktion in den Arbeitsbereich geholt. Über das Kürzel as kann eine Art Schnellzugriff festgelegt werden. Die Namen der Kürzel sind überlicherweise per Konvention festgelegt.

#Python
import pandas as pd
print(pd.__version__)
## 1.2.1

Als nächstes folgt der Import des horror movie ratingsDatensatzes. Zum Einlesen von Dateien beinhaltet das R Tidyverse das Paket readr. Dieses funktioniert oft reibungsloser mit den Import- Funktionen aus Base R. Mit readr eigelesener Code steht zudem als tibble bereit. Als Variable verwenden wir horror_movies_raw.

#R
horror_movies_raw <- readr::read_csv("https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2019/2019-10-22/horror_movies.csv")
## 
## -- Column specification --------------------------------------------------------
## cols(
##   title = col_character(),
##   genres = col_character(),
##   release_date = col_character(),
##   release_country = col_character(),
##   movie_rating = col_character(),
##   review_rating = col_double(),
##   movie_run_time = col_character(),
##   plot = col_character(),
##   cast = col_character(),
##   language = col_character(),
##   filming_locations = col_character(),
##   budget = col_character()
## )

Das Einlesen des Datensatzes geschieht in Python ganz ähnlich wie in der R Programmiersprache über eine simple Variablenzuweisung. Zu beachten ist besonders der Unterschied zwischen den Zuweisungs-Operatoren. Zwar kann wie in Python auch in R das = als Operator benutzt werden, üblicherweise verwendet man allerdings den Pfeil <- als Zuweisungsoperator.

Wir lesen den Datensatz über die Funktion pd.read_csv() ein und ordnen dem Ergebnis die Variable horror_movies_raw zu. Wie R besitzt Pandas die Fähigkeit, den ausgewählten Datensatz über eine Webadresse einzulesen, von welcher wir hier Gebrauch machen. Dabei ist darauf zu achten, bei einem Datensatz auf github immer die sogenannte “raw”-Datei, also den Rohdatensatz zu verlinken.

Lesen wir URL ein und lassen uns die ersten Zeilen des Datensatzes anzeigen.

#Python
url = r"https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2019/2019-10-22/horror_movies.csv"
horror_movies_raw = pd.read_csv(url) #Einlesen des Datensatzes über pd.read_csv()
horror_movies_raw 
##                                      title  ...       budget
## 0                               Gut (2012)  ...          NaN
## 1          The Haunting of Mia Moss (2017)  ...      $30,000
## 2                      Sleepwalking (2017)  ...          NaN
## 3      Treasure Chest of Horrors II (2013)  ...          NaN
## 4                           Infidus (2015)  ...          NaN
## ...                                    ...  ...          ...
## 3323            Victor Frankenstein (2015)  ...  $40,000,000
## 3324  The Exorcism of Molly Hartley (2015)  ...          NaN
## 3325                    Talon Falls (2017)  ...          NaN
## 3326    BloodRayne: The Third Reich (2011)  ...  $10,000,000
## 3327                     13 Cameras (2015)  ...          NaN
## 
## [3328 rows x 12 columns]

R head() und Python .head()

Fast jede Datenanalyse beginnt mit dem Anzeigen der ersten Zeilen des Datensatze, dem “Kopf” der Daten, um eine erste Übersicht über die Struktur der Daten zu erhalten. Dies geschieht mithilfe der Funktion head() in R, bzw. .head() in Pandas. Die große Ähnlichkeit des R- und Pandas-Funktionen kann schnell zu Verwechslungen führen.

#R
head(horror_movies_raw, 5)
## # A tibble: 5 x 12
##   title        genres    release_date release_country movie_rating review_rating
##   <chr>        <chr>     <chr>        <chr>           <chr>                <dbl>
## 1 Gut (2012)   Drama| H~ 26-Oct-12    USA             <NA>                   3.9
## 2 The Hauntin~ Horror    13-Jan-17    USA             <NA>                  NA  
## 3 Sleepwalkin~ Horror    21-Oct-17    Canada          <NA>                  NA  
## 4 Treasure Ch~ Comedy| ~ 23-Apr-13    USA             NOT RATED              3.7
## 5 Infidus (20~ Crime| D~ 10-Apr-15    USA             <NA>                   5.8
## # ... with 6 more variables: movie_run_time <chr>, plot <chr>, cast <chr>,
## #   language <chr>, filming_locations <chr>, budget <chr>
#Python
horror_movies_raw.head(5)
##                                  title  ...   budget
## 0                           Gut (2012)  ...      NaN
## 1      The Haunting of Mia Moss (2017)  ...  $30,000
## 2                  Sleepwalking (2017)  ...      NaN
## 3  Treasure Chest of Horrors II (2013)  ...      NaN
## 4                       Infidus (2015)  ...      NaN
## 
## [5 rows x 12 columns]

R arrange() und Python sort_values()

Nachdem wir den Datensatz erfolgreich eingelesen und einen ersten Blick auf die Daten geworfen haben, widmen wir uns der explorativen Erforschung des Datensatzes. Gehen wir mit David Robinson mit R zunächst der Frage nach, welcher der bestbewertete Horrofilm in unserem Datensatz ist. Dazu müssen wir nur die Werte der Spalte review_rating absteigend sortieren. Im Tidyverse nutzen wir dazu die Funktion arrange().

#R
horror_movies <- horror_movies_raw %>%
  arrange(desc(review_rating))
horror_movies
## # A tibble: 3,328 x 12
##    title      genres     release_date release_country movie_rating review_rating
##    <chr>      <chr>      <chr>        <chr>           <chr>                <dbl>
##  1 Bonehill ~ Horror     27-Oct-17    USA             <NA>                   9.8
##  2 The Theta~ Action| H~ 13-Oct-17    USA             <NA>                   9.6
##  3 The Carmi~ Comedy| H~ 26-Oct-17    Canada          <NA>                   9.6
##  4 The Small~ Horror     1-Oct-17     UK              <NA>                   9.5
##  5 Hotel Inf~ Action| H~ 29-Sep-17    UK              <NA>                   9.5
##  6 Flesh of ~ Horror     21-Oct-17    USA             <NA>                   9.5
##  7 Bong of t~ Horror     20-Oct-17    USA             <NA>                   9.4
##  8 The Thirt~ Horror     15-May-17    UK              <NA>                   9.4
##  9 Take 2: T~ Horror     1-Feb-15     USA             <NA>                   9.3
## 10 Johann Ka~ Action| A~ 1-Sep-16     USA             <NA>                   9.3
## # ... with 3,318 more rows, and 6 more variables: movie_run_time <chr>,
## #   plot <chr>, cast <chr>, language <chr>, filming_locations <chr>,
## #   budget <chr>

In Code verketten wir gleich zwei Befehle mithilfe des aus dem magittr Paket entstammenden %>%oder Pipe-Operators (grob “Rohrpost-Operator”). Der Pipe-Operator erlaubt den Nutzern des tidyverse Funktionen miteinander zu verketten, deren Ergebnis immer eine gleichbleibende Datenstruktur ist. Auf diese Art und Weise kann ein Datensatz zeilenweise immer weiter modifiziert werden, ohne unübersichtliche, ineinander verschachtelte Ausdrücke wie func(g(h(df), arg1=a), arg2=b, arg3=c) verwenden zu müssen oder den Datensatz immer wieder zwischenspeichern zu müssen.

Inspiriert vom Pipe-Operator %>% hat auch Pandas seine eigene Funktion zum Verknüpfen von Funktionen erhalten, die Funktion .pipe(). Man sollte beachten, dass man den gesamten Ausdruck in Klammern() setzt, damit alle verketteten Befehle ausgeführt werden. In Pandas nutzen wir .sort_vallues, um den Datensatz genau wie im Tidyverse absteiged zu sortieren. Über den Parameter ascending=bool können wir zusätzlich steuern wie die Werte sortiert werden sollen. In diesem Fall mit ascending=False, also absteigend sortieren.

#Pandas
horror_movies = (horror_movies_raw  #hier müssen Klammern benutzt werden
                   .sort_values("review_rating", ascending=False))
horror_movies[['title', 'review_rating']].head(3)   #Anzeigen der ersten 3 Zeilen des dataframe mit den Spalten 'title' und 'review_rating'     
##                           title  review_rating
## 2569       Bonehill Road (2017)            9.8
## 3314  The Carmilla Movie (2017)            9.6
## 1961      The Theta Girl (2017)            9.6

Die Sortierung zeigt: Der bestbewertete Film des Datensatzes lautet “Bonehill Road”. Um den jeweils höchsten Wert einer Spalte abzurufen, stellt Pandas sogar eine noch praktischere Methode namens .idxmax() bereit.

#Python
(horror_movies
  .loc[horror_movies['review_rating']
  .idxmax()])
## title                                             Bonehill Road (2017)
## genres                                                          Horror
## release_date                                                 27-Oct-17
## release_country                                                    USA
## movie_rating                                                       NaN
## review_rating                                                      9.8
## movie_run_time                                                     NaN
## plot                 Directed by Todd Sheets. With Andrew Baltes, C...
## cast                 Andrew Baltes|Clinton Baysinger|Logan Boese|Aa...
## language                                                       English
## filming_locations                                                  NaN
## budget                                                             NaN
## Name: 2569, dtype: object

R extract() und Python str.extract()

David Robinson stellt sich in R als nächstes der Frage, aus welchem Zeitraum die Horror-Filme des Datensatzes eigentlich stammen. Dazu sehen wir uns in Python mit der Selektionsmethode .iloc[] gleich noch einmal die erste Zeile des Datensatzes an. Das kryptische erscheinende .iloc[] steht schlicht und einfach für “integer location”, wir wählen also einen Bereich des Datensatzes über Ganzzahlen - und nicht Zeilen oder Spaltennamen - aus.

#Python
horror_movies_raw.iloc[0:1, :]
##         title                   genres  ... filming_locations budget
## 0  Gut (2012)  Drama| Horror| Thriller  ...     New York, USA    NaN
## 
## [1 rows x 12 columns]

Wir stoßen gleich auf ein Problem, was die Organisation der Daten angeht: Wie kann man die Verteilung der Filme anhand des Erscheinungsjahres aufzeigen, wenn gar keine “Jahr”-Spalte existiert? Wir haben zwei Möglichkeiten: Entweder man extrahiert das Jahr aus der Spalte release_date oder wir entnehmen das Erscheinungsjahr aus der Spalte title, der immer nach dem Prinzip Filmtitel (Jahr) aufgebaut ist. Weil das Jahr in der Spalte title im Gegensatz zu release_date bereits vierstellig vorliegt, entscheiden wir uns für title.

#R
horror_movies <- horror_movies_raw %>%
  extract(title, "release_year", "\\((\\d\\d\\d\\d)\\)$", remove = FALSE, convert = TRUE) %>% 
  arrange(release_year) 
  
horror_movies
## # A tibble: 3,328 x 13
##    title      release_year genres      release_date release_country movie_rating
##    <chr>             <int> <chr>       <chr>        <chr>           <chr>       
##  1 Warning S~         1923 Drama| Fan~ 29-Nov-16    Germany         UNRATED     
##  2 The Nest ~         1965 Drama| Hor~ 16-Oct-17    France          <NA>        
##  3 Belladonn~         1973 Animation|~ 12-Jul-16    USA             UNRATED     
##  4 Play Mote~         1979 Crime| Hor~ 25-Aug-15    USA             X           
##  5 Strasek, ~         1982 Horror      1-Jul-14     Switzerland     <NA>        
##  6 Mad Mutil~         1983 Horror      4-Dec-12     France          <NA>        
##  7 The Sea S~         1985 Adventure|~ 29-Jun-15    USA             <NA>        
##  8 Nekromant~         1991 Horror      10-Feb-15    USA             NOT RATED   
##  9 Viewer Di~         1998 Comedy| Ho~ 1-May-12     USA             NOT RATED   
## 10 I Zombie:~         1998 Drama| Hor~ 18-Feb-14    USA             <NA>        
## # ... with 3,318 more rows, and 7 more variables: review_rating <dbl>,
## #   movie_run_time <chr>, plot <chr>, cast <chr>, language <chr>,
## #   filming_locations <chr>, budget <chr>

Um Worte und Zahlen aus Spalten zu extrahieren, existiert im Tidyverse die Funktion extract(). Dabei gibt der erste Parameter title die Basis für die Extraktion an, der zweite Parameter den Namen der neuen, zu erstellenden Spalte (hier: year) und der dritte Parameter eine regular expression, um das Jahr aus dem string heraus zu filtern.

Die Verwendung einer regular expression besitzt beim Wechsel zwischen R und Python den Vorteil, dass sie größtenteils sprachenunabhängig verwendet werden kann. Was hilfreich in R ist, darf ruhigen Gewissens auch (angepasst) in Pandas verwendet werden. Da es sich bei den Werten in der Spalte title um strings, also Text handelt, können wir weiterhin über das im Tidyverse enthaltene Paket stringr auf ein umfangreiches Arsenal an string-Funktionen zurückgreifen, um mithilfe von regular expressions das Jahr aus dem string in der title-Spalte zu extrahieren.

#Python
horror_movies = (horror_movies_raw
    .assign(release_year=lambda x: x['title'].str.extract("\\((\\d\\d\\d\\d)\\)$"))) #Filtern des Jahres
horror_movies['release_year']
## 0       2012
## 1       2017
## 2       2017
## 3       2013
## 4       2015
##         ... 
## 3323    2015
## 3324    2015
## 3325    2017
## 3326    2011
## 3327    2015
## Name: release_year, Length: 3328, dtype: object

Achtung, alles was wir bis jetzt getan haben, ist, aus der Spalte title die Jahreswerte zu extrahieren und den Daten die Spalte release_year zuzuweisen. Eine kurze Überprüfung des Datentyps der Spalte mithilfe der .dtypes Funktion zeigt, wie es sich beim Typus der neuen Jahresspalte noch um einen Objekt-Typ (konkret, einen string) handelt. Warum ist das überhaupt von Bedeutung? Wenn wir die Veröffentlichungen pro Jahr später mit einem Histogramm visualisieren möchten, “versteht” das Visualisierungstool matplotlib keine Objekt-Werte, denn wie sollten Wörter sinnvoll in einem Graph angeordnet werden?

#Python
horror_movies['release_year'].dtypes #die Spalte release_year ist ein "Objekt"-string
## dtype('O')

Um den Datentyp der Spalte zu ändern, müssen wir also die Spalte release_year anhand von pd.to_numeric() in ein float-Objekt verwandeln, um die Daten visualisieren zu können.

#Python
horror_movies = (horror_movies
    .assign(release_year=lambda x: pd.to_numeric(x['release_year'])))
horror_movies.release_year.dtypes #ein kurzer Test, ob die Umwandlung funktioniert hat
## dtype('float64')

R qplot() und Python .plot.X()

Nach dieser Transformation kann der Datensatz sowohl im Tidyverse als auch in Pandas zum ersten Mal visualisiert werden. Wir verwenden in R die Funktion quickplot oder kurz qplot(). Weil es relativ wenig vor 2005 erschienen Filme im Datensatz existieren, beschränken wir die Auswahl auf Filme zwischen 2005 und 2020 und teilen die Filme in 10 Klassen (“bins”) ein.

#R
qplot(horror_movies$release_year, geom = "histogram", bins = 15, xlim = c(2005,2020)) 
## Warning: Removed 15 rows containing non-finite values (stat_bin).
## Warning: Removed 2 rows containing missing values (geom_bar).

So schnell und reibungslos wie wie über qplot() lassen sich auch in Pandas Plots über die Funktion des jeweiligen Graphentyp erstellen, nämlich in unserem Fall über plot.hist, also ein Histogramm. Wir entscheiden uns für 15 “bins” (Abschnitte) und plotten den Zeitraum von 2005 bis 2020.

#Python
horror_movies.plot.hist(by='release_year', bins=15, range=[2005,2020])

Wir sparen noch mehr Code in Pandas, wenn wir statt .plot.hist() direkt auf die Plotting-Methode .hist() zurückgreifen.

#Python
horror_movies.hist(column='release_year', bins=15, range=[2005,2020])
## array([[<AxesSubplot:title={'center':'release_year'}>]], dtype=object)

R .count() und Python .value_counts()

Wie viele verschiedene Sprachen sind im Datensatz abgebildet? Beides lässt sich im Tidyverse und Pandas ähnlich leicht heraus finden. Zunächst das Tidyverse.

#R
horror_movies %>% 
  count(language, sort = TRUE) 
## # A tibble: 188 x 2
##    language             n
##    <chr>            <int>
##  1 English           2421
##  2 Spanish             96
##  3 Japanese            77
##  4 <NA>                71
##  5 Hindi               37
##  6 Filipino|Tagalog    34
##  7 Thai                34
##  8 English|Spanish     30
##  9 Turkish             30
## 10 Tamil               29
## # ... with 178 more rows

Beide Funktionen gleichen sich in R und Python stark. Die count(), bzw. .value_count() Funktionen vereinen mehrere Arbeitsschritte: Die Daten werden zunächst auf ihre singuläre Werte überprüft, dann gruppiert und schlussendlich summiert.

#Python
horror_movies['language'].value_counts(sort=True)
## English                     2421
## Spanish                       96
## Japanese                      77
## Hindi                         37
## Thai                          34
##                             ... 
## Catalan|English                1
## Basque                         1
## Filipino|Tagalog|Visayan       1
## Hindi|Marathi                  1
## Divehi                         1
## Name: language, Length: 187, dtype: int64

Im Ergebnis liegen die meisten Filme im Datensatz in Englisch vor und mit weitem Abstand folgen Spanisch und Japanisch. Mit der bereits erwähnten Funktion .idxmax() können wir den Spitzenreiter Englisch in Python erneut isoliert betrachten.

#Python
(horror_movies['language']
    .value_counts()
    .idxmax(1))
## 'English'

R convert und Python .astype()

Abschließend wollen wir uns auf David Robinsons Vorschlag hin die Frage stellen, ob ein Zusammenhang zwischen der Höhe des Filmbudgets und der Kritiker-Bewertung der Filme besteht. Das erscheint logisch, je mehr Geld für die Qualität der Produktion zur Verfügung steht, desto höher ist das Rating.

Beim Blick auf die Spalte buget fällt jedoch auf, wie die Spalte keine reinen Zahlen, sondern auch Dollar-Werte und Kommata enthält. Für die Darstellung im Histogramm ist es nötig, die Zahlen aus den Spalten zu isolieren sowie Dollarzeichen und Kommta zu entfernen. Das geht im Tidyverse wunderbar einfach dank der Funktion mutate(column = parse_number(colum).

#R
horror_movies <- horror_movies_raw %>%
  mutate(budget = parse_number(budget))

Leider läßt sich die Funktion parse_number() nicht eins zu eins auf Pandas übertragen. Wie schon bei der Extraktion der Jahreszahlen müssen wir alternativ auf regular expresssions zurück greifen, um die Zahlen in der Spalte “budget” zu isolieren.

#Python
horror_movies['budget']
## 0               NaN
## 1           $30,000
## 2               NaN
## 3               NaN
## 4               NaN
##            ...     
## 3323    $40,000,000
## 3324            NaN
## 3325            NaN
## 3326    $10,000,000
## 3327            NaN
## Name: budget, Length: 3328, dtype: object

Eine zusätzliche Herausforderung in Pandas ist das Entfernen der unterschiedlichen Währungsangaben wie Dollar, Euro usw. Zudem weist die Spalte ein Drittel fehlende Werte, also NaN-Werte (“not a number”), auf.

#Python
horror_movies = horror_movies_raw
horror_movies["budget_in_numbers_only"] = (horror_movies_raw["budget"]                      
                          .pipe(lambda x: x.str.extract(r"\D*([0-9]*[,]*[0-9]*[,]*[0-9]*)", expand=False)) #ohne expand=False wird ein Dataframe und kein Spalte an den nächsten pipe-Befehlt weiter gereicht und erzeugt einen Attribute-Fehler
                          .pipe(lambda x: x.str.split(","))                               
                          .pipe(lambda x: x.str.join("")))
                          
horror_movies["budget_in_numbers_only"]
## 0            NaN
## 1          30000
## 2            NaN
## 3            NaN
## 4            NaN
##           ...   
## 3323    40000000
## 3324         NaN
## 3325         NaN
## 3326    10000000
## 3327         NaN
## Name: budget_in_numbers_only, Length: 3328, dtype: object

R geom_point() und Python .plot.scatter()

So bleibt nur noch die Visualisierung der Werte als Histogramm, die wir in R mit dem im Tidyverse enthaltenen Paket ggplot durchführen können.

#R
horror_movies %>%
  ggplot(aes(budget, review_rating)) +
  geom_point() +
  scale_x_log10(labels = scales::dollar)
## Warning: Removed 2180 rows containing missing values (geom_point).

Mit den jetzt vorliegenden Zahlen in der Spalte budget können wir auch in Pandas den Zusammenhang zwischen Budget und Rating als Histogramm visualisieren.

#Python
import matplotlib 
ax  = horror_movies.plot.scatter(x="budget_in_numbers_only",
                      y='review_rating',
                      c='Black',
                      logx=True)
ax.set_xticks([100, 100000, 100000000])
## [<matplotlib.axis.XTick object at 0x000000006970E670>, <matplotlib.axis.XTick object at 0x000000006970E640>, <matplotlib.axis.XTick object at 0x00000000696A61F0>]
ax.get_xaxis().set_major_formatter(matplotlib.ticker.ScalarFormatter())
ax.set_xticklabels(["$100", "$100,000", "$100,000,000"])
## [Text(100, 0, '$100'), Text(100000, 0, '$100,000'), Text(100000000, 0, '$100,000,000')]
ax

Fazit

Der Code-Vergleich hat gezeigt, dass sich zumindest die genannten R-Analyseschritte auch in Pandas umsetzbar sind. Dass bestimmte R-Funktionen in Pandas hier oft aufwändiger “nachzubauen” scheinen, mag sicher auch damit zusammenhängen, welche Sprache den Ausgangspunkt für die Übersetzung bildet.

Tim Fangmeyer
Tim Fangmeyer
Aspiring data engineer and part-time wordsmith