Capítulo7 Transformación: Operaciones booleanas y lógicas

Fecha de la última revisión

## [1] "2026-09-09"

El tema proviene de los siguientes sitios.
English: https://r4ds.had.co.nz/transform.html

Español: https://es.r4ds.hadley.nz/05-transform.html


library(tidyverse)
library(datos)
head(vuelos)
## # A tibble: 6 × 19
##    anio   mes   dia horario_salida salida_programada atraso_salida
##   <int> <int> <int>          <int>             <int>         <dbl>
## 1  2013     1     1            517               515             2
## 2  2013     1     1            533               529             4
## 3  2013     1     1            542               540             2
## 4  2013     1     1            544               545            -1
## 5  2013     1     1            554               600            -6
## 6  2013     1     1            554               558            -4
## # ℹ 13 more variables: horario_llegada <int>, llegada_programada <int>,
## #   atraso_llegada <dbl>, aerolinea <chr>, vuelo <int>, codigo_cola <chr>,
## #   origen <chr>, destino <chr>, tiempo_vuelo <dbl>, distancia <dbl>,
## #   hora <dbl>, minuto <dbl>, fecha_hora <dttm>

Tema de este módulo:

  • Filtrar filas con filter()
  • Seleccionar columnas con select()
  • Agrupar con group_by()
  • Resumir con summarise()
  • Ordenar con arrange()
  • funciones Booleanas y lógicas

7.1 Agrupar por múltiples variables

Aquí se agrupa por día y mes

7.2 Operaciones lógicas boolean:

  • & “ampersand” == “conjunción”
  • “|” or = “o”
  • !, no incluye, excluir….
  • %in%, filtrar para múltiples valores
  • ==, es igual a… “exactamente”
  • “<” menor que
  • “>” mayor que
  • “<=” es menor o igual
  • “>=” es mayor o igual

7.3 Cómo piensa R una condición

Todo lo de este capítulo descansa sobre una idea sola: una condición no devuelve filas, devuelve TRUE y FALSE. Cuando escribes filter(vuelos, mes == 12), R evalúa mes == 12 para las 336,776 filas y obtiene un vector de 336,776 valores lógicos. Después filter() se queda con las filas donde ese vector dice TRUE.

Puedes verlo por separado, y vale la pena hacerlo la primera vez:

head(vuelos$mes == 12)          # TRUE / FALSE, uno por vuelo
sum(vuelos$mes == 12)           # cuántos TRUE hay: TRUE cuenta como 1
mean(vuelos$mes == 12)          # qué proporción son TRUE

Ese truco de sum() y mean() sobre una condición es de los más útiles del curso: sum() de un vector lógico cuenta los casos que la cumplen, y mean() da directamente la proporción, porque TRUE vale 1 y FALSE vale 0.

Los operadores lógicos combinan esos TRUE y FALSE. Su comportamiento se resume en una tabla pequeña que conviene tener clara:

x y x & y x \| y !x
TRUE TRUE TRUE TRUE FALSE
TRUE FALSE FALSE TRUE FALSE
FALSE TRUE FALSE TRUE TRUE
FALSE FALSE FALSE FALSE TRUE

Dos advertencias sobre la palabra “o”. En español, “quiero los vuelos de enero o de diciembre” significa uno u otro, y | hace exactamente eso. Pero en español también decimos “los vuelos de enero y de diciembre” queriendo decir lo mismo, y si se traduce literalmente a mes == 1 & mes == 12 el resultado son cero filas, porque ningún vuelo puede ser de enero y de diciembre a la vez. La traducción correcta de esa frase es |, o mejor, %in% c(1, 12).

& se evalúa antes que |, igual que la multiplicación antes que la suma. Así que a | b & c significa a | (b & c) y no (a | b) & c, que es una tabla completamente distinta. Cuando mezcles los dos operadores, pon paréntesis siempre, aunque creas que no hacen falta. Es gratis y evita un error que no da ningún aviso.

filter() con operadores lógicos: combinar condiciones.

  • & (y), | (o), ! (no / excluir).
  • %in% c(...): ¿el valor está en esta lista? (evita repetir ==).
  • between(x, a, b): equivale a x >= a & x <= b.
  • Comparaciones: ==, !=, <, >, <=, >=.

Nunca compares dos números con decimales usando ==. La computadora guarda los decimales de manera aproximada, así que sqrt(2)^2 == 2 devuelve FALSE. Para eso está near(x, y), de dplyr, que pregunta si dos números son iguales dentro de una tolerancia razonable. Con enteros, con texto y con factores, == funciona sin problema.

Y hay un tercer valor que casi nadie espera. Además de TRUE y FALSE, una comparación puede devolver NA, y lo hace siempre que uno de los lados sea NA. La lógica es correcta: si no sé cuánto se atrasó un vuelo, tampoco sé si se atrasó más de una hora. Por eso NA > 60 es NA, y por eso NA == NA también es NA (dos cosas que no conozco no tienen por qué ser iguales).

Esto tiene una consecuencia práctica muy concreta: filter() descarta los NA, porque solo conserva lo que es TRUE. Si quieres verlos, hay que pedirlo con is.na():

NA > 60          # NA
NA == NA         # NA
is.na(NA)        # TRUE, esta es la manera correcta de preguntar

sum(is.na(vuelos$atraso_salida))    # 8255 vuelos sin dato de salida

El tema de los valores faltantes tiene su propio capítulo a continuación.

Bollean, que incluye múltiples opciones %in%

No confundas = con ==. Un solo = asigna un valor, mientras que == compara si dos valores son iguales; dentro de filter() siempre se usa ==. Para comparar contra varios valores usa %in% en lugar de encadenar muchos ==. Encontrarás más errores frecuentes y cómo resolverlos en el capítulo Errores comunes (14).

vuelos %>%
  dplyr::select(anio, mes, dia, atraso_llegada, destino) %>%
  filter(destino == "SJU") %>%
  filter(mes %in% c(6:12) & dia == 25) %>%
  group_by(dia, mes) %>%
  summarise(atraso = mean(atraso_llegada, na.rm = TRUE))
## # A tibble: 7 × 3
## # Groups:   dia [1]
##     dia   mes atraso
##   <int> <int>  <dbl>
## 1    25     6  34.3 
## 2    25     7   3.58
## 3    25     8  11.1 
## 4    25     9 -15.2 
## 5    25    10  -3.77
## 6    25    11 -18.9 
## 7    25    12   3.57

Lee la cadena de arriba abajo, que es como está escrita: toma vuelos, quédate con cinco columnas, quédate con los vuelos a San Juan, quédate con los que sean del 25 de un mes entre junio y diciembre, agrupa por día y mes, y calcula el atraso promedio de llegada.

Fíjate en mes %in% c(6:12). El 6:12 genera el vector 6 7 8 9 10 11 12, y %in% pregunta si el mes de cada vuelo está en esa lista. Escribirlo con | habría requerido siete condiciones.


vuelos %>%
  dplyr::select(anio, mes, dia, atraso_salida, aerolinea) %>%
  group_by(mes, aerolinea) %>%
  filter(mes %in% c(1:6)) %>% # mes entre 1 y 6
  filter(aerolinea %in% c("AA", "UA")) %>% 
  summarise(atraso = mean(atraso_salida, na.rm = TRUE))
## # A tibble: 12 × 3
## # Groups:   mes [6]
##      mes aerolinea atraso
##    <int> <chr>      <dbl>
##  1     1 AA          6.93
##  2     1 UA          8.33
##  3     2 AA          8.28
##  4     2 UA          7.71
##  5     3 AA          8.70
##  6     3 UA         11.7 
##  7     4 AA         11.7 
##  8     4 UA         13.7 
##  9     5 AA          9.66
## 10     5 UA         12.3 
## 11     6 AA         14.6 
## 12     6 UA         20.3

Aquí hay algo poco habitual: el group_by() está antes de los filter(). Funciona, porque filter() respeta la agrupación y la mantiene, pero es confuso de leer. La convención es agrupar justo antes de resumir, que es cuando la agrupación hace falta.

Aprovecha para notar el orden general que se repite en casi todo el curso: seleccionar y filtrar primero, agrupar después, resumir al final. Filtrar antes de agrupar es además más rápido, porque el cálculo pesado se hace sobre menos filas.

7.3.1 Otra alternativa para filtrar

7.3.1.1 “|” or

vuelos %>%
  group_by(mes) %>%
  dplyr::select(anio, mes, dia, atraso_salida) %>%
  filter(mes == 1 | mes == 12) |>  # mes 1 o 12
  summarise(atraso = mean(atraso_salida, na.rm = TRUE))
## # A tibble: 2 × 2
##     mes atraso
##   <int>  <dbl>
## 1     1   10.0
## 2    12   16.6

Este es el caso del “o”. mes == 1 | mes == 12 conserva los vuelos de enero y también los de diciembre. Con dos valores, | se lee bien; con cinco, conviene pasarse a %in% c(1, 2, 11, 12, ...).

7.3.1.2 & el “y”

names(vuelos)
##  [1] "anio"               "mes"                "dia"               
##  [4] "horario_salida"     "salida_programada"  "atraso_salida"     
##  [7] "horario_llegada"    "llegada_programada" "atraso_llegada"    
## [10] "aerolinea"          "vuelo"              "codigo_cola"       
## [13] "origen"             "destino"            "tiempo_vuelo"      
## [16] "distancia"          "hora"               "minuto"            
## [19] "fecha_hora"
vuelos %>%
  dplyr::select(anio, mes, dia, atraso_salida, aerolinea) %>%
  filter(mes == 12 & dia == 25 & aerolinea == "AA")
## # A tibble: 78 × 5
##     anio   mes   dia atraso_salida aerolinea
##    <int> <int> <int>         <dbl> <chr>    
##  1  2013    12    25             2 AA       
##  2  2013    12    25            -4 AA       
##  3  2013    12    25             1 AA       
##  4  2013    12    25             0 AA       
##  5  2013    12    25            -3 AA       
##  6  2013    12    25            -2 AA       
##  7  2013    12    25            26 AA       
##  8  2013    12    25            -7 AA       
##  9  2013    12    25            -6 AA       
## 10  2013    12    25            21 AA       
## # ℹ 68 more rows

Y este es el caso del “y”. Las tres condiciones tienen que cumplirse a la vez, y cada una habla de una variable distinta, que es cuando & tiene sentido. Este mismo filtro se puede escribir separando las condiciones con comas, filter(mes == 12, dia == 25, aerolinea == "AA"), que es la forma que prefiere la documentación de dplyr.


7.4 Ejercicios

Como se seleccionaría los meses de diciembre pero desde la del 25 o más


7.5

!, excluye algo, En este caso estamos excluyendo las aerolíneas “AA” y “DL”

vuelos %>%
  dplyr::select(anio, mes, dia, atraso_salida, aerolinea) %>%
  filter(mes == 11 & !aerolinea %in% c("AA", "DL")) %>%
  group_by(aerolinea) %>%
  summarise(atraso = mean(atraso_salida, na.rm = TRUE))
## # A tibble: 14 × 2
##    aerolinea atraso
##    <chr>      <dbl>
##  1 9E         7.56 
##  2 AS         3.08 
##  3 B6         3.52 
##  4 EV         9.83 
##  5 F9        13.5  
##  6 FL        16.9  
##  7 HA        -5.44 
##  8 MQ         3.28 
##  9 OO         0.8  
## 10 UA         6.37 
## 11 US         0.576
## 12 VX         7.80 
## 13 WN        11.0  
## 14 YV        10.5

El ! niega, es decir, invierte los TRUE y los FALSE. Aquí se lee “que la aerolínea no esté entre AA y DL”.

Vale la pena mirar dónde está colocado: !aerolinea %in% c("AA", "DL") niega el resultado completo de %in%, que es lo que queremos. La colocación funciona porque %in% se evalúa antes que !. Aun así, muchos prefieren escribirlo con paréntesis, !(aerolinea %in% c("AA", "DL")), que dice exactamente lo mismo y no obliga a recordar la precedencia.

Después de un filtro con !, comprueba el resultado con unique(): si las aerolíneas que querías excluir ya no aparecen, el filtro hizo lo que creías. Es una comprobación de dos segundos que evita conclusiones equivocadas.


Ejercicios:

Hacer los ejercicios en la sección 5.2.4 del libro en español

5.2.4 Ejercicios

  1. Encuentra todos los vuelos que:
  1. Tuvieron un retraso de llegada de dos o más horas

  2. Volaron a Houston (IAH o HOU)

Cuáles son estos aeropuertos, Hou == Houston, IAH == George Bush, Texas

  1. Selecciona solamente los vuelos que fueron operados por United, American o Delta

  2. Selecciona solamente los vuelos que partieron en invierno del hemisferio sur (julio, agosto y septiembre)

  3. Selecciona solamente los vuelos excluyendo los días del 2 al 29 de cualquier mes

vuelos %>%
  filter(atraso_salida <=0 & atraso_llegada >=120)
## # A tibble: 29 × 19
##     anio   mes   dia horario_salida salida_programada atraso_salida
##    <int> <int> <int>          <int>             <int>         <dbl>
##  1  2013     1    27           1419              1420            -1
##  2  2013    10     7           1350              1350             0
##  3  2013    10     7           1357              1359            -2
##  4  2013    10    16            657               700            -3
##  5  2013    11     1            658               700            -2
##  6  2013     3    18           1844              1847            -3
##  7  2013     4    17           1635              1640            -5
##  8  2013     4    18            558               600            -2
##  9  2013     4    18            655               700            -5
## 10  2013     5    22           1827              1830            -3
## # ℹ 19 more rows
## # ℹ 13 more variables: horario_llegada <int>, llegada_programada <int>,
## #   atraso_llegada <dbl>, aerolinea <chr>, vuelo <int>, codigo_cola <chr>,
## #   origen <chr>, destino <chr>, tiempo_vuelo <dbl>, distancia <dbl>,
## #   hora <dbl>, minuto <dbl>, fecha_hora <dttm>
  1. Selecciona solamente los vuelos que llegaron más de dos horas tarde, pero no salieron tarde

  2. Selecciona solamente los vuelos que se retrasaron por lo menos una hora, pero repusieron más de 30 minutos en vuelo

  3. Selecciona solamente los vuelos que partieron entre la medianoche y las 6a.m. (incluyente)

  4. Otra función de dplyr que es útil para usar filtros es between(). ¿Qué hace? ¿Puedes usarla para simplificar el código necesario para responder a los desafíos anteriores?

between(x, izq, der) es un atajo equivalente a x >= izq & x <= der e incluye ambos extremos. Hace tus filtros más cortos y legibles.

Ojo con un detalle de este ejemplo concreto: la pregunta pide los vuelos entre la medianoche y las 6 de la mañana, y horario_salida está escrito como número de reloj (517 son las 5:17), no en minutos. Así que el límite es 600, las seis en punto, y no 360. Es exactamente el tipo de confusión que produce guardar una hora como si fuera un número corriente, y la razón por la que existe el capítulo de fechas y horas.

between() viene en dplyr, pero el paquete data.table tiene una función con el mismo nombre. Si has cargado data.table en la sesión, between() puede dejar de comportarse como esperas. En ese caso, dplyr::between().

  1. Determinar cual “aerolinea” tiene más vuelos

7.5.1 La función arrange()

arrange()

arrange(): ordenar las filas.

  • Qué hace: reordena el data frame según una o más columnas (ascendente por defecto).
  • desc(col): orden descendente (de mayor a menor).

En el primer bloque de abajo, arrange() se llama sin argumentos. Eso no ordena nada: devuelve la tabla tal cual, sin error y sin aviso. Es un buen ejemplo de código que parece hacer algo y no hace nada. Compáralo con el segundo bloque, donde arrange(desc(horario_salida)) sí ordena.

Dos cosas más sobre arrange(). La primera: acepta varias variables, y las aplica en orden de prioridad. arrange(aerolinea, desc(atraso_salida)) ordena alfabéticamente por aerolínea y, dentro de cada una, por atraso de mayor a menor. La segunda: los NA van siempre al final, ordenes como ordenes. Si buscas los valores extremos de una variable con faltantes, esa es la razón por la que la cola de la tabla está llena de NA en vez de los valores más grandes.


vuelos %>% 
  dplyr::select(horario_salida) %>% 
  arrange() |> 
  head(n=10)
## # A tibble: 10 × 1
##    horario_salida
##             <int>
##  1            517
##  2            533
##  3            542
##  4            544
##  5            554
##  6            554
##  7            555
##  8            557
##  9            557
## 10            558
vuelos %>% 
  arrange(desc(horario_salida)) |> 
  head(n=10)
## # A tibble: 10 × 19
##     anio   mes   dia horario_salida salida_programada atraso_salida
##    <int> <int> <int>          <int>             <int>         <dbl>
##  1  2013    10    30           2400              2359             1
##  2  2013    11    27           2400              2359             1
##  3  2013    12     5           2400              2359             1
##  4  2013    12     9           2400              2359             1
##  5  2013    12     9           2400              2250            70
##  6  2013    12    13           2400              2359             1
##  7  2013    12    19           2400              2359             1
##  8  2013    12    29           2400              1700           420
##  9  2013     2     7           2400              2359             1
## 10  2013     2     7           2400              2359             1
## # ℹ 13 more variables: horario_llegada <int>, llegada_programada <int>,
## #   atraso_llegada <dbl>, aerolinea <chr>, vuelo <int>, codigo_cola <chr>,
## #   origen <chr>, destino <chr>, tiempo_vuelo <dbl>, distancia <dbl>,
## #   hora <dbl>, minuto <dbl>, fecha_hora <dttm>

5.3.1 Ejercicios

Ordena vuelos para encontrar los vuelos más retrasados en salida.

Encuentra los vuelos que salieron más temprano.

Ordena vuelos para encontrar los vuelos más rápidos (que viajaron a mayor velocidad).

¿Cuáles vuelos viajaron más lejos?

¿Cuál viajó más cerca?

  • Otra función

    • desc()

7.6 2 Ejercicios:

Hacer los ejercicios en la sección 5.3.1 del libro en español



7.7 Ejercicios del capítulo

Sobre los ejercicios de este capítulo. Cada ejercicio se entrega con tres cosas: el script, el resultado (que el bloque corra) y una explicación en palabras. Los ejercicios usan diamantes del paquete datos, no vuelos, que es el conjunto con el que se enseña en el capítulo. La hoja completa para entregar está en Ejercicios/Ejercicios_Capitulo_07_booleanas.Rmd.

7.7.1 Ejercicio 1. Contar sin filtrar

Datos: diamantes. Sin usar filter(), y usando solo sum() y mean() sobre una condición, averigua cuántos diamantes cuestan más de 15,000 dólares y qué proporción del total representan.

Verificación: nrow(diamantes) es 53,940. Tu proporción tiene que quedar entre 0 y 1, y multiplicada por 53,940 tiene que devolver tu conteo.

En palabras: explica por qué sum() de una condición devuelve un conteo. ¿Qué valor numérico tienen TRUE y FALSE para R?

# Escribe tu código aquí

7.7.2 Ejercicio 2. & frente a |

Datos: diamantes. Construye dos tablas: una con los diamantes de corte "Ideal" y precio menor de 1,000; otra con los de corte "Ideal" o precio menor de 1,000.

Verificación: la tabla del & tiene que tener menos filas que la del |, y las dos tienen que tener menos de 53,940.

En palabras: un compañero dice “quiero los diamantes Ideal y Premium” y escribe filter(diamantes, corte == "Ideal" & corte == "Premium"). ¿Cuántas filas le devuelve y por qué? Escribe la versión correcta.

# Escribe tu código aquí

7.7.3 Ejercicio 3. Excluir con ! y %in%

Datos: diamantes. La variable color tiene 7 niveles. Construye una tabla que excluya los colores "D" y "E", usando ! y %in%.

Verificación: en tu tabla deben quedar exactamente 5 colores distintos (compruébalo con unique()), y las filas de tu tabla más las de la tabla complementaria tienen que sumar 53,940.

En palabras: escribe también filter(diamantes, !color == "D" & !color == "E") y comprueba que da el mismo resultado. ¿Cuál de las dos versiones prefieres y por qué?

# Escribe tu código aquí

7.7.4 Ejercicio 4. between()

Datos: diamantes. Quédate con los diamantes cuyo quilate esté entre 1 y 2, extremos incluidos, primero con >= y <= y después con between().

Verificación: las dos versiones tienen que devolver el mismo número de filas. En la tabla resultante, min(quilate) tiene que ser exactamente 1 y max(quilate) exactamente 2.

En palabras: ¿between() incluye o excluye los extremos? Explica cómo lo comprobaste con los datos, y no solo con la ayuda.

# Escribe tu código aquí

7.7.5 Ejercicio 5. La lógica de los NA

diamantes no tiene valores faltantes, así que constrúyete uno: x <- c(3, 7, NA, 12). Evalúa x > 5, x == NA, is.na(x), sum(x) y sum(x, na.rm = TRUE). Después usa filter() sobre un tibble que contenga x con la condición x > 5 y mira cuántas filas quedan.

Verificación: x > 5 tiene que devolver un NA en la tercera posición, no un FALSE. x == NA tiene que devolver cuatro NA. El filter() tiene que devolver 2 filas, no 3.

En palabras: explica por qué NA == NA no es TRUE, usando una frase que no mencione a R. Y explica qué pasó con la fila del NA en el filter().

# Escribe tu código aquí

7.7.6 Ejercicio 6. La precedencia

Datos: diamantes. Corre estas dos consultas y compara el número de filas:

  1. filter(diamantes, corte == "Ideal" | corte == "Premium" & precio < 500)
  2. filter(diamantes, (corte == "Ideal" | corte == "Premium") & precio < 500)

Verificación: las dos tienen que dar números muy distintos. En la versión (b), todos los precios tienen que ser menores de 500; en la (a), no.

En palabras: explica en palabras qué pregunta contesta cada una de las dos. ¿Cuál habrías escrito tú si te pidieran “los diamantes Ideal o Premium que cuesten menos de 500”?

# Escribe tu código aquí

7.7.7 Ejercicio 7 (BONO). Ordenar con dos claves

Datos: diamantes. Ordena la tabla por corte y, dentro de cada corte, por precio de mayor a menor. Quédate con las 15 primeras filas y con las columnas corte, quilate, color y precio.

Verificación: el precio máximo de todo el conjunto es 18,823 y el mínimo es 326. Si tu primera fila no tiene un precio cercano al máximo dentro de su corte, revisa el orden de tus argumentos.

En palabras: corte es un factor ordenado (Regular < Bueno < Muy bueno < Premium < Ideal). ¿En qué orden aparecen los cortes en tu resultado, alfabético o de calidad? Explica por qué, y qué habría pasado si corte fuera texto corriente.

# Escribe tu código aquí