Podstawowe operacje w R - część 3.

Eksploracja danych

Inga Guberska

2022-11-23

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