library('DT')
library('tidyverse')
library('expss')
1998
1 Nombres y valores
1.1 Introducción
En R, es importante entender la distinción entre un objeto y su nombre, pues:
- Estimamos con mayor precisión el rendimiento y el uso de memoria del código.
- Escribimos código más rápido evitando copias accidentales, una fuente importante de código lento.
- Comprendemos mejor las herramientas de programación funcional de R.
El objetivo del capítulo es ayudarte a comprender la distinción entre nombres y valores, y cuándo R copiará un objeto.
Preguntas que resolveremos serán:
- Dado el siguiente data frame, ¿cómo creo una nueva columna llamada “3” que contiene la suma de 1 y 2? Sólo puedes usar $, no [[. ¿Qué hace que 1, 2, y 3 desafiantes como nombres de variables?
<- data.frame(runif(3), runif(3))
df names(df) <- c(1, 2)
- En el siguiente código, ¿cuánta memoria ocupa y?
<- runif(1e6)
x <- list(x, x, x) y
- ¿En qué línea a es copiada copia en el siguiente ejemplo?
<- c(1, 5, 3, 2)
a <- a
b 1]] <- 10 b[[
1.2 Resumen de la sección
2.2 presenta la distinción entre nombres y valores, y describe cómo <- crea un enlace, o referencia, entre un nombre y un valor.
2.3 describe cuándo R hace una copia: cada vez que modificas un vector, es casi seguro que está creando un nuevo vector modificado. Aprenderás a usar \(tracemem()\) para averiguar cuándo se produce realmente una copia. A continuación, explorarás las implicaciones a medida que se aplican a las llamadas a funciones, listas, data frames y vectores de caracteres.
2.4 explora las implicaciones de las dos secciones anteriores sobre la cantidad de memoria que ocupa un objeto. Dado que su intuición puede estar profundamente equivocada y ya que \(utils::object.size()\) desafortunadamente es inexacto, aprenderás a usar \(lobstr::obj\_size()\)
2.5 describe las dos excepciones importantes para copiar al modificar: con entornos y valores con un solo nombre, los objetos se modifican realmente en su lugar.
2.6 concluye con una discusión sobre el recolector de basura, que libera la memoria utilizada por los objetos a los que ya no se hace referencia con un nombre.
1.3 Prerrequisitos
Usaremos el paquete \(lobstr\) para profundizar en la representación interna de los objetos R.
library(lobstr)
1.4 Fuentes
Los detalles de la administración de memoria de R no se documentan en un solo lugar. Gran parte de la información en éste capítulo se obtuvo de una lectura de la documentación (en particular ?Memory y ?gc), la sección de perfiles de memoria de Escribiendo extensiones con R2 y la sección SEXPs de R internals3. El resto se obtuvo leyendo el código fuente de C, realizando pequeños experimentos y haciendo preguntas sobre el desarrollo de R.
2.2 Conceptos básicos en vínculos (binding)
Considera este código:
<- c(1, 2, 3) x
Es fácil leerlo como: “crear un objeto llamado ‘x’, que contenga los valores 1, 2 y 3”. Desgraciadamente ésa es una simplificación que conducirá a predicciones inexactas sobre lo que R realmente está haciendo detrás de escena. Es más exacto decir que este código está haciendo dos cosas:
- crear un objeto, un vector de valores, .c(1, 2, 3), y
- está vinculando ese objeto a un nombre, x.
En otras palabras, el objeto, o valor, no tiene un nombre; en realidad es el nombre el que tiene un valor.
Para aclarar aún más esta distinción, dibujaré diagramas como este:
El nombre, x, se dibuja con un rectángulo redondeado. Tiene una flecha que apunta (enlaza o hace referencia) al valor, el vector c(1, 2, 3). La flecha apunta en dirección opuesta a la asignación: <-, crea un enlace desde el nombre en el lado izquierdo hasta el objeto en el lado derecho.
Por lo tanto, puedes pensar en un nombre como una referencia a un valor. Por ejemplo, si ejecutas éste código, no obtienes otra copia del valor c(1, 2, 3), obtendrás otro enlace al objeto existente:
<- x y
Nota que el valor c(1, 2, 3) tiene una etiqueta: 0x74b. Mientras el vector no tenga un nombre, ocasionalmente necesitaremos referirnos a un objeto independiente de sus enlaces. Para que eso sea posible, etiquetaremos los valores con un identificador único. Éstos identificadores tienen una forma especial que se parece a la “dirección” de memoria del objeto, es decir, la ubicación en la memoria donde se almacena el objeto. Pero debido a que las direcciones de memoria reales cambian cada vez que se ejecuta el código, usamos estos identificadores en su lugar.
Puedes acceder al identificador de un objeto con \(lobstr::obj\_addr()\). Hacerlo le permite ver ambos, x e y y apuntar al mismo identificador:
obj_addr(x)
## [1] "0x20a2a780"
obj_addr(y)
## [1] "0x20a2a780"
Estos identificadores son largos y cambian cada vez que reinicia R.
Puede llevar algún tiempo entender la distinción entre nombres y valores, pero comprenderlo es realmente útil en la programación funcional donde las funciones pueden tener diferentes nombres en diferentes contextos.
1.4.1 Nombres no sintácticos
R tiene reglas estrictas sobre lo que constituye un nombre válido. Un nombre sintáctico debe consistir en letras, dígitos, puntos y _, pero no puede comenzar con _ ó un dígito. Además, no puede usar ninguna de las palabras reservadas como TRUE, NULL, if, y function, (consulta la lista completa en ?Reserved). Un nombre que no sigua estas reglas es un nombre no sintáctico; si intentas usarlos, obtendrás un error:
#_abc <- 1
#> Error: unexpected input in "_"
#if <- 10
#> Error: unexpected assignment in "if <-"
Es posible anular estas reglas y usar cualquier nombre, es decir, cualquier secuencia de caracteres, rodeándola de marcas de retroceso (backticks):
`_abc` <- 1
`_abc`
## [1] 1
#> [1] 1
`if` <- 10
`if`
## [1] 10
#> [1] 10
Si bien es poco probable que cree deliberadamente nombres tan locos, debes comprender cómo funcionan estos nombres locos porque los encontrarás, más comúnmente cuando cargues datos que se han creado fuera de R.
También puedes crear enlaces no sintácticos utilizando comillas simples o dobles (por ejemplo, “_abc” <- 1) en lugar de backticks, pero no deberías, porque tendrás que usar una sintaxis diferente para recuperar los valores. La capacidad de usar cadenas en el lado izquierdo de la flecha de asignación es un artefacto histórico, utilizado antes de las marcas de retroceso compatibles con R.
1.4.2 Ejercicios
- Explique la relación entre , y en el código siguiente:abcd
<- 1:10
a <- a
b <- b
c <- 1:10 d
- El código siguiente accede a la función media de varias maneras. ¿Todos apuntan al mismo objeto de función subyacente? Verifique esto con .lobstr::obj_addr()
mean
## function (x, ...)
## UseMethod("mean")
## <bytecode: 0x000000001525adf8>
## <environment: namespace:base>
::mean base
## function (x, ...)
## UseMethod("mean")
## <bytecode: 0x000000001525adf8>
## <environment: namespace:base>
get("mean")
## function (x, ...)
## UseMethod("mean")
## <bytecode: 0x000000001525adf8>
## <environment: namespace:base>
evalq(mean)
## function (x, ...)
## UseMethod("mean")
## <bytecode: 0x000000001525adf8>
## <environment: namespace:base>
match.fun("mean")
## function (x, ...)
## UseMethod("mean")
## <bytecode: 0x000000001525adf8>
## <environment: namespace:base>
De forma predeterminada, las funciones de importación de datos de R base, como , convertirán automáticamente los nombres no sintácticos en sintácticos. ¿Por qué podría ser esto problemático? ¿Qué opción le permite suprimir este comportamiento?read.csv()
¿Qué reglas se utilizan para convertir nombres no sintácticos en sintácticos?make.names()
Simplifiqué ligeramente las reglas que rigen los nombres sintácticos. ¿Por qué no es un nombre sintáctico? Lea para obtener todos los detalles..123e1?make.names
2.3 Copiar al modificar (Copy-on-modify)
Considere el siguiente código. Enlaza y y x al mismo valor subyacente, luego modifica y.
<- c(1, 2, 3)
x <- x
y
3]] <- 4
y[[ x
## [1] 1 2 3
#> [1] 1 2 3
La modificación en y claramente no modificó x. Entonces, ¿qué pasó con la vinculación compartida? Si bien el valor asociado con y cambió, el objeto original no lo hizo. En su lugar, R creó un nuevo objeto, 0xcd2, una copia de 0x74b con un valor cambiado, luego rebotó y en ese objeto.
Este comportamiento se denomina copy-on-modify.Entenderlo mejorará radicalmente su intuición sobre el rendimiento del código R. Una forma relacionada de describir este comportamiento es decir que los objetos R no se pueden cambiar o bien, son inmutables. Sin embargo, generalmente evitaremos ese término porque hay un par de excepciones importantes para copiar y modificar que aprenderás en la Sección 2.5.
Cuando explores copy-on-modify de forma interactiva, ten en cuenta que obtendrás diferentes resultados dentro de RStudio. Esto se debe a que el panel de entorno debe hacer una referencia a cada objeto para mostrar información al respecto. Esto distorsiona la exploración interactiva, pero no afecta al código dentro de las funciones, por lo que no afecta al rendimiento durante el análisis de datos. Para la experimentación, recomiendo ejecutar R directamente desde el terminal o usar RMarkdown.
1.4.3 \(tracemem()\)
Puedes ver cuándo se copia un objeto con la ayuda de \(base::tracemem()\). Una vez que llamas a esa función con un objeto, obtendrás la dirección actual del objeto:
<- c(1, 2, 3)
x cat(tracemem(x), "\n")
## <000000001F8C5D48>
A partir de entonces, cada vez que se copie ese objeto, \(tracemem()\) imprimirá un mensaje que te indicará qué objeto se copió, su nueva dirección y la secuencia de llamadas que llevaron a la copia:
<- x
y 3]] <- 4L y[[
## tracemem[0x000000001f8c5d48 -> 0x0000000026b60e10]: eval eval withVisible withCallingHandlers handle timing_fn evaluate_call <Anonymous> evaluate in_dir eng_r block_exec call_block process_group.block process_group withCallingHandlers process_file <Anonymous> <Anonymous>
Si modificas y de nuevo, no se copiará. Esto se debe a que el nuevo objeto ahora sólo tiene un solo nombre vinculado, por lo que R aplica una optimización modify-in-place. Volveremos a esto en la Sección 2.5.
3]] <- 5L
y[[untracemem(x)
untracemem() es lo opuesto a tracemem(); desactiva el rastreo.
1.4.4 Llamadas a funciones
Las mismas reglas para copiar también se aplican a las llamadas a funciones. Tome este código:
<- function(a) {
f
a
}
<- c(1, 2, 3)
x cat(tracemem(x), "\n")
## <000000001E728078>
<- f(x)
z # no hay copia aquí!
untracemem(x)
Mientras f() se está ejecutando, a, al interior de la función apunta al mismo valor que el que hace x fuera de la función:
Obtendrás más información sobre las convenciones utilizadas en este diagrama en la Sección 7.4.4. En resumen: la función está representada por el objeto amarillo de la derecha. Tiene un argumento formal, a, que se convierte en un enlace (indicado por una línea negra punteada) en el entorno de ejecución (el cuadro gris) cuando se ejecuta la función.
Una vez completado \(f()\), x e y apuntará al mismo objeto. 0x74b nunca se copia porque nunca se modifica. Si f() modificara x, R crearía una nueva copia, y a continuación, z se enlazaría a ese objeto.
1.4.5 Listas
No son sólo nombres (por ejemplo, las variables) los que apuntan a los valores; los elementos de las listas también lo hacen. Considere la siguiente lista, que es superficialmente muy similar al vector numérico anterior:
<- list(1, 2, 3) l1
Esta lista es más compleja porque en lugar de almacenar los valores en sí, almacena referencias a ellos:
Esto es particularmente importante cuando modificamos una lista:
<- l1 l2
3]] <- 4 l2[[
Al igual que los vectores, las listas utilizan un comportamiento copy-on-modify; la lista original se deja sin cambios y R crea una copia modificada. Esto, sin embargo, es una copia superficial: el objeto lista y sus enlaces se copian, pero los valores señalados por los enlaces no. Lo opuesto a una copia superficial es una copia profunda donde se copia el contenido de cada referencia. Antes de R 3.1.0, las copias siempre eran copias profundas.
Para ver los valores que se comparten entre listas, utiliza lobstr::ref(). ref() imprime la dirección de memoria de cada objeto, junto con un identificador local ID para que puedas hacer fácilmente referencias cruzadas a los componentes compartidos.
ref(l1, l2)
## o [1:0x1e84af30] <list>
## +-[2:0x1c9c4c18] <dbl>
## +-[3:0x1c9c4cf8] <dbl>
## \-[4:0x1c9c4d30] <dbl>
##
## o [5:0x20d9fab0] <list>
## +-[2:0x1c9c4c18]
## +-[3:0x1c9c4cf8]
## \-[6:0x1e7a5c40] <dbl>
1.4.6 Marcos de datos (Data frames)
Los Data frames son listas de vectores, por lo que copy-on-modify (copiar al modificar) tiene consecuencias importantes al modificar un data frame. Toma éste data frame como ejemplo:
<- data.frame(x = c(1, 5, 6), y = c(2, 4, 3)) d1
Si modificas una columna, sólo es necesario modificar esa columna; las otras seguirán apuntando a sus referencias originales:
<- d1
d2 2] <- d2[, 2] * 2 d2[,
Sin embargo, si modificas una fila, se modifica cada columna, lo que significa que cada columna debe copiarse:
<- d1
d3 1, ] <- d3[1, ] * 3 d3[
1.4.7 Vectores de caracteres
El último lugar donde R usa referencias es con vectores de caracteres. Se suelen dibujar vectores de caracteres de ésta forma:
<- c("a", "a", "abc", "d") x
Pero eso es lo políticamente corecto. R en realidad utiliza un grupo de cadenas global donde cada elemento de un vector de caracteres es un puntero a una cadena única en el grupo:
Puedes solicitar que ref() muestre estas referencias estableciendo el argumento character a TRUE:
ref(x, character = TRUE)
## o [1:0x1f7c79f8] <chr>
## +-[2:0x12ef9c78] <string: "a">
## +-[2:0x12ef9c78]
## +-[3:0x1f981a48] <string: "abc">
## \-[4:0x130f1bf0] <string: "d">
Esto tiene un profundo impacto en la cantidad de memoria que usa un vector de caracteres, pero por lo demás generalmente no es importante, por lo que en otra parte del trabajo se construirán vectores de caracteres como si las cadenas vivieran dentro de un vector.
1.4.8 Ejercicios
- ¿Por qué \(tracemem(1:10)\) no es útil?
tracemem(1:10)
## [1] "<000000001EF16DC0>"
- Explique por qué \(tracemem()\) muestra dos copias al ejecutar este código. Sugerencia: observa cuidadosamente la diferencia entre este código y el código mostrado anteriormente en la sección.
<- c(1L, 2L, 3L)
x tracemem(x)
## [1] "<0000000025717E80>"
3]] <- 4 x[[
## tracemem[0x0000000025717e80 -> 0x000000002571c498]: eval eval withVisible withCallingHandlers handle timing_fn evaluate_call <Anonymous> evaluate in_dir eng_r block_exec call_block process_group.block process_group withCallingHandlers process_file <Anonymous> <Anonymous>
## tracemem[0x000000002571c498 -> 0x000000001f80b938]: eval eval withVisible withCallingHandlers handle timing_fn evaluate_call <Anonymous> evaluate in_dir eng_r block_exec call_block process_group.block process_group withCallingHandlers process_file <Anonymous> <Anonymous>
- Esboza la relación entre los siguientes objetos:
<- 1:10
a <- list(a, a)
b <- list(b, a, 1:10) c
- ¿Qué sucede cuando se ejecuta este código?
<- list(1:10)
x 2]] <- x x[[
Haz un dibujo.
2.4 Tamaño de un objeto
Puedes averiguar cuánta memoria toma un objeto con \(lobstr::obj\_size()\):
obj_size(letters)
## 1,712 B
obj_size(ggplot2::diamonds)
## 3,456,344 B
Dado que los elementos de las listas son referencias a valores, el tamaño de una lista puede ser mucho menor de lo que se espera:
<- runif(1e6)
x obj_size(x)
## 8,000,048 B
<- list(x, x, x)
y obj_size(y)
## 8,000,128 B
y es sólo 80 bytes más grande que x. Ese es el tamaño de una lista vacía con tres elementos:
obj_size(list(NULL, NULL, NULL))
## 80 B
Del mismo modo, debido a que R utiliza un grupo de strings global, los vectores de caracteres ocupan menos memoria de la que cabría esperar. Repetir una cadena 100 veces no hace que ocupe 100 veces más memoria.
<- "bananas bananas bananas"
banana obj_size(banana)
## 136 B
obj_size(rep(banana, 100))
## 928 B
Las referencias también hacen que sea difícil pensar en el tamaño de los objetos individuales. \(obj_size(x) + obj_size(y)\) sólo será igual a \(obj_size(x, y)\) si no hay valores compartidos. Aquí, el tamaño combinado de x e y es del mismo que el tamaño de y.
obj_size(x, y)
## 8,000,128 B
Finalmente, R 3.5.0 y versiones posteriores tienen una característica que podría llevar a sorpresas: ALTREP, abreviatura de “representación alternativa”. Esto permite a R representar ciertos tipos de vectores de forma muy compacta. El lugar donde es más probable que veas esto es con \(:\), porque en lugar de almacenar cada número en la secuencia, R solo almacena el primer y último número. Esto significa que cada secuencia, no importa cuán grande sea, tiene el mismo tamaño::
obj_size(1:3)
## 680 B
obj_size(1:1e3)
## 680 B
obj_size(1:1e6)
## 680 B
obj_size(1:1e9)
## 680 B
1.4.9 Ejercicios
- En el siguiente ejemplo, ¿por qué son object.size(y) y obj_size(y) son tan radicalmente diferentes? Consulta la documentación de object.size().
<- rep(list(runif(1e4)), 100)
y object.size(y)
## 8005648 bytes
obj_size(y)
## 80,896 B
- Toma la siguiente lista. ¿Por qué su tamaño es algo engañoso?
<- list(mean, sd, var)
funs obj_size(funs)
## 17,608 B
- Predice el resultado del siguiente código:
<- runif(1e6)
a obj_size(a)
## 8,000,048 B
<- list(a, a)
b obj_size(b)
## 8,000,112 B
obj_size(a, b)
## 8,000,112 B
1]][[1]] <- 10
b[[obj_size(b)
## 16,000,160 B
obj_size(a, b)
## 16,000,160 B
2]][[1]] <- 10
b[[obj_size(b)
## 16,000,160 B
obj_size(a, b)
## 24,000,208 B
2.5 Modificación in situ (Modify-in-place)
Como hemos visto anteriormente, la modificación de un objeto R generalmente crea una copia. Hay dos excepciones:
Los objetos con un solo enlace obtienen una optimización especial del rendimiento.
Los entornos, un tipo especial de objeto, siempre se modifican en in situ.
2.5.1 Objetos con un solo enlace
Si un objeto tiene un solo nombre enlazado a él, R lo modificará in situ:
<- c(1, 2, 3) v
3]] <- 4 v[[
(Ten en cuenta los identificadores de objeto aquí: continúa enlazando al mismo objeto, 0x207)
Dos complicaciones hacen que predecir exactamente cuándo R aplica esta optimización sea un desafío:
Cuando se trata de enlaces, R puede actualmente solo cuenta 0, 1 o muchos. Eso significa que si un objeto tiene dos enlaces, y uno desaparece, el recuento de referencias no vuelve a 1: uno menos que muchos sigue siendo muchos. A su vez, esto significa que R hará copias cuando a veces no lo necesite.
Cada vez que llamas a la gran mayoría de las funciones, haces referencia al objeto. La única excepción son las funciones C “primitivas” especialmente escritas. Estos solo pueden ser escritas por el R-core y ocurren principalmente en el paquete base.
Juntas, estas dos complicaciones hacen que sea difícil predecir si se producirá o no una copia. En cambio, es mejor determinarlo empíricamente con \(tracemem()\).
Exploremos las sutilezas con un estudio de caso usando bucles. Los bucles tienen la reputación de ser lentos en R, pero a menudo esa lentitud es causada por cada iteración del bucle que creando una copia. Considere el siguiente código. Resta la mediana de cada columna de un data frame grande:
<- data.frame(matrix(runif(5 * 1e4), ncol = 5))
x <- vapply(x, median, numeric(1))
medians
for (i in seq_along(medians)) {
<- x[[i]] - medians[[i]]
x[[i]] }
Este bucle es sorprendentemente lento porque cada iteración del bucle copia el marco de datos. Puede ver esto usando: \(tracemem()\)
cat(tracemem(x), "\n")
## <000000001E69C8C8>
for (i in 1:5) {
<- x[[i]] - medians[[i]]
x[[i]] }
## tracemem[0x000000001e69c8c8 -> 0x0000000025774f10]: eval eval withVisible withCallingHandlers handle timing_fn evaluate_call <Anonymous> evaluate in_dir eng_r block_exec call_block process_group.block process_group withCallingHandlers process_file <Anonymous> <Anonymous>
## tracemem[0x0000000025774f10 -> 0x0000000025774ea0]: [[<-.data.frame [[<- eval eval withVisible withCallingHandlers handle timing_fn evaluate_call <Anonymous> evaluate in_dir eng_r block_exec call_block process_group.block process_group withCallingHandlers process_file <Anonymous> <Anonymous>
## tracemem[0x0000000025774ea0 -> 0x0000000025774e30]: eval eval withVisible withCallingHandlers handle timing_fn evaluate_call <Anonymous> evaluate in_dir eng_r block_exec call_block process_group.block process_group withCallingHandlers process_file <Anonymous> <Anonymous>
## tracemem[0x0000000025774e30 -> 0x000000002578a5f0]: [[<-.data.frame [[<- eval eval withVisible withCallingHandlers handle timing_fn evaluate_call <Anonymous> evaluate in_dir eng_r block_exec call_block process_group.block process_group withCallingHandlers process_file <Anonymous> <Anonymous>
## tracemem[0x000000002578a5f0 -> 0x000000002578a510]: eval eval withVisible withCallingHandlers handle timing_fn evaluate_call <Anonymous> evaluate in_dir eng_r block_exec call_block process_group.block process_group withCallingHandlers process_file <Anonymous> <Anonymous>
## tracemem[0x000000002578a510 -> 0x000000002578a430]: [[<-.data.frame [[<- eval eval withVisible withCallingHandlers handle timing_fn evaluate_call <Anonymous> evaluate in_dir eng_r block_exec call_block process_group.block process_group withCallingHandlers process_file <Anonymous> <Anonymous>
## tracemem[0x000000002578a430 -> 0x000000002578a350]: eval eval withVisible withCallingHandlers handle timing_fn evaluate_call <Anonymous> evaluate in_dir eng_r block_exec call_block process_group.block process_group withCallingHandlers process_file <Anonymous> <Anonymous>
## tracemem[0x000000002578a350 -> 0x000000002578a270]: [[<-.data.frame [[<- eval eval withVisible withCallingHandlers handle timing_fn evaluate_call <Anonymous> evaluate in_dir eng_r block_exec call_block process_group.block process_group withCallingHandlers process_file <Anonymous> <Anonymous>
## tracemem[0x000000002578a270 -> 0x000000002578a040]: eval eval withVisible withCallingHandlers handle timing_fn evaluate_call <Anonymous> evaluate in_dir eng_r block_exec call_block process_group.block process_group withCallingHandlers process_file <Anonymous> <Anonymous>
## tracemem[0x000000002578a040 -> 0x0000000025789e80]: [[<-.data.frame [[<- eval eval withVisible withCallingHandlers handle timing_fn evaluate_call <Anonymous> evaluate in_dir eng_r block_exec call_block process_group.block process_group withCallingHandlers process_file <Anonymous> <Anonymous>
untracemem(x)
De hecho, cada iteración copia el data.frame no una, ni dos veces, ¡sino tres veces! Dos copias son hechas por \([[.data.frame\), y una copia adicional se realiza porque \([[.data.frame\) es una función regular que incrementa el recuento de referencias de x.
Podemos reducir el número de copias utilizando una lista en lugar de un data.frame. La modificación de una lista utiliza código C interno, por lo que las referencias no se incrementan y sólo se realiza una copia:
<- as.list(x)
y cat(tracemem(y), "\n")
## <000000002589F5F8>
for (i in 1:5) {
<- y[[i]] - medians[[i]]
y[[i]] }
## tracemem[0x000000002589f5f8 -> 0x000000001f787648]: eval eval withVisible withCallingHandlers handle timing_fn evaluate_call <Anonymous> evaluate in_dir eng_r block_exec call_block process_group.block process_group withCallingHandlers process_file <Anonymous> <Anonymous>
Si bien no es difícil determinar cuándo se hace una copia, es difícil prevenirla. Si recurres a trucos exóticos para evitar copias, puede ser el momento de reescribir tu función en C++, como se describe en el Capítulo 25.
1.4.10 Entornos (Environments)
Aprenderás más sobre los entornos en el Capítulo 7, pero es importante mencionarlos aquí porque su comportamiento es diferente al de otros objetos: los entornos siempre se modifican en su lugar (modified in place). Esta propiedad a veces se describe como referencia semántica porque cuando se modifica un entorno, todos los enlaces existentes a ese entorno siguen teniendo la misma referencia.
Tomemos este entorno, al que unimos: e1 y e2
<- rlang::env(a = 1, b = 2, c = 3)
e1 <- e1 e2
Si cambiamos un enlace, el entorno se modifica en su lugar:
$c <- 4
e1$c e2
## [1] 4
Esta idea básica se puede utilizar para crear funciones que “recuerden” su estado anterior. Consulta la Sección 10.2.4 para obtener más detalles. Esta propiedad también se utiliza para implementar el sistema de programación orientada a objetos R6, el tema del Capítulo 14.
Una consecuencia de esto es que los entornos pueden contenerse a sí mismos:
<- rlang::env()
e $self <- e
e
ref(e)
## o [1:0x1faca320] <env>
## \-self = [1:0x1faca320]
¡Esta es una propiedad única de los entornos!
1.4.11 Ejercicios
- Explique por qué el código siguiente no crea una lista circular.
<- list()
x 1]] <- x x[[
Envuelva los dos métodos para restar medianas dentro de dos funciones, luego use el paquete ‘bench’ para comparar cuidadosamente sus velocidades. ¿Cómo cambia el rendimiento a medida que aumenta el número de columnas?
¿Qué sucede si intentas usar tracemem() sobre un entorno?
2.6 Desvinculación (Unbinding) y recolector de basura (garbage collector)
Considera este código:
<- 1:3 x
<- 2:4 x
rm(x)
Creamos dos objetos, pero cuando el código termina, ninguno de los objetos está enlazado a un nombre. ¿Cómo se eliminan estos objetos? Ese es el trabajo del recolector de basura, o GC para abreviar. El GC libera memoria eliminando objetos R que ya no se utilizan y solicitando más memoria al sistema operativo si es necesario.
R utiliza un rastreo de GC. Esto significa que rastrea cada objeto al que se puede llegar desde el ambiente global, y todos los objetos que son, a su vez, accesibles desde esos objetos (es decir, las referencias en listas y entornos se buscan recursivamente). El recolector de basura no utiliza el recuento de referencias modify-in-place descrito anteriormente. Si bien estas dos ideas están estrechamente relacionadas, las estructuras de datos internas están optimizadas para diferentes casos de uso.
El recolector de basura (GC) se ejecuta automáticamente cada vez que R necesita más memoria para crear un nuevo objeto. Mirando desde afuera, es básicamente imposible predecir cuándo se ejecutará el GC. De hecho, ni siquiera deberías intentarlo. Si deseas saber cuándo se ejecuta el GC, llama a gcinfo(TRUE) y GC imprimirá un mensaje en la consola cada vez que se ejecute.
Puedes forzar la recolección de basura llamando a gc(). Pero a pesar de lo que podrías haber leído en otro lugar, nunca hay necesidad de llamar a gc() por ti mismo. Las únicas razones por las que es posible que desees llamar a gc() es para pedirle a R que devuelva memoria a tu sistema operativo para que otros programas puedan usarla, o para el efecto secundario que te indica cuánta memoria se está utilizando actualmente:
gc()
## used (Mb) gc trigger (Mb) max used (Mb)
## Ncells 1136782 60.8 1993842 106.5 1993842 106.5
## Vcells 5392542 41.2 10146329 77.5 7554365 57.7
lobstr::mem_used() es un contenedor alrededor de gc() que imprime el número total de bytes utilizados:
mem_used()
## 106,797,264 B
Este número no estará de acuerdo con la cantidad de memoria reportada por su sistema operativo. Hay tres razones para ello:
Incluye objetos creados por R pero no por el intérprete de R.
Tanto R como el sistema operativo son perezosos: no recuperarán memoria hasta que sea realmente necesario. R podría estar reteniendo la memoria porque el sistema operativo aún no lo ha solicitado de vuelta.
R cuenta la memoria ocupada por los objetos, pero pueden haber espacios vacíos debido a los objetos eliminados. Este problema se conoce como fragmentación de la memoria.
2.7 Respuestas al cuestionario
- Debes citar nombres no sintácticos con backticks: \(`\) : por ejemplo, las variables 1, 2, y 3.
<- data.frame(runif(3), runif(3))
df names(df) <- c(1, 2)
$`3` <- df$`1` + df$`2` df
2 Ocupa unos 8 MB.
<- runif(1e6)
x <- list(x, x, x)
y obj_size(y)
## 8,000,128 B
- a se copia cuando b es modificado, b[[1]] <- 10
2 Vectores
2.1 Introducción
En este capítulo se analiza la familia más importante de tipos de datos en la base R: los vectores. Si bien es probable que ya hayas utilizado muchos (si no todos) los diferentes tipos de vectores, es posible que no hayas pensado profundamente en cómo están interrelacionados. En este capítulo, no se cubrirán los tipos de vectores individuales con demasiado detalle, pero se mostrará cómo encajan todos los tipos como un todo.
Los vectores vienen en dos tipos: vectores atómicos y listas. Difieren en términos de los tipos de sus elementos: para los vectores atómicos, todos los elementos deben tener el mismo tipo; para las listas, los elementos pueden tener diferentes tipos. Si bien no es un vector, NULL está estrechamente relacionado con los vectores y a menudo cumple el papel de un vector genérico de longitud cero. Este diagrama, que ampliaremos a lo largo de este capítulo, ilustra las relaciones básicas:
Cada vector también puede tener atributos, que se pueden considerar como una lista con nombres de metadatos arbitrarios. Dos atributos son particularmente importantes:
El atributo dimension convierte los vectores en matrices y arrays y el atributo class potencia el sistema de objetos S3. Si bien aprenderás a usar S3 en el Capítulo 13, aquí aprenderás sobre algunos de los vectores S3 más importantes: factores, fechas y horas, data frames y tibbles. Y aunque las estructuras 2D como matrices y data frames no son necesariamente lo que te viene a la mente cuando piensas en vectores, también aprenderás por qué R los considera vectores.
Examen
¿Cuáles son los cuatro tipos comunes de vectores atómicos? ¿Cuáles son los dos tipos raros?
¿Qué son los atributos? ¿Cómo los consigues y los configuras?
¿En qué se diferencia una lista de un vector atómico? ¿En qué se diferencia una matriz de un data frame?
¿Se puede tener una lista que sea una matriz? ¿Puede un data frame tener una columna que sea una matriz?
¿Cómo se comportan los tibbles de manera diferente a los data frames?
Resumen del capítulo
3.2 te presenta los vectores atómicos: lógicos, enteros, dobles y de carácteres. Éstas son las estructuras de datos más simples de R.
3.3 toma un pequeño desvío para discutir los atributos, R es flexiblel a la especificación de metadatos. Los atributos más importantes son los nombres, las dimensiones y las clases.
3.4 discute la importancia de los tipos de vectores que se construyen combinando vectores atómicos con atributos especiales. Éstos incluyen factores, fechas, fechas-horas y duraciones.
3.5 se sumerge en las listas. Las listas son muy similares a los vectores atómicos, pero tienen una diferencia clave: un elemento de una lista puede ser cualquier tipo de datos, incluida otra lista. Esto los hace adecuados para representar datos jerárquicos.
3.6 enseña acerca de los data frames y tibbles, que se utilizan para representar datos rectangulares. Combinan el comportamiento de listas y matrices para crear una estructura ideal para las necesidades de datos estadísticos.
2.2 Vectores atómicos
Hay cuatro tipos principales de vectores atómicos: lógicos, enteros, double y character (los cuales contienen cadenas). Colectivamente, los vectores enteros y dobles se conocen como vectores numéricos. Hay dos tipos raros: complejos y raw. No se discutirán porque los números complejos rara vez se necesitan en estadística, y los vectores sin procesar son un tipo especial que solo se necesita cuando se manejan datos binarios.
2.2.1 Escalares
Cada uno de los cuatro tipos principales tiene una sintaxis especial para crear un valor individual, también conocido como escalar:
Los lógicos se pueden escribir en su totalidad (TRUE or FALSE), o abreviados (T or F).
Los doubles se pueden especificar en forma decimal (0.1234), científica (1.23e4) o hexadecimal (0xcafe). Hay tres valores especiales únicos para los doubles: Inf, -Inf, y NaN (no un número). Estos son valores especiales definidos por la coma flotante estándar.
Los enteros se escriben de manera similar a los doubles, pero deben ir seguidos de L (1234L, 1e4L, o 0xcafeL), y no puede contener valores fraccionarios.
Los Strings están rodeadas por ” (“hi”) or ’ (‘bye’). Los caracteres especiales se escapan con ; ve más detalles con ?Quotes
2.2.2 Creación de vectores más largos con c()
Para crear vectores más largos a partir de vectores más cortos, use c(), abreviatura de combine: c()
<- c(TRUE, FALSE)
lgl_var <- c(1L, 6L, 10L)
int_var <- c(1, 2.5, 4.5)
dbl_var <- c("these are", "some strings") chr_var
Cuando las entradas son vectores atómicos, c() siempre crea otro vector atómico; es decir, aplana (it flattens):
c(c(1, 2), c(3, 4))
## [1] 1 2 3 4
En los diagramas, los vectores serán representados como rectángulos conectados, por lo que el código anterior podría dibujarse de la siguiente manera:
Puedes determinar el tipo de vector con typeof() y su longitud con length()
typeof(lgl_var)
## [1] "logical"
typeof(int_var)
## [1] "integer"
typeof(dbl_var)
## [1] "double"
typeof(chr_var)
## [1] "character"
2.2.3 Valores faltantes
R representa valores faltantes o desconocidos, con un valor centinela especial: NA (abreviatura de no aplicable). Los valores faltantes tienden a ser infecciosos: la mayoría de los cálculos que involucran un valor faltante devolverán otro valor faltante.
NA > 5
## [1] NA
10 * NA
## [1] NA
!NA
## [1] NA
Sólo hay unas pocas excepciones a esta regla. Éstas ocurren cuando alguna identidad se mantiene para todas las entradas posibles:
NA ^ 0
## [1] 1
NA | TRUE
## [1] TRUE
NA & FALSE
## [1] FALSE
La propagación de los valores faltantes conduce a un error común al determinar qué valores en un vector faltan:
<- c(NA, 5, NA, 10)
x == NA x
## [1] NA NA NA NA
Este resultado es correcto (aunque un poco sorprendente) porque no hay razón para creer que un valor faltante tiene el mismo valor que otro. En su lugar, use is.na() para probar la presencia de valores faltantes:
is.na(x)
## [1] TRUE FALSE TRUE FALSE
Nota: Técnicamente faltan cuatro valores, uno para cada uno de los tipos atómicos:
NA (logical), NA_integer_ (integer), NA_real_ (double), y NA_character_ (character)
Esta distinción generalmente no es importante porque NA será automáticamente coaccionado al tipo correcto cuando sea necesario.
2.2.4 Pruebas y el concepto de coacción
En general, puede probar si un vector es de un tipo dado con la función is.*(), pero estas funciones deben usarse con cuidado.
is.logical(), is.integer(), is.double(), and is.character() hacen lo que cabría esperar: prueban si un vector es un carácter, double, entero o lógico.
Evite is.vector(), is.atomic(), y is.numeric(): no prueban si tienes un vector, un vector atómico o un vector numérico; deberás leer cuidadosamente la documentación para averiguar qué hacen realmente.
Para los vectores atómicos, el tipo es una propiedad de todo el vector: todos los elementos deben ser del mismo tipo. Cuando intentes combinar diferentes tipos, se coercionarán en un orden fijo: carácter → double → entero → lógico. Por ejemplo, al combinar un carácter y un entero se obtiene un carácter:
str(c("a", 1))
## chr [1:2] "a" "1"
La coerción a menudo ocurre automáticamente. La mayoría de las funciones matemáticas (+, log, abs, etc.) se se coercionarán a numéricos. Esta coerción es particularmente útil para vectores lógicos porque TRUE se convierte en 1 y FALSE se convierte en 0.
<- c(FALSE, FALSE, TRUE)
x as.numeric(x)
## [1] 0 0 1
# El numero total de TRUEs
sum(x)
## [1] 1
# Proporción que son TRUE
mean(x)
## [1] 0.3333333
Generalmente, puedes coaccionar deliberadamente mediante el uso de una función como as.*(), como as.logical(), as.integer(), as.double(), or as.character()
La coerción fallida de las cadenas genera una advertencia y un valor faltante:
as.integer(c("1", "1.5", "a"))
## Warning: NAs introducidos por coerción
## [1] 1 1 NA
2.2.5 Ejercicios
¿Cómo se crean escalares crudos y complejos? (Ver ?raw y ?complex.)
Pruebe su conocimiento de las reglas de coerción vectorial prediciendo la salida de los siguientes usos de:c()
c(1, FALSE)
## [1] 1 0
c("a", 1)
## [1] "a" "1"
c(TRUE, 1L)
## [1] 1 1
¿Por qué 1 == “1” es cierto?, ¿Por qué -1 < FALSE es cierto?, ¿Por qué “one” < 2 es falso?
¿Por qué el valor predeterminado que falta, NA, es un vector lógico? ¿Qué tienen de especial los vectores lógicos? (Sugerencia: piense en c(FALSE, NA_character_)
¿Exactamente qué prueban, is.atomic(), is.numeric() y is.vector()
2.3 Atributos
Es posible que hayas notado que el conjunto de vectores atómicos no incluyen una serie de estructuras de datos importantes como matrices, arrays, factores o datos de fecha-hora. Estos tipos se construyen sobre vectores atómicos mediante la adición de atributos. En esta sección, aprenderás los conceptos básicos de los atributos y cómo el atributo dim crea matrices y matrices. En la siguiente sección aprenderás cómo se usa el atributo de clase para crear vectores S3, incluidos factores, fechas y fecha-horas.
2.3.1 Getting y setting
Puedes pensar en los atributos como pares nombre-valor que adjuntan metadatos a un objeto. Los atributos individuales se pueden recuperar y modificar con attr(), o recuperarse en masa con attributes(), y establecerse en masa con structure()
<- 1:3
a attr(a, "x") <- "abcdef"
attr(a, "x")
## [1] "abcdef"
#> [1] "abcdef"
attr(a, "y") <- 4:6
str(attributes(a))
## List of 2
## $ x: chr "abcdef"
## $ y: int [1:3] 4 5 6
# O equivalentemente:
<- structure(
a 1:3,
x = "abcdef",
y = 4:6
)str(attributes(a))
## List of 2
## $ x: chr "abcdef"
## $ y: int [1:3] 4 5 6
Los atributos generalmente deben considerarse efímeros. Por ejemplo, la mayoría de los atributos se pierden en la mayoría de las operaciones:
attributes(a[1])
## NULL
attributes(sum(a))
## NULL
Solo existen dos atributos que se conservan de forma rutinaria:
names, un vector de caracteres que da un nombre a cada elemento.
dim, abreviatura de dimensiones, un vector entero, utilizado para convertir vectores en matrices o arrays.
Para conservar otros atributos, deberás crear tu propia clase S3, el tema del Capítulo 13.
2.3.2 Nombres
Puedes nombrar un vector de tres maneras:
# Al crearlo:
<- c(a = 1, b = 2, c = 3)
x x
## a b c
## 1 2 3
# Asignando un vector de caracteres a names()
<- 1:3
x names(x) <- c("a", "b", "c")
x
## a b c
## 1 2 3
# En línea, con setNames():
<- setNames(1:3, c("a", "b", "c"))
x x
## a b c
## 1 2 3
Evita usar attr(x, “names”), ya que requiere más escritura y es menos legible que names(x). Puedes quitar nombres de un vector mediante x <- unname(x) o names(x) <- NULL.
Para ser técnicamente correcto, al dibujar el vector nombrado, debería ser dibujado así:
Sin embargo, los nombres son tan especiales y tan importantes, que a menos que estés tratando específicamente de llamar la atención sobre la estructura de datos de atributos, los usaremos para etiquetar el vector directamente:
Para ser útil con la subconfiguración de caracteres, los nombres deben ser únicos y no faltar, pero R no fuerza a ello. Dependiendo de cómo se establezcan los nombres, los nombres que faltan pueden ser “” o NA_character_.
Si faltan todos los nombres, names() devolverá NULL.
2.3.3 Dimensiones
Agregar un atributo dim a un vector le permite comportarse como una matriz de 2 dimensiones o un arreglo multidimensional. Las matrices y arreglos son principalmente herramientas matemáticas y estadísticas, no herramientas de programación, por lo que se usarán con poca frecuencia y sólo se cubrirán brevemente en éste texto. Su característica más importante es la subseteo multidimensional, que se trata en la Sección 4.2.3.
Puedes crear matrices y arreglos con matrix() y array(), o mediante el formulario de asignación de dim():
# Dos argumentos escalares especifican tamaños de fila y columna
<- matrix(1:6, nrow = 2, ncol = 3)
x x
## [,1] [,2] [,3]
## [1,] 1 3 5
## [2,] 2 4 6
# Un argumento vectorial para describir todas las dimensiones
<- array(1:12, c(2, 3, 2))
y y
## , , 1
##
## [,1] [,2] [,3]
## [1,] 1 3 5
## [2,] 2 4 6
##
## , , 2
##
## [,1] [,2] [,3]
## [1,] 7 9 11
## [2,] 8 10 12
# También puede modificar un objeto en su lugar configurando dim()
<- 1:6
z dim(z) <- c(3, 2)
z
## [,1] [,2]
## [1,] 1 4
## [2,] 2 5
## [3,] 3 6
Muchas de las funciones para trabajar con vectores tienen generalizaciones para matrices y arreglos:
Vector Matriz Arreglo names() rownames(), colnames() dimnames() length() nrow(), ncol() dim() c() rbind(), cbind() abind::abind() — t() aperm() is.null(dim(x)) is.matrix() is.array()
Vector | Matriz | Arreglo | ||
---|---|---|---|---|
names() | rownames(), colnames() | dimnames() | ||
length() | nrow(), ncol() | dim() | ||
c() | rbind(), cbind() | abind::abind() | ||
- | t() | aperm() | ||
is.null(dim(x)) | is.matrix() | is.array() |
Un vector sin un conjunto de atributos dim a menudo se considera como 1-dimensional, pero en realidad tiene NULL dimensiones. También puedes tener matrices con una sola fila o una sola columna, o arreglos con una sola dimensión. Pueden ser impresos de manera similar, pero se comportarán de manera diferente. Las diferencias no son demasiado importantes, pero es útil saber que existen en caso de que obtengas una salida extraña de una función (tapply() es un transgresor frecuente). Como siempre, utiliza str() para revelar las diferencias.
# 1d vectorial
str(1:3)
## int [1:3] 1 2 3
# vector columna
str(matrix(1:3, ncol = 1))
## int [1:3, 1] 1 2 3
# vector fila
str(matrix(1:3, nrow = 1))
## int [1, 1:3] 1 2 3
# vector de "matriz"
str(array(1:3, 3))
## int [1:3(1d)] 1 2 3
2.3.4 Ejercicios
¿Cómo se implementa setNames()? ¿Cómo se implementa unname()? Lea el código fuente.
¿Qué devuelve dim() cuando se aplica a un vector de 1 dimensión? ¿Cuándo podrías usar NROW() o NCOL()?
¿Cómo describirías los siguientes tres objetos? ¿Qué los hace diferentes de 1:5?
<- array(1:5, c(1, 1, 5))
x1 <- array(1:5, c(1, 5, 1))
x2 <- array(1:5, c(5, 1, 1)) x3
x1
## , , 1
##
## [,1]
## [1,] 1
##
## , , 2
##
## [,1]
## [1,] 2
##
## , , 3
##
## [,1]
## [1,] 3
##
## , , 4
##
## [,1]
## [1,] 4
##
## , , 5
##
## [,1]
## [1,] 5
x2
## , , 1
##
## [,1] [,2] [,3] [,4] [,5]
## [1,] 1 2 3 4 5
x3
## , , 1
##
## [,1]
## [1,] 1
## [2,] 2
## [3,] 3
## [4,] 4
## [5,] 5
- Un borrador temprano utilizó este código para ilustrar: structure():
structure(1:5, comment = "my attribute")
## [1] 1 2 3 4 5
Pero cuando imprimes ese objeto no ves el atributo comment. ¿Por qué? ¿Falta el atributo o hay algo más especial al respecto? (Sugerencia: intente usar help).
2.4 Vectores atómicos S3
Uno de los atributos vectoriales más importantes es class, sobre el que subyace al sistema de objetos S3. Tener un atributo class convierte un objeto en un objeto S3, lo que significa que se comportará de manera diferente a un vector regular cuando se pasa a una función genérica.
Cada objeto S3 se construye sobre un tipo base y, a menudo, almacena información adicional en otros atributos. Aprenderás los detalles del sistema de objetos de S3 y cómo crear sus propias clases S3 en el Capítulo 13.
En esta sección, discutiremos cuatro vectores S3 importantes utilizados en la base R:
Datos categóricos, donde los valores provienen de un conjunto fijo de niveles registrados en vectores factoriales.
Fechas (con resolución de día), que se registran en vectores Date.
Fecha-tiempos (con resolución de segundo o subsegundo), que se almacenan en vectores POSIXct.
Duraciones, que se almacenan en vectores difftime.
2.4.1 Factores
Un factor es un vector que sólo puede contener valores predefinidos. Se utilizan para almacenar datos categóricos. Los factores se construyen sobre un vector entero con dos atributos: una clase, “factor”, que hace que se comporte de manera diferente a los vectores enteros regulares, y niveles, que define el conjunto de valores permitidos.
<- factor(c("a", "b", "b", "a"))
x x
## [1] a b b a
## Levels: a b
typeof(x)
## [1] "integer"
attributes(x)
## $levels
## [1] "a" "b"
##
## $class
## [1] "factor"
Los factores son útiles cuando conoce el conjunto de valores posibles, pero no todos están presentes en un conjunto de datos determinado. A diferencia de un vector de caracteres, cuando tabulas un factor obtendrás recuentos de todas las categorías, incluso las no observadas:
<- c("m", "m", "m")
sex_char <- factor(sex_char, levels = c("m", "f")) sex_factor
table(sex_char)
## sex_char
## m
## 3
table(sex_factor)
## sex_factor
## m f
## 3 0
Los factores ordenados son una variación menor de los factores. En general, se comportan como factores regulares, pero el orden de los niveles es significativo (bajo, medio, alto) (una propiedad que se aprovecha automáticamente mediante algunas funciones de modelado y visualización).
<- ordered(c("b", "b", "a", "c"), levels = c("c", "b", "a"))
grade grade
## [1] b b a c
## Levels: c < b < a
La base R tiende a encontrar factores con mucha frecuencia porque muchas funciones R base (como read.csv() y data.frame() ) convierten automáticamente vectores de caracteres en factores. Esto es subóptimo porque no hay forma de que esas funciones conozcan el conjunto de todos los niveles posibles o su orden correcto: los niveles son una propiedad de la teoría o de un diseño experimental, no de los datos. En su lugar, use el argumento \(stringsAsFactors = FALSE\) para suprimir este comportamiento y luego convierte manualmente los vectores de caracteres en factores utilizando su conocimiento de los datos “teóricos”.
Si bien los factores se parecen (y a menudo se comportan como) vectores de caracteres, se construyen sobre enteros. Así que ten cuidado al tratarlos como si fueran strings. Algunos métodos de cadena (como gsub() y grepl()) coaccionarán automáticamente factores a cadenas, otros (como nchar()) generarán un error y otros (como c()) usarán los valores enteros subyacentes. Por esta razón, generalmente es mejor convertir explícitamente los factores en vectores de caracteres si necesitas un comportamiento similar a una cadena.
2.4.2 Fechas
Los vectores de fecha se construyen sobre vectores double. Tienen clase “Date” y no tienen otros atributos:
<- Sys.Date() today
typeof(today)
## [1] "double"
attributes(today)
## $class
## [1] "Date"
El valor del doble (que se puede ver quitando la clase), representa el número de días desde 1970-01-01:
<- as.Date("1970-02-01")
date unclass(date)
## [1] 31
2.4.3 Fecha-hora
La base R proporciona dos formas de almacenar información de fecha y hora, POSIXct y POSIXlt. Estos son nombres ciertamente extraños: POSIX es la abreviatura de Portable Operating System Interface, que es una familia de estándares multiplataforma. ct significa hora de calendario (el tipo time_t en C), y lt para hora local (el tipo struct tm en C). Aquí nos centraremos en POSIXct, porque es el más simple, está construido sobre un vector atómico y es el más apropiado para su uso en data frames. Los vectores POSIXct se construyen sobre vectores double, donde el valor representa el número de segundos desde 1970-01-01.
<- as.POSIXct("2018-08-01 22:00", tz = "UTC")
now_ct now_ct
## [1] "2018-08-01 22:00:00 UTC"
typeof(now_ct)
## [1] "double"
attributes(now_ct)
## $class
## [1] "POSIXct" "POSIXt"
##
## $tzone
## [1] "UTC"
El atributo tzone controla únicamente cómo se formatea la fecha y hora; no controla el instante de tiempo representado por el vector. Ten en cuenta que la hora no se imprime si es medianoche.
structure(now_ct, tzone = "Asia/Tokyo")
## [1] "2018-08-02 07:00:00 JST"
structure(now_ct, tzone = "America/New_York")
## [1] "2018-08-01 18:00:00 EDT"
structure(now_ct, tzone = "Australia/Lord_Howe")
## [1] "2018-08-02 08:30:00 +1030"
structure(now_ct, tzone = "Europe/Paris")
## [1] "2018-08-02 CEST"
2.4.4 Duraciones
Las duraciones, que representan la cantidad de tiempo entre pares de fechas o fechas-horas, se almacenan en difftimes. Los difftimes se construyen sobre doubles y tienen un atributo units que determina cómo se debe interpretar el entero:
<- as.difftime(1, units = "weeks")
one_week_1 one_week_1
## Time difference of 1 weeks
typeof(one_week_1)
## [1] "double"
attributes(one_week_1)
## $class
## [1] "difftime"
##
## $units
## [1] "weeks"
<- as.difftime(7, units = "days")
one_week_2 one_week_2
## Time difference of 7 days
typeof(one_week_2)
## [1] "double"
attributes(one_week_2)
## $class
## [1] "difftime"
##
## $units
## [1] "days"
2.4.5 Ejercicios
¿Qué tipo de objeto devuelve table()? ¿Cuál es su tipo? ¿Qué atributos tiene? ¿Cómo cambia la dimensionalidad a medida que se tabulan más variables?
¿Qué le sucede a un factor cuando modificas sus niveles?
<- factor(letters)
f1 levels(f1) <- rev(levels(f1))
- ¿Qué hace este código? ¿Cómo f2 y f3 difeieren de f1?
<- rev(factor(letters))
f2 <- factor(letters, levels = rev(letters)) f3
2.5 Listas
Las listas son un paso adelante en la complejidad de los vectores atómicos: cada elemento puede ser de cualquier tipo, no solo vectores. Técnicamente hablando, cada elemento de una lista es en realidad del mismo tipo porque, como se vio en la Sección 2.3.3, cada elemento es realmente una referencia a otro objeto, que puede ser de cualquier tipo.
2.5.1 Creación
Las listas se construyen con: list():
<- list(
l1 1:3,
"a",
c(TRUE, FALSE, TRUE),
c(2.3, 5.9)
)
typeof(l1)
## [1] "list"
str(l1)
## List of 4
## $ : int [1:3] 1 2 3
## $ : chr "a"
## $ : logi [1:3] TRUE FALSE TRUE
## $ : num [1:2] 2.3 5.9
Dado que los elementos de una lista son referencias, la creación de una lista no implica copiar los componentes en la lista. Por esta razón, el tamaño total de una lista puede ser más pequeño de lo que cabría esperar.
::obj_size(mtcars) lobstr
## 7,208 B
<- list(mtcars, mtcars, mtcars, mtcars)
l2 ::obj_size(l2) lobstr
## 7,288 B
Las listas pueden contener objetos complejos, por lo que no es posible elegir un único estilo visual que funcione para cada lista. Generalmente las listas se dibujarán como vectores, usando el color para recordarte la jerarquía.
Las listas a veces se denominan vectores recursivos porque una lista puede contener otras listas. Esto las hace fundamentalmente diferentes de los vectores atómicos.
<- list(list(list(1)))
l3 str(l3)
## List of 1
## $ :List of 1
## ..$ :List of 1
## .. ..$ : num 1
c() combinará varias listas en una. Dada una combinación de vectores atómicos y listas, c() forzará los vectores a listas antes de combinarlos. Compare los resultados de y list() y c():
<- list(list(1, 2), c(3, 4))
l4 <- c(list(1, 2), c(3, 4))
l5 str(l4)
## List of 2
## $ :List of 2
## ..$ : num 1
## ..$ : num 2
## $ : num [1:2] 3 4
str(l5)
## List of 4
## $ : num 1
## $ : num 2
## $ : num 3
## $ : num 4
2.5.2 Pruebas y coerciones
El typeof() de una lista es list. Puede probar una lista con is.list(), y forzarla a una lista con as.list().
list(1:3)
## [[1]]
## [1] 1 2 3
as.list(1:3)
## [[1]]
## [1] 1
##
## [[2]]
## [1] 2
##
## [[3]]
## [1] 3
Puede convertir una lista en un vector atómico con unlist(). Las reglas para el tipo resultante son complejas, no están bien documentadas y no siempre son equivalentes a lo que obtendría con c().
2.5.3 Matrices y arreglos
Con los vectores atómicos, el atributo dimensión se usa comúnmente para crear matrices. Con las listas, el atributo dimensión se puede utilizar para crear listas de matrices de o listas de arreglos:
<- list(1:3, "a", TRUE, 1.0)
l dim(l) <- c(2, 2)
l
## [,1] [,2]
## [1,] integer,3 TRUE
## [2,] "a" 1
1, 1]] l[[
## [1] 1 2 3
Estas estructuras de datos son relativamente extrañas, pero pueden ser útiles si deseas organizar objetos en una estructura similar a una cuadrícula. Por ejemplo, si estás ejecutando modelos en una cuadrícula espacio-temporal, podría ser más intuitivo almacenar los modelos en una matriz 3D que coincida con la estructura de la cuadrícula.
2.5.4 Ejercicios
Enumere todas las formas en las que una lista difiere de un vector atómico.
¿Por qué necesita usar unlist() para convertir una lista en un vector atómico? ¿Por qué no funciona as.vector()?
Comparar y contrastar c() y unlist() al combinar una fecha y una fecha-hora en un solo vector.
2.6 Data frames y tibbles
Los dos vectores S3 más importantes construidos sobre las listas son los data frames y los tibbles.
Si haces análisis de datos en R, vas a utilizar data frames. Un data frame es una lista con nombre de vectores con atributos para (columna) names, row.names, y su clase, “data.frame”:
<- data.frame(x = 1:3, y = letters[1:3])
df1 typeof(df1)
## [1] "list"
attributes(df1)
## $names
## [1] "x" "y"
##
## $class
## [1] "data.frame"
##
## $row.names
## [1] 1 2 3
A diferencia de una lista regular, un data frame tiene una restricción adicional: la longitud de cada uno de sus vectores debe ser la misma. Esto da a los data frames su estructura rectangular y explica por qué comparten las propiedades de matrices y listas:
Un marco de datos tiene rownames() y colnames(). Los names() de un marco de datos son los nombres de columna.
Un data frame tiene nrow() filas y ncol() columnas. El length() de un data frame da el número de columnas.
Los data frames son una de las ideas más grandes e importantes en R, y una de las cosas que hacen que R sea diferente de otros lenguajes de programación. Sin embargo, en los más de 20 años transcurridos desde su creación, las formas en que las personas usan R han cambiado, y algunas de las decisiones de diseño que tenían sentido en el momento en que se crearon los data frames ahora causan frustración.
Esta frustración lleva a la creación del tibble, una reinvención moderna del data frame. Los Tibbles están diseñados para ser (tanto como sea posible) reemplazos directos para data frames que solucionan esas frustraciones. Una forma concisa y divertida de resumir las principales diferencias es que los tibbles son perezosos y hoscos: hacen menos y se quejan más. Verá lo que eso significa a medida que trabaje en esta sección.
Los tibbles son proporcionados por el paquete tibble y comparten la misma estructura que los data frames. La única diferencia es que el vector de clase es más largo e incluye tbl_df. Esto permite que los tibbles se comporten de manera diferente en las formas clave que discutiremos a continuación.
library(tibble)
<- tibble(x = 1:3, y = letters[1:3])
df2 typeof(df2)
## [1] "list"
attributes(df2)
## $class
## [1] "tbl_df" "tbl" "data.frame"
##
## $row.names
## [1] 1 2 3
##
## $names
## [1] "x" "y"
2.6.1 Creación
Para crear un data frame, proporcione pares nombre-vector a data.frame():
<- data.frame(
df x = 1:3,
y = c("a", "b", "c")
)str(df)
## 'data.frame': 3 obs. of 2 variables:
## $ x: int 1 2 3
## $ y: chr "a" "b" "c"
Ten cuidado con la conversión por defecto de cadenas a factores. Utilíza stringsAsFactors = FALSE para suprimir esto y mantener los vectores de caracteres como vectores de caracteres.
<- data.frame(
df1 x = 1:3,
y = c("a", "b", "c"),
stringsAsFactors = FALSE
)str(df1)
## 'data.frame': 3 obs. of 2 variables:
## $ x: int 1 2 3
## $ y: chr "a" "b" "c"
Crear un tibble es similar a crear un data frame. La diferencia entre los dos es que los tibbles nunca coercionan su entrada (esta es una característica que los hace perezosos):
<- tibble(
df2 x = 1:3,
y = c("a", "b", "c")
)str(df2)
## tibble [3 x 2] (S3: tbl_df/tbl/data.frame)
## $ x: int [1:3] 1 2 3
## $ y: chr [1:3] "a" "b" "c"
Además, mientras que los data frames transforman automáticamente nombres no sintácticos (a menos que check.names = FALSE), los tibbles no lo hacen (aunque sí imprimen nombres no sintácticos rodeados de `).
names(data.frame(`1` = 1))
## [1] "X1"
names(tibble(`1` = 1))
## [1] "1"
Si bien cada elemento de un data frame (o tibble) ambos deben tener la misma longitud, ambos data.frame() y tibble() reciclarán las entradas más cortas. Sin embargo, mientras que los data frames reciclan automáticamente las columnas que son un múltiplo entero de la columna más larga, los tibbles solo reciclarán vectores de longitud uno.
data.frame(x = 1:4, y = 1:2)
## x y
## 1 1 1
## 2 2 2
## 3 3 1
## 4 4 2
# data.frame(x = 1:4, y = 1:3)
tibble(x = 1:4, y = 1)
## # A tibble: 4 x 2
## x y
## <int> <dbl>
## 1 1 1
## 2 2 1
## 3 3 1
## 4 4 1
# tibble(x = 1:4, y = 1:2)
Hay una última diferencia: tibble() permite referirse a variables creadas durante la construcción:
tibble(
x = 1:3,
y = x * 2
)
## # A tibble: 3 x 2
## x y
## <int> <dbl>
## 1 1 2
## 2 2 4
## 3 3 6
(Las entradas se evalúan de izquierda a derecha).
Al dibujar marcos de datos y tibbles, en lugar de centrarse en los detalles de la implementación, es decir, los atributos:
Los dibujaremon de la misma manera que una lista con nombre, pero los organizaremos para enfatizar su estructura columnar.
2.6.2 Nombres de filas
Los data frames le permiten etiquetar cada fila con un nombre, un vector de caracteres que contiene solo valores únicos:
<- data.frame(
df3 age = c(35, 27, 18),
hair = c("blond", "brown", "black"),
row.names = c("Bob", "Susan", "Sam")
) df3
## age hair
## Bob 35 blond
## Susan 27 brown
## Sam 18 black
Puede obtener y establecer nombres de fila con rownames(), y puede usarlos para subsetear filas:
rownames(df3)
## [1] "Bob" "Susan" "Sam"
"Bob", ] df3[
## age hair
## Bob 35 blond
Los nombres de fila surgen naturalmente si piensas en los data frames como estructuras 2D como matrices: las columnas (variables) tienen nombres, por lo que las filas (observaciones) también deberían tenerlas.
La mayoría de las matrices son numéricas, por lo que es importante tener un lugar para almacenar etiquetas de caracteres. Pero esta analogía con las matrices es engañosa porque las matrices poseen una propiedad importante que los data frames no tienen: se pueden transponer. En las matrices, las filas y las columnas son intercambiables, y la transposición de una matriz te entrega otra matriz (la transposición nuevamente te da la matriz original). Sin embargo, con los data frames, las filas y columnas no son intercambiables: la transposición de un data frame no es un data frame.
Hay tres razones por las que los nombres de fila no son deseables:
Los metadatos son datos, por lo que almacenarlos de una manera diferente al resto de los datos es fundamentalmente una mala idea. También significa que necesitas aprender un nuevo conjunto de herramientas para trabajar con nombres de filas; no puede usar lo que ya sabe sobre la manipulación de columnas.
Los nombres de fila son una abstracción pobre para etiquetar filas porque sólo funcionan cuando una fila se puede identificar con una sola cadena. Esto falla en muchos casos, por ejemplo, cuando desea identificar una fila con un vector sin caracteres (por ejemplo, un punto de tiempo) o con múltiples vectores (por ejemplo, posición, codificada por latitud y longitud).
Los nombres de fila deben ser únicos, por lo que cualquier duplicación de filas (por ejemplo, desde el bootstrapping) creará nuevos nombres de fila. Si desea hacer coincidir filas de antes y después de la transformación, deberá realizar una cirugía de string complicada.
c(1, 1, 1), ] df3[
## age hair
## Bob 35 blond
## Bob.1 35 blond
## Bob.2 35 blond
Por estas razones, los tibbles no admiten nombres de fila. En su lugar, el paquete tibble proporciona herramientas para convertir fácilmente los nombres de fila en una columna normal con rownames_to_column(), o el argumento rownames en as_tibble():
as_tibble(df3, rownames = "name")
## # A tibble: 3 x 3
## name age hair
## <chr> <dbl> <chr>
## 1 Bob 35 blond
## 2 Susan 27 brown
## 3 Sam 18 black
2.6.3 Impresión
Una de las diferencias más obvias entre tibbles y data frames es cómo se imprimen. Se asume que ya estás familiarizado con la forma en que se imprimen los data frames, por lo que aquí se destacarán algunas de las mayores diferencias utilizando un conjunto de datos de ejemplo incluido en el paquete dplyr:
::starwars dplyr
## # A tibble: 87 x 14
## name height mass hair_color skin_color eye_color birth_year sex gender
## <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr>
## 1 Luke S~ 172 77 blond fair blue 19 male mascu~
## 2 C-3PO 167 75 <NA> gold yellow 112 none mascu~
## 3 R2-D2 96 32 <NA> white, bl~ red 33 none mascu~
## 4 Darth ~ 202 136 none white yellow 41.9 male mascu~
## 5 Leia O~ 150 49 brown light brown 19 fema~ femin~
## 6 Owen L~ 178 120 brown, grey light blue 52 male mascu~
## 7 Beru W~ 165 75 brown light blue 47 fema~ femin~
## 8 R5-D4 97 32 <NA> white, red red NA none mascu~
## 9 Biggs ~ 183 84 black light brown 24 male mascu~
## 10 Obi-Wa~ 182 77 auburn, wh~ fair blue-gray 57 male mascu~
## # ... with 77 more rows, and 5 more variables: homeworld <chr>, species <chr>,
## # films <list>, vehicles <list>, starships <list>
Los tibbles sólo muestran las primeras 10 filas y todas las columnas que cabrán en la pantalla. Las columnas adicionales se muestran en la parte inferior.
Cada columna está etiquetada con su tipo, abreviado a tres o cuatro letras.
Las columnas anchas se truncan para evitar que un sólo string largo ocupe una fila entera. (Esto todavía es un trabajo en progreso: es una compensación difícil entre mostrar tantas columnas como sea posible y mostrar columnas en su totalidad).
Cuando es usado en entornos de consola que lo admiten, el color se usa juiciosamente para resaltar información importante y restar énfasis a los detalles suplementarios.
2.6.4 Subsetting
Se puede subsetear un marco de datos o un tibble como una estructura 1D (donde se comporta como una lista), o una estructura 2D (donde se comporta como una matriz).
Los data frames tienen dos comportamientos de subseteado indeseables:
Cuando subseteas columnas con df[, vars], obtendrás un vector si vars selecciona una variable, de lo contrario obtendrás un data frame. Esta es una fuente frecuente de errores cuando se usa (en una función, a menos que siempre recuerde usar df[, vars, drop = FALSE].
Cuando intenta extraer una sola columna con df$x y no hay tal columna, un data frame seleccionará en su lugar con cualquier variable que comience con x. Si ninguna variable comienza con x, \(df\)x$ devolverá NULL. Esto hace que sea fácil seleccionar la variable incorrecta o seleccionar una variable que no existe.
Tibbles modifica estos comportamientos para que un [ siempre devuelva un tibble, y un $ no haga una coincidencia parcial y advierta si no puede encontrar una variable (esto es lo que hace que tibbles sea hosco).
<- data.frame(xyz = "a")
df1 <- tibble(xyz = "a") df2
df1
## xyz
## 1 a
df2
## # A tibble: 1 x 1
## xyz
## <chr>
## 1 a
str(df1$x)
## chr "a"
str(df2$x)
## Warning: Unknown or uninitialised column: `x`.
## NULL
La insistencia de un tibble en devolver un dataframe desde [ puede causar problemas con el código heredado, que a menudo utiliza df[, “col”] para extraer una sola columna. Si quieres una sola columna, es recomendable usar df[[“col”]]. Esto comunica claramente tu intención y funciona tanto con dataframes como con tibbles.
2.6.5 3.6.5 Pruebas y coerción
Para comprobar si un objeto es un dataframe o tibble, utiliza: is.data.frame():
is.data.frame(df1)
## [1] TRUE
is.data.frame(df2)
## [1] TRUE
Por lo general, no debería importar si tienes un tibble o un dataframe, pero si necesitas estar seguro, usa: is_tibble():
is_tibble(df1)
## [1] FALSE
is_tibble(df2)
## [1] TRUE
Puedes forzar un objeto a ser un dataframe con as.data.frame() o a un tibble con as_tibble().
2.6.6 Columnas de lista
Dado que un dataframe es una lista de vectores, es posible tenga una columna que sea una lista. Esto es muy útil porque una lista puede contener cualquier objeto: esto significa que puede colocar cualquier objeto en un dataframe. Esto te permite mantener los objetos relacionados juntos en una fila, sin importar cuán complejos sean los objetos individuales. Puedes ver una aplicación de esto en el capítulo “Muchos Modelos” de R for Data Science4
Las columnas de lista están permitidas en los dataframes, pero debes hacer un poco de trabajo adicional agregando la columna de lista después de la creación o envolviendo la lista en I().
<- data.frame(x = 1:3)
df $y <- list(1:2, 1:3, 1:4)
df
data.frame(
x = 1:3,
y = I(list(1:2, 1:3, 1:4))
)
## x y
## 1 1 1, 2
## 2 2 1, 2, 3
## 3 3 1, 2, 3, 4
Las columnas de lista son más fáciles de usar con tibbles porque se puede incluir directamente en el interior de tibble() y se imprimirán ordenadamente:
tibble(
x = 1:3,
y = list(1:2, 1:3, 1:4)
)
## # A tibble: 3 x 2
## x y
## <int> <list>
## 1 1 <int [2]>
## 2 2 <int [3]>
## 3 3 <int [4]>
2.6.7 3.6.7 Matrices y columnas de dataframes
Siempre que el número de filas coincida con el dateframe, también es posible tener una matriz o un arreglo como columna de un dataframe. (Esto requiere una ligera extensión de nuestra definición de un dataframe: no es length() de cada columna lo que debe ser igual, sino el NROW()) En cuanto a las columnas de lista, debes agregarlo después de su creación o envolverlo en I().
<- data.frame(
dfm x = 1:3 * 10
)$y <- matrix(1:9, nrow = 3)
dfm$z <- data.frame(a = 3:1, b = letters[1:3], stringsAsFactors = FALSE)
dfm
str(dfm)
## 'data.frame': 3 obs. of 3 variables:
## $ x: num 10 20 30
## $ y: int [1:3, 1:3] 1 2 3 4 5 6 7 8 9
## $ z:'data.frame': 3 obs. of 2 variables:
## ..$ a: int 3 2 1
## ..$ b: chr "a" "b" "c"
Las matrices y dataframes como columnas requieren un poco de precaución. Muchas funciones que funcionan con data frames asumen que todas las columnas son vectores. Además, el despliegue en la pantalla puede ser confusa.
1, ] dfm[
## x y.1 y.2 y.3 z.a z.b
## 1 10 1 4 7 3 a
2.6.8 Ejercicios
¿Se puede tener un marco de datos con cero filas? ¿Qué pasa con las columnas cero?
¿Qué sucede si intenta establecer nombres de fila que no son únicos?
Si df es un data frame, ¿qué puede decir sobre t(df), y t(t(df))? Realice algunos experimentos, asegurándose de probar diferentes tipos de columnas.
¿Qué sucede cuando as.matrix() se aplica a un data frame con columnas de diferentes tipos? ¿En qué se diferencia de data.matrix()?
2.7 NULL
Una última estructura de datos importante que está estrechamente relacionada con los vectores es NULL. NULL es especial porque tiene un tipo único, siempre tiene longitud cero y no puede tener ningún atributo:
typeof(NULL)
## [1] "NULL"
length(NULL)
## [1] 0
# x <- NULL
attr(x, "y") <- 1
Puedes realizar pruebas para NULLs con is.null():
is.null(NULL)
## [1] TRUE
Hay dos usos comunes para NULL:
- Representar un vector vacío (un vector de longitud cero) de tipo arbitrario. Por ejemplo, si usas c() pero no incluye ningún argumento, obtendrás NULL, y concatenar a un vector lo dejará sin cambios:
c()
## NULL
- Para representar un vector ausente. Por ejemplo, NULL a menudo se usa como argumento por defecto en una funcion, cuando el argumento es opcional pero el valor por defecto requiere algún cálculo. Contrasta esto con NA utilizado para indicar que un elemento de un vector está ausente.
Si está familiarizado con SQL, sabrás sobre el NULL relacional y podrías esperar que sea lo mismo que R. Sin embargo, la base de datos NULL es en realidad equivalente a los NA de R.
2.8 Respuestas al cuestionarios
Los cuatro tipos comunes de vector atómico son lógico, entero, double y carácter. Los dos tipos más raros son complejos y raw.
Los atributos permiten asociar metadatos adicionales arbitrarios a cualquier objeto. Puede obtener y establecer atributos individuales con attr(x, “y”) y attr(x, “y”) <- value; o puedes obtener y establecer todos los atributos a la vez con attributes().
Los elementos de una lista pueden ser de cualquier tipo (incluso una lista); los elementos de un vector atómico son todos del mismo tipo. Del mismo modo, cada elemento de una matriz debe ser del mismo tipo; en un data frame, diferentes columnas pueden tener diferentes tipos.
Puede crear una matriz de lista asignando dimensiones a una lista. Puede convertir una matriz en una columna de un data frame con
\[ df$x <- matrix() \]
o mediante el uso de I() al crear un nuevo data frame:
\[ data.frame(x = I(matrix())) \]
- Los Tibbles tienen un método de impresión mejorado, nunca obligan a convertir cadenas a factores y proporcionan métodos de subseteo más estrictos.
3 Subseteo (subsetting)
3.1 Introducción
Los operadores de subsetting de R son rápidos y potentes. Dominarlos te permite realizar sucintamente operaciones complejas de una manera que pocos otros lenguajes pueden igualar. Subsetear en R es fácil de aprender pero difícil de dominar porque necesitas internalizar una serie de conceptos interrelacionados:
Hay seis formas de subsetear vectores atómicos.
Hay tres operadores de subseteo, [[, [, y $.
Los operadores de subseteo interactúan de manera diferente con diferentes tipos de vectores (por ejemplo, vectores atómicos, listas, factores, matrices y data frames).
El subseteo se puede combinar con la asignación.
La subconfiguración es un complemento natural de str(). Si bien str() te muestra todas las piezas de cualquier objeto (su estructura), El subseteo te permite extraer las piezas que te interesan.
Preguntas que resolveremos:
1 ¿Cuál es el resultado de subsetear un vector con enteros positivos, enteros negativos, un vector lógico o un vector de caracteres?
2 ¿Cuál es la diferencia entre [, [[, y $ cuando se aplica a una lista?
3 ¿Cuándo debieses usar drop = FALSE?
4 Si x es una matriz, ¿qué hace x[] <- 0? ¿En qué se diferencia de x <- 0?
5 ¿Cómo se puede usar un vector con nombre para reetiquetar variables categóricas?
Contenidos
1 Aprenderás a utilizar [ y las seis formas de subsetear vectores atómicos. A continuación, aprenderás cómo actúan esas seis formas cuando se utilizan para subsetear listas, matrices y dataframes.
2 Ampliarás tu conocimiento de los operadores de subseteo para incluir [[ y $ concentrándonos en los importantes principios de simplificar frente a preservar.
3 Aprenderás el arte de la subasignación, que combina el subseteo y la asignación para modificar partes de un objeto.
4 Serás guiado a través de ocho aplicaciones importantes, pero no obvias, del subsetting para resolver problemas que a menudo encontrarás en el análisis de datos.
3.2 Selección de múltiples elementos
[ se utiliza para seleccionar cualquier número de elementos de un vector. Para ilustrar, lo aplicaremos a un vector atómicos de 1D, y luego mostraré cómo esto se generaliza a objetos más complejos y de más dimensiones.
3.2.1 Vectores atómicos
Exploremos los diferentes tipos de subsetting con un vector simple, x.
<- c(2.1, 4.2, 3.3, 5.4)
x x
## [1] 2.1 4.2 3.3 5.4
Ten en cuenta que el número después del punto decimal representa la posición original en el vector.
Hay seis cosas que puedes usar para subsetear un vector:
1 Los enteros positivos devuelven elementos en las posiciones especificadas:
c(3, 1)] x[
## [1] 3.3 2.1
order(x)] x[
## [1] 2.1 3.3 4.2 5.4
# Los índices duplicados duplicarán los valores
c(1, 1)] x[
## [1] 2.1 2.1
# Los números reales se truncan silenciosamente a enteros
c(2.1, 2.9)] x[
## [1] 4.2 4.2
2 Los enteros negativos excluyen elementos en las posiciones especificadas:
-c(3, 1)] x[
## [1] 4.2 5.4
Ten en cuenta que no puedes mezclar enteros positivos y negativos en un solo subconjunto:
# x[c(-1, 2)]
3 Los vectores lógicos seleccionan elementos donde el valor lógico correspondiente es TRUE. Este es probablemente el tipo de subsetting más útil porque puedes escribir una expresión que utilice un vector lógico:
c(TRUE, TRUE, FALSE, FALSE)] x[
## [1] 2.1 4.2
> 3] x[x
## [1] 4.2 3.3 5.4
En x[y], ¿qué sucede si x e y poseen diferentes longitudes? El comportamiento está controlado por las reglas de reciclaje donde el más corto de los dos se recicla a la longitud del más largo. Esto es conveniente y fácil de entender cuando uno de x e y poseen longitud uno, pero se recomienda evitar el reciclaje para otras longitudes porque las reglas se aplican de manera inconsistente a través de la base R.
c(TRUE, FALSE)] x[
## [1] 2.1 3.3
Es equivalente a
c(TRUE, FALSE, TRUE, FALSE)] x[
## [1] 2.1 3.3
Ten en cuenta que un valor que falta en el índice siempre produce un valor faltante en la salida:
c(TRUE, TRUE, NA, FALSE)] x[
## [1] 2.1 4.2 NA
4 Nada devuelve el vector original. Esto no es útil para vectores 1D, pero, es muy útil para matrices, data frames y arreglos. También puede ser útil junto con la asignación.
x[]
## [1] 2.1 4.2 3.3 5.4
5 Cero devuelve un vector de longitud cero. Esto no es algo que normalmente se hace a propósito, pero puede ser útil para generar datos de prueba.
0] x[
## numeric(0)
6 Si el vector tiene nombre, también puede usar vectores de caracteres para devolver elementos con nombres coincidentes.
<- setNames(x, letters[1:4])) (y
## a b c d
## 2.1 4.2 3.3 5.4
c("d", "c", "a")] y[
## d c a
## 5.4 3.3 2.1
Al igual que los índices enteros, puede repetir índices
c("a", "a", "a")] y[
## a a a
## 2.1 2.1 2.1
Al crear un subconjunto con [, los nombres siempre coinciden exactamente
<- c(abc = 1, def = 2)
z c("a", "d")] z[
## <NA> <NA>
## NA NA
Nota: Los factores no se tratan especialmente cuando se hace subsetting. Esto significa que el subsetting utilizará el vector entero subyacente, no los niveles de caracteres. Esto suele ser inesperado, por lo que debes evitar el subsetting con factores:
factor("b")] y[
## a
## 2.1
3.2.2 Listas
El subseteo de una lista funciona de la misma manera que el subsetting de un vector atómico. Usar [ siempre devuelve una lista; [[ y $, te permiten extraer elementos de una lista.
3.2.3 Matrices y arreglos
Puedes subsetear estructuras de dimensiones superiores de tres maneras:
- Con múltiples vectores.
- Con un solo vector.
- Con una matriz.
La forma más común de subestablecer matrices (2D) y arreglos (>2D) es una generalización simple de un subseteo 1D: proporcionar un índice 1D para cada dimensión, separado por una coma. El subconjunto en blanco ahora es útil porque te permite mantener todas las filas o todas las columnas.
<- matrix(1:9, nrow = 3)
a colnames(a) <- c("A", "B", "C")
1:2, ] a[
## A B C
## [1,] 1 4 7
## [2,] 2 5 8
c(TRUE, FALSE, TRUE), c("B", "A")] a[
## B A
## [1,] 4 1
## [2,] 6 3
0, -2] a[
## A C
De forma predeterminada, [ simplifica los resultados a la dimensionalidad más baja posible. Por ejemplo, las dos expresiones siguientes devuelven vectores 1D. Aprenderás a cómo evitar “soltar” (dropping) dimensiones más adelante.
1, ] a[
## A B C
## 1 4 7
1, 1] a[
## A
## 1
Debido a que tanto las matrices como los arreglos son solo vectores con atributos especiales, puedes subsetearlos con un solo vector, como si fueran un vector 1D. Ten en cuenta que los arreglos en R se almacenan en el orden de la columna principal:
<- outer(1:5, 1:5, FUN = "paste", sep = ",")
vals vals
## [,1] [,2] [,3] [,4] [,5]
## [1,] "1,1" "1,2" "1,3" "1,4" "1,5"
## [2,] "2,1" "2,2" "2,3" "2,4" "2,5"
## [3,] "3,1" "3,2" "3,3" "3,4" "3,5"
## [4,] "4,1" "4,2" "4,3" "4,4" "4,5"
## [5,] "5,1" "5,2" "5,3" "5,4" "5,5"
c(4, 15)] vals[
## [1] "4,1" "5,3"
También puede subestablecer estructuras de datos de dimensiones superiores con una matriz entera (o, si se denomina, una matriz de caracteres). Cada fila de la matriz especifica la ubicación de un valor y cada columna corresponde a una dimensión de la matriz. Esto significa que puede usar una matriz de 2 columnas para subestablecer una matriz, una matriz de 3 columnas para subestablecer una matriz 3D, etc. El resultado es un vector de valores:
select <- matrix(ncol = 2, byrow = TRUE, c( 1, 1, 3, 1, 2, 4 )) vals[select] #> [1] “1,1” “3,1” “2,4” 4.2.4 Marcos de datos y tibbles Los marcos de datos tienen las características tanto de las listas como de las matrices:
Al subestablecer con un solo índice, se comportan como listas e indexan las columnas, por lo que selecciona las dos primeras columnas.df[1:2]
Al subestablecer con dos índices, se comportan como matrices, por lo que selecciona las tres primeras filas (y todas las columnas)df[1:3, ]33.
df <- data.frame(x = 1:3, y = 3:1, z = letters[1:3])
df[df$x == 2, ] #> x y z #> 2 2 2 b df[c(1, 3), ] #> x y z #> 1 1 3 a #> 3 3 1 c
4 There are two ways to select columns from a data frame
5 Like a list
df[c(“x”, “z”)] #> x z #> 1 1 a #> 2 2 b #> 3 3 c # Like a matrix df[, c(“x”, “z”)] #> x z #> 1 1 a #> 2 2 b #> 3 3 c
6 There’s an important difference if you select a single
7 column: matrix subsetting simplifies by default, list
8 subsetting does not.
str(df[“x”]) #> ‘data.frame’: 3 obs. of 1 variable: #> $ x: int 1 2 3 str(df[, “x”]) #> int [1:3] 1 2 3 La subconfiguración de un tibble con siempre devuelve un tibble:[
df <- tibble::tibble(x = 1:3, y = 3:1, z = letters[1:3])
str(df[“x”]) #> tibble [3 × 1] (S3: tbl_df/tbl/data.frame) #> $ x: int [1:3] 1 2 3 str(df[, “x”]) #> tibble [3 × 1] (S3: tbl_df/tbl/data.frame) #> $ x: int [1:3] 1 2 3 4.2.5 Preservar la dimensionalidad De forma predeterminada, la subconfiguración de una matriz o marco de datos con un solo número, un solo nombre o un vector lógico que contenga un solo , simplificará la salida devuelta, es decir, devolverá un objeto con menor dimensionalidad. Para preservar la dimensionalidad original, debe usar .TRUEdrop = FALSE
Para matrices y matrices, se eliminarán las dimensiones con longitud 1:
a <- matrix(1:4, nrow = 2) str(a[1, ]) #> int [1:2] 1 3
str(a[1, , drop = FALSE]) #> int [1, 1:2] 1 3 Los marcos de datos con una sola columna devolverán solo el contenido de esa columna:
df <- data.frame(a = 1:2, b = 1:2) str(df[, “a”]) #> int [1:2] 1 2
str(df[, “a”, drop = FALSE]) #> ‘data.frame’: 2 obs. of 1 variable: #> $ a: int 1 2 El comportamiento predeterminado es una fuente común de errores en las funciones: comprueba su código con un marco de datos o matriz con varias columnas, y funciona. Seis meses después, usted (o alguien más) lo usa con un marco de datos de una sola columna y falla con un error desconcertante. Al escribir funciones, adquiera el hábito de usar siempre al subestablecer un objeto 2D. Por esta razón, tibbles por defecto a , y siempre devuelve otro tibble.drop = TRUEdrop = FALSEdrop = FALSE[
La subconfiguración de factores también tiene un argumento, pero su significado es bastante diferente. Controla si se conservan o no los niveles (en lugar de las dimensiones) y el valor predeterminado es . Si descubres que estás usando mucho, a menudo es una señal de que deberías usar un vector de caracteres en lugar de un factor.dropFALSEdrop = TRUE
z <- factor(c(“a”, “b”)) z[1] #> [1] a #> Levels: a b z[1, drop = TRUE] #> [1] a #> Levels: a 4.2.6 Ejercicios Corrija cada uno de los siguientes errores comunes de subconfiguración de tramas de datos:
mtcars[mtcars$cyl = 4, ] mtcars[-1:4, ] mtcars[mtcars$cyl <= 5] mtcars[mtcars$cyl == 4 | 6, ] ¿Por qué el siguiente código produce cinco valores faltantes? (Sugerencia: ¿por qué es diferente de ?)x[NA_real_]
x <- 1:5 x[NA] #> [1] NA NA NA NA NA ¿Qué devuelve? ¿Cómo funciona la subconfiguración de una matriz con ella? ¿Necesitamos alguna regla de subconfiguración adicional para describir su comportamiento?upper.tri()
x <- outer(1:5, 1:5, FUN = “*“) x[upper.tri(x)] ¿Por qué devuelve un error? ¿En qué se diferencia de lo similar?mtcars[1:20]mtcars[1:20, ]
Implemente su propia función que extrae las entradas diagonales de una matriz (debe comportarse como donde es una matriz).diag(x)x
¿Qué hace? ¿Cómo funciona?df[is.na(df)] <- 0
4.3 Selección de un solo elemento Hay otros dos operadores de subconfiguración: y . se utiliza para extraer elementos individuales, mientras que es una abreviatura útil para .[[\([[x\)yx[[“y”]]
4.3.1 [[ [[ es más importante cuando se trabaja con listas porque la subconfiguración de una lista con siempre devuelve una lista más pequeña. Para ayudar a que esto sea más fácil de entender, podemos usar una metáfora:[
Si la lista es un tren que transporta objetos, entonces es el objeto en el vagón 5; es un tren de coches 4-6.xx[[5]]x[4:6]
— @RLangTip, https://twitter.com/RLangTip/status/268375867468681216
Usemos esta metáfora para hacer una lista simple:
x <- list(1:3, “a”, 4:6)
Al extraer un solo elemento, tiene dos opciones: puede crear un tren más pequeño, es decir, menos vagones, o puede extraer el contenido de un vagón en particular. Esta es la diferencia entre y :[[[
Al extraer múltiples (¡o incluso cero!) elementos, tienes que hacer un tren más pequeño:
Dado que solo puede devolver un único elemento, debe usarlo con un único entero positivo o una sola cadena. Si utiliza un vector con , se subestablecerá recursivamente, es decir, es equivalente a . Esta es una característica peculiar que pocos conocen, por lo que recomiendo evitarla a favor de , que aprenderá en la Sección 4.3.3.[[[[x[[c(1, 2)]]x[[1]][[2]]purrr::pluck()
Si bien debe usarlo cuando trabaje con listas, también recomendaría usarlo con vectores atómicos siempre que desee extraer un solo valor. Por ejemplo, en lugar de escribir:[[
for (i in 2:length(x)) { out[i] <- fun(x[i], out[i - 1]) } Es mejor escribir:
for (i in 2:length(x)) { out[[i]] <- fun(x[[i]], out[[i - 1]]) } Hacerlo refuerza la expectativa de que está obteniendo y estableciendo valores individuales.
4.3.2 $ \( es un operador abreviado: es aproximadamente equivalente a . A menudo se usa para acceder a variables en un marco de datos, como en o . Un error común es usarlo cuando tienes el nombre de una columna almacenada en una variable:x\)yx[[“y”]]mtcars\(cyldiamonds\)carat$
var <- “cyl” # Doesn’t work - mtcars\(var translated to mtcars[["var"]] mtcars\)var #> NULL
9 Instead use [[
mtcars[[var]] #> [1] 6 6 4 6 8 6 8 4 4 6 6 8 8 8 8 8 8 4 4 4 4 8 8 8 8 4 4 4 8 6 8 4 La única diferencia importante entre y es que hace (de izquierda a derecha) coincidencia parcial:\([[\)
x <- list(abc = 1) x$a #> [1] 1 x[[“a”]] #> NULL Para ayudar a evitar este comportamiento, recomiendo encarecidamente establecer la opción global en:warnPartialMatchDollarTRUE
options(warnPartialMatchDollar = TRUE) x\(a #> Warning in x\)a: partial match of ‘a’ to ‘abc’ #> [1] 1 (Para las tramas de datos, también puede evitar este problema mediante el uso de tibbles, que nunca hacen coincidencias parciales).
4.3.3 Índices faltantes y fuera de los límites Es útil entender lo que sucede cuando se utiliza un índice “no válido”. En la tabla siguiente se resume lo que sucede cuando se subestablece un vector lógico, una lista y con un objeto de longitud cero (como o ), valores fuera de los límites (OOB) o un valor faltante (por ejemplo, ) con . Cada celda muestra el resultado de subestablecer la estructura de datos nombrada en la fila por el tipo de índice descrito en la columna. Solo he mostrado los resultados para vectores lógicos, pero otros vectores atómicos se comportan de manera similar, devolviendo elementos del mismo tipo (NB: int = entero; chr = carácter).[[NULLNULLlogical()NA_integer_[[
row[[col]] Longitud cero OOB (int) OOB (chr) Desaparecido
Atómico Error Error Error Error
Lista Error Error NULL NULL
NULL NULL NULL NULL NULL
Si se nombra el vector que se está indexando, los nombres de OOB, faltante o componentes serán .NULL
Las inconsistencias en la tabla anterior condujeron al desarrollo de y . Cuando falta el elemento, siempre devuelve (o el valor del argumento) y siempre arroja un error. El comportamiento de hace que sea adecuado para la indexación en estructuras de datos profundamente anidadas donde el componente que desea puede no existir (como es común cuando se trabaja con datos JSON de API web). también permite mezclar índices enteros y de caracteres, y proporciona un valor predeterminado alternativo si no existe un elemento:purrr::pluck()purrr::chuck()pluck()NULL.defaultchuck()pluck()pluck()
x <- list( a = list(1, 2, 3), b = list(3, 4, 5) )
purrr::pluck(x, “a”, 1) #> [1] 1
purrr::pluck(x, “c”, 1) #> NULL
purrr::pluck(x, “c”, 1, .default = NA) #> [1] NA 4.3.4 y @slot() Hay dos operadores de subconfiguración adicionales, que son necesarios para los objetos S4: (equivalente a ) y (equivalente a ). es más restrictivo que en que devolverá un error si la ranura no existe. Estos se describen con más detalle en el Capítulo 15.@\(slot()[[@\)
4.3.5 Ejercicios Haga una lluvia de ideas de tantas maneras como sea posible para extraer el tercer valor de la variable en el conjunto de datos.cylmtcars
Dado un modelo lineal, por ejemplo, , extraer los grados residuales de libertad. A continuación, extraiga la R al cuadrado del resumen del modelo (mod <- lm(mpg ~ wt, data = mtcars)summary(mod))
4.4 Subconfiguración y asignación Todos los operadores de subconfiguración se pueden combinar con la asignación para modificar los valores seleccionados de un vector de entrada: esto se denomina subasignación. La forma básica es:x[i] <- value
x <- 1:5 x[c(1, 2)] <- c(101, 102) x #> [1] 101 102 3 4 5 Te recomiendo que te asegures de que es lo mismo que , y que es único. Esto se debe a que, si bien R se reciclará si es necesario, esas reglas son complejas (especialmente si contienen valores faltantes o duplicados) y pueden causar problemas.length(value)length(x[i])ii
Con las listas, puede utilizar para quitar un componente. Para agregar un literal, utilice :x[[i]] <- NULLNULLx[i] <- list(NULL)
x <- list(a = 1, b = 2) x[[“b”]] <- NULL str(x) #> List of 1 #> $ a: num 1
y <- list(a = 1, b = 2) y[“b”] <- list(NULL) str(y) #> List of 2 #> $ a: num 1 #> $ b: NULL La subconfiguración con nada puede ser útil con la asignación porque conserva la estructura del objeto original. Compare las dos expresiones siguientes. En el primero, sigue siendo un marco de datos porque solo está cambiando el contenido de , no en sí mismo. En el segundo, se convierte en una lista porque está cambiando el objeto al que está enlazado.mtcarsmtcarsmtcarsmtcars
mtcars[] <- lapply(mtcars, as.integer) is.data.frame(mtcars) #> [1] TRUE
mtcars <- lapply(mtcars, as.integer) is.data.frame(mtcars) #> [1] FALSE 4.5 Aplicaciones Los principios descritos anteriormente tienen una amplia variedad de aplicaciones útiles. Algunos de los más importantes se describen a continuación. Si bien muchos de los principios básicos de la subconfiguración ya se han incorporado a funciones como , , y , una comprensión más profunda de cómo se han implementado esos principios será valiosa cuando se encuentre con situaciones en las que las funciones que necesita no existan.subset()merge()dplyr::arrange()
4.5.1 Tablas de búsqueda (subestablecimiento de caracteres) La coincidencia de caracteres es una forma eficaz de crear tablas de búsqueda. Digamos que desea convertir abreviaturas:
x <- c(“m”, “f”, “u”, “f”, “f”, “m”, “m”) lookup <- c(m = “Male”, f = “Female”, u = NA) lookup[x] #> m f u f f m m #> “Male” “Female” NA “Female” “Female” “Male” “Male” Tenga en cuenta que si no desea nombres en el resultado, utilícelos para eliminarlos.unname()
unname(lookup[x]) #> [1] “Male” “Female” NA “Female” “Female” “Male” “Male” 4.5.2 Emparejamiento y fusión a mano (subconfiguración de enteros) También puede tener tablas de búsqueda más complicadas con varias columnas de información. Por ejemplo, supongamos que tenemos un vector de grados enteros y una tabla que describe sus propiedades:
grades <- c(1, 2, 2, 3, 1)
info <- data.frame( grade = 3:1, desc = c(“Excellent”, “Good”, “Poor”), fail = c(F, F, T) ) Luego, digamos que queremos duplicar la tabla para que tengamos una fila para cada valor en . Una forma elegante de hacerlo es combinando y subconfigurando enteros ( devuelve la posición donde cada uno se encuentra en el ).infogradesmatch()match(needles, haystack)needlehaystack
id <- match(grades, info$grade) id #> [1] 3 2 2 1 3 info[id, ] #> grade desc fail #> 3 1 Poor TRUE #> 2 2 Good FALSE #> 2.1 2 Good FALSE #> 1 3 Excellent FALSE #> 3.1 1 Poor TRUE Si coincide en varias columnas, primero deberá contraerlas en una sola columna (con, por ejemplo, ). Normalmente, sin embargo, es mejor cambiar a una función diseñada específicamente para unir varias tablas como , o .interaction()merge()dplyr::left_join()
4.5.3 Muestras aleatorias y bootstraps (subconfiguración de enteros) Puede utilizar índices enteros para muestrear o arrancar aleatoriamente un vector o marco de datos. Simplemente use para generar una permutación aleatoria de , y luego use los resultados para subestablecer los valores:sample(n)1:n
df <- data.frame(x = c(1, 2, 3, 1, 2), y = 5:1, z = letters[1:5])
10 Randomly reorder
df[sample(nrow(df)), ] #> x y z #> 5 2 1 e #> 3 3 3 c #> 4 1 2 d #> 1 1 5 a #> 2 2 4 b
11 Select 3 random rows
df[sample(nrow(df), 3), ] #> x y z #> 4 1 2 d #> 2 2 4 b #> 1 1 5 a
12 Select 6 bootstrap replicates
df[sample(nrow(df), 6, replace = TRUE), ] #> x y z #> 5 2 1 e #> 5.1 2 1 e #> 5.2 2 1 e #> 2 2 4 b #> 3 3 3 c #> 3.1 3 3 c Los argumentos de controlar el número de muestras a extraer, y también si el muestreo se realiza con o sin reemplazo.sample()
4.5.4 Ordenación (subconfiguración de enteros) order() toma un vector como entrada y devuelve un vector entero que describe cómo ordenar el vector subestablecido34:
x <- c(“b”, “c”, “a”) order(x) #> [1] 3 1 2 x[order(x)] #> [1] “a” “b” “c” Para romper los vínculos, puede proporcionar variables adicionales a . También puede cambiar el orden de ascendente a descendente utilizando . De forma predeterminada, los valores que falten se colocarán al final del vector; sin embargo, puede quitarlos con o ponerlos en la parte delantera con .order()decreasing = TRUEna.last = NAna.last = FALSE
Para dos o más dimensiones, la subconfiguración de enteros facilita el orden de las filas o columnas de un objeto:order()
13 Randomly reorder df
df2 <- df[sample(nrow(df)), 3:1] df2 #> z y x #> 5 e 1 2 #> 1 a 5 1 #> 4 d 2 1 #> 2 b 4 2 #> 3 c 3 3
df2[order(df2$x), ] #> z y x #> 1 a 5 1 #> 4 d 2 1 #> 5 e 1 2 #> 2 b 4 2 #> 3 c 3 3 df2[, order(names(df2))] #> x y z #> 5 2 1 e #> 1 1 5 a #> 4 1 2 d #> 2 2 4 b #> 3 3 3 c Puede ordenar vectores directamente con , o de manera similar , para ordenar un marco de datos.sort()dplyr::arrange()
4.5.5 Expansión de recuentos agregados (subconfiguración de enteros) A veces se obtiene un marco de datos donde se han contraído filas idénticas en una y se ha agregado una columna de recuento. y la subconfiguración de enteros hacen que sea fácil descolapsar, porque podemos aprovechar la vectorización s: repite veces.rep()rep()rep(x, y)x[i]y[i]
df <- data.frame(x = c(2, 4, 1), y = c(9, 11, 6), n = c(3, 5, 1)) rep(1:nrow(df), df$n) #> [1] 1 1 1 2 2 2 2 2 3
df[rep(1:nrow(df), df$n), ] #> x y n #> 1 2 9 3 #> 1.1 2 9 3 #> 1.2 2 9 3 #> 2 4 11 5 #> 2.1 4 11 5 #> 2.2 4 11 5 #> 2.3 4 11 5 #> 2.4 4 11 5 #> 3 1 6 1 4.5.6 Eliminación de columnas de marcos de datos (carácter) Hay dos formas de quitar columnas de un marco de datos. Puede establecer columnas individuales en:NULL
df <- data.frame(x = 1:3, y = 3:1, z = letters[1:3]) df$z <- NULL O bien, puede subestablecer para devolver solo las columnas que desee:
df <- data.frame(x = 1:3, y = 3:1, z = letters[1:3]) df[c(“x”, “y”)] #> x y #> 1 1 3 #> 2 2 2 #> 3 3 1 Si solo conoce las columnas que no desea, use las operaciones establecidas para determinar qué columnas mantener:
df[setdiff(names(df), “z”)] #> x y #> 1 1 3 #> 2 2 2 #> 3 3 1 4.5.7 Selección de filas en función de una condición (subconfiguración lógica) Debido a que la subconfiguración lógica le permite combinar fácilmente condiciones de varias columnas, es probablemente la técnica más utilizada para extraer filas de un marco de datos.
mtcars[mtcars$gear == 5, ] #> mpg cyl disp hp drat wt qsec vs am gear carb #> Porsche 914-2 26.0 4 120.3 91 4.43 2.14 16.7 0 1 5 2 #> Lotus Europa 30.4 4 95.1 113 3.77 1.51 16.9 1 1 5 2 #> Ford Pantera L 15.8 8 351.0 264 4.22 3.17 14.5 0 1 5 4 #> Ferrari Dino 19.7 6 145.0 175 3.62 2.77 15.5 0 1 5 6 #> Maserati Bora 15.0 8 301.0 335 3.54 3.57 14.6 0 1 5 8
mtcars[mtcars\(gear == 5 & mtcars\)cyl == 4, ] #> mpg cyl disp hp drat wt qsec vs am gear carb #> Porsche 914-2 26.0 4 120.3 91 4.43 2.14 16.7 0 1 5 2 #> Lotus Europa 30.4 4 95.1 113 3.77 1.51 16.9 1 1 5 2 Recuerde usar los operadores booleanos vectoriales y , no los operadores escalares de cortocircuito y , que son más útiles dentro de las sentencias if. Y no olvides las leyes de De Morgan,que pueden ser útiles para simplificar las negaciones:&|&&||
!(X & Y) es lo mismo que !X | !Y !(X | Y) es lo mismo que !X & !Y Por ejemplo, simplifica a , y luego a .!(X & !(Y | Z))!X | !!(Y|Z)!X | Y | Z
4.5.8 Álgebra booleana versus conjuntos (lógicos y enteros) Es útil ser consciente de la equivalencia natural entre las operaciones de conjunto (subconfiguración de enteros) y el álgebra booleana (subconfiguración lógica). El uso de operaciones de conjunto es más efectivo cuando:
Desea encontrar el primero (o el último).TRUE
Tienes muy pocas s y muchísimas s; una representación de conjunto puede ser más rápida y requerir menos almacenamiento.TRUEFALSE
which() permite convertir una representación booleana en una representación entera. No hay una operación inversa en la base R, pero podemos crear fácilmente una:
x <- sample(10) < 4 which(x) #> [1] 2 3 4
unwhich <- function(x, n) { out <- rep_len(FALSE, n) out[x] <- TRUE out } unwhich(which(x), 10) #> [1] FALSE TRUE TRUE TRUE FALSE FALSE FALSE FALSE FALSE FALSE Vamos a crear dos vectores lógicos y sus equivalentes enteros, y luego explorar la relación entre las operaciones booleanas y establecidas.
(x1 <- 1:10 %% 2 == 0) #> [1] FALSE TRUE FALSE TRUE FALSE TRUE FALSE TRUE FALSE TRUE (x2 <- which(x1)) #> [1] 2 4 6 8 10 (y1 <- 1:10 %% 5 == 0) #> [1] FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE TRUE (y2 <- which(y1)) #> [1] 5 10
14 X & Y <-> intersect(x, y)
x1 & y1 #> [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE intersect(x2, y2) #> [1] 10
15 X | Y <-> union(x, y)
x1 | y1 #> [1] FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE FALSE TRUE union(x2, y2) #> [1] 2 4 6 8 10 5
16 X & !Y <-> setdiff(x, y)
x1 & !y1 #> [1] FALSE TRUE FALSE TRUE FALSE TRUE FALSE TRUE FALSE FALSE setdiff(x2, y2) #> [1] 2 4 6 8
17 xor(X, Y) <-> setdiff(union(x, y), intersect(x, y))
xor(x1, y1) #> [1] FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE FALSE FALSE setdiff(union(x2, y2), intersect(x2, y2)) #> [1] 2 4 6 8 5 Cuando se aprende por primera vez la subconfiguración, un error común es usar en lugar de . Aquí no logra nada: cambia de subconfiguración lógica a entera, pero el resultado es exactamente el mismo. En casos más generales, hay dos diferencias importantes.x[which(y)]x[y]which()
Cuando el vector lógico contiene , la subconfiguración lógica reemplaza estos valores con mientras que simplemente elimina estos valores. No es raro usarlo para este efecto secundario, pero no lo recomiendo: nada sobre el nombre “que” implica la eliminación de valores faltantes.NANAwhich()which()
x[-which(y)] no es equivalente a : si es todo FALSO, será y sigue siendo , por lo que no obtendrá valores, en lugar de todos los valores.x[!y]ywhich(y)integer(0)-integer(0)integer(0)
En general, evite cambiar de subconfiguración lógica a entera a menos que desee, por ejemplo, el primer o último valor.TRUE
4.5.9 Ejercicios ¿Cómo permutaría aleatoriamente las columnas de un marco de datos? (Esta es una técnica importante en bosques aleatorios). ¿Puede permutar simultáneamente las filas y columnas en un solo paso?
¿Cómo seleccionaría una muestra aleatoria de filas de un marco de datos? ¿Qué pasaría si la muestra tuviera que ser contigua (es decir, con una fila inicial, una fila final y cada fila intermedia)?m
¿Cómo podría colocar las columnas en un marco de datos en orden alfabético?
4.6 Respuestas a la prueba Los enteros positivos seleccionan elementos en posiciones específicas, los enteros negativos dejan caer elementos; Los vectores lógicos mantienen los elementos en posiciones correspondientes a ; los vectores de caracteres seleccionan elementos con nombres coincidentes.TRUE
[ selecciona sublistas: siempre devuelve una lista. Si lo usa con un solo entero positivo, devuelve una lista de longitud uno. selecciona un elemento dentro de una lista. es una taquigrafía conveniente: es equivalente a .[[\(x\)yx[[“y”]]
Utilícelo si va a subestablecer una matriz, matriz o marco de datos y desea conservar las dimensiones originales. Casi siempre debe usarlo cuando se subestablece dentro de una función.drop = FALSE
Si es una matriz, reemplazará cada elemento por 0, manteniendo el mismo número de filas y columnas. Por el contrario, reemplaza completamente la matriz con el valor 0.xx[] <- 0x <- 0
Un vector de caracteres con nombre puede actuar como una tabla de búsqueda simple:
Éste trabajo se basa en la segunda edición de Advanced R, de Hadley Wickham: https://adv-r.hadley.nz/index.html↩︎
R Core Team, “Writing R Extensions,” R Foundation for Statistical Computing 2018, https://cran.r-project.org/doc/manual/r-devel/R-exts.html↩︎
R Core Team, “R Internals,” R Foundation for Statistical Computing 2018, https://cran.r-project.org/doc/manuals/r-devel/R-ints.html↩︎