library(arrow)
#>
#> Attaching package: 'arrow'
#> The following object is masked from 'package:utils':
#>
#> timestamp
library(dplyr)
#>
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#>
#> filter, lag
#> The following objects are masked from 'package:base':
#>
#> intersect, setdiff, setequal, union
library(summarisebig)summarisebig solve?Arrow is very good at filtering, transforming, grouping, and aggregating data without first loading an entire dataset into an R data frame. The difficult case is when a grouped calculation eventually needs ordinary R code that Arrow cannot execute directly.
summarise_big() uses the following hierarchy:
.strategy = "map_reduce" and let R perform only
the final combination.parallel_chunks or shared_chunk.The examples below use a tiny Parquet dataset so that they are safe to run in a vignette. The same interface is intended for datasets much larger than memory.
d <- data.frame(
grp = rep(c("A", "B", "C"), each = 4),
row_id = rep(1:4, 3),
x = c(1, 2, 4, 8, 10, 12, 14, 18, 3, 6, 9, 15),
y = c(2, 3, 5, 9, 9, 13, 15, 20, 5, 5, 11, 14),
txt = c("alpha", "bravo", "charlie", "delta",
"echo", "foxtrot", "golf", "hotel",
"india", "juliet", "kilo", "lima")
)
path <- tempfile("summarisebig-vignette-")
arrow::write_dataset(d, path, format = "parquet")
ds <- arrow::open_dataset(path)The ordinary strategies first try the entire expression in Arrow. For a supported grouped summary, raw observations never need to become an R data frame.
summarise_big(
ds,
result = mean(x),
.by = grp
) |>
arrange(grp)
#> # A tibble: 3 × 2
#> grp result
#> <chr> <dbl>
#> 1 A 3.75
#> 2 B 13.5
#> 3 C 8.25A fairly complicated expression can still remain entirely in Arrow if all its parts can be translated by Arrow. Complexity by itself is therefore not a reason to use MapReduce or materialization.
Sometimes the final statistic is awkward to express as one native Arrow grouped summary, but it can still be reconstructed from a small fixed set of sufficient statistics.
A useful example is simple linear-regression inference. Fitting
lm(y ~ x) in R requires the raw observations, but the
slope, its standard error, and its p-value can be reconstructed from six
compact group-level quantities: n, sum(x),
sum(y), sum(x^2), sum(y^2), and
sum(x*y).
summarise_big(
ds,
.by = grp,
.strategy = "map_reduce",
.map_reduce = list(
n = ~ dplyr::n(),
sum_x = ~ sum(x),
sum_y = ~ sum(y),
sum_x2 = ~ sum(x * x),
sum_y2 = ~ sum(y * y),
sum_xy = ~ sum(x * y)
),
.finalize = function(state) {
state |>
mutate(
centered_xx = sum_x2 - sum_x^2 / n,
centered_yy = sum_y2 - sum_y^2 / n,
centered_xy = sum_xy - sum_x * sum_y / n,
slope = centered_xy / centered_xx,
residual_ss = centered_yy - slope * centered_xy,
slope_se = sqrt((residual_ss / (n - 2)) / centered_xx),
t_value = slope / slope_se,
p_value = 2 * stats::pt(-abs(t_value), df = n - 2)
) |>
select(grp, slope, slope_se, p_value)
}
) |>
arrange(grp)
#> # A tibble: 3 × 4
#> grp slope slope_se p_value
#> <chr> <dbl> <dbl> <dbl>
#> 1 A 1 0 0
#> 2 B 1.33 0.118 0.00777
#> 3 C 0.829 0.206 0.0566Arrow performs the large-data reduction; only one compact row of
sufficient statistics per group enters ordinary R. The finalizer can
then use ordinary R functions such as stats::pt() without
materializing the original groups.
map_reduce does not discover the
decomposition automatically. The user supplies the Arrow-computable
building blocks and the R finalizer. This is often the most attractive
route after pure Arrow because the large-data work remains inside Arrow
and only a few numbers per group cross into R.
The point is not to reimplement statistics that Arrow already supports natively. For example, Arrow already has variance aggregation, so using MapReduce merely to reconstruct a variance would normally be unnecessary.
parallel_chunksSome summaries genuinely need the raw values. Here is an intentionally ordinary R function based on exact quantiles:
interquartile_span <- function(x) {
q <- stats::quantile(
x,
probs = c(0.25, 0.75),
names = FALSE,
type = 7
)
q[[2]] - q[[1]]
}We disable the Arrow fast path in this example so that the materialized route is exercised explicitly. Complete groups are packed into bounded chunks and separate chunks can be processed by different Mirai workers.
summarise_big(
ds,
result = interquartile_span(x),
.by = grp,
.strategy = "parallel_chunks",
.workers = 2,
.chunk_rows = 4,
.try_arrow = FALSE
) |>
arrange(grp)
#> # A tibble: 3 × 2
#> grp result
#> <chr> <dbl>
#> 1 A 3.25
#> 2 B 3.5
#> 3 C 5.25For expensive user-defined R functions this strategy can benefit substantially from multiple workers. For very cheap functions, parallel startup and scheduling overhead can dominate, so more workers are not automatically faster.
A useful decision rule is:
| Situation | Preferred route |
|---|---|
| Arrow can translate the complete expression | Arrow fast path |
| Final answer needs R but compact Arrow state is sufficient | map_reduce |
| Arbitrary R needs raw rows and throughput matters most | parallel_chunks |
| Arbitrary R needs raw rows and peak memory matters most | shared_chunk |
For the two materialized strategies, a complete statistical group is never split. Therefore a genuinely opaque R calculation still requires each individual group to fit in memory. If one group is itself too large, the calculation must be reformulated in Arrow or as a compact reduction if that is mathematically possible.