The shape of it
tabl filter 'qc == "pass"' data.tsv \
| tabl mutate 'margin = (sales-cost)/sales' \
| tabl summarize -g region 'n=n()' 'avg=mean(margin)' \
| tabl arrange -avg \
| tabl view
- Row 1 is the header unless
-H (columns become V1, V2…).
- Missing reads as
"" / NA / na / NaN; computed values write NA, input cells pass through.
- Types are per-value:
age > 30 compares numerically, city == "NY" as text.
# lines are comments everywhere, counted on stderr; a #-prefixed header (#chrom start end) is understood. --keep-comments reads them as data.
- Every verb takes
-H and -h. FILE may be omitted or - for stdin.
Rows
filter EXPR | Keep rows where true; NA rows drop |
arrange SPEC | -col descends, NAs last, stable |
distinct [SPEC] | Unique; -a keeps all columns |
head / tail | -n N, default 10 |
view | Aligned print; -n rows buffered (1000000), -w cell width (40) |
tabl filter 'sales > 10 && region == "east"' d.tsv
tabl filter 'is_na(beta)' d.tsv
tabl arrange region,-sales d.tsv
tabl distinct -a region d.tsv
Columns
select SPEC | -e drops, -r regex; reorders too |
mutate N=EXPR… | Left to right; may replace a column |
rename NEW=OLD… | Header rewrite |
cols | Index + name, one per line |
tabl select name,region,sales d.tsv
tabl select -e cost d.tsv # all but cost
tabl select -r '^ctrl_' d.tsv
tabl mutate 'profit = sales - cost' \
'm = round(profit/sales, 3)' d.tsv
Column specs
sales | By name |
4 | 1-based index |
sales:cost | Inclusive range |
cost:sales | Reversed range |
name,3,jan:dec | Mixed, in output order |
-r '^s' | POSIX ERE on names |
Group & aggregate
tabl summarize -g region,year \
'n=n()' 'total=sum(sales)' \
'span=max(sales) - min(sales)' \
'cv=round(sd(x)/mean(x), 3)' d.tsv
tabl count -s region d.tsv # -s: by count desc
tabl summarize 'm=mean(x)' d.tsv # whole table
- One streaming pass, hashed groups — no pre-sort needed.
- Groups emit in first-seen order; pipe to
arrange to sort.
- Aggregates compose inside expressions, as above.
--grouped when rows of a group are already adjacent:
O(1) memory, results stream out. 5M rows / 2M groups —
558 MB → 5.8 MB. Unchecked; see below.
Aggregates (summarize only)
n() | Rows, NAs included |
sum mean | NA-skipping; empty sum is 0 |
sd var | Sample, n−1; NA if n<2 |
median | R type-7 |
quantile(x,p) | R type-7; literal p in [0,1] |
min max | NA-skipping |
first last | Input order |
n_distinct(x) | NA is its own level |
median, quantile, n_distinct retain values per group; the rest are O(1).
Reshape
# wide → long
tabl longer -c ctrl_1:treat_2 -n sample -v expr d.tsv
tabl longer -r -c '^treat' --drop-na d.tsv
# long → wide
tabl wider -n sample -v expr long.tsv
longer = tidyr's pivot_longer, wider = pivot_wider.
Both long names work as aliases, as do gather/spread.
- Defaults:
-n name, -v value. Unlisted columns become the id columns.
- Exact inverses on tidy data.
longer streams; wider must buffer.
wider: new columns in first-seen order; duplicate cells keep the last and warn.
Join
tabl join a.tsv b.tsv # left, shared keys
tabl join --inner -k region a.tsv b.tsv
tabl join -k 'sample=sample_name' a.tsv b.tsv
cmd | tabl join b.tsv # left = stdin
- Keys default to every column both headers share.
--left (default) · --inner · --full
- Output: left columns, then right's non-key columns; clashes get
_y.
- Multiple right matches multiply rows, as in dplyr.
Operators
^- !* / %+ -== != < <= > >=&&||
- Listed by precedence, tightest first.
^ is right-associative.
- Single
& | = work as the doubled forms.
- Backticks quote odd names:
`my col`.
- NA propagates.
&&/|| are three-valued: NA && FALSE is FALSE, NA && TRUE is NA.
Functions (row-wise)
abs sqrt exp | Elementary |
log(x[,base]) | Natural by default; log2 log10 |
round(x[,d]) | Also floor ceil |
pow(x,y) | Same as x ^ y |
min(…) max(…) | Across args (R's pmin/pmax) |
if_else(c,a,b) | NA when c is NA; alias ifelse |
is_na(x) | Never NA itself |
coalesce(…) | First non-NA |
num(x) str(x) | Force interpretation |
len substr | substr is 1-based |
cat(…) | Concatenate; alias paste |
upper lower | ASCII |
Recipes
# top row per group
tabl arrange region,-sales d.tsv \
| tabl summarize -g region 'best=first(name)'
# percent of group total
tabl summarize -g region 'tot=sum(sales)' d.tsv > t.tsv
tabl join d.tsv t.tsv \
| tabl mutate 'pct = round(100*sales/tot, 1)'
# long betas → probe × sample matrix
tabl select Probe_ID,sample_name,beta d.tsv \
| tabl wider -n sample_name -v beta
# bin a continuous variable
tabl mutate 'decile = floor(beta*10)/10' d.tsv \
| tabl count -s decile
# straight into a plot
tabl summarize -g dose 'm=mean(y)' \
'se=sd(y)/sqrt(n())' d.tsv \
| cinderplot -x dose -y m -o fig.pdf
From dplyr
group_by() | summarize -g, not a verb |
desc(x) | -x |
%>% | | |
mutate(a=1, b=2) | Separate quoted args |
summarise() | Works; so do sort gather spread |
- Summarized groups are not sorted — add
arrange.
- No windowed mutate (
lag, cumsum, grouped mutate) — use summarize + join.
From bedtools groupby
# bedtools groupby -i in.bed -g 1,4 -c 5 -o mean
tabl summarize -H -g 1,4 'mean=mean(V5)' in.bed
# several ops at once, no repeated -c/-o
tabl summarize -H -g 1,4 'mean=mean(V5)' \
'sum=sum(V5)' 'n=n()' in.bed
-H for headerless BED; columns become V1…Vn. -g
takes numbers directly; expressions use the V names.
- No pre-sort needed. bedtools groups adjacent rows and
silently splits a key that appears in two places; tabl hashes, so
one key is always one group.
- Add
--grouped to opt into bedtools' behaviour exactly —
adjacency, O(1) memory, streaming output, repeated groups if the
input isn't grouped. Nothing verifies the claim: checking would
need to remember every closed key, which is the hash table this
mode exists to skip, and a cheap order check would have to assume
a collation (byte order says chr10 < chr2).
- Ranges are
1:4, not bedtools' 1-4.
sd is the sample sd — bedtools' sstdev, not its stdev.
- Not yet built:
mode, antimode, collapse,
distinct, concat, freqasc/desc.
Speed
| group-by, 3 aggs | 0.42s vs awk 2.63s |
| numeric filter | 0.49s vs awk 1.78s |
| projection | 0.48s |
- 5M rows / 86 MB, Apple silicon, to
/dev/null.
- Streams: filter, select, mutate, longer, distinct, head, rename, summarize.
- Buffers: arrange, wider, join's right side, tail, view.
summarize holds O(distinct groups); --grouped makes it O(1).
- Lines split in place; group keys hashed, never sorted; column indices resolved at parse time.
No card matches that.