Recipe Group By - dathere/qsv GitHub Wiki
Tier: Intermediate
Commands used: sqlp, pivotp, sort, sortcheck, sample, fill, enum, luau, partition, foreach
Anchor dataset: Boston 311 (resources/test/boston311-100.csv)
You want to run an operation per group — number rows within each department, keep the latest two records per department, count or tally per group, sample from each group, or run any qsv command on each group and stitch the results back together ("split → apply → combine").
qsv has no single --groupby flag shared by every command (#1764).
It doesn't need one for most tasks: sqlp window functions, pivotp's group-by mode and a few
command-specific options cover them. This page maps each common "per-group X" to its recipe.
cp ~/GitHub/qsv/resources/test/boston311-100.csv . # 100 rows, 29 columns
qsv frequency -s department --limit 0 boston311-100.csvThe examples group by department (7 groups). In sqlp, _t_1 refers to the first input file.
| Per-group task | Recipe |
|---|---|
| Count rows per group |
pivotp --index <keys> (group-by mode) |
Number rows within each group (enum per group) |
sqlp + row_number() over (partition by …)
|
| Group size / running total on every row |
sqlp + count(*) over (partition by …)
|
| Frequency per group |
sqlp + group by <key>, <col>
|
Top/first/last N per group (slice per group) |
sqlp + qualify row_number() over (…) <= N
|
| Sort within groups / check sort within groups |
sort -s <keys>,<col> / sortcheck -s <keys>,<col>
|
| Reverse within groups |
enum + sqlp … order by <key>, rownum desc
|
| Sample N per group |
sample --stratified <col> N (or --cluster) |
| Fill empty cells from previous row in the same group | fill --groupby <keys> |
| Stateful per-group logic |
luau map with a Lua table keyed by group |
| Any qsv command per group |
partition + foreach --unify
|
When <on-cols> is omitted, pivotp does a group-by aggregation. With no --values it produces a count column.
qsv pivotp --index department boston311-100.csv
qsv pivotp --index department,source boston311-100.csv # multi-column keyPass --values <cols> --agg <sum|mean|median|…> for other aggregations. See pivotp help.
qsv sqlp boston311-100.csv "
select department, case_enquiry_id, open_dt,
row_number() over (partition by department order by open_dt) as seq
from _t_1
order by department, seq"# group size repeated on each row
qsv sqlp boston311-100.csv "
select department, case_enquiry_id,
count(*) over (partition by department) as dept_n
from _t_1"
# running count within each group
qsv sqlp boston311-100.csv "
select department, open_dt,
count(*) over (partition by department order by open_dt
rows between unbounded preceding and current row) as running
from _t_1
order by department, open_dt"qsv sqlp boston311-100.csv "
select department, case_status, count(*) as n
from _t_1
group by department, case_status
order by department, n desc"To get frequency's own output format (percentages, ranks, Other) per group, use the generic recipe in step 9.
QUALIFY filters on a window function without a subquery:
# latest 2 requests per department
qsv sqlp boston311-100.csv "
select department, case_enquiry_id, open_dt
from _t_1
qualify row_number() over (partition by department order by open_dt desc) <= 2
order by department, open_dt desc"Use = 1 for first-per-group, or between 3 and 5 for a middle slice.
Group-then-sort is just a sort whose first key is the group:
qsv sort -s department,open_dt boston311-100.csv > by_dept.csv
qsv sortcheck -s department,open_dt by_dept.csv && echo "sorted within groups"To reverse the original row order within each group, number the rows first:
qsv enum -c rownum boston311-100.csv |
qsv sqlp - "select * from _t_1 order by department, rownum desc"qsv sample --stratified department 2 --seed 42 boston311-100.csvGroups with fewer than N rows are returned whole. --stratified takes a single column. For a
multi-column stratum, build a composite key first (see Variations). --cluster <col> samples
whole groups instead of rows within them.
A Lua table keyed by the group value persists across rows:
qsv luau map dept_seq '
counts = counts or {}
counts[department] = (counts[department] or 0) + 1
return counts[department]
' boston311-100.csvThe same pattern works for per-group running sums, "changed since previous row in this group" flags, and so on. See Scripting (Luau / Python).
This is the general split → apply → combine. partition writes one file per group. foreach runs a
command per file. --unify stitches the outputs under one header, and --new-column records which
group each row came from.
rm -rf by_dept
qsv partition department by_dept boston311-100.csv
# build the file list right away, before any command writes caches into by_dept/
(echo file; ls by_dept/*.csv) > dept_files.csv
# per-department stats
qsv foreach file --unify --new-column dept_file --dry-run false \
"qsv stats -s 'latitude,longitude' {}" dept_files.csv |
qsv select dept_file,field,mean,stddev
# per-department frequency, in frequency's own format
qsv foreach file --unify --new-column dept_file --dry-run false \
"qsv frequency -s case_status {}" dept_files.csvImportant
-
foreachdefaults to--dry-run true(it only prints the commands). Pass--dry-run falseto execute. -
foreachtokenizes the command itself rather than through a shell. An unquoted argument containing characters other than letters, digits and_ . + / -gets split, so-s latitude,longitudebecomes-s latitude longitude. Quote such arguments inside the command string:-s 'latitude,longitude'. -
--unifyassumes every child prints CSV with a header row and the same columns. Commands that print a bare value (e.g.count) or change the header per group (e.g.transpose) don't combine cleanly. Usepivotpfor counts. -
statswrites*.stats.csvcache files next to its input, which is why the file list is built first.
Multi-column group key for single-column commands (partition, sample --stratified): derive a composite column first.
qsv sqlp boston311-100.csv "select *, department || '|' || source as dept_source from _t_1" > keyed.csv
qsv sample --stratified dept_source 1 --seed 42 keyed.csvMany groups: partition --limit <n> caps the number of simultaneously open files. For very large
inputs see Larger-than-RAM CSV.
-
sqlpandpivotprun on Polars and are multithreaded. Prefer them overpartition+foreach, which starts one process per group. -
pivotp's default--agg smartpicks an aggregation from the stats cache, computing streaming stats if needed. A priorstats --everythingormoarstatsrun gives it more signals.