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 ratings
Datensatzes. 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 “i
nteger loc
ation”, 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.