Загружаем датасет про супергероев:
library("tidyverse")
## ── Attaching packages ─────────────────────────────────────── tidyverse 1.3.2 ──
## ✔ ggplot2 3.4.0 ✔ purrr 1.0.1
## ✔ tibble 3.1.8 ✔ dplyr 1.0.10
## ✔ tidyr 1.3.0 ✔ stringr 1.5.0
## ✔ readr 2.1.3 ✔ forcats 0.5.2
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
heroes <- read_csv("https://raw.githubusercontent.com/Pozdniakov/tidy_stats/master/data/heroes_information.csv",
na = c("-", "-99"))
## New names:
## • `` -> `...1`
## Warning: One or more parsing issues, call `problems()` on your data frame for details,
## e.g.:
## dat <- vroom(...)
## problems(dat)
## Rows: 734 Columns: 11
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (8): name, Gender, Eye color, Race, Hair color, Publisher, Skin color, A...
## dbl (3): ...1, Height, Weight
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
Теперь создадим следующие тибблы и сохраним их как dc
,
marvel
и other_publishers
:
dc <- heroes %>%
filter(Publisher == "DC Comics") %>%
group_by(Gender) %>%
summarise(weight_mean = mean(Weight, na.rm = TRUE))
dc
## # A tibble: 3 × 2
## Gender weight_mean
## <chr> <dbl>
## 1 Female 76.8
## 2 Male 113.
## 3 <NA> NaN
marvel <- heroes %>%
filter(Publisher == "Marvel Comics") %>%
group_by(Gender) %>%
summarise(weight_mean = mean(Weight, na.rm = TRUE))
marvel
## # A tibble: 3 × 2
## Gender weight_mean
## <chr> <dbl>
## 1 Female 80.1
## 2 Male 134.
## 3 <NA> 129.
other_publishers <- heroes %>%
filter(!(Publisher %in% c("DC Comics","Marvel Comics"))) %>%
group_by(Gender) %>%
summarise(weight_mean = mean(Weight, na.rm = TRUE))
other_publishers
## # A tibble: 3 × 2
## Gender weight_mean
## <chr> <dbl>
## 1 Female 70.8
## 2 Male 111.
## 3 <NA> NaN
Вертикально объединяем тибблы с помощью функции
bind_rows()
. Для корректного объединения тибблы должны
иметь одинаковые названия колонок.
bind_rows(dc, marvel)
## # A tibble: 6 × 2
## Gender weight_mean
## <chr> <dbl>
## 1 Female 76.8
## 2 Male 113.
## 3 <NA> NaN
## 4 Female 80.1
## 5 Male 134.
## 6 <NA> 129.
Горизонтально объединяем тибблы с помощью функции
bind_cols()
.
bind_cols(dc, marvel)
## New names:
## • `Gender` -> `Gender...1`
## • `weight_mean` -> `weight_mean...2`
## • `Gender` -> `Gender...3`
## • `weight_mean` -> `weight_mean...4`
## # A tibble: 3 × 4
## Gender...1 weight_mean...2 Gender...3 weight_mean...4
## <chr> <dbl> <chr> <dbl>
## 1 Female 76.8 Female 80.1
## 2 Male 113. Male 134.
## 3 <NA> NaN <NA> 129.
Функции bind_rows()
и bind_cols()
могут
работать с тремя и более датафреймами.
bind_rows(dc, marvel, other_publishers)
## # A tibble: 9 × 2
## Gender weight_mean
## <chr> <dbl>
## 1 Female 76.8
## 2 Male 113.
## 3 <NA> NaN
## 4 Female 80.1
## 5 Male 134.
## 6 <NA> 129.
## 7 Female 70.8
## 8 Male 111.
## 9 <NA> NaN
На входе в функции bind_rows()
и
bind_cold()
можно подавать как сами датафреймы или тибблы
через запятую, так и список из датафреймов/тибблов.
heroes_list_of_df <- list(DC = dc,
Marvel = marvel,
Other = other_publishers)
bind_rows(heroes_list_of_df)
## # A tibble: 9 × 2
## Gender weight_mean
## <chr> <dbl>
## 1 Female 76.8
## 2 Male 113.
## 3 <NA> NaN
## 4 Female 80.1
## 5 Male 134.
## 6 <NA> 129.
## 7 Female 70.8
## 8 Male 111.
## 9 <NA> NaN
Чтобы не потерять, из какого датафрейма какие данные, можно указать
любое строковое значение (название будущей колонки) для необязательного
аргумента .id =
.
bind_rows(heroes_list_of_df, .id = "Publisher")
## # A tibble: 9 × 3
## Publisher Gender weight_mean
## <chr> <chr> <dbl>
## 1 DC Female 76.8
## 2 DC Male 113.
## 3 DC <NA> NaN
## 4 Marvel Female 80.1
## 5 Marvel Male 134.
## 6 Marvel <NA> 129.
## 7 Other Female 70.8
## 8 Other Male 111.
## 9 Other <NA> NaN
bind_rows()
- если данные хранятся в разных файлах с
одинаковой структурой. Читаем все таблицы, заводим их список и
объединяем в единое целое.
*_join()
от dplyr
Функционал left_join()
, right_join()
,
full_join()
и inner_join()
аналогичен pandas в
Python и join-функциям в запросах к базам данным. Ключ
- одна или несколько колонок в каждой из табличек, по которым мы можем
объединить данные из табличек. Ключ должен однозначно идентифицировать
наблюдения и содержать уникальные значения. Если ключи неуникальные, то
функции *_join()
не будут выдавать ошибку. Вместо этого они
добавят в итоговую таблицу все возможные пересечения повторяющихся
ключей.
Возьмем тибблы band_members
и
band_instruments
, встроенные в dplyr
для
демонстрации работы функций *_join()
. Ключ - колонка
name
, одноименная в двух тибблах.
band_members
## # A tibble: 3 × 2
## name band
## <chr> <chr>
## 1 Mick Stones
## 2 John Beatles
## 3 Paul Beatles
band_instruments
## # A tibble: 3 × 2
## name plays
## <chr> <chr>
## 1 John guitar
## 2 Paul bass
## 3 Keith guitar
left_join()
: данные из левого тиббла дополняются
информацией из правого тиббла. Все уникальные наблюдения в левом тиббле
сохраняются, но отбрасываются строки в правом тиббле, не нашедшие
соответствия в левой таблице. В ячейках, которым не нашлось соотвествия
в правой таблице, ставится значение NA
.band_members %>%
left_join(band_instruments)
## Joining, by = "name"
## # A tibble: 3 × 3
## name band plays
## <chr> <chr> <chr>
## 1 Mick Stones <NA>
## 2 John Beatles guitar
## 3 Paul Beatles bass
Чтобы явно задать колонки-ключи, используем параметр
by =
. По умолчанию объединение производится по всем
колонкам с одинаковыми названиями в двух тибблах.
band_members %>%
left_join(band_instruments, by = "name")
## # A tibble: 3 × 3
## name band plays
## <chr> <chr> <chr>
## 1 Mick Stones <NA>
## 2 John Beatles guitar
## 3 Paul Beatles bass
Если колонки-ключи называются по-разному в двух тибблах, можно вручную прописать соответствия:
band_members %>%
left_join(band_instruments2, by = c("name" = "artist"))
## # A tibble: 3 × 3
## name band plays
## <chr> <chr> <chr>
## 1 Mick Stones <NA>
## 2 John Beatles guitar
## 3 Paul Beatles bass
right_join()
: Все с точностью наборот: строки левой
таблицы, не нашедшие соответствия в правой таблице, отбрасываются.band_members %>%
right_join(band_instruments)
## Joining, by = "name"
## # A tibble: 3 × 3
## name band plays
## <chr> <chr> <chr>
## 1 John Beatles guitar
## 2 Paul Beatles bass
## 3 Keith <NA> guitar
full_join()
: Сохраняет все строки из левой и правой
таблицы, при несовпадении ключей вставляется значение
NA
.band_members %>%
full_join(band_instruments)
## Joining, by = "name"
## # A tibble: 4 × 3
## name band plays
## <chr> <chr> <chr>
## 1 Mick Stones <NA>
## 2 John Beatles guitar
## 3 Paul Beatles bass
## 4 Keith <NA> guitar
inner_join()
: Сохраняет только строки, общие для левой
и правой таблиц.band_members %>%
inner_join(band_instruments)
## Joining, by = "name"
## # A tibble: 2 × 3
## name band plays
## <chr> <chr> <chr>
## 1 John Beatles guitar
## 2 Paul Beatles bass
semi_join()
: Функции semi_join()
и
anti_join()
не присоединяют второй датафрейм/тиббл к
первому. Вместо этого они используются как некоторый словарь-фильтр для
отделения только тех значений в левой таблице, которые есть в правой
(semi_join()
) или, наоборот, которых нет в правой
(anti_join()
).band_members %>%
semi_join(band_instruments)
## Joining, by = "name"
## # A tibble: 2 × 2
## name band
## <chr> <chr>
## 1 John Beatles
## 2 Paul Beatles
anti_join()
:band_members %>%
anti_join(band_instruments)
## Joining, by = "name"
## # A tibble: 1 × 2
## name band
## <chr> <chr>
## 1 Mick Stones
tidyr::pivot_longer()
,
tidyr::pivot_wider()
Принцип tidy data предполагает, что каждая строчка содержит в себе одно наблюдение (измерение), а каждая колонка - одну характеристику. Но как именно хранить повторные измерения?
tidyr::pivot_longer()
: из широкого в
длинный формат
tidyr::pivot_wider()
: из длинного в
широкий формат
new_diet <- tibble(
student = c("Маша", "Рома", "Антонина"),
before_r_course = c(70, 80, 86),
after_r_course = c(63, 74, 71)
)
new_diet
## # A tibble: 3 × 3
## student before_r_course after_r_course
## <chr> <dbl> <dbl>
## 1 Маша 70 63
## 2 Рома 80 74
## 3 Антонина 86 71
Тиббл new_diet
- это пример широкого формата данных.
Превратим тиббл new_diet
длинный:
new_diet %>%
pivot_longer(cols = before_r_course:after_r_course,
names_to = "measurement_time",
values_to = "weight_kg")
## # A tibble: 6 × 3
## student measurement_time weight_kg
## <chr> <chr> <dbl>
## 1 Маша before_r_course 70
## 2 Маша after_r_course 63
## 3 Рома before_r_course 80
## 4 Рома after_r_course 74
## 5 Антонина before_r_course 86
## 6 Антонина after_r_course 71
А теперь обратно в короткий:
new_diet %>%
pivot_longer(cols = before_r_course:after_r_course,
names_to = "measurement_time",
values_to = "weight_kg") %>%
pivot_wider(names_from = "measurement_time",
values_from = "weight_kg")
## # A tibble: 3 × 3
## student before_r_course after_r_course
## <chr> <dbl> <dbl>
## 1 Маша 70 63
## 2 Рома 80 74
## 3 Антонина 86 71
dplyr::across()
Посчитаем среднюю массу и рост супергероев, группируя по полу. Топорный способ - перечислить все функции через запятую:
heroes %>%
group_by(Gender) %>%
summarise(height = mean(Height, na.rm = TRUE),
weight = mean(Weight, na.rm = TRUE))
## # A tibble: 3 × 3
## Gender height weight
## <chr> <dbl> <dbl>
## 1 Female 175. 78.8
## 2 Male 192. 126.
## 3 <NA> 177. 129.
dplyr::across()
- аналог apply()
в
tydyverse, использует tidyselect для выбора колонок.
Функция across()
появилась в пакете dplyr
относительно недавно, до этого для работы с множественными колонками в
tidyverse использовались многочисленные функции *_at()
,
*_if()
, *_all()
, например,
summarise_at()
, summarise_if()
,
summarize_all()
. Эти функции до сих пор присутствуют в
dplyr
, но считаются устаревшими. Другая альтернатива -
использование пакета purrr
(@sec-purrr) или семейства функций
apply()
(@sec-apply_f).
Таким образом, конструкции с функцией across()
можно
разбить на три части:
across()
. Первый аргумент
.col
– колонки, выбранные на первом этапе с помощью
tidyselect, по умолчанию это everything()
, т.е. все
колонки. Второй аргумент .fns
– это функция или целый
список из функций, которые будут применены к выбранным колонкам. Если
функции требуют дополнительных аргументов, то они могут быть перечислены
внутри across()
.summarise()
или другой функции
dplyr
. В этом случае в качестве аргумента для функции
используется результат работы функции across()
.heroes %>%
group_by(Gender) %>%
summarise(across(c(Height,Weight), mean))
## # A tibble: 3 × 3
## Gender Height Weight
## <chr> <dbl> <dbl>
## 1 Female NA NA
## 2 Male NA NA
## 3 <NA> NA NA
Функция mean()
при столкновении хотя бы с одним
NA
будет возвращать NA
, если мы не изменим
параметр na.rm =
. Дополнительные для функции аргументы
можно перечислить через запятую после названия функции:
heroes %>%
group_by(Gender) %>%
summarise(across(c(Height, Weight), mean, na.rm = TRUE))
## # A tibble: 3 × 3
## Gender Height Weight
## <chr> <dbl> <dbl>
## 1 Female 175. 78.8
## 2 Male 192. 126.
## 3 <NA> 177. 129.
Посчитаем среднее для всех numeric колонок:
heroes %>%
drop_na(Height, Weight) %>%
group_by(Gender) %>%
summarise(across(where(is.numeric), mean, na.rm = TRUE))
## # A tibble: 3 × 4
## Gender ...1 Height Weight
## <chr> <dbl> <dbl> <dbl>
## 1 Female 394. 174. 78.3
## 2 Male 369. 193. 126.
## 3 <NA> 375. 182 129.
Или длину строк для строковых колонок – с помощью анонимной функции
function()
.
heroes %>%
group_by(Gender) %>%
summarise(across(where(is.character),
function(x) mean(nchar(x), na.rm = TRUE)))
## # A tibble: 3 × 8
## Gender name `Eye color` Race `Hair color` Publisher `Skin color` Alignment
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Female 9.04 4.68 6.42 5.05 11.5 4.57 3.88
## 2 Male 9.05 4.53 6.75 5.48 11.4 5.02 3.78
## 3 <NA> 9.48 5.16 10.1 6.44 11.9 4 3.96
Два across()
внутри одного summarise()
:
heroes %>%
group_by(Gender) %>%
summarise(across(where(is.numeric), mean, na.rm = TRUE),
across(where(is.character),
function(x) mean(nchar(x), na.rm = TRUE)))
## # A tibble: 3 × 11
## Gender ...1 Height Weight name Eye c…¹ Race Hair …² Publi…³ Skin …⁴ Align…⁵
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Female 395. 175. 78.8 9.04 4.68 6.42 5.05 11.5 4.57 3.88
## 2 Male 357. 192. 126. 9.05 4.53 6.75 5.48 11.4 5.02 3.78
## 3 <NA> 329 177. 129. 9.48 5.16 10.1 6.44 11.9 4 3.96
## # … with abbreviated variable names ¹`Eye color`, ²`Hair color`, ³Publisher,
## # ⁴`Skin color`, ⁵Alignment
Внутри одного across()
можно применить не одну функцию к
каждой из выбранных колонок, а сразу несколько функций для каждой из
колонок. Для этого нам нужно использовать список функций (желательно -
проименованный).
heroes %>%
group_by(Gender) %>%
summarise(across(c(Height, Weight),
list(minimum = min,
average = mean,
maximum = max),
na.rm = TRUE))
## # A tibble: 3 × 7
## Gender Height_minimum Height_average Height_maximum Weight_m…¹ Weigh…² Weigh…³
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Female 62.5 175. 366 41 78.8 630
## 2 Male 15.2 192. 975 2 126. 900
## 3 <NA> 108 177. 198 39 129. 383
## # … with abbreviated variable names ¹Weight_minimum, ²Weight_average,
## # ³Weight_maximum
Cписок функций:
heroes %>%
group_by(Gender) %>%
summarise(across(c(Height, Weight),
list(min = function(x) min(x, na.rm = TRUE),
mean = function(x) mean(x, na.rm = TRUE),
max = function(x) max(x, na.rm = TRUE),
na_n = function(x, ...) sum(is.na(x)))
)
)
## # A tibble: 3 × 9
## Gender Height_min Height_mean Height…¹ Heigh…² Weigh…³ Weigh…⁴ Weigh…⁵ Weigh…⁶
## <chr> <dbl> <dbl> <dbl> <int> <dbl> <dbl> <dbl> <int>
## 1 Female 62.5 175. 366 56 41 78.8 630 58
## 2 Male 15.2 192. 975 147 2 126. 900 166
## 3 <NA> 108 177. 198 14 39 129. 383 15
## # … with abbreviated variable names ¹Height_max, ²Height_na_n, ³Weight_min,
## # ⁴Weight_mean, ⁵Weight_max, ⁶Weight_na_n
Хотя основное применение функции across()
– это массовое
подытоживание с помощью summarise()
, across()
можно использовать и с другими функциями dplyr
. Массовые
операции с колонками с помощью mutate()
:
heroes %>%
mutate(across(where(is.character), as.factor))
## # A tibble: 734 × 11
## ...1 name Gender Eye c…¹ Race Hair …² Height Publi…³ Skin …⁴ Align…⁵
## <dbl> <fct> <fct> <fct> <fct> <fct> <dbl> <fct> <fct> <fct>
## 1 0 A-Bomb Male yellow Human No Hair 203 Marvel… <NA> good
## 2 1 Abe Sapien Male blue Icth… No Hair 191 Dark H… blue good
## 3 2 Abin Sur Male blue Unga… No Hair 185 DC Com… red good
## 4 3 Abomination Male green Huma… No Hair 203 Marvel… <NA> bad
## 5 4 Abraxas Male blue Cosm… Black NA Marvel… <NA> bad
## 6 5 Absorbing … Male blue Human No Hair 193 Marvel… <NA> bad
## 7 6 Adam Monroe Male blue <NA> Blond NA NBC - … <NA> good
## 8 7 Adam Stran… Male blue Human Blond 185 DC Com… <NA> good
## 9 8 Agent 13 Female blue <NA> Blond 173 Marvel… <NA> good
## 10 9 Agent Bob Male brown Human Brown 178 Marvel… <NA> good
## # … with 724 more rows, 1 more variable: Weight <dbl>, and abbreviated variable
## # names ¹`Eye color`, ²`Hair color`, ³Publisher, ⁴`Skin color`, ⁵Alignment
Внутри count()
вместе с функцией
n_distinct()
, которая считает количество уникальных
значений в векторе:
heroes %>%
count(across(where(function(x) n_distinct(x) <= 6)))
## # A tibble: 11 × 3
## Gender Alignment n
## <chr> <chr> <int>
## 1 Female bad 35
## 2 Female good 161
## 3 Female neutral 4
## 4 Male bad 165
## 5 Male good 316
## 6 Male neutral 18
## 7 Male <NA> 6
## 8 <NA> bad 7
## 9 <NA> good 19
## 10 <NA> neutral 2
## 11 <NA> <NA> 1
purrr
purrr
– пакет для функционального программирования в
tidyverse. Здесь речь пойдет об аналогах функций семейства
apply()
из базового R.
lapply()
в качестве первого аргумента функция
lapply()
принимает список (или то, что может быть в него
превращено, например, датафрейм), в качестве второго - функцию, которая
будет применена к каждому элементу списка. На выходе получается список
такой же длины.lapply(heroes, class)
## $...1
## [1] "numeric"
##
## $name
## [1] "character"
##
## $Gender
## [1] "character"
##
## $`Eye color`
## [1] "character"
##
## $Race
## [1] "character"
##
## $`Hair color`
## [1] "character"
##
## $Height
## [1] "numeric"
##
## $Publisher
## [1] "character"
##
## $`Skin color`
## [1] "character"
##
## $Alignment
## [1] "character"
##
## $Weight
## [1] "numeric"
purrr::map()
работает по тому же принципу:map(heroes, class)
## $...1
## [1] "numeric"
##
## $name
## [1] "character"
##
## $Gender
## [1] "character"
##
## $`Eye color`
## [1] "character"
##
## $Race
## [1] "character"
##
## $`Hair color`
## [1] "character"
##
## $Height
## [1] "numeric"
##
## $Publisher
## [1] "character"
##
## $`Skin color`
## [1] "character"
##
## $Alignment
## [1] "character"
##
## $Weight
## [1] "numeric"
map()
можно встроить в канал с пайпом (впрочем, как и
lapply()
):
heroes %>%
map(class)
## $...1
## [1] "numeric"
##
## $name
## [1] "character"
##
## $Gender
## [1] "character"
##
## $`Eye color`
## [1] "character"
##
## $Race
## [1] "character"
##
## $`Hair color`
## [1] "character"
##
## $Height
## [1] "numeric"
##
## $Publisher
## [1] "character"
##
## $`Skin color`
## [1] "character"
##
## $Alignment
## [1] "character"
##
## $Weight
## [1] "numeric"
sapply()
из базового R упрощает результат до
вектора, если это возможно.
vapply()
из базового R добавляет управление типом
данных на выходе, но она не очень удобная.
map_*()
множество функций из purrr
, где
вместо звездочки - нужный формат на выходе.
map_chr()
:heroes %>%
map_chr(class)
## ...1 name Gender Eye color Race Hair color
## "numeric" "character" "character" "character" "character" "character"
## Height Publisher Skin color Alignment Weight
## "numeric" "character" "character" "character" "numeric"
map_df()
возвращает результат как датафрейм:heroes %>%
map_df(class)
## # A tibble: 1 × 11
## ...1 name Gender Eye c…¹ Race Hair …² Height Publi…³ Skin …⁴ Align…⁵ Weight
## <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
## 1 nume… char… chara… charac… char… charac… numer… charac… charac… charac… numer…
## # … with abbreviated variable names ¹`Eye color`, ²`Hair color`, ³Publisher,
## # ⁴`Skin color`, ⁵Alignment
Так же как и функции семейства apply()
, функции
map_*()
сочетаются с анонимными функциями:
heroes %>%
map_int(function(x) sum(is.na(x)))
## ...1 name Gender Eye color Race Hair color Height
## 0 0 29 172 304 172 217
## Publisher Skin color Alignment Weight
## 0 662 7 239
Более короткий способ записи анонимных функций:
function(arg)
заменяется на ~
, а
arg
на .
.
heroes %>%
map_int(~sum(is.na(.)))
## ...1 name Gender Eye color Race Hair color Height
## 0 0 29 172 304 172 217
## Publisher Skin color Alignment Weight
## 0 662 7 239
Если нужно итерироваться сразу по нескольким спискам, то есть функции
map2_*()
(для двух списков) и pmap_*()
(для
нескольких списков).
nest()
Ранее мы говорили о том, что датафрейм – это по своей сути список из векторов разной длины. На самом деле, это не совсем так: колонки обычного датафрейма вполне могут быть списками. Однако делать так обычно не рекомендуется, пусть R это и не запрещает создавать такие колонки: многие функции предполагают, что все колонки датафрейма являются векторами.
tidyverse
лучше заточен на использование списков в
качестве колонок (колонок-списков (list columns)). -
tidyr::nest()
С помощью tidyselect нужно выбрать сжимаемые
колонки, которые будут агрегированы по невыбранным колонками - это и
есть нестинг.
heroes %>%
nest(!Gender)
## Warning: Supplying `...` without names was deprecated in tidyr 1.0.0.
## ℹ Please specify a name for each selection.
## ℹ Did you want `data = !Gender`?
## # A tibble: 3 × 2
## Gender data
## <chr> <list>
## 1 Male <tibble [505 × 10]>
## 2 Female <tibble [200 × 10]>
## 3 <NA> <tibble [29 × 10]>
Заметьте, у нас появилась колонка data
, в которой
содержатся тибблы. Туда и спрятались все наши данные.
Нестинг похож на агрегирование с помощью group_by()
.
Если сделать нестинг сгруппированного с помощью group_by()
тиббла, то сожмутся все колонки кроме тех, которые выступают в качестве
групп:
heroes %>%
group_by(Gender) %>%
nest()
## # A tibble: 3 × 2
## # Groups: Gender [3]
## Gender data
## <chr> <list>
## 1 Male <tibble [505 × 10]>
## 2 Female <tibble [200 × 10]>
## 3 <NA> <tibble [29 × 10]>
Теперь можно работать с колонкой-списком как с обычной колонкой.
Например, применять функцию для каждой строчки (то есть для каждого
тиббла) с помощью map()
и записывать результат в новую
колонку с помощью mutate()
.
heroes %>%
group_by(Gender) %>%
nest() %>%
mutate(dim = map(data, dim))
## # A tibble: 3 × 3
## # Groups: Gender [3]
## Gender data dim
## <chr> <list> <list>
## 1 Male <tibble [505 × 10]> <int [2]>
## 2 Female <tibble [200 × 10]> <int [2]>
## 3 <NA> <tibble [29 × 10]> <int [2]>
В конце концов нам нужно “разжать” сжатую колонку-список. Сделать это
можно с помощью unnest()
, выбрав с помощью tidyselect
нужные колонки.
heroes %>%
group_by(Gender) %>%
nest() %>%
mutate(dim = map(data, dim)) %>%
unnest(dim)
## # A tibble: 6 × 3
## # Groups: Gender [3]
## Gender data dim
## <chr> <list> <int>
## 1 Male <tibble [505 × 10]> 505
## 2 Male <tibble [505 × 10]> 10
## 3 Female <tibble [200 × 10]> 200
## 4 Female <tibble [200 × 10]> 10
## 5 <NA> <tibble [29 × 10]> 29
## 6 <NA> <tibble [29 × 10]> 10
Разжатая колонка обычно больше сжатой, поэтому разжатие привело к
удлинению тиббла. Вместо удлинения тиббла, его можно расширить с помощью
unnest_wider()
.
heroes %>%
group_by(Gender) %>%
nest() %>%
mutate(dim = map(data, dim)) %>%
unnest_wider(dim, names_sep = "_")
## # A tibble: 3 × 4
## # Groups: Gender [3]
## Gender data dim_1 dim_2
## <chr> <list> <int> <int>
## 1 Male <tibble [505 × 10]> 505 10
## 2 Female <tibble [200 × 10]> 200 10
## 3 <NA> <tibble [29 × 10]> 29 10
Пример применения нестинга – решение проблемы с несколькими значениями в одной ячейки, которые записаны через запятую или какой-либо другой разделитель.
films <- tribble(
~film, ~genres,
"Ирония Судьбы", "comedy, drama",
"Большой Лебовски", "comedy, criminal",
"Аватар", "fantasy, drama"
)
films
## # A tibble: 3 × 2
## film genres
## <chr> <chr>
## 1 Ирония Судьбы comedy, drama
## 2 Большой Лебовски comedy, criminal
## 3 Аватар fantasy, drama
strsplit()
разбивает значения вектора по выбранному
разделителю. Поскольку результат – список, перезаписанная колонка
genres
станет колонкой-списком.films %>%
mutate(genres = strsplit(genres, ", "))
## # A tibble: 3 × 2
## film genres
## <chr> <list>
## 1 Ирония Судьбы <chr [2]>
## 2 Большой Лебовски <chr [2]>
## 3 Аватар <chr [2]>
Теперь нам нужно сделать unnest()
films %>%
mutate(genres = strsplit(genres, ", ")) %>%
unnest()
## Warning: `cols` is now required when using `unnest()`.
## ℹ Please use `cols = c(genres)`.
## # A tibble: 6 × 2
## film genres
## <chr> <chr>
## 1 Ирония Судьбы comedy
## 2 Ирония Судьбы drama
## 3 Большой Лебовски comedy
## 4 Большой Лебовски criminal
## 5 Аватар fantasy
## 6 Аватар drama
Теперь у нас данные в длинном виде! Результат можно расширить с
помощью уже знакомого pivot_wider()
и дополнительной
колонки со значениями TRUE
. Если соответствующей пары нет в
тиббле, то в итоговой широкой таблице будет NA
, мы можем
поменять их на FALSE
с помощью параметра
values_fill =
.
films %>%
mutate(genres = strsplit(genres, ", ")) %>%
unnest() %>%
mutate(value = TRUE) %>%
pivot_wider(names_from = "genres",
values_from = "value", values_fill = FALSE)
## Warning: `cols` is now required when using `unnest()`.
## ℹ Please use `cols = c(genres)`.
## # A tibble: 3 × 5
## film comedy drama criminal fantasy
## <chr> <lgl> <lgl> <lgl> <lgl>
## 1 Ирония Судьбы TRUE TRUE FALSE FALSE
## 2 Большой Лебовски TRUE FALSE TRUE FALSE
## 3 Аватар FALSE TRUE FALSE TRUE
tidyr::separate_rows()
: заменяет связку
strsplit()
с unnest()
:films %>%
separate_rows(genres, sep = ", ") %>%
mutate(value = TRUE) %>%
pivot_wider(names_from = "genres",
values_from = "value", values_fill = FALSE)
## # A tibble: 3 × 5
## film comedy drama criminal fantasy
## <chr> <lgl> <lgl> <lgl> <lgl>
## 1 Ирония Судьбы TRUE TRUE FALSE FALSE
## 2 Большой Лебовски TRUE FALSE TRUE FALSE
## 3 Аватар FALSE TRUE FALSE TRUE
Наибольшее распространение нестинг получил в смычке с пакетом
broom
для расчета множественных статистических моделей.