filter(): subconjuntos por condición

r
tidyverse
El verbo más usado de dplyr. Condiciones simples y compuestas, el gotcha de NA, operadores útiles (between, %in%, if_any/if_all) y trampas reales.

Por qué filter() es el verbo más usado

Si te pones a contar verbos en cualquier análisis real de R, filter() gana por goleada. La razón es simple: los datos crudos siempre contienen más de lo que necesitas para una pregunta concreta. Casi todo análisis empieza con “quédate solo con las filas donde…”.

En R base esto se hacía con [:

ventas[ventas$año == 2024, ]

Funcional pero ilegible cuando crecen las condiciones. filter() resuelve dos problemas a la vez: legibilidad y composición con el pipe.

La forma básica: una condición

library(dplyr)

ventas |>
  filter(año == 2024)

Lo que está dentro del paréntesis es una condición que se evalúa fila a fila. Cuando devuelve TRUE, la fila pasa. Cuando devuelve FALSE o NA, no pasa.

Operadores típicos:

Operador Significado
== Igual a
!= Distinto de
> < Mayor / menor que
>= <= Mayor o igual / menor o igual
%in% Pertenece al conjunto
!x Negación de x

Recordatorio: = es asignación, == es comparación. Confundirlos es uno de los errores tipográficos que más sufren los analistas que vienen de Excel.

Múltiples condiciones: las tres formas

Si quieres filtrar por más de una condición, tienes tres formas equivalentes:

# Forma 1: separadas por coma (AND implícito)
ventas |> filter(año == 2024, region == "Norte")

# Forma 2: con & explícito
ventas |> filter(año == 2024 & region == "Norte")

# Forma 3: en líneas separadas (cuando el filtro es largo)
ventas |> filter(
  año    == 2024,
  region == "Norte",
  unidades > 100
)

Las tres dan el mismo resultado. La convención del tidyverse es separar con comas: es ligeramente más legible y deja claro que cada condición es independiente.

Para OR, en cambio, hay que usar | explícito:

ventas |> filter(region == "Norte" | region == "Sur")

Que normalmente se simplifica con %in%:

ventas |> filter(region %in% c("Norte", "Sur"))

NA: el gotcha número uno

Esta es la trampa que sorprende a casi todo el mundo:

df |> filter(precio == NA)   # NO funciona como esperas

No devuelve las filas donde precio es NA. Devuelve cero filas, sin error. Porque en R, cualquier_cosa == NA evalúa a NA, no a TRUE o FALSE. Y filter() solo deja pasar TRUE.

La forma correcta:

df |> filter(is.na(precio))    # filas con NA
df |> filter(!is.na(precio))   # filas sin NA

Más sutil aún: cuando filtras por una condición sobre una columna que contiene NAs, esas filas con NA tampoco pasan:

df |> filter(precio > 100)   # NO incluye filas donde precio es NA

Esto es consistente con la regla anterior (cualquier comparación con NA da NA, no TRUE), pero te quema cuando esperabas que esas filas siguieran ahí. Si necesitas conservarlas, sé explícito:

df |> filter(precio > 100 | is.na(precio))

Operadores útiles para condiciones complejas

between(): rangos numéricos

ventas |> filter(between(precio, 100, 500))
# equivalente a: filter(precio >= 100, precio <= 500)

%in%: pertenencia a un conjunto

ventas |> filter(region %in% c("Norte", "Sur", "Este"))

str_detect(): patrones de texto

library(stringr)
ventas |> filter(str_detect(producto, "café|té"))

near(): comparación de floats con tolerancia

# Esto puede fallar por precisión de coma flotante:
df |> filter(precio == 0.1 + 0.2)   # ¡no devuelve filas donde precio == 0.3!

# Esto sí funciona:
df |> filter(near(precio, 0.3))

Si alguna vez te has preguntado por qué un filtro sobre números decimales no encuentra lo que crees que debería, esta es la respuesta.

if_any() y if_all(): filtrar a través de columnas

A veces quieres filtrar filas donde alguna de varias columnas cumple una condición, o donde todas la cumplen. Sin estos helpers, tendrías que escribir cada columna a mano.

# Filas donde CUALQUIER columna numérica tiene NA
df |> filter(if_any(where(is.numeric), is.na))

# Filas donde TODAS las columnas de respuesta están completas
df |> filter(if_all(starts_with("resp_"), ~ !is.na(.x)))

Este patrón es especialmente útil en preprocesado: detectar filas con datos perdidos sin escribir 14 is.na(columna_i) a mano.

Trampas habituales

  • filter(x == NA) devuelve cero filas en silencio. Usa is.na(x) siempre que quieras encontrar NAs.
  • Las filas con NA se eliminan silenciosamente al filtrar por la columna. Si filtras por precio > 100 y hay precios NA, las filas con NA también se van. Tenlo en cuenta si auditas pérdidas de filas en el pipeline.
  • Filtrar después de mutate() que crea NAs. Si calculas una nueva columna que produce algunos NA y luego filtras por ella sin pensar, pierdes filas que quizá no querías perder.
  • filter() no es subset(). Aunque hagan lo mismo en casos simples, subset() (R base) tiene comportamientos diferentes con scoping y se desaconseja en código de producción.

En la siguiente entrega

Has aprendido a quedarte con las filas que te interesan. El siguiente verbo, select(), se ocupa de las columnas. Es más simple que filter() pero tiene helpers que casi nadie conoce y que ahorran mucho código.