h2oplyr provides familiar transformation verbs while
keeping the observations inside an H2O cluster. The only
external runtime dependency is h2o. The
tidytable API inspires parts of the interface, but neither
tidytable nor dplyr participates in a
calculation. knitr and rmarkdown are optional
documentation-building dependencies.
The workflow is:
CSV file → H2OFrame → h2o_tbl → remote operations → H2OFrame
An h2o_tbl is a small R object containing a remote frame
handle and grouping column names. R assembles expressions and inspects
metadata; H2O processes the rows. Scalar reductions and category domains
can reach R, but no transformation uses a local data frame, matrix or
vector as its computational backend. For display only, the previews
below request a few scalar values and categorical labels. Displaying a
value necessarily brings that value to the reader; it does not change
where selection, aggregation, joins and reshaping run.
This tutorial is for R users familiar with selecting columns, filtering rows and grouping data. You will learn to inspect remote results, handle missing values, check joins and verify a reshape round trip. Java and a matching H2O client/server are the only external runtime requirements.
This tutorial uses measurements of 344 penguins from three species in
the Palmer Archipelago, collected during 2007–2009. The CSV is included
in the package, so examples do not depend on a future download or a
palmerpenguins installation.
| Practical question | Tools used below | Visible evidence |
|---|---|---|
| Which variables and rows do I need? | h_select(), h_filter(),
h_drop_na() |
Values, column names and row counts |
| How do I derive and summarize measurements? | h_mutate(), h_summarize(),
h_across() |
Derived units and species means |
| Can I combine and reshape without losing information? | h_left_join(), pivot verbs |
Preserved row counts and matching values |
| How do ordering and sampling affect the result? | Slice verbs, h_arrange(),
h_lag() |
Sample rows, tied maxima and lag values |
Reading the examples. Each transformation is followed by values returned from H2O and a green check based on an executed assertion. A preview is a small window into the result; the caption reports the full result dimensions.
Install the package archive using
install.packages(path, repos = NULL, type = "source") after
installing h2o. Start a matching H2O server explicitly:
# Run this yourself, once per session. Loading h2oplyr never starts a server.
h2o::h2o.init(nthreads = 2, max_mem_size = "2G")
library(h2oplyr)These helpers belong to this vignette, not to the package API.
preview_h2o() accepts a remote table, a title, a few column
names and a row limit (at most six). It prints HTML and returns no local
analytical table. Numbers use scalar H2O reductions; text uses the
categorical domain of a single selected value. H2O may internally cache
small metadata previews. This is a deliberately small, slow display
routine, not a data export facility. verify_output() stops
the render if its condition fails. All helpers are defined here before
their first use.
# Escape labels before placing them inside HTML.
html_text <- function(x) {
x <- gsub("&", "&", x, fixed = TRUE)
x <- gsub("<", "<", x, fixed = TRUE)
gsub(">", ">", x, fixed = TRUE)
}
# Show bounded remote values without creating a local data.frame.
preview_h2o <- function(x, title, columns = names(as_h2o(x)), rows = 4L) {
frame <- as_h2o(x)
stopifnot(inherits(frame, "H2OFrame"), rows >= 1L, rows <= 6L,
all(columns %in% names(frame)))
types <- unlist(h2o::h2o.getTypes(frame[, columns]), use.names = FALSE)
shown <- min(rows, nrow(frame))
cat('\n<div class="result"><h3>', html_text(title), '</h3>', sep = "")
cat('<p class="result-meta">H2OFrame · ', nrow(frame), ' rows × ',
ncol(frame), ' columns · showing ', shown, ' rows and ',
length(columns), ' columns</p><div class="table-scroll"><table><thead><tr>',
sep = "")
for (j in seq_along(columns)) {
alignment <- if (types[j] %in% c("int", "real")) ' class="num"' else ""
cat('<th scope="col"', alignment, '>', html_text(columns[j]), '</th>', sep = "")
}
cat('</tr></thead><tbody>')
for (i in seq_len(shown)) {
cat('<tr>')
for (j in seq_along(columns)) {
# Two-step indexing keeps the cell as a remote frame in H2O.
cell <- frame[i, ][, columns[j]]
stopifnot(inherits(cell, "H2OFrame"))
numeric_column <- types[j] %in% c("int", "real")
alignment <- if (numeric_column) ' class="num"' else ""
if (h2o::h2o.sum(is.na(cell)) == 1) {
value <- '<span class="missing">NA</span>'
} else if (numeric_column) {
number <- h2o::h2o.sum(cell)
value <- if (number == floor(number)) sprintf("%.0f", number) else sprintf("%.2f", number)
} else {
# Re-encode in H2O so the domain contains only this cell's label.
label <- h2o::h2o.levels(h2o::h2o.asfactor(h2o::h2o.ascharacter(cell)))
stopifnot(length(label) == 1L)
value <- html_text(as.character(label[[1L]]))
}
cat('<td', alignment, '>', value, '</td>', sep = "")
}
cat('</tr>')
}
cat('</tbody></table></div></div>\n\n')
invisible(NULL)
}
# Only a successful, evaluated assertion receives a green check.
verify_output <- function(condition, label) {
stopifnot(isTRUE(condition))
cat('\n<p class="check"><strong>Verified.</strong> ', html_text(label), '</p>\n\n', sep = "")
}# Import the bundled CSV and confirm its dimensions.
penguins <- h_penguins()
stopifnot(nrow(as_h2o(penguins)) == 344)| species | island | bill_length_mm | body_mass_g |
|---|---|---|---|
| Adelie | Torgersen | 39.10 | 3750 |
| Adelie | Torgersen | 39.50 | 3800 |
| Adelie | Torgersen | 40.30 | 3250 |
| Adelie | Torgersen | NA | NA |
Verified. 344 observations and 8 variables imported into H2O.
h_penguins() uploads the local CSV through
h2o.uploadFile() and declares "NA" as missing
in every column. Species, island and sex are categorical; measurements
are numeric. For your own files, upload directly with
h2o::h2o.uploadFile() and wrap the returned frame with
h2o_tbl().
The default table printer displays metadata only. This vignette adds
bounded value previews so you can inspect the results directly.
h_collect(penguins) and as_h2o(penguins) both
return the remote H2OFrame, preserving the backend
contract. This is an intentional change from the original standalone
prototype.
The eight variables are species, island, bill length and depth (mm),
flipper length (mm), body mass (g), sex and year. A missing measurement
appears as NA, not as zero. The same penguin can have one
measurement missing and others present.
Recognize the object. The result is an
H2OFrame wrapped by h2o_tbl. The small HTML
table above is a display of selected values, not a new data backend.
# Keep relevant columns, then rename and order the bill measurements.
bills <- h_select(penguins, species, starts_with("bill_"))
measurements <- h_select(penguins, where(is.numeric))
renamed <- h_rename(bills, bill_length = bill_length_mm)
relocated <- h_relocate(renamed, species, .before = bill_length)| species | bill_length_mm | bill_depth_mm |
|---|---|---|
| Adelie | 39.10 | 18.70 |
| Adelie | 39.50 | 17.40 |
| Adelie | 40.30 | 18 |
| Adelie | NA | NA |
| bill_length_mm | bill_depth_mm | flipper_length_mm | body_mass_g | year |
|---|---|---|---|---|
| 39.10 | 18.70 | 181 | 3750 | 2007 |
| 39.50 | 17.40 | 186 | 3800 | 2007 |
| 40.30 | 18 | 195 | 3250 | 2007 |
| species | bill_length | bill_depth_mm |
|---|---|---|
| Adelie | 39.10 | 18.70 |
| Adelie | 39.50 | 17.40 |
| Adelie | 40.30 | 18 |
| Adelie | NA | NA |
Verified. Selection, renaming and final column order match the requested schema.
Selection operates on column names and H2O schema types.
where() supports the base predicates
is.numeric, is.character and
is.factor; arbitrary predicates are deliberately rejected.
all_of() requires every requested column, while
any_of() allows absent names. Ranges such as
species:island and exclusions such as -year
are supported. A grouped selection retains its grouping columns.
Takeaway. Selection changes the schema, not the observations. All three previews retain the original 344 rows.
# Compare complete-case filtering with two measurement thresholds.
complete_penguins <- h_drop_na(penguins)
stopifnot(nrow(as_h2o(complete_penguins)) == 333)
cat("Complete observations:", nrow(as_h2o(complete_penguins)), "\n")
#> Complete observations: 333
large <- h_filter(penguins, body_mass_g >= 5000)
cat("Penguins weighing at least 5000 g:", nrow(as_h2o(large)), "\n")
#> Penguins weighing at least 5000 g: 67
bill_range <- h_filter(penguins, h_between(bill_length_mm, 40, 50))
cat("Penguins with bill length from 40 to 50 mm:", nrow(as_h2o(bill_range)), "\n")
#> Penguins with bill length from 40 to 50 mm: 190| species | bill_length_mm | body_mass_g | sex |
|---|---|---|---|
| Adelie | 39.10 | 3750 | male |
| Adelie | 39.50 | 3800 | female |
| Adelie | 40.30 | 3250 | female |
| Adelie | 36.70 | 3450 | female |
| species | body_mass_g |
|---|---|
| Gentoo | 5700 |
| Gentoo | 5700 |
| Gentoo | 5400 |
| Gentoo | 5200 |
| species | bill_length_mm |
|---|---|
| Adelie | 40.30 |
| Adelie | 42 |
| Adelie | 41.10 |
| Adelie | 42.50 |
Verified. Complete-case count and both filter boundaries checked over the full remote results.
A missing filter condition is dropped. The expression interpreter
propagates missing comparisons explicitly, because H2O’s raw comparison
behavior can differ from R’s. h_filter_out() negates the
combined predicate and also drops missing conditions. Use
is.na() when you intend to select missing measurements.
# Select the missing body masses explicitly.
missing_mass <- h_filter(penguins, is.na(body_mass_g))
stopifnot(nrow(as_h2o(missing_mass)) == 2)| species | bill_length_mm | body_mass_g |
|---|---|---|
| Adelie | NA | NA |
| Gentoo | NA | NA |
Verified. Both retained rows have a missing body mass.
Dropping all incomplete rows yields 333 penguins, whereas a mass-only analysis can retain 342. Those are different analysis populations. Replacing missing body mass with zero would invent a biologically impossible measurement, so this tutorial removes missing mass only where it is needed.
Takeaway. Choose the missing-data rule for the question: requiring every variable to be complete removes more rows than requiring body mass alone.
# Drop only missing mass, then derive units and a mass category.
prepared <- penguins |>
h_drop_na(body_mass_g) |>
h_mutate(
mass_kg = body_mass_g / 1000,
mass_band = h_case_when(
body_mass_g >= 5000 ~ "large",
body_mass_g >= 3500 ~ "medium",
TRUE ~ "small"
)
)
stopifnot(nrow(as_h2o(prepared)) == 342)
cat("Total measured mass (kg):",
h2o::h2o.sum(as_h2o(prepared)[, "mass_kg"]), "\n")
#> Total measured mass (kg): 1437| species | body_mass_g | mass_kg | mass_band |
|---|---|---|---|
| Adelie | 3750 | 3.75 | medium |
| Adelie | 3800 | 3.80 | medium |
| Adelie | 3250 | 3.25 | small |
| Adelie | 3450 | 3.45 | small |
Verified. 342 measured masses retained; the kg-to-g conversion agrees for every row.
Named mutations run sequentially, so a later expression can use a
newly created column. Arithmetic, comparisons, selected H2O mathematical
and text functions, and the package’s vector helpers are supported.
Calls to arbitrary R functions or other package namespaces raise an
error. h_transmute() retains only the new named columns.
Grouped mutation is not implemented: ungroup explicitly rather than
accidentally calculate a global value inside a group.
The native |> pipe and the package’s %.%
pipe can both chain these verbs.
Takeaway. Read the derived columns side by side. The numerical assertion checks every measured mass, not just the displayed rows.
# Compute counts and means inside H2O, grouped by species.
by_species <- h_summarize(
penguins,
observations = n(),
mean_mass_g = mean(body_mass_g, na.rm = TRUE),
mean_bill_mm = mean(bill_length_mm, na.rm = TRUE),
.by = species
)| species | observations | mean_mass_g | mean_bill_mm |
|---|---|---|---|
| Adelie | 152 | 3700.66 | 38.79 |
| Chinstrap | 68 | 3733.09 | 48.83 |
| Gentoo | 124 | 5076.02 | 47.50 |
Verified. Three groups account for all 344 observations.
The output has one row per species. n() counts all
observations; each mean removes missing values independently. Gentoo
penguins have the highest mean body mass in this sample. This is a
descriptive comparison, not a causal effect of species or island.
.by applies grouping to this call only. The equivalent
persistent grouping form is
h_group_by(penguins, species) |> h_summarize(...);
summaries themselves return ungrouped results. .add = TRUE
adds grouping columns, and h_group_vars() reports the
current grouping metadata.
Supported reductions are n(),
n_distinct(column), sum, mean,
min, max, sd, var
and median. Each reduction takes a column, optionally with
na.rm. Create transformed columns with
h_mutate() before aggregating. The adapter rejects
empty-input summaries explicitly.
Takeaway. Group sizes count penguins, while each mean uses its own non-missing measurements. A group count is not necessarily the denominator of every mean.
# Apply unit conversion and grouped means to multiple columns.
scaled <- h_mutate(
bills,
h_across(starts_with("bill_"), list(cm = function(z) z / 10))
)
average_bills <- bills |>
h_drop_na() |>
h_group_by(species) |>
h_summarize_across(starts_with("bill_"), list(average = mean))| species | bill_length_mm | bill_length_mm_cm | bill_depth_mm_cm |
|---|---|---|---|
| Adelie | 39.10 | 3.91 | 1.87 |
| Adelie | 39.50 | 3.95 | 1.74 |
| Adelie | 40.30 | 4.03 | 1.80 |
| Adelie | NA | NA | NA |
| species | bill_length_mm_average | bill_depth_mm_average |
|---|---|---|
| Adelie | 38.79 | 18.35 |
| Chinstrap | 48.83 | 18.42 |
| Gentoo | 47.50 | 14.98 |
Verified. Across conversion and the three grouped summaries checked.
h_across() builds column expressions with names such as
bill_length_mm_cm. The one-argument function body is
interpreted through the same remote expression rules as
h_mutate(). Summary function identity is used, so an alias
such as average = mean still computes the mean. Across
summaries default to na.rm = FALSE; this example
deliberately removes missing bill measurements first. The standalone
h_mutate_across() wrapper is retained for
compatibility.
Takeaway. Across applies the same expression to several columns; the suffix records the transformation or summary that produced each output.
# Attach a unique species summary to each observed mass.
enriched <- h_left_join(
prepared, by_species,
by = "species", relationship = "many-to-one"
)
stopifnot(nrow(as_h2o(enriched)) == nrow(as_h2o(prepared)))
enriched <- h_mutate(enriched, mass_minus_species_mean = body_mass_g - mean_mass_g)| species | body_mass_g | mean_mass_g | mass_minus_species_mean |
|---|---|---|---|
| Adelie | 2850 | 3700.66 | -850.66 |
| Adelie | 2850 | 3700.66 | -850.66 |
| Adelie | 2900 | 3700.66 | -800.66 |
| Adelie | 2900 | 3700.66 | -800.66 |
Verified. The join preserves all 342 measured penguins and matches a species mean to each.
The relationship check ensures the right table has at most one record
per species, preventing accidental multiplication of penguin rows.
Inner, left, right and full equijoins are available.
h_semi_join() selects left rows with a match;
h_anti_join() selects those without one. Named keys map
different column names, for example
by = c(species = "species_code").
A full join is implemented using a left join plus unmatched right
rows, all inside H2O. Missing-key matching follows H2O, and join row
order is unspecified. Always use h_arrange() when
subsequent calculations depend on ordering.
Takeaway. A valid many-to-one join must preserve the left row count. Sorting the preview makes it readable; join order itself is unspecified.
# Use species-level summaries so the ID is naturally unique.
wide <- h_summarize(
penguins,
bill_length_mm = mean(bill_length_mm, na.rm = TRUE),
bill_depth_mm = mean(bill_depth_mm, na.rm = TRUE),
.by = species
)
long <- h_pivot_longer(
wide, starts_with("bill_"),
names_to = "measurement", values_to = "mean_mm"
)
stopifnot(nrow(as_h2o(long)) == 6)
restored <- h_pivot_wider(long, measurement, mean_mm)
stopifnot(nrow(as_h2o(restored)) == 3)| species | bill_length_mm | bill_depth_mm |
|---|---|---|
| Adelie | 38.79 | 18.35 |
| Chinstrap | 48.83 | 18.42 |
| Gentoo | 47.50 | 14.98 |
| species | measurement | mean_mm |
|---|---|---|
| Adelie | bill_length_mm | 38.79 |
| Adelie | bill_depth_mm | 18.35 |
| Chinstrap | bill_length_mm | 48.83 |
| Chinstrap | bill_depth_mm | 18.42 |
| Gentoo | bill_length_mm | 47.50 |
| Gentoo | bill_depth_mm | 14.98 |
| species | bill_length_mm | bill_depth_mm |
|---|---|---|
| Adelie | 38.79 | 18.35 |
| Chinstrap | 48.83 | 18.42 |
| Gentoo | 47.50 | 14.98 |
Verified. Wide → long → wide preserves both mean measurements for every species.
The long result contains six species/measurement combinations.
Widening gets the output names from the remote categorical domain and
combines values with remote joins. Repeated ID/name combinations are
rejected: decide on an aggregation before reshaping. This avoids
silently choosing one measurement or creating a local list-column.
values_fill replaces missing result cells, including
explicitly missing input values.
For text transformations, h_separate() uses H2O
regular-expression splitting. h_unite() uses H2O
categorical interaction with encoded original underscores; its current
separator cannot contain dollar signs or backslashes. High-cardinality
interaction domains can consume substantial cluster memory.
Takeaway. The round-trip check compares both measurements across all species. Matching row counts alone would not prove that the values survived.
# Sample reproducibly, retain tied maxima and compute a sorted lag.
sample_penguins <- h_slice_sample(prepared, n = 12, seed = 2026)
stopifnot(nrow(as_h2o(sample_penguins)) == 12)
heaviest <- h_slice_max(prepared, body_mass_g, n = 3, with_ties = TRUE)
cat("Rows among the three heaviest positions, including ties:",
nrow(as_h2o(heaviest)), "\n")
#> Rows among the three heaviest positions, including ties: 4
ordered <- h_arrange(prepared, body_mass_g)
ordered <- h_mutate(ordered, previous_mass = h_lag(body_mass_g))| species | body_mass_g | bill_length_mm |
|---|---|---|
| Adelie | 2975 | 37.50 |
| Adelie | 3550 | 39.70 |
| Gentoo | 5550 | 50 |
| Adelie | 3350 | 35.50 |
| species | body_mass_g |
|---|---|
| Gentoo | 6300 |
| Gentoo | 6050 |
| Gentoo | 6000 |
| Gentoo | 6000 |
| species | body_mass_g | previous_mass |
|---|---|---|
| Chinstrap | 2700 | NA |
| Adelie | 2850 | 2700 |
| Adelie | 2850 | 2850 |
| Adelie | 2900 | 2850 |
Verified. Sample size is 12; the first lag is missing and every later lag matches the preceding mass.
Sampling uses H2O random columns. Sampling with replacement creates
draw indices in H2O and joins them to rows. With
seed = NULL (the default), no seed is passed to H2O; supply
a seed explicitly when reproducibility is needed. Identical seeds are
reproducible on the same frame and cluster layout; they are not a
promise of identical samples across versions or distributed layouts.
with_ties = TRUE can return more than n
rows.
Lag, lead, cumulative operations and h_fill() use
current row order. Penguins are not a longitudinal time series: the lag
example illustrates an ordered comparison, not a temporal
interpretation. Grouped windows and grouped slicing are rejected in this
release.
Takeaway. A seeded sample is reproducible within the documented setting. A lag is meaningful only after choosing an order; penguins here are not a longitudinal panel.
Every exported function has an executable example with an assertion. The package also includes regression tests for missing values, aggregation, ties, empty slices, join multiplicity, invalid expressions and input preservation. Integration tests compare H2O function bindings before and after execution without modifying the H2O namespace. A static audit rejects local-table calls in package functions. Additional maintainer-only download instrumentation is kept outside the package.
Run all examples and regressions against a cluster you started explicitly:
h2o::h2o.init(nthreads = 2, max_mem_size = "2G")
library(h2oplyr)
source(system.file("examples", "examples.R", package = "h2oplyr"))
for (nm in names(examples)) eval(examples[[nm]])
source(system.file("examples", "regressions.R", package = "h2oplyr"))This package is an explicit subset of tidy-style manipulation, not a
complete drop-in replacement for dplyr or
tidytable. It has no local rowwise/map backend,
non-equality joins, automatic list-column nesting, arbitrary R
callbacks, grouped mutation or implicit duplicate-cell pivot
aggregation. h_bind_rows() requires matching column-name
sets. h_nest_by() returns a list of remote group handles
and is intended for a modest number of groups.
When no cluster is connected, rebuilding this vignette retains the code but skips H2O execution. The distributed HTML was built against an explicitly connected server. The package never starts or shuts down a user’s cluster while loading, printing, or running its ordinary examples.
The green checks accompany the tutorial’s calculations. The installed package also contains 73 asserted API examples and semantic regression tests; those are a separate suite, not 73 examples all reproduced on this page. The commands in “Connect once” and “Validation” are instructions to run yourself, rather than claims that this HTML launched a cluster or reran the complete package test suite.
Executed with H2O. R 4.6.1; h2oplyr 0.2.6; h2o 3.44.0.3. All tutorial assertions completed during rendering.
Try changing the mass threshold or selecting another species, then
rerun the section and inspect both the values and the assertion. Do not
infer a successful transformation merely from seeing an object name or
from a silent stopifnot().
Data: Horst AM, Hill AP, Gorman KB (2020), palmerpenguins: Palmer
Archipelago (Antarctica) penguin data, doi:10.5281/zenodo.3960218.
The data are available under CC0. Measurements were collected by Kristen
Gorman and Palmer Station LTER. The research description is Gorman KB,
Williams TD, Fraser WR (2014), doi:10.1371/journal.pone.0090081.
The bundled CSV’s source, retrieval date and SHA-256 are recorded in
system.file("extdata", "PROVENANCE.md", package = "h2oplyr").