tabl

latest

dplyr/tidyr verbs for TSV pipelines, in pure C.

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 EXPRKeep 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
viewAligned 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
colsIndex + 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

salesBy name
41-based index
sales:costInclusive range
cost:salesReversed range
name,3,jan:decMixed, 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 meanNA-skipping; empty sum is 0
sd varSample, n−1; NA if n<2
medianR type-7
quantile(x,p)R type-7; literal p in [0,1]
min maxNA-skipping
first lastInput 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 expElementary
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 substrsubstr is 1-based
cat(…)Concatenate; alias paste
upper lowerASCII

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 aggs0.42s vs awk 2.63s
numeric filter0.49s vs awk 1.78s
projection0.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.