README.md

basetable

R-CMD-check Lifecycle: stable License: MIT

basetable is a fast in-memory data-manipulation package for R with a base-R interface and no dependencies. You write subset(), transform(), aggregate(), merge(), split(), and the work runs on the package's own C++ engine. There is no data.table, no dplyr, no Arrow underneath, and nothing in Imports beyond the base and recommended packages (parallel, stats, utils).

Every verb returns a basetable: an ordinary data.frame with one extra class so it prints compactly and [ keeps the class. as.data.frame() strips it back to a plain frame.

This is a deliberately focused tool. It is aimed at

Design

Installation

Install the released version from CRAN:

install.packages("basetable")

Install the development version from GitHub:

# install.packages("pak")
pak::pak("ielbadisy/basetable")

Minimal examples

library(basetable)

# nested
describe(
  transform(
    subset(mtcars, cyl == 6, select = c("mpg", "hp", "wt", "cyl")),
    power = hp / wt
  )
)

# pipe
mtcars |>
  pick(c("mpg", "hp", "wt", "cyl")) |>
  transform(power = hp / wt) |>
  aggregate(by = "cyl", value = c("mpg", "power"), fun = mean)

# table-1 style summary
summarytab(
  transform(mtcars, am = factor(am, labels = c("Automatic", "Manual"))),
  vars = c("mpg", "hp"), by = "am", p_value = TRUE
)

Performance

Timing and memory below come from the bench package at 1,000,000 rows on one Linux machine (inst/benchmarks/make-readme-figures.R regenerates the figures; the Benchmarks vignette has the full reproducible report). basetable is compared with data.table and dplyr.

Speed

Median runtime by engine at 1e6 rows

Memory

Memory allocated by engine at 1e6 rows

| Operation | basetable | data.table | dplyr | basetable mem | data.table mem | dplyr mem | | --- | ---: | ---: | ---: | ---: | ---: | ---: | | filter | 7 ms | 10 ms | 10 ms | 15 MB | 21 MB | 28 MB | | sort (string key) | 58 ms | 44 ms | 102 ms | 34 MB | 47 MB | 69 MB | | distinct | 5 ms | 8 ms | 13 ms | 0.03 MB | 20 MB | 12 MB | | count by group | 23 ms | 40 ms | 738 ms | 1 MB | 30 MB | 30 MB | | sd by group | 10 ms | 16 ms | 42 ms | 0.05 MB | 27 MB | 36 MB | | equi join | 16 ms | 15 ms | 66 ms | 8 MB | 8 MB | 101 MB | | semi join | 13 ms | 67 ms | 52 ms | 4 MB | 58 MB | 82 MB |

(equi join pins data.table to sort = FALSE, matching basetable::merge(), which returns rows in input order.)

basetable is faster than data.table on filter, distinct, grouped count, sd by group and semi join, and is level with it on equi join. Against dplyr it is faster on every operation here, by more than 30x on high-cardinality count. The one operation it loses is string sort.

Memory, ranked by advantage

basetable allocates the least (or tied least) on every operation measured. The size of the edge splits in two: overwhelming on grouped reductions, where the result is tiny and nothing intermediate is materialised in R; modest on operations that return a full table, where the output frame itself sets a floor.

| Operation | basetable | data.table | dplyr | basetable vs data.table | | --- | ---: | ---: | ---: | ---: | | distinct | 0.03 MB | 20 MB | 12 MB | ~700x less | | sd by group | 0.05 MB | 27 MB | 36 MB | ~500x less | | count by group | 1 MB | 30 MB | 30 MB | ~30x less | | semi join | 4 MB | 58 MB | 82 MB | ~15x less | | filter | 15 MB | 21 MB | 28 MB | ~1.4x less | | sort (string key) | 34 MB | 47 MB | 69 MB | ~1.4x less | | equi join | 8 MB | 8 MB | 101 MB | ~parity |

These are R-level allocations as reported by bench. The C++ engine also uses malloc'd scratch buffers (radix keys, per-thread row-position vectors) that bench does not count, so peak process memory during a sort or filter is higher than the figure above; data.table does the same.

The one gap is sorting: orderrows() is a stable parallel radix, ~20x faster than base order(), but still ~1.3x of data.table, whose hand-tuned parallel radix is the one operation basetable does not match.

Positioning

data.table is faster on some workloads (notably sorting) and has a far larger ecosystem; dplyr is the tidyverse standard. basetable is a good fit when you want:

Grouping is a by argument on the verb that needs it (aggregate(), count(), summaries(), transform(), subset(), samplerows(), firstby(), ...), not a stateful group_by(). The group is named at the call and never persists, so there is no ungroup() to forget.

Using basetable alongside dplyr and data.table

basetable reuses base-R verb names (subset(), merge(), transform(), split(), aggregate()) on purpose. It does not ship the dplyr-coined verbs (filter(), select(), mutate(), arrange(), summarise(), distinct(), glimpse(), ...), so it can be attached next to dplyr without shadowing its grammar. The two names it shares with dplyr are count() and pick(), kept because they read as base-style verbs; with both packages attached, whichever was attached last wins for those (and for the base-R names data.table also defines). Two fixes:

Operation dictionary

| Family | Exported functions | Base reference | | --- | --- | --- | | Row subsetting | subset() | base::subset() | | Column keep / drop / rename | pick(), drop(), renamecols() | [, names<-() | | Transformation | transform(), within() | base equivalents | | Ordering | orderrows() | order() | | Distinct / duplicates | uniquerows(), duplicaterows(), removeduplicates() | unique(), duplicated() | | Aggregation | aggregate(), count(), summaries() | aggregate(), table() | | Recoding | recode(), collapsevalues(), casewhen(), replacewhere() | ifelse(), switch() | | Joins | merge(), semimerge(), antimerge(), updatemerge(), crossmerge(), nonequimerge(), overlapmerge(), rangemerge(), rollingmerge() | merge() | | Row / column bind | rbindfill() | rbind() | | Split / apply | split(), applyby() | split() | | Reshaping | tolong(), towide(), reshape(), stack(), unstack() | base equivalents | | Completion | completegrid() | expand.grid() + join | | File I/O | btread(), btwrite(); aggregate() / count() / uniquerows() / freq() also take a file path | read.delim(), fused file to result | | Inspection | preview(), dims(), types(), headtail() | str(), dim(), head() | | EDA | describe(), missingness(), profile(), freq(), summarytab(), compare() | base summaries |

btread() memory-maps the file and, with lazy = TRUE, returns columns as ALTREP vectors parsed on first access. aggregate(), count(), uniquerows() and freq() accept a single file path as their first argument and fuse the parse with the grouping, so unused columns are never materialised.

Status

Every exported function has direct test coverage. Vignettes cover getting started, data manipulation, exploration, a complete function reference, and benchmarks. CI checks release R on Linux, macOS and Windows plus oldrel and devel.



Try the basetable package in your browser

Any scripts or data that you put into this service are public.

basetable documentation built on Sept. 18, 2026, 9:13 a.m.