Spis treści:
Eksploracja danych z bibliotekami dplyr, tidyr oraz
stringr
- Podzbiory kolumn
- Filtrowanie wierszy
- Operatory logiczne, algebra Boola, prawa de Morgana
- Tworzenie nowych kolumn (1x Challenge)
- Wartości brakujące
- Manipulowanie tekstem (3x Challenge)
- Agregacja danych (1x Challenge)
- Tabele przestawne, dane w formacie long oraz wide
- Łączenie tabel
Przydatne materiały:
- dplyr
cheatsheet
- tidyr
cheatsheet
- stringr
cheatsheet
- ggplot2
cheatsheet
- A.
Kassambara - Guide to Create Beautiful Graphics in R.
Dane pochodzą ze strony https://flixgem.com/ (wersja zbioru danych z dnia 12 marca 2021). Dane zawierają informacje na temat 9425 filmów i seriali dostępnych na Netlix.
Eksploracja danych z bibliotekami dplyr oraz tidyr
Podzbiory kolumn
Kolumny wybieramy po ich nazwach za pomocą funkcji select(). Możemy też usuwać kolumny, poprzedzając nazwę danej kolumny symbolem -.
dane %>%
select(Title, Runtime, IMDb.Score, Release.Date) %>%
head(5)
dane %>%
select(-Netflix.Link, -IMDb.Link, -Image, -Poster, -TMDb.Trailer)%>%
head(5)
dane %>%
select(1:10)%>%
head(5)
dane %>%
select(Title:Runtime)%>%
head(5)
Przydatne funkcje podczas wybierania/usuwania kolumn: - starts_with() - wybieramy lub usuwamy kolumny zaczynające się danym ciągiem znaków - ends_with() - wybieramy lub usuwamy kolumny kończące się danym ciągiem znaków - contains() - wybieramy lub usuwamy kolumny zawierające dany ciąg znaków.
dane %>%
select(starts_with('IMDb'))%>%
head(10)
dane %>%
select(ends_with('Score'))%>%
head(10)
dane %>%
select(contains('Date'))%>%
head(10)
Za pomocą funkcji matches() wybieramy lub usuwamy kolumny zawierające dane wyrażenie regularne. Przydatne narzędzie w budowaniu i testowaniu wyrażeń regularnych jest pod linkiem https://regex101.com/.
dane %>%
select(matches('^[a-z]{5,6}$')) %>%
head(10)
dane %>%
select(-matches('\\.'))%>%
head(10)
Funkcja select() zawsze zwraca ramkę danych, natomiast mamy też możliwość zwrócenia wektora za pomocą funkcji pull().
dane %>%
select(IMDb.Score)%>%
head(10)
# dane %>%
# select(IMDb.Score) %>%
# unlist(use.names = FALSE)
dane %>%
pull(IMDb.Score)%>%
head(10)
dane %>%
pull(IMDb.Score, Title)%>%
head(10)
Filtrowanie wierszy
Wiersze filtrujemy za pomocą funkcji filter() korzystając z operatorów ==, !=, >, >=, <, <=, between().
dane %>%
filter(Series.or.Movie == "Series")%>%
head(10)
dane %>%
filter(IMDb.Score > 8)%>%
head(10)
Operatory logiczne, algebra Boola, prawa de Morgana
Operator logiczny AND oznaczany symbolem & - FALSE & FALSE = FALSE - FALSE & TRUE = FALSE - TRUE & FALSE = FALSE - TRUE & TRUE = TRUE
dane %>%
filter(IMDb.Score >= 8 & Series.or.Movie == 'Series')%>%
head(10)
Operator logiczny OR oznaczany symbolem | - FALSE | FALSE = FALSE - FALSE | TRUE = TRUE - TRUE | FALSE = TRUE - TRUE | TRUE = TRUE
dane %>%
filter(IMDb.Score >= 9 | IMDb.Votes < 1000)%>%
head(10)
Prawa de Morgana mówią, że gdy wchodzimy z negacją pod nawias, to OR zamienia się na AND (i na odwrót). not (A & B) = (not A) | (not B) not (A | B) = (not A) & (not B)
dane %>%
filter(!(IMDb.Score >= 9 | IMDb.Votes < 1000))%>%
head(10)
dane %>%
filter(!(IMDb.Score >= 9) & !(IMDb.Votes < 1000))%>%
head(10)
Tworzenie nowych kolumn
Za pomocą funkcji mutate() dodajemy nowe kolumny do ramki danych albo edytujemy już istniejące kolumny.
dane %>%
mutate(score_category = if_else(IMDb.Score >= 5, 'Good', 'Poor')) %>%
select(Title, IMDb.Score, score_category)%>%
head(10)
dane %>%
transmute(
Release = Release.Date %>% as.Date(format = '%m/%d/%Y')
,Netflix.Release = Netflix.Release.Date %>% as.Date(format = '%m/%d/%Y')
)
CHALLENGE 1: Jaki jest najstarszy film Woody’ego Allena dostępny na Netflixie?
dane %>%
filter(Director == "Woody Allen", Series.or.Movie == "Movie") %>%
mutate(Release = Release.Date %>% as.Date(format = '%m/%d/%Y')) %>%
select(Title, Director, Release) %>%
arrange(Release)
Odp: Jak można odczytać z tabeli, najstarszy film Woddy’ego Allena na Netflixie to “Everything You Always Wanted to Know About Sex But Were Afraid to Ask” z 1972 roku :)
W przypadku funkcji case_when() nie musimy pisać warunków tworzących zbiory wzajemnie rozłączne. Ewaluacja następuje po spełnieniu pierwszego z warunków, po czym natychmiastowo następuje kolejna iteracja.
dane %>%
mutate(score_category = case_when(
IMDb.Score <= 2 ~ 'Very Poor'
,IMDb.Score <= 4 ~ 'Poor'
,IMDb.Score <= 6 ~ 'Medium'
,IMDb.Score <= 8 ~ 'Good'
,IMDb.Score <= 10 ~ 'Very Good'
)) %>%
select(Title, IMDb.Score, score_category)%>%
head(10)
Działania matematyczne wykonywane dla każdego wiersza i bazujące na kilku kolumnach wykonujemy przy pomocy funkcji rowwise().
dane %>%
mutate(avg_score = mean(c(IMDb.Score * 10
,Hidden.Gem.Score * 10
,Rotten.Tomatoes.Score
,Metacritic.Score)
,na.rm = TRUE) %>%
round(2)) %>%
select(Title, avg_score)%>%
head(10)
dane %>%
rowwise() %>%
mutate(avg_score = mean(c(IMDb.Score * 10
,Hidden.Gem.Score * 10
,Rotten.Tomatoes.Score
,Metacritic.Score)
,na.rm = TRUE) %>%
round(2)) %>%
select(Title, avg_score)%>%
head(10)
Domyślnie kolumny tworzone są pomocą mutate() są na końcu tabeli. Za pomocą relocate() możemy zmieniać pozycje poszczególnych kolumn w tabeli.
dane %>%
mutate(Popularity = if_else(IMDb.Votes > quantile(IMDb.Votes, 0.90, na.rm = TRUE), 'High', 'Not High')) %>%
relocate(Popularity, .after = Title)
Zmieniamy nazwy kolumn za pomocą funkcji rename().
dane %>%
rename(
Tytul = Title
,Gatunek = Genre
)
Wartości brakujące
Za pomocą funkcji z biblioteki tidyr możemy okiełznać wartości brakujące: - drop_na() - usuwamy wiersze zawierające wartości brakujące we wskazanych kolumnach - replace_na() - zastępujemy wartości brakujące określoną stałą - fill() - zastępujemy wartości brakujące poprzednią lub następną dostępną wartością.
dane %>%
sapply(function(x) is.na(x) %>% sum())
dane %>%
drop_na(Hidden.Gem.Score)
dane %>%
mutate(Hidden.Gem.Score = replace_na(Hidden.Gem.Score, median(Hidden.Gem.Score, na.rm = TRUE))) %>%
sapply(function(x) is.na(x) %>% sum())
dane %>%
replace_na(list(Hidden.Gem.Score = median(dane$Hidden.Gem.Score, na.rm = TRUE))) %>%
sapply(function(x) is.na(x) %>% sum())
Manipulowanie tekstem
Biblioteka stringr zawiera dużo przydatnych funkcji do manipulacji tekstem oraz wyrażeniami regularnymi. Większość funkcji z tej biblioteki zaczyna się od str_.
Q: Co można poprawić w poniższym kodzie, aby była zachowana konwencja stylu tidyverse?
gatunki = dane$Genre %>%
paste0(collapse = ', ') %>%
str_extract_all('[A-Za-z]+') %>%
unlist() %>%
table() %>%
as.data.frame()
gatunki %>%
arrange(-Freq)
Odp: Aby było bardziej ‘tidyverse’ można by zacząć od “dane %>% select(Genre) %>%” i potem resztę kodu od paste0 do as.data.frame. Po tym dodać fajkję i dopisać “arrange(-Freq)” dzięki tym zabiegom można całość wykonać w jednym ciągu łącząc cały kod fajkami.
dane %>%
mutate(poland_available = str_detect(Country.Availability, 'Poland')) %>%
filter(poland_available == TRUE) %>%
pull(Title)%>%
head(10)
Za pomocą separate() możemy rozdzielać jedną kolumną na kilką oraz łączyć kilka kolumn w jedną za pomocą funkcji unite().
dane %>%
unite(
col = 'Scores'
,c('Hidden.Gem.Score', 'IMDb.Score', 'Rotten.Tomatoes.Score', 'Metacritic.Score')
,sep = ', '
) %>%
select(Title, Scores)%>%
head(10)
CHALLENGE 2: Jakie są trzy najwyżej oceniane komedie dostępne w języku polskim?
dane %>%
rowwise() %>%
mutate(avg_score = mean(c(IMDb.Score * 10
,Hidden.Gem.Score * 10
,Rotten.Tomatoes.Score
,Metacritic.Score)
,na.rm = TRUE) %>%
round(2)) %>%
mutate(polish_available = str_detect(Languages, 'Polish')) %>%
filter(polish_available == TRUE,
Genre == "Comedy") %>%
select(Title, avg_score)%>%
arrange(desc(avg_score))
Odp: 3 najwyżej oceniane komedie dostępne w języku polskim to: “Teddy Bear”, “Drunk History - Pól litra historii”, “Ranczo”.
CHALLENGE 3: Dla produkcji z lat 2019 oraz 2020 jaki jest średni czas między premierą a pojawieniem się na Netflixie?
dane %>%
mutate(Release = Release.Date %>% as.Date(format = '%m/%d/%Y'),
Netflix.Release = Netflix.Release.Date %>% as.Date(format = '%m/%d/%Y')) %>%
mutate(release_year = format(Release, format = "%Y")) %>%
filter(release_year == 2019 | release_year == 2020) %>%
summarize(sredni_czas = mean(difftime(Netflix.Release, Release, units = "days")))
Odp: Średni czas między premierą a pojawieniem się filmu na Netflixie (dla filmów z 2019 i 2020 roku) to około 107 dni.
CHALLENGE 4: Jakie są najpopularniejsze tagi dla produkcji dostępnych w języku polskim?
dane %>%
filter(Languages %>%
str_detect('Polish')) %>%
pull(Tags) %>%
str_c(collapse = ',') %>%
str_split(',') %>%
table() %>%
as.data.frame() %>%
arrange(-Freq)
Odp: Najpopularniejsze tagi to m.in.: Dramas, Polish Movies, Polish Dramas i TV Dramas co pokazuje co najbardziej lubimy kręcić :)
Agregacja danych
Za pomocą funkcji group_by() oraz summarize() wykonujemy operacje na zagregowanych danych.
dane %>%
group_by(Series.or.Movie) %>%
summarize(
count = n()
,avg_imdb_score = mean(IMDb.Score, na.rm = TRUE) %>% round(2)
,avg_imdb_votes = mean(IMDb.Votes, na.rm = TRUE) %>% round(0)
,sum_awards = sum(Awards.Received, na.rm = TRUE)
)
dane %>%
group_by(Series.or.Movie, Runtime) %>%
summarize(n = n()) %>%
arrange(-n)
CHALLENGE 5: Jakie są średnie oceny filmów wyprodukowanych w poszczególnych dekadach (tzn. lata 60, 70, 80, 90 etc.)?
floor_decade = function(rok){ return(rok - rok %% 10) }
dane %>%
filter(Series.or.Movie == "Movie") %>%
mutate(Release = Release.Date %>% as.Date(format = '%m/%d/%Y') %>% format("%Y")) %>%
mutate(Release_num = as.numeric(Release)) %>%
mutate(Dekada = floor_decade(Release_num)) %>%
group_by(Dekada) %>%
summarize(avg_score = mean(c(IMDb.Score * 10
,Hidden.Gem.Score * 10
,Rotten.Tomatoes.Score
,Metacritic.Score)
,na.rm = TRUE) %>%
round(2))
Widoczne są wyniki dla wszystkich dekad, można np zabserwować że najwyższe średnie oceny mają filmy z lat 30-tych. Może bardziej miarodajne byłyby wyniki biorące pod uwagę liczbę filmów w każdym przedziale :)
Tabele przestawne, dane w formacie long oraz wide
Dane w formacie wide: - wiersze reprezentują pojedyncze obserwacje - kolumny reprezentują atrybuty tych obserwacji - w komórkach znajdują się wartości poszczególnych atrybutów dla poszczególnych obserwacji.
Dane w formacie long: - w pierwszej kolumnie mamy obserwacje (klucz obserwacji może składać się też z więcej niż jednej kolumny) - w drugiej kolumnie mamy atrybuty - w trzeciej kolumnie mamy wartości.
Format long jest przydatny m. in. przy tworzeniu wykresów w bibliotece ggplot2.
dane_pivot = dane %>%
select(Title, ends_with('Score'))
dane_pivot = dane_pivot %>%
pivot_longer(
cols = 2:5
,names_to = 'Attribute'
,values_to = 'Value'
)
Dla każdego filmu zamiast jednego są teraz cztery rzędy (po jednym na każdą stronę z ocenami) zamiast czterech kolumn ze stronami powstała więc jedna z nazwą Attribute, a wartości z tych kolumn zostały przeniesione do nowej kolumny Value.
dane_pivot = dane_pivot %>%
pivot_wider(
id_cols = 1
,names_from = 'Attribute'
,values_from = 'Value'
)
## Warning: Values from `Value` are not uniquely identified; output will contain list-cols.
## * Use `values_fn = list` to suppress this warning.
## * Use `values_fn = {summary_fun}` to summarise duplicates.
## * Use the following dplyr code to identify duplicates.
## {data} %>%
## dplyr::group_by(Title, Attribute) %>%
## dplyr::summarise(n = dplyr::n(), .groups = "drop") %>%
## dplyr::filter(n > 1L)
Pivot_wider znów zmniejsza ilość wierszy z 4 do 1, a zwiększa liczbę kolumn z 1 do 4, tak jak było w sytuacji początkowej. Jednak dzięki tej operacji widzimy też, że w bazie były powtarzające się po kilka razy filmy o tej samej nazwie, np. Joker.
Łączenie tabel
oceny_metacritic = dane %>%
select(Title, Metacritic.Score) %>%
.[1:100,] %>%
drop_na()
oceny_rotten_tomatoes = dane %>%
select(Title, Rotten.Tomatoes.Score) %>%
.[1:100,] %>%
drop_na()
Tabele łączymy po odpowiednich kluczach tak samo, jak robimy to w SQL.
oceny_metacritic %>%
left_join(oceny_rotten_tomatoes, by = c('Title' = 'Title'))
Left join podaje wszystkie obserwacje z oceny_metacritic, nawet jeśli ten sam film nie istnieje w bazie oceny_rotten_tomatoes
oceny_metacritic %>%
right_join(oceny_rotten_tomatoes, by = c('Title' = 'Title'))
Right join podaje wszystkie obserwacje z oceny_rotten_tomatoes, nawet jeśli ten sam film nie istnieje w bazie oceny_metacritic
oceny_metacritic %>%
inner_join(oceny_rotten_tomatoes, by = c('Title' = 'Title'))
Inner join podaje tylko te filmy, które są w obu bazach
oceny_metacritic %>%
full_join(oceny_rotten_tomatoes, by = c('Title' = 'Title'))
Full join podaje wszystkie wartości z obu baz (nieważne czy film wystepuje tylko w którejś z nich czy w obu)
oceny_metacritic %>%
anti_join(oceny_rotten_tomatoes, by = c('Title' = 'Title'))
Antijoin napisany w ten sposóp podaje tylko filmy, które były w oceny_metacritic a nie było ich w oceny_rotten_tomatoes
oceny_rotten_tomatoes %>%
anti_join(oceny_metacritic, by = c('Title' = 'Title'))
Antijoin napisany w ten sposóp podaje tylko filmy, które były w oceny_rotten_tomatoes a nie było ich w oceny_metacritic