-
Notifications
You must be signed in to change notification settings - Fork 7
/
120-tidyverse_advanced.qmd
499 lines (360 loc) · 32.7 KB
/
120-tidyverse_advanced.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
# Продвинутый tidyverse {#sec-tidyverse_advanced}
## Объединение нескольких датафреймов {#sec-tidy_several}
### Соединение структурно схожих датафреймов: bind_rows(), bind_cols() {#sec-tidy_bind}
Для начала подключим `tidyverse` и возьмем уже знакомый нам датасет про супергероев:
```{r}
library("tidyverse")
heroes <- read_csv("https://raw.githubusercontent.com/Pozdniakov/tidy_stats/master/data/heroes_information.csv",
na = c("-", "-99"))
```
Теперь создадим следующие тибблы и сохраним их как `dc`, `marvel` и `other_publishers`:
```{r}
dc <- heroes %>%
filter(Publisher == "DC Comics") %>%
group_by(Gender) %>%
summarise(weight_mean = mean(Weight, na.rm = TRUE))
dc
marvel <- heroes %>%
filter(Publisher == "Marvel Comics") %>%
group_by(Gender) %>%
summarise(weight_mean = mean(Weight, na.rm = TRUE))
marvel
other_publishers <- heroes %>%
filter(!(Publisher %in% c("DC Comics","Marvel Comics"))) %>%
group_by(Gender) %>%
summarise(weight_mean = mean(Weight, na.rm = TRUE))
other_publishers
```
Несколько тибблов можно объединить вертикально с помощью функции `bind_rows()`. Для корректного объединения тибблы должны иметь одинаковые названия колонок.
```{r}
bind_rows(dc, marvel)
```
Чтобы соединить тибблы горизонтально, воспользуйтесь функцией `bind_cols()`.
```{r}
bind_cols(dc, marvel)
```
Функции `bind_rows()` и `bind_cols()` могут работать не только с двумя, но сразу с несколькими датафреймами.
```{r}
bind_rows(dc, marvel, other_publishers)
```
На входе в функции `bind_rows()` и `bind_cold()` можно подавать как сами датафреймы или тибблы через запятую, так и список из датафреймов/тибблов.
```{r}
heroes_list_of_df <- list(DC = dc,
Marvel = marvel,
Other = other_publishers)
bind_rows(heroes_list_of_df)
```
Чтобы не потерять, из какого датафрейма какие данные, можно указать любое строковое значение (название будущей колонки) для необязательного аргумента `.id =`.
```{r}
bind_rows(heroes_list_of_df, .id = "Publisher")
```
`bind_rows()` обычно используется, когда ваши данные находятся в разных файлах с одинаковой структурой. Тогда вы можете прочитать все таблицы в папке, сохранить их в качестве списка из датафреймов и объединить в один датафрейм с помощью `bind_rows()`.
### Реляционные данные: `*_join()` {#sec-tidy_join}
В реальности иногда возникает ситуация, когда нужно соединить две таблички, у которых есть общий столбец (или несколько столбцов), но все остальные столбцы различаются. Табличек может быть и больше, это может быть целая сеть таблиц, некоторые из которых содержат основные данные, а некоторые - дополнительные, которые необходимо на определенном этапе "включить" в анализ. Например, таблица с расшифровкой аббревиатур или сокращений вроде коротких названий стран или таблица телефонных кодов разных стран. Совокупность нескольких связанных друг с другом таблиц называют реляционными данными.
В случае с реляционными данными простых `bind_rows()` и `bind_cols()` становится недостаточно.
Эти две таблички нужно объединить **(*join*).** Эта задача обычно возникает не очень часто, обычно это происходит один-два раза в одном проекте, когда нужно дополнить имеющиеся данные дополнительной информацией извне или объединить два набора данных, обрабатывавшихся в разных программах. Однако каждый раз, когда такая задача возникает, это доставляет много боли. `dplyr` предлагает интуитивно понятный инструмент для объединения реляционных данных - семейство функций `*_join()`.
Возьмем для примера два тиббла `band_members` и `band_instruments`, встроенных в `dplyr` специально для демонстрации работы функций `*_join()`.
```{r}
band_members
band_instruments
```
У этих двух тибблов есть колонка с одинаковым названием, которая по своему смыслу соединяет данные обоих тибблов. Такая колонка называется **ключом**. Ключ должен однозначно идентифицировать наблюдения[^120-tidyverse_advanced-1].
[^120-tidyverse_advanced-1]: Если ключи будут неуникальными, то функции `*_join()` не будут выдавать ошибку. Вместо этого они добавят в итоговую таблицу все возможные пересечения повторяющихся ключей. С этим нужно быть очень осторожным, поэтому рекомендуется, во-первых, проверять уникальность ключей на входе и, во-вторых, проверять тиббл на выходе. Ну или использовать эту особенность работы функции `*_join()` себе во благо.
Давайте попробуем посоединять `band_members` и `band_instruments` разными вариантами `*_join()` и посмотрим, что у нас получится. Все эти функции имеют на входе два обязательных аргумента (`x =` и `y =`) в которые мы должны подставить два датафрейма/тиббла которые мы хотим объединить. Главное различие между этими функциями заключается в том, что они будут делать, если уникальные значения в ключах `x` и `y` не соответствуют друг другу.
![](images/120-tidyverse_advanced_joins.png)
- `left_join()`:
```{r}
band_members %>%
left_join(band_instruments)
```
`left_join()` - это самая простая для понимания и самая используемая функция из семейства `*_join()`. Она как бы "дополняет" информацию из первого тиббла вторым тибблом. В этом случае сохраняются все уникальные наблюдения в `x`, но отбрасываются лишние наблюдения в тиббле `y`. Тем значениям, которым не нашлось соотвествия в `y`, в колонках, взятых их `y`, ставятся значения `NA`.
Вы можете сами задать колонки-ключи параметром `by =`, по умолчанию это все колонки с одинаковыми названиями в двух тибблах.
```{r}
band_members %>%
left_join(band_instruments, by = "name")
```
Часто случается, что колонки-ключи называются по-разному в двух тибблах. Их необязательно переименовывать, можно поставить соответстие вручную используя проименованный вектор:
```{r}
band_members %>%
left_join(band_instruments2, by = c("name" = "artist"))
```
- `right_join()`:
```{r}
band_members %>%
right_join(band_instruments)
```
`right_join()` отбрасывает строчки в `x`, которых не было в `y`, но сохраняет соответствующие строчки `y` - `left_join()` наоборот.
- `full_join()`:
```{r}
band_members %>%
full_join(band_instruments)
```
Функция `full_join()` сохраняет все строчки и из `x` и `y`. Пожалуй, наиболее используемая функция после `left_join()` -- благодаря `full_join()` вы точно ничего не потеряете при объединении.
- `inner_join()`:
```{r}
band_members %>%
inner_join(band_instruments)
```
Функция `inner_join()` сохраняет только строчки, которые присутствуют и в `x`, и в `y`.
- `semi_join()`:
```{r}
band_members %>%
semi_join(band_instruments)
```
- `anti_join()`:
```{r}
band_members %>%
anti_join(band_instruments)
```
Функции `semi_join()` и `anti_join()` не присоединяют второй датафрейм/тиббл (`y`) к первому. Вместо этого они используются как некоторый словарь-фильтр для отделения только тех значений в `x`, которые есть в `y` (`semi_join()`) или, наоборот, которых нет в `y` (`anti_join()`).
## Tidy data: широкий и длинный форматы данных {#sec-tidy_data}
Принцип ***tidy data*** предполагает, что каждая строка содержит в себе одно измерение, каждая колонка - одну характеристику, а в одной ячейке только одно значение. Тем не менее, это не говорит однозначно о том, как именно хранить повторные измерения. Их можно хранить в **широком формате *(wide data)*** как одну колонку для каждого измерения. Одной строчке соответствует один объект измерения. Если измерений много, то такой датафрейм может получиться очень широким.
Другой способ хранения данных в датафрейме -- **длинный формат *(long data).*** В этом случае создаются две колонки: одна колонка - для идентификатора измерения (например, время измерения), другая колонка - для записи самого измерения. Каждая строка соответствует одному измерению объекта, а объект измерения имеет несколько строк в датафрейме. Если измерений много, то датафрейм становится очень длинным.
Это лучше пояснить на примере. Например, измерим массу студентов до и после прохождения курса по R. Как это лучше записать - **в широком формате,** как два числовых столбца (один -- измерение до, второй -- измерение после)? Или же **в длинном формате:** создать строковую колонку, в которой будет написано время измерения ("До курса по R", "После курса по R") и числовую колонку со значением массы?
- **Широкий формат:**
| Студент | До курса по R | После курса по R |
|----------|---------------|------------------|
| Маша | 70 | 63 |
| Рома | 80 | 74 |
| Антонина | 86 | 71 |
- **Длинный формат:**
| Студент | Время измерения | Масса (кг) |
|----------|------------------|------------|
| Маша | До курса по R | 70 |
| Рома | До курса по R | 80 |
| Антонина | До курса по R | 86 |
| Маша | После курса по R | 63 |
| Рома | После курса по R | 74 |
| Антонина | После курса по R | 71 |
На самом деле, оба варианта приемлемы, оба варианта встречаются в реальных данных, а разные функции и статистические пакеты могут требовать от вас как **длинный,** так и **широкий форматы.**
Таким образом, нам нужно научиться переводить из широкого формата в длинный и наоборот. Для этого в tidyverse есть функции:
- `tidyr::pivot_longer()`: переводит из **широкого** в **длинный формат,**
- `tidyr::pivot_wider()`: переводит из **длинного** в **широкий формат.**
```{r, echo = FALSE, results = 'asis'}
if (knitr:::is_latex_output()) {
cat("\n\nК сожалению, в PDF нельзя вставить .gif анимацию, посмотреть
ее можно по [ссылке](https://github.com/Pozdniakov/tidy_stats/blob/master/images/120-tidyverse_advanced_tidyr-longer-wider.gif)")
} else {
knitr::include_graphics("images/120-tidyverse_advanced_tidyr-longer-wider.gif")
}
```
```{r}
new_diet <- tibble(
student = c("Маша", "Рома", "Антонина"),
before_r_course = c(70, 80, 86),
after_r_course = c(63, 74, 71)
)
new_diet
```
Тиббл `new_diet` - это пример широкого формата данных.
Превратим тиббл `new_diet` длинный:
```{r}
new_diet %>%
pivot_longer(cols = before_r_course:after_r_course,
names_to = "measurement_time",
values_to = "weight_kg")
```
А теперь обратно в короткий:
```{r}
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")
```
## Трансформация нескольких колонок: `dplyr::across()` {#sec-tidy_across}
Допустим, вы хотите посчитать среднюю массу и рост, группируя по полу супергероев. Можно посчитать это внутри одного `summarise()`, использую запятую:
```{r}
heroes %>%
group_by(Gender) %>%
summarise(height = mean(Height, na.rm = TRUE),
weight = mean(Weight, na.rm = TRUE))
```
Если таких колонок будет много, то это уже станет сильно неудобным, нам придется много копировать код, а это чревато ошибками и очень скучно.
Поэтому в `dplyr` есть функция для операций над несколькими колонками сразу: `dplyr::across()`[^120-tidyverse_advanced-2]. Эта функция работает похожим образом на функции семейства `apply()` и использует tidyselect для выбора колонок.
[^120-tidyverse_advanced-2]: Функция `across()` появилась в пакете `dplyr` относительно недавно, до этого для работы с множественными колонками в tidyverse использовались многочисленные функции `*_at()`, `*_if()`, `*_all()`, например, `summarise_at()`, `summarise_if()`, `summarize_all()`. Эти функции до сих пор присутствуют в `dplyr`, но считаются устаревшими. Другая альтернатива - использование пакета `purrr` (@sec-purrr) или семейства функций `apply()` (@sec-apply_f).
Таким образом, конструкции с функцией `across()` можно разбить на три части:
1. Выбор колонок с помощью tidyselect. Здесь работают все те приемы, которые мы изучили при выборе колонок (@sec-tidyselect).
2. Собственно применение функции `across()`. Первый аргумент `.col` -- колонки, выбранные на первом этапе с помощью tidyselect, по умолчанию это `everything()`, т.е. все колонки. Второй аргумент `.fns` -- это функция или целый список из функций, которые будут применены к выбранным колонкам. Если функции требуют дополнительных аргументов, то они могут быть перечислены внутри `across()`.
3. Использование `summarise()` или другой функции `dplyr`. В этом случае в качестве аргумента для функции используется результат работы функции `across()`.
Вот такой вот бутерброд выходит. Давайте посмотрим, как это работает на практике и посчитаем среднее значение по колонкам `Height` и `Weight`.
```{r}
heroes %>%
group_by(Gender) %>%
summarise(across(c(Height,Weight), mean))
```
Здесь мы столкнулись с уже известной нам проблемой: функция `mean()` при столкновении хотя бы с одним `NA` будет возвращать `NA`, если мы не изменим параметр `na.rm =`. Как и в случае с функциями семейства `apply()` (@sec-apply_f), дополнительные параметры для функции можно перечислить через запятую после самой функции:
```{r}
heroes %>%
group_by(Gender) %>%
summarise(across(c(Height, Weight), mean, na.rm = TRUE))
```
До этого мы просто использовали выбор колонок по их названию. Но именно внутри `across()` использование tidyselect раскрывается как удивительно элегантный и мощный инструмент. Например, можно посчитать среднее для всех numeric колонок:
```{r}
heroes %>%
drop_na(Height, Weight) %>%
group_by(Gender) %>%
summarise(across(where(is.numeric), mean, na.rm = TRUE))
```
Или длину строк для строковых колонок. Для этого нам понадобится вспомнить, как создавать анонимные функции (\@ref(anon_f)).
```{r}
heroes %>%
group_by(Gender) %>%
summarise(across(where(is.character),
function(x) mean(nchar(x), na.rm = TRUE)))
```
Или же даже посчитать и то, и другое внутри одного `summarise()`!
```{r}
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)))
```
Внутри одного `across()` можно применить не одну функцию к каждой из выбранных колонок, а сразу несколько функций для каждой из колонок. Для этого нам нужно использовать список функций (желательно - проименованный).
```{r}
heroes %>%
group_by(Gender) %>%
summarise(across(c(Height, Weight),
list(minimum = min,
average = mean,
maximum = max),
na.rm = TRUE))
```
> Вот нам и понадобился список функций (\@ref(functions_objects))!
```{r}
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)))
)
)
```
Хотя основное применение функции `across()` -- это массовое подытоживание с помощью `summarise()`, `across()` можно использовать и с другими функциями `dplyr`. Например, можно делать массовые операции с колонками с помощью `mutate()`:
```{r}
heroes %>%
mutate(across(where(is.character), as.factor))
```
Конструкция `across()` работает не только внутри `summarise()` и `mutate()`, можно применять `across()` и с другими функциями, которые используют data-masking. Например, можно использовать `across()` внутри `count()` вместе с функцией `n_distinct()`, которая считает количество уникальных значений в векторе. Это позволяет посмотреть таблицу частот для группирующих переменных:
```{r}
heroes %>%
count(across(where(function(x) n_distinct(x) <= 6)))
```
## Функциональное программирование: `purrr` {#sec-purrr}
`purrr` -- это пакет для функционального программирования в tidyverse. Как и многие пакеты tidyverse, `purrr` пытается заменить собой базовый функционал R на более понятный и удобный. В данном случае, речь в первую очередь идет о функциях семейства `apply()`, с которыми мы работали ранее (\@ref(apply_f)).
Давайте вспомним, как работает `lapply()`. В качестве первого аргумента функция `lapply()` принимает список (или то, что может быть в него превращено, например, датафрейм), в качестве второго - функцию, которая будет применена к каждому элементу списка. На выходе мы получим список такой же длины.
```{r}
lapply(heroes, class)
```
Функция `purrr::map()` работает по тому же принципу: можно просто заменить `lapply()` на `map()`, и мы получим тот же результат.
```{r}
map(heroes, class)
```
`map()` можно встроить в канал с пайпом (впрочем, как и `lapply()`):
```{r}
heroes %>%
map(class)
```
Как и `lapply()`, `map()` всегда возвращает список. Из-за этого мы больше пользовались функцией `sapply()`, а не `lapply()`. Функция `sapply()` упрощала результат до вектора, если это возможно. Подобное упрощение может показаться удобным пока не сталкиваешься с тем, что иногда очень сложно предсказать, какой тип данных получится на выходе. Есть функция `vapply()` в которой можно управлять типом данных на выходе, но она не очень удобная. В `purrr` эта проблема решена просто: есть множество функций `map_*()`, где вместо звездочки - нужный формат на выходе.
Например, если мы хотим получить строковый вектор на выходе, то нам нужна функция `map_chr()`.
```{r}
heroes %>%
map_chr(class)
```
Можно превратить результат в датафрейм с помощью `map_df()`.
```{r}
heroes %>%
map_df(class)
```
Так же как и функции семейства `apply()`, функции `map_*()` отлично сочетаются с анонимными функциями.
```{r}
heroes %>%
map_int(function(x) sum(is.na(x)))
```
Однако у `purrr` есть свой, более короткий способ записи анонимных функций: `function(arg)` заменяется на `~`, а `arg` на `.`.
```{r}
heroes %>%
map_int(~sum(is.na(.)))
```
Если нужно итерироваться сразу по нескольким спискам, то есть функции `map2_*()` (для двух списков) и `pmap_*()` (для нескольких списков).
## Колонки-списки и нестинг: `nest()` {#sec-list_colums_nest}
Ранее мы говорили о том, что датафрейм -- это по своей сути список из векторов разной длины. На самом деле, это не совсем так: колонки обычного датафрейма вполне могут быть списками. Однако делать так обычно не рекомендуется, пусть R это и не запрещает создавать такие колонки: многие функции предполагают, что все колонки датафрейма являются векторами.
`tidyverse` гораздо дружелюбнее относится к использовании списка в качестве колонки. Такие колонки называются **колонками-списками (list columns)**. Основной способ их создания - использования функции `tidyr::nest()`. С помощью tidyselect нужно выбрать сжимаемые колонки, которые будут агрегированы по невыбранным колонками. Это и называется нестингом.
```{r}
heroes %>%
nest(!Gender)
```
Заметьте, у нас появилась колонка `data`, в которой содержатся тибблы. Туда и спрятались все наши данные.
Нестинг похож на агрегирование с помощью `group_by()`. Если сделать нестинг сгруппированного с помощью `group_by()` тиббла, то сожмутся все колонки кроме тех, которые выступают в качестве групп:
```{r}
heroes %>%
group_by(Gender) %>%
nest()
```
Теперь можно работать с колонкой-списком как с обычной колонкой. Например, применять функцию для каждой строчки (то есть для каждого тиббла) с помощью `map()` и записывать результат в новую колонку с помощью `mutate()`.
```{r}
heroes %>%
group_by(Gender) %>%
nest() %>%
mutate(dim = map(data, dim))
```
В конце концов нам нужно "разжать" сжатую колонку-список. Сделать это можно с помощью `unnest()`, выбрав с помощью tidyselect нужные колонки.
```{r}
heroes %>%
group_by(Gender) %>%
nest() %>%
mutate(dim = map(data, dim)) %>%
unnest(dim)
```
Разжатая колонка обычно больше сжатой, поэтому разжатие привело к удлинению тиббла. Вместо удлинения тиббла, его можно расширить с помощью `unnest_wider()`.
```{r}
heroes %>%
group_by(Gender) %>%
nest() %>%
mutate(dim = map(data, dim)) %>%
unnest_wider(dim, names_sep = "_")
```
Нестинг - это мощный инструмент tidyverse, хотя во многих случаях можно обойтись и без него. Наиболее эффективна эта конструкция именно в тех ситуациях, где вы делаете операции над целыми тибблами. Поэтому наибольшее распространение нестинг получил в смычке с пакетом `broom` для расчета множественных статистических моделей.
Другое применение нестинга -- решение проблемы с несколькими значениями в одной ячейки, которые записаны через запятую или какой-либо другой знак. Такое часто встречается в данных, поэтому хорошо бы уметь с этим работать!
Возьмем небольшой искусственный пример:
```{r}
films <- tribble(
~film, ~genres,
"Ирония Судьбы", "comedy, drama",
"Большой Лебовски", "comedy, criminal",
"Аватар", "fantasy, drama"
)
films
```
Для этого разобъем значения колонки `genres` с помощью встроенной функции `strsplit()`. Она разбивает значения вектора по выбранному разделителю. Здесь нам нужен разделитель `", "` (с пробелом после запятой!). На выходе мы получим список такой же длины, что и исходный вектором, а каждый элемент этого списка будет строковым вектором. Количество значений внутри векторов может быть каким угодно. Поскольку результат -- список, перезаписанная колонка `genres` станет колонкой-списком.
```{r}
films %>%
mutate(genres = strsplit(genres, ", "))
```
Теперь нам нужно сделать `unnest()`
```{r}
films %>%
mutate(genres = strsplit(genres, ", ")) %>%
unnest()
```
Теперь у нас данные в длинном виде! Результат можно расширить с помощью уже знакомого `pivot_wider()` и дополнительной колонки со значениями `TRUE`. Если соответствующей пары нет в тиббле, то в итоговой широкой таблице будет `NA`, мы можем поменять их на `FALSE` с помощью параметра `values_fill =`.
```{r}
films %>%
mutate(genres = strsplit(genres, ", ")) %>%
unnest() %>%
mutate(value = TRUE) %>%
pivot_wider(names_from = "genres",
values_from = "value", values_fill = FALSE)
```
Правда, то же самое можно сделать чуть проще, без колонок списков. Специально для этой задачи есть функция `tidyr::separate_rows()`, которая заменяет связку `strsplit()` с `unnest()`:
```{r}
films %>%
separate_rows(genres, sep = ", ") %>%
mutate(value = TRUE) %>%
pivot_wider(names_from = "genres",
values_from = "value", values_fill = FALSE)
```