forked from hadley/r4ds
-
Notifications
You must be signed in to change notification settings - Fork 130
/
25-model-many.qmd
569 lines (405 loc) · 24.9 KB
/
25-model-many.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
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
# Muchos modelos
## Introducción
En este capítulo vas a aprender tres ideas poderosas que te van a ayudar a trabajar fácilmente con un gran número de modelos:
1. Usar muchos modelos simples para entender mejor conjuntos de datos complejos.
1. Usar columnas-lista (_list-columns_) para almacenar estructuras de datos arbitrarias en un _data frame_.
Esto, por ejemplo, te permitirá tener una columna que contenga modelos lineales.
1. Usar el paquete __broom__, de David Robinson, para transformar modelos en datos ordenados.
Esta es una técnica poderosa para trabajar con un gran número de modelos
porque una vez que tienes datos ordenados, puedes aplicar todas las técnicas que
has aprendido anteriormente en el libro.
Empezaremos entrando de lleno en un ejemplo motivador usando datos sobre la esperanza de vida alrededor del mundo. Es un conjunto de datos pequeño pero que ilustra cuán importante puede ser modelar para mejorar tus visualizaciones. Utilizaremos un número grande de modelos simples para separar algunas de las señales más fuertes y así poder ver las señales sutiles que permanecen. También veremos cómo las medidas de resumen de los modelos nos pueden ayudar a encontrar datos atípicos y tendencias inusuales.
Las siguientes secciones ahondarán en más detalles acerca de cada una de estas técnicas:
1. En [columnas-lista] aprenderás más acerca de la estructura de datos _columna-lista_
y por qué es válido poner listas en _data frames_.
1. En [creando columnas-lista] aprenderás las tres maneras principales en las que
crearás columnas-lista.
1. En [simplificando columnas-lista] aprenderás cómo convertir columnas-lista de vuelta
a vectores atómicos regulares (o conjuntos de vectores atómicos) para que puedas trabajar
con ellos más fácilmente.
1. En [haciendo datos ordenados con broom] aprenderás sobre el conjunto de herramientas completo
provisto por __broom__ (_escoba_, en inglés) y verás cómo puede ser aplicado a otros tipos de
estructuras de datos.
Este capítulo es en cierta medida aspiracional: si este libro es tu primera introducción a R, este capítulo probablemente será un desafío. Requiere que tengas profundamente internalizadas ideas acerca de modelado, estructuras de datos e iteración. Así que no te preocupes si no lo entiendes --- solo aparta este capítulo por un par de meses, y vuelve cuando quieras ejercitar tu cerebro.
### Prerrequisitos
Trabajar con muchos modelos requiere muchos de los paquetes del __tidyverse__ (para exploración de datos, manejo y programación) y __modelr__ para facilitar el modelado.
```{r setup, message = FALSE}
library(modelr)
library(tidyverse)
```
## gapminder
Como motivación para entender el poder de muchos modelos simples, vamos a mirar los datos de "gapminder". Estos datos fueron popularizados por Hans Rosling, un doctor y estadístico sueco. Si nunca has escuchado de él, para de leer este capítulo ahora mismo y revisa algunos de sus videos! Es un fantástico presentador de datos e ilustra cómo puedes usar datos para presentar una historia convincente. Un buen lugar para empezar es este video corto filmado en conjunto con la BBC: <https://www.youtube.com/watch?v=jbkSRLYSojo>.
Los datos de gapminder resumen la progresión de países a través del tiempo, mirando estadísticos como esperanza de vida y PIB. Los datos son de fácil acceso en R gracias a Jenny Bryan, quien creó el paquete __gapminder__. Utilizaremos la versión en español contenida en el paquete __datos__, que incorpora este dataset en el objeto `paises`.
```{r}
library(datos)
paises
```
En este caso de estudio, nos enfocaremos solo en tres variables para responder la pregunta "¿Cómo la esperanza de vida (`esperanza_de_vida`) cambia a través del tiempo (`anio`) para cada país (`pais`)?". Un buen lugar para empezar es con un gráfico:
```{r}
paises %>%
ggplot(aes(anio, esperanza_de_vida, group = pais)) +
geom_line(alpha = 1 / 3)
```
Es un conjunto de datos pequeño: solo tiene ~1,700 observaciones y 3 variables. ¡Pero aun así es difícil ver qué está pasando! En general, pareciera que la esperanza de vida ha estado mejorando en forma constante. Sin embargo, si miras de cerca, puedes notar que algunos países no siguen este patrón. ¿Cómo podemos hacer que esos países se vean más fácilmente?
Una forma es usar el mismo enfoque que en el último capítulo: hay una señal fuerte (un crecimiento lineal general) que hace difícil ver tendencias más sutiles. Separaremos estos factores estimando un modelo con una tendencia lineal. El modelo captura el crecimiento estable en el tiempo y los residuos mostrarán lo que queda fuera.
Ya sabes cómo hacer eso si tenemos un solo país:
```{r, out.width = "33%", fig.asp = 1, fig.width = 3, fig.align='default'}
nz <- filter(paises, pais == "Nueva Zelanda")
nz %>%
ggplot(aes(anio, esperanza_de_vida)) +
geom_line() +
ggtitle("Datos completos = ")
nz_mod <- lm(esperanza_de_vida ~ anio, data = nz)
nz %>%
add_predictions(nz_mod) %>%
ggplot(aes(anio, pred)) +
geom_line() +
ggtitle("Tendencia lineal + ")
nz %>%
add_residuals(nz_mod) %>%
ggplot(aes(anio, resid)) +
geom_hline(yintercept = 0, colour = "white", size = 3) +
geom_line() +
ggtitle("Patrón restante")
```
¿Cómo podemos ajustar fácilmente ese modelo para cada país?
### Datos anidados
Te puedes imaginar copiando y pegando ese código múltiples veces; sin embargo, ¡ya has aprendido una mejor forma! Extrae el código en común con una función y repítelo usando una función map del paquete __purrr__. Este problema se estructura un poco diferente respecto a lo que has visto antes. En lugar de repetir una acción por cada variable, bnhgqueremos repetirla para cada país, es decir, un subconjunto de filas. Para hacer esto, necesitamos una nueva estructura de datos: el __*data frame* anidado__ (_nested data frame_). Para crear un _data frame_ anidado empezamos con un _data frame_ agrupado, y lo "anidamos":
```{r}
por_pais <- paises %>%
group_by(pais, continente) %>%
nest()
por_pais
```
(Estamos haciendo un poco de trampa agrupando por `continente` y `pais` al mismo tiempo. Dado el `pais`, `continente` es fijo, así que no agrega ningún grupo más, pero es una forma fácil de llevarnos una variable adicional para el camino.)
Esto crea un _data frame_ que tiene una fila por grupo (por país), y una columna bastante inusual: `data`. `data` es una lista de _data frames_ (o _tibbles_, para ser precisos). Esto parece una idea un poco loca: ¡tenemos un _data frame_ con una columna que es una lista de otros _data frames_! Explicaré brevemente por qué pienso que es una buena idea.
La columna `data` es un poco difícil de examinar porque es una lista moderadamente complicada y todavía estamos trabajando para tener buenas herramientas para explorar estos objetos. Desafortunadamente, usar `str()` no es recomendable porque usualmente producirá un _output_ (salida de código) muy extenso. Pero si extraes un solo elemento de la columna `data` verás que contiene todos los datos para ese país (en este caso, Afganistán).
```{r}
por_pais$data[[1]]
```
Nota la diferencia entre un _data frame_ agrupado estándar y un _data frame_ anidado: en un _data frame_ agrupado cada fila es una observación; en un _data frame_ anidado, cada fila es un grupo. Otra forma de pensar en un conjunto de datos anidado es que ahora tenemos una meta-observación: una fila que representa todo el transcurso de tiempo para un país, en lugar de solo un punto en el tiempo.
### Columnas-lista
Ahora que tenemos nuestro _data frame_ anidado, estamos en una buena posición para ajustar algunos modelos. Tenemos una función para ajustar modelos:
```{r}
modelo_pais <- function(df) {
lm(esperanza_de_vida ~ anio, data = df)
}
```
Y queremos aplicarlo a cada _data frame_. Los _data frames_ están en una lista, así que podemos usar `purrr::map()` para aplicar `modelo_pais` a cada elemento:
```{r}
modelos <- map(por_pais$data, modelo_pais)
```
Sin embargo, en lugar de dejar la lista de modelos como un objeto suelto, lo mejor sería almacenarlo como una columna en el _data frame_ `por_pais`. Almacenar objetos relacionados en columnas es una parte clave del valor de los _data frames_, y por eso creemos que las columnas-lista son tan buena idea. En el transcurso de nuestro trabajo con estos países vamos a tener muchas listas en las que tenemos un elemento por país. ¿Por qué no almacenarlos todos juntos en un _data frame_?
En otras palabras, en lugar de crear un nuevo objeto en el entorno global, vamos a crear una nueva variable en el _data frame_ `por_pais`. Ese es un trabajo para `dplyr::mutate()`:
```{r}
por_pais <- por_pais %>%
mutate(modelo = map(data, modelo_pais))
por_pais
```
Esto tiene una gran ventaja: como todos los objetos relacionados están almacenados juntos, no necesitas manualmente mantenerlos sincronizados cuando filtras o reordenas. La semántica del _data frame_ se ocupa de esto por ti:
```{r}
por_pais %>%
filter(continente == "Europa")
por_pais %>%
arrange(continente, pais)
```
Si tu lista de _data frames_ y lista de modelos fueran objetos separados, tendrías que acordarte de que cuando reordenas o seleccionas un subconjunto de un vector, necesitas reordenar o seleccionar el subconjunto de todos los demás para mantenerlos sincronizados. Si te olvidas, tu código va a seguir funcionando, ¡pero va a devolver la respuesta equivocada!
### Desanidando
Previamente calculamos los residuos de un único modelo con un conjunto de datos también único. Ahora tenemos 142 _data frames_ y 142 modelos. Para calcular los residuos, necesitamos llamar a la función `add_residuals()` (_adicionar residuos_, en inglés) con cada par modelo-datos:
```{r}
por_pais <- por_pais %>%
mutate(
residuos = map2(data, modelo, add_residuals)
)
por_pais
```
¿Pero cómo puedes graficar una lista de _data frames_? En lugar de luchar para contestar esa pregunta, transformemos la lista de _data frames_ de vuelta en un _data frame_ regular. Previamente usamos `nest()` (_anidar_) para transformar un _data frame_ regular en uno anidado; ahora desanidaremos con `unnest()`:
```{r}
residuos <- unnest(por_pais, residuos)
residuos
```
Nota que cada columna regular está repetida una vez por cada fila en la columna anidada.
Ahora que tenemos un _data frame_ regular, podemos graficar los residuos:
```{r}
residuos %>%
ggplot(aes(anio, resid)) +
geom_line(aes(group = pais), alpha = 1 / 3) +
geom_smooth(se = FALSE)
```
Separar facetas por continente es particularmente revelador:
```{r}
residuos %>%
ggplot(aes(anio, resid, group = pais)) +
geom_line(alpha = 1 / 3) +
facet_wrap(~continente)
```
Parece que hemos perdido algunos patrones suaves. También hay algo interesante pasando en África: vemos algunos residuos muy grandes, lo que sugiere que nuestro modelo no está ajustando muy bien. Exploraremos más eso en la próxima sección, atacando el problema desde un ángulo un poco diferente.
### Calidad del modelo
En lugar de examinar los residuos del modelo, podríamos examinar algunas medidas generales de la calidad del modelo. Aprendiste cómo calcular algunas medidas específicas en el capítulo anterior. Aquí mostraremos un enfoque diferente usando el paquete __broom__. El paquete __broom__ provee un conjunto de funciones generales para transformar modelos en datos ordenados. Aquí utilizaremos `broom::glance()` (_vistazo_, en inglés) para extraer algunas métricas de la calidad del modelo. Si lo aplicamos a un modelo, obtenemos un _data frame_ con una única fila:
```{r}
broom::glance(nz_mod)
```
Podemos usar `mutate()` y `unnest()` para crear un _data frame_ con una fila por cada país:
```{r}
glance <- por_pais %>%
mutate(glance = map(modelo, broom::glance)) %>%
select(-data, -modelo, -residuos) %>% # remover las listas-columnas innecesarias
unnest(glance)
glance
```
(Presta atención a las variables que no se imprimieron: hay mucha información útil allí).
Con este _data frame_ podemos empezar a buscar modelos que no se ajustan bien:
```{r}
glance %>%
arrange(r.squared)
```
Los peores modelos parecieran estar todos en África. Vamos a chequear esto con un gráfico. Tenemos un número relativamente chico de observaciones y una variable discreta, así que usar `geom_jitter()` es efectivo:
```{r}
glance %>%
ggplot(aes(continente, r.squared)) +
geom_jitter(width = 0.5)
```
Podríamos quitar los países con un $R^2$ particularmente malo y graficar los datos:
```{r}
mal_ajuste <- filter(glance, r.squared < 0.25)
paises %>%
semi_join(mal_ajuste, by = "pais") %>%
ggplot(aes(anio, esperanza_de_vida, colour = pais)) +
geom_line()
```
Vemos dos efectos principales aquí: las tragedias de la epidemia de VIH/SIDA y el genocidio de Ruanda.
### Ejercicios
1. Una tendencia lineal parece ser demasiado simple para la tendencia general.
¿Puedes hacerlo mejor con un polinomio cuadrático? ¿Cómo puedes interpretar
el coeficiente del término cuadrático? (Pista: puedes querer transformar
`year` para que tenga media cero.)
1. Explora otros métodos para visualizar la distribución del $R^2$ por
continente. Puedes querer probar el paquete __ggbeeswarm__, que provee
métodos similares para evitar superposiciones como jitter, pero usa métodos
determinísticos.
1. Para crear el último gráfico (mostrando los datos para los países con los
peores ajustes del modelo), precisamos dos pasos: creamos un _data frame_ con
una fila por país y después hicimos un _semi-join_ al conjunto de datos original.
Es posible evitar este _join_ si usamos `unnest()` en lugar de
`unnest(.drop = TRUE)`. ¿Cómo?
## Columnas-lista
Ahora que has visto un flujo de trabajo básico para manejar muchos modelos, vamos a sumergirnos en algunos detalles. En esta sección, exploraremos en más detalle la estructura de datos columna-lista. Solo recientemente hemos comenzado a apreciar realmente la idea de la columna-lista. Esta estructura está implícita en la definición de _data frame_: un _data frame_ es una lista nombrada de vectores de igual largo. Una lista es un vector, así que siempre ha sido legítimo usar una lista como una columna de un _data frame_. Sin embargo, R base no hace las cosas fáciles para crear columnas-lista, y `data.frame()` trata a la lista como una lista de columnas:
```{r}
data.frame(x = list(1:3, 3:5))
```
Puedes prevenir que `data.frame()` haga esto con `I()`, pero el resultado no se imprime particularmente bien:
```{r}
data.frame(
x = I(list(1:3, 3:5)),
y = c("1, 2", "3, 4, 5")
)
```
_Tibble_ mitiga este problema siendo más _perezoso_ (`tibble()` no modifica sus _inputs_) y proporcionando un mejor método de impresión:
```{r}
tibble(
x = list(1:3, 3:5),
y = c("1, 2", "3, 4, 5")
)
```
Es incluso más fácil con `tribble()`, ya que automáticamente puede interpretar que necesitas una lista:
```{r}
tribble(
~x, ~y,
1:3, "1, 2",
3:5, "3, 4, 5"
)
```
Las columnas-lista son usualmente más útiles como estructuras de datos intermedias. Es difícil trabajar con ellas directamente, porque la mayoría de las funciones de R trabaja con vectores atómicos o _data frames_, pero la ventaja de mantener ítems relacionados juntos en un _data frame_ hace que valga la pena un poco de molestia.
Generalmente hay tres partes en un _pipeline_ efectivo de columnas-lista:
1. Creas la columna-lista usando alguna de estas opciones: `nest()` o `summarise()` + `list()`
o `mutate()` + una función map, como se describe en [Creando columnas-lista].
1. Creas otra columna-lista intermedia transformando columnas lista
existentes con `map()`, `map2()` o `pmap()`. Por ejemplo,
en el caso de estudio de arriba, creamos una columna-lista de modelos transformando
una columna-lista de _data frames_.
1. Simplificas la columna-lista de vuelta en un _data frame_ o vector atómico,
como se describe en [Simplificando columnas-lista].
## Creando columnas-lista
Típicamente, no tendrás que crear columnas-lista con `tibble()`, sino a partir de columnas regulares usando uno de estos tres métodos:
1. Con `tidyr::nest()` para convertir un _data frame_ agrupado en uno anidado
en el que tengas columnas-lista de _data frames_.
1. Con `mutate()` y funciones vectorizadas que retornan una lista.
1. Con `summarise()` y funciones de resumen que retornan múltiples resultados.
Alternativamente, podrías crearlas a partir de una lista nombrada, usando `tibble::enframe()`.
Generalmente, cuando creas columnas-lista, debes asegurarte de que sean homogéneas: cada elemento debe contener el mismo tipo de cosa. No hay chequeos para asegurarte de que sea así, pero si usas __purrr__ y recuerdas lo que aprendiste sobre funciones de tipo estable (_type-stable functions_), encontrarás que eso pasa naturalmente.
### Con anidación
`nest()` crea un _data frame_ anidado, que es un _data frame_ con una columna-lista de _data frames_. En un _data frame_ anidado cada fila es una meta-observación: las otras columnas son variables que definen la observación (como país y continente arriba), y la columna-lista de _data frames_ tiene las observaciones individuales que construyen la meta-observación.
Hay dos formas de usar `nest()`. Hasta ahora has visto cómo usarlo con un _data frame_ agrupado. Cuando se aplica a un _data frame_ agrupado, `nest()` mantiene las columnas que agrupan tal cual, y envuelve todo lo demás en la columna-lista:
```{r}
paises %>%
group_by(pais, continente) %>%
nest()
```
También lo puedes usar en un _data frame_ no agrupado, especificando cuáles columnas quieres anidar:
```{r}
paises %>%
nest(data = anio:pib_per_capita)
```
### A partir de funciones vectorizadas
Algunas funciones útiles toman un vector atómico y retornan una lista. Por ejemplo, en [cadenas de caracteres] aprendiste sobre `stringr::str_split()`, que toma un vector de caracteres y retorna una lista de vectores de caracteres. Si lo usas dentro de `mutate`, obtendrás una columna-lista:
```{r}
df <- tribble(
~x1,
"a,b,c",
"d,e,f,g"
)
df %>%
mutate(x2 = stringr::str_split(x1, ","))
```
`unnest()` sabe cómo manejar estas listas de vectores:
```{r}
df %>%
mutate(x2 = stringr::str_split(x1, ",")) %>%
unnest(x2)
```
(Si usas mucho este patrón, asegúrate de chequear `tidyr::separate_rows()`, que es un _wrapper_ alrededor de este patrón común).
Otro ejemplo de este patrón es usar `map()`, `map2()`, `pmap()` de __purrr__. Por ejemplo, podríamos tomar el ejemplo final de [Invocando distintas functions] y reescribirlo usando `mutate()`:
```{r}
sim <- tribble(
~f, ~params,
"runif", list(min = -1, max = 1),
"rnorm", list(sd = 5),
"rpois", list(lambda = 10)
)
sim %>%
mutate(sims = invoke_map(f, params, n = 10))
```
Nota que técnicamente `sim` no es homogéneo porque contiene tanto vectores de dobles como vectores de enteros. Sin embargo, es probable que esto no cause muchos problemas porque ambos vectores son numéricos.
### A partir de medidas de resumen con más de un valor
Una restricción de `summarise()` es que solo funciona con funciones de resumen que retornan un único valor. Eso significa que no puedes usarlo con funciones como `quantile()`, que retorna un vector de largo arbitrario:
```{r, error = TRUE}
mtautos %>%
group_by(cilindros) %>%
summarise(q = quantile(millas))
```
Sin embargo, ¡puedes envolver el resultado en una lista! Esto obedece el contrato de `summarise()`, porque cada resumen ahora es una lista (un vector) de largo 1.
```{r}
mtautos %>%
group_by(cilindros) %>%
summarise(q = list(quantile(millas)))
```
Para producir resultados útiles con `unnest`, también necesitarás capturar las probabilidades:
```{r}
probs <- c(0.01, 0.25, 0.5, 0.75, 0.99)
mtautos %>%
group_by(cilindros) %>%
summarise(p = list(probs), q = list(quantile(millas, probs))) %>%
unnest(c(p, q))
```
### A partir de una lista nombrada
Los _data frames_ con columnas-lista proveen una solución a un problema común: ¿qué haces si quieres iterar sobre el contenido de una lista y también sobre sus elementos? En lugar de tratar de juntar todo en un único objeto, usualmente es más fácil hacer un _data frame_: una columna puede contener los elementos y otra columna la lista. Una forma fácil de crear un _data frame_ como este desde una lista es `tibble::enframe()`.
```{r}
x <- list(
a = 1:5,
b = 3:4,
c = 5:6
)
df <- enframe(x)
df
```
La ventaja de esta estructura es que se generaliza de una forma relativamente sencilla - los nombres son útiles si tienes un vector de caracteres con los metadatos, pero no ayudan para otros tipos de datos o para múltiples vectores.
Ahora, si quieres iterar sobre los nombres y valores en paralelo, puedes usar `map2()`:
```{r}
df %>%
mutate(
smry = map2_chr(name, value, ~ stringr::str_c(.x, ": ", .y[1]))
)
```
### Ejercicios
1. Lista todas las funciones en las que puedas pensar que tomen como _input_ un vector atómico y
retornen una lista.
1. Piensa en funciones de resumen útiles que, como `quantile()`, retornen
múltiples valores.
1. ¿Qué es lo que falta en el siguiente _data frame_? ¿Cómo `quantile()` retorna
eso que falta? ¿Por qué eso no es tan útil aquí?
```{r}
mtautos %>%
group_by(cilindros) %>%
summarise(q = list(quantile(millas))) %>%
unnest(q)
```
1. ¿Qué hace este código? ¿Por qué podría ser útil?
```{r, eval = FALSE}
mtautos %>%
group_by(cilindros) %>%
summarise_each(funs(list))
```
## Simplificando columnas-lista
Para aplicar las técnicas de manipulación de datos y visualización que has aprendido en este libro, necesitarás simplificar la columna-lista de vuelta a una columna regular (un vector atómico) o conjunto de columnas. La técnica que usarás para volver a una estructura más sencilla depende de si quieres un único valor por elemento, o múltiples valores.
1. Si quieres un único valor, usa `mutate()` con `map_lgl()`,
`map_int()`, `map_dbl()`, y `map_chr()` para crear un vector atómico.
1. Si quieres varios valores, usa `unnest()` para convertir columnas-lista de vuelta a
columnas regulares, repitiendo las filas tantas veces como sea necesario.
Estas técnicas están descritas con más detalle abajo.
### Lista a vector
Si puedes reducir tu columna lista a un vector atómico entonces será una columna regular. Por ejemplo, siempre puedes resumir un objeto con su tipo y largo, por lo que este código funcionará sin importar cuál tipo de columna-lista tengas:
```{r}
df <- tribble(
~x,
letters[1:5],
1:3,
runif(5)
)
df %>% mutate(
tipo = map_chr(x, typeof),
largo = map_int(x, length)
)
```
Esta es la misma información básica que obtienes del método de impresión por defecto de _tbl_ , solo que ahora lo puedes usar para filtrar. Es una técnica útil si tienes listas heterogéneas y quieres remover las partes que no te sirven.
No te olvides de los atajos de `map_*()` - puedes usar `map_chr(x, "manzana")` para extraer la cadena de caracteres almacenada en `manzana` para cada elemento de `x`. Esto es útil para separar listas anidadas en columnas regulares. Usa el argumento `.null` para proveer un valor para usar si el elemento es un valor faltante (en lugar de retornar `NULL`):
```{r}
df <- tribble(
~x,
list(a = 1, b = 2),
list(a = 2, c = 4)
)
df %>% mutate(
a = map_dbl(x, "a"),
b = map_dbl(x, "b", .null = NA_real_)
)
```
### Desanidando
`unnest()` trabaja repitiendo la columna regular una vez para cada elemento de la columna-lista. Por ejemplo, en el siguiente ejemplo sencillo repetimos la primera fila 4 veces (porque el primer elemento de `y` tiene largo cuatro) y la segunda fila una vez:
```{r}
tibble(x = 1:2, y = list(1:4, 1)) %>% unnest(y)
```
Esto significa que no puedes simultáneamente desanidar dos columnas que contengan un número diferente de elementos:
```{r, error = TRUE}
# Funciona, porque y y z tienen el mismo número de elementos en
# cada fila
df1 <- tribble(
~x, ~y, ~z,
1, c("a", "b"), 1:2,
2, "c", 3
)
df1
df1 %>% unnest(c(y, z))
# No funciona porque y y z tienen un número diferente de elementos
df2 <- tribble(
~x, ~y, ~z,
1, "a", 1:2,
2, c("b", "c"), 3
)
df2
df2 %>% unnest(c(y, z))
```
El mismo principio aplica al desanidar columnas-lista de _data frames_. Puedes desanidar múltiples columnas-lista siempre que todos los _data frames_ de cada fila tengan la misma cantidad de filas.
### Ejercicios
1. ¿Por qué podría ser útil la función `lengths()` para crear columnas de
vectores atómicos a partir de columnas-lista?
1. Lista los tipos de vectores más comúnes que se encuentran en un _data frame_. ¿Qué hace que las
listas sean diferentes?
## Haciendo datos ordenados con broom
El paquete __broom__ provee tres herramientas generales para transformar modelos en _data frames_ ordenados:
1. `broom::glance(modelo)` retorna una fila para cada modelo. Cada columna tiene una
medida de resumen del modelo: o bien una medida de la calidad del modelo, o bien complejidad, o
una combinación de ambos.
1. `broom::tidy(modelo)` retorna una fila por cada coeficiente en el modelo. Cada
columna brinda información acerca de la estimación o su variabilidad.
1. `broom::augment(modelo, data)` retorna una fila por cada fila en `data`, agregando
valores adicionales como residuos, y estadísticos de influencia.