Podstawowe operacje w R - część 3.

Eksploracja danych

Twoje imię i nazwisko

2022-11-25

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(X.U.FEFF.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(X.U.FEFF.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('\\.'))%>% #wszystkie oprócz zawierające '.'
  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, X.U.FEFF.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(X.U.FEFF.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') %>%
  arrange(Release.Date) %>%
  select(X.U.FEFF.Title,Release.Date) %>%
  head(1)

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(X.U.FEFF.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(X.U.FEFF.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(X.U.FEFF.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 = X.U.FEFF.Title)

Zmieniamy nazwy kolumn za pomocą funkcji rename().

dane %>%
  rename(
    Tytul = X.U.FEFF.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)
dane %>%
  mutate(poland_available = str_detect(Country.Availability, 'Poland')) %>%
  filter(poland_available == TRUE) %>%
  pull(X.U.FEFF.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(X.U.FEFF.Title, Scores)%>% 
  head(10)

CHALLENGE 2: Jakie są trzy najwyżej oceniane komedie dostępne w języku polskim?

dane %>%
  select(X.U.FEFF.Title,Languages,Genre,Hidden.Gem.Score) %>%
  filter(Languages=="Polish",Genre=="Comedy") %>%
  arrange(desc(Hidden.Gem.Score)) %>%
  head(3)

CHALLENGE 3: Dla produkcji z lat 2019 oraz 2020 jaki jest średni czas między premierą a pojawieniem się na Netflixie?

dane$Release.Date <- as.Date(dane$Release.Date, format = "%m/%d/%Y")
dane$Netflix.Release.Date <- as.Date(dane$Netflix.Release.Date, format = "%m/%d/%Y")
dane %>%
  drop_na(Release.Date, Netflix.Release.Date) %>%
  mutate(year = lubridate::year(Release.Date), differ = Netflix.Release.Date - Release.Date) %>%
  filter(year %>% between(2019,2020)) %>%
  group_by(year) %>%
  summarize(movie_mean = mean(differ))

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) %>%
  top_n(3)

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.)?

dane$Release.Date <- as.Date(dane$Release.Date, format = "%m/%d/%Y")
dane %>% 
  mutate(rok = lubridate::year(Release.Date), decade = floor(rok/10)*10) %>% 
      group_by(decade) %>% 
      summarize(count = n(),
                avg_imdb_score = mean(IMDb.Score, na.rm = TRUE) %>% round(2)) 

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(X.U.FEFF.Title, ends_with('Score'))
#dane_pivot = dane_pivot %>%
 # pivot_longer(
   # cols = 2:5
    #,names_to = 'Attribute'
    #,values_to = 'Value'
 # )
#dane_pivot = dane_pivot %>%
 # pivot_wider(
    #id_cols = 1
    #,names_from = 'Attribute'
    #,values_from = 'Value'
 # )

Łączenie tabel

#oceny_metacritic = dane %>%
 # select(X.U.FEFF.Title, Metacritic.Score) %>%
  #[1:100,] %>%
 # drop_na()

#oceny_rotten_tomatoes = dane %>%
  #select(X.U.FEFF.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'))
#oceny_metacritic %>%
  #right_join(oceny_rotten_tomatoes, by = c('Title' = 'Title'))
#oceny_metacritic %>%
  #inner_join(oceny_rotten_tomatoes, by = c('Title' = 'Title'))
#oceny_metacritic %>%
  #full_join(oceny_rotten_tomatoes, by = c('Title' = 'Title'))
#oceny_metacritic %>%
  #anti_join(oceny_rotten_tomatoes, by = c('Title' = 'Title'))
#oceny_rotten_tomatoes %>%
 # anti_join(oceny_metacritic, by = c('Title' = 'Title'))