8 Собственные функции и использование их в tidyverse

library(tidyverse)

8.1 Напоминание: логические операции

Логическое “и”:

& TRUE FALSE
TRUE TRUE FALSE
FALSE FALSE FALSE
TRUE & TRUE
[1] TRUE
TRUE & FALSE
[1] FALSE

Логическое “или”:

| TRUE FALSE
TRUE TRUE TRUE
FALSE TRUE FALSE
TRUE | TRUE
[1] TRUE
TRUE | FALSE
[1] TRUE

Логическое “не”:

!TRUE
[1] FALSE

Сравнение:

2 == 2
[1] TRUE
2 == 3
[1] FALSE
2 != 3
[1] TRUE
2 != c(2, 3)
[1] FALSE  TRUE

8.2 Создание собственных функций

Собственные функции можно сделать с помощью функции function(). Ее можно записать в переменную и использовать:

n_syllables <- function(x){
  str_count(x, "[АОУЁЕЭЯИЮаоуёеэяию]")
}

n_syllables("корова")
[1] 3
n_syllables("слон")
[1] 1

Вообще, функции немного зависят от загруженных пакетов, так что безопаснее либо эксплицитно включать билиотеку внутри функции, или же говорить, из какой библиотеки функция:

n_syllables <- function(x){
  stringr::str_count(x, "[АОУЁЕЭЯИЮаоуёеэяию]")
}

Функцию можно вызывать внутри себя самой (рекурсия), вот так будет выглядеть функция, которая считает факториал:

factorial <- function(x){
  if(x-1 > 0){
    x*factorial(x-1)
  } else {
    1
  }
}

factorial(3)
[1] 6
factorial(4)
[1] 24
factorial(8)
[1] 40320
factorial(0)
[1] 1

Напишите свою функцию, которая будет сравнивать, какое слово длиннее:

which_is_longer("роза", "цветок")
[1] "цветок"
which_is_longer("олень", "животное")
[1] "животное"
which_is_longer("воробей", "птица")
[1] "воробей"

8.3 Использование условий в tidyverse

8.3.1 Условия в строчках

Мы уже визуализировали данные из статьи на Pudding про английские пабы. Часть названий этих пабов имеет слово Inn, давайте построим график распределения 30 самых популярных пабов с этим словом в названии и без него. Используя известные нам инструменты можно получить что-то в этом роде:

uk_pubs <- read_csv("https://raw.githubusercontent.com/agricolamz/DS_for_DH/master/data/UK_pubs.csv")
uk_pubs %>% 
  count(pub_name, sort = TRUE) %>% 
  mutate(inn = str_detect(pub_name, "Inn")) %>% 
  group_by(inn) %>% 
  slice(1:20) %>% 
  ggplot(aes(fct_reorder(pub_name, n), n))+
  geom_col()+
  coord_flip()+
  facet_wrap(~inn, scale = "free")+
  labs(x = "", y = "", caption = "https://pudding.cool/2019/10/pubs/")

Получилось в целом то, что мы хотели, однако названия TRUE и FALSE не самые удачные. Исправить положение можно при помощи функции ifelse(), у которой три аргумента:

  • условие,
  • значение, если условие принимает значение TRUE,
  • значение, если условие принимает значение FALSE.
ifelse(2+2 == 4, "правильно", "неправильно")
[1] "правильно"
ifelse(2+2 == 5, "правильно", "неправильно")
[1] "неправильно"

Вставим эту функцию в уже написанные код:

uk_pubs %>% 
  count(pub_name, sort = TRUE) %>% 
  mutate(inn = ifelse(str_detect(pub_name, "Inn"), 
                      "with 'inn'", 
                      "without 'inn'")) %>%
  group_by(inn) %>% 
  slice(1:20) %>% 
  ggplot(aes(fct_reorder(pub_name, n), n))+
  geom_col()+
  coord_flip()+
  facet_wrap(~inn, scale = "free")+
  labs(x = "", y = "", caption = "https://pudding.cool/2019/10/pubs/")

А что если условий больше? В целом, выражение ifelse() можно вложить в выражение ifelse(), однако для таких случаев придумали функцию case_when(). У нее немного необычный синтаксис:

case_when(
  условие 1 ~ значение x,
  условие 2 ~ значение y,
  ...
  условие n ~ значение z
)

Давайте в том же датасете посмотрим на названия со словами Inn, Hotel, Bar, House и Tavern:

uk_pubs %>% 
  count(pub_name, sort = TRUE) %>% 
  mutate(place = case_when(
    str_detect(pub_name, "Inn") ~ "inn",
    str_detect(pub_name, "Hotel") ~ "hotel",
    str_detect(pub_name, "Bar") ~ "bar",
    str_detect(pub_name, "House") ~ "house",
    str_detect(pub_name, "Tavern") ~ "tavern")) %>%
  group_by(place) %>% 
  slice(1:10) %>% 
  ggplot(aes(fct_reorder(pub_name, n), n))+
  geom_col()+
  coord_flip()+
  facet_wrap(~place, scale = "free")+
  labs(x = "", y = "", caption = "https://pudding.cool/2019/10/pubs/")

Варинат NA, как видно из графика, соответствует всем оставшимся вариантам, которые не подпали ни под одно из наших условий. Чтобы учесть и этот случай, нужно добавить условие TRUE:

uk_pubs %>% 
  count(pub_name, sort = TRUE) %>% 
  mutate(place = case_when(
    str_detect(pub_name, "Inn") ~ "inn",
    str_detect(pub_name, "Hotel") ~ "hotel",
    str_detect(pub_name, "Bar") ~ "bar",
    str_detect(pub_name, "House") ~ "house",
    str_detect(pub_name, "Tavern") ~ "tavern",
    TRUE ~ "other")) %>%
  group_by(place) %>% 
  slice(1:10) %>% 
  ggplot(aes(fct_reorder(pub_name, n), n))+
  geom_col()+
  coord_flip()+
  facet_wrap(~place, scale = "free")+
  labs(x = "", y = "", caption = "https://pudding.cool/2019/10/pubs/")

Мы уже визуализировали данные из статьи на Pudding “Finding Forever Homes”, заполните пропус, чтобы получить возростно-половую пирамиду собак в США. Когда построите приведенный график, раскомментируйте закомментированную строчку и посмотрите на результат.

dogs <- read_csv("https://raw.githubusercontent.com/r-classes/2019_2020_ds4dh_hw_2_dplyr_tidyr_ggplot2/master/data/dog_names.csv")
dogs %>% 
  filter(sex != "Unknown") %>% 
  count(sex, contact_state) %>% 
  group_by(contact_state) %>% 
  mutate(
  ...
  ) %>% 
  ggplot(aes(fct_reorder(contact_state, sum), n, fill = sex))+
  geom_col()+
#  scale_y_continuous(breaks = -2:2*1000, labels = abs(-2:2)*1000)+
  coord_flip()+
  labs(x = "", y = "", caption = "data from https://pudding.cool/2019/10/shelters/")+
  scale_fill_brewer(palette ="Dark2")

8.3.2 Условия в столбцах

Что если хочется применить summarise() или mutate() лишь к определенным колонкам? Для этого можно использовать функции summarise_at() или mutate_at().Например, посчитать среднее во всех колонках датасета iris, которые начинаются со слова “Sepal”.

iris %>% 
  head()
iris %>% 
  summarise_at(vars(starts_with("Sepal")), mean)

На месте функции starts_with() могут быть и другие:

  • ends_with() – заканчивается
iris %>% 
  summarise_at(vars(ends_with("Width")), mean)
  • matches() – соответствует регулярному выражению
iris %>% 
  summarise_at(vars(matches("Sepal")), mean)
  • one_off() – из предложенного вектора значений
diamonds %>% 
  summarise_at(vars(one_of(c("depth", "price", "carat"))), mean)

Так же, используя функцию summarise_if(), можно применять какую-то операцию к каждой колонке, если она соответствует какому-то условию (обычно это используют для проверки типов переменных):

diamonds %>% 
  summarise_if(is.numeric, mean)

Вот несколько примеров с mutate_..():

diamonds