R dplyr - between() - Filter Between Values

The `between()` function in dplyr filters rows where values fall within a specified range, inclusive of both boundaries. The syntax is straightforward:

Key Insights

  • The between() function provides a cleaner, more readable alternative to complex logical expressions when filtering data within a range, automatically handling boundary conditions inclusively
  • between() integrates seamlessly with dplyr::filter() and works with numeric, date, and datetime types, making it essential for time-series analysis and numerical range queries
  • Performance-wise, between() is optimized for large datasets and avoids the pitfalls of chained comparison operators, particularly when dealing with NA values

Understanding between() Syntax

The between() function in dplyr filters rows where values fall within a specified range, inclusive of both boundaries. The syntax is straightforward:

library(dplyr)

# Basic syntax
between(x, left, right)

# In practice with filter()
df %>% filter(between(column_name, lower_bound, upper_bound))

The function returns TRUE when left <= x <= right, making it equivalent to x >= left & x <= right but more concise and readable.

# Create sample dataset
sales_data <- tibble(
  product_id = 1:10,
  price = c(15.99, 25.50, 42.00, 18.75, 55.00, 
            12.99, 38.50, 29.99, 45.00, 22.50),
  quantity = c(5, 12, 8, 15, 3, 20, 6, 10, 4, 18)
)

# Filter products priced between $20 and $45
sales_data %>%
  filter(between(price, 20, 45))
# A tibble: 5 × 3
  product_id price quantity
       <int> <dbl>    <dbl>
1          2  25.5       12
2          3  42          8
3          7  38.5        6
4          8  30.0       10
5         10  22.5       18

Comparing between() to Traditional Filtering

Traditional range filtering requires explicit comparison operators, which becomes verbose and error-prone:

# Traditional approach
sales_data %>%
  filter(price >= 20 & price <= 45)

# Using between() - cleaner
sales_data %>%
  filter(between(price, 20, 45))

# Multiple conditions without between()
sales_data %>%
  filter(price >= 20 & price <= 45 & quantity >= 5 & quantity <= 15)

# Multiple conditions with between() - more readable
sales_data %>%
  filter(between(price, 20, 45), between(quantity, 5, 15))

The between() approach reduces cognitive load when reading code and minimizes the risk of logical errors like using | instead of &.

Working with Date and DateTime Ranges

between() excels when filtering temporal data, a common requirement in analytics workflows:

library(lubridate)

# Create time-series dataset
transactions <- tibble(
  transaction_id = 1:100,
  transaction_date = seq(ymd("2024-01-01"), ymd("2024-04-09"), by = "1 day"),
  amount = runif(100, 10, 500)
)

# Filter Q1 2024 transactions
q1_transactions <- transactions %>%
  filter(between(transaction_date, ymd("2024-01-01"), ymd("2024-03-31")))

# Filter specific week
week_transactions <- transactions %>%
  filter(between(transaction_date, ymd("2024-02-12"), ymd("2024-02-18")))

# DateTime filtering with timestamps
events <- tibble(
  event_id = 1:50,
  timestamp = seq(ymd_hms("2024-01-01 00:00:00"), 
                  ymd_hms("2024-01-01 23:00:00"), 
                  by = "30 min"),
  event_type = sample(c("login", "purchase", "logout"), 50, replace = TRUE)
)

# Filter business hours (9 AM to 5 PM)
business_hours <- events %>%
  filter(between(hour(timestamp), 9, 17))

Handling NA Values

between() handles NA values gracefully, returning NA when the input value is NA:

# Dataset with missing values
incomplete_data <- tibble(
  id = 1:8,
  score = c(45, 67, NA, 82, 55, NA, 73, 61)
)

# between() preserves NA behavior
incomplete_data %>%
  filter(between(score, 50, 80))
# A tibble: 4 × 2
     id score
  <int> <dbl>
1     2    67
2     5    55
3     7    73
4     8    61

To explicitly handle NAs:

# Include rows with NA values
incomplete_data %>%
  filter(between(score, 50, 80) | is.na(score))

# Exclude NA and filter
incomplete_data %>%
  filter(!is.na(score) & between(score, 50, 80))

Combining between() with Other dplyr Verbs

between() integrates seamlessly with the entire dplyr ecosystem:

# Complex analytical pipeline
sales_analysis <- sales_data %>%
  filter(between(price, 20, 50)) %>%
  mutate(
    revenue = price * quantity,
    price_category = case_when(
      between(price, 20, 30) ~ "Low",
      between(price, 30, 40) ~ "Medium",
      between(price, 40, 50) ~ "High"
    )
  ) %>%
  group_by(price_category) %>%
  summarize(
    total_revenue = sum(revenue),
    avg_quantity = mean(quantity),
    .groups = "drop"
  )

# Using between() in conditional mutations
sales_data %>%
  mutate(
    discount_eligible = between(quantity, 10, 20),
    discount_rate = if_else(discount_eligible, 0.15, 0.05)
  )

Performance Considerations with Large Datasets

For large datasets, between() offers performance advantages:

# Generate large dataset
set.seed(123)
large_data <- tibble(
  id = 1:1e6,
  value = runif(1e6, 0, 1000)
)

# Benchmark comparison
library(microbenchmark)

microbenchmark(
  traditional = large_data %>% filter(value >= 250 & value <= 750),
  between_func = large_data %>% filter(between(value, 250, 750)),
  times = 100
)

The between() function typically performs comparably or slightly better than traditional operators while providing superior readability.

Practical Use Cases

Financial Data Analysis

# Stock price analysis
stock_data <- tibble(
  date = seq(ymd("2024-01-01"), ymd("2024-03-31"), by = "1 day"),
  price = cumsum(rnorm(91, 0, 2)) + 100,
  volume = sample(1000000:5000000, 91, replace = TRUE)
)

# Find trading days with moderate volatility
moderate_days <- stock_data %>%
  mutate(price_range = max(price) - min(price)) %>%
  filter(between(price, quantile(price, 0.25), quantile(price, 0.75)))

# High volume trading days within price range
high_volume_range <- stock_data %>%
  filter(
    between(price, 95, 105),
    between(volume, 3000000, 5000000)
  )

Customer Segmentation

# Customer database
customers <- tibble(
  customer_id = 1:500,
  age = sample(18:75, 500, replace = TRUE),
  annual_spend = rnorm(500, 5000, 2000),
  visits_per_year = rpois(500, 12)
)

# Target demographic: ages 25-45, moderate spenders
target_segment <- customers %>%
  filter(
    between(age, 25, 45),
    between(annual_spend, 3000, 7000),
    between(visits_per_year, 8, 20)
  ) %>%
  arrange(desc(annual_spend))

# Calculate segment statistics
target_segment %>%
  summarize(
    segment_size = n(),
    avg_spend = mean(annual_spend),
    avg_visits = mean(visits_per_year)
  )

Common Pitfalls and Solutions

Boundary Confusion

Remember that between() is inclusive on both ends:

# between() includes boundaries
tibble(x = c(5, 10, 15)) %>%
  filter(between(x, 5, 15))  # Returns all three rows

# For exclusive boundaries, use traditional operators
tibble(x = c(5, 10, 15)) %>%
  filter(x > 5 & x < 15)  # Returns only 10

Type Mismatches

Ensure consistent data types across boundaries:

# Incorrect - mixing types
# df %>% filter(between(numeric_col, "10", "20"))  # Error

# Correct - matching types
df %>% filter(between(numeric_col, 10, 20))

# Dates require proper conversion
df %>% filter(between(date_col, as.Date("2024-01-01"), as.Date("2024-12-31")))

The between() function streamlines range-based filtering in dplyr, reducing code complexity while maintaining performance. Its inclusive boundary behavior and NA handling make it the preferred choice for range queries across numeric, date, and datetime columns.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.