DataFrames, Arrow and DuckDB
kwker.frame sorts, ranks and groups pandas DataFrames, Polars DataFrames and pyarrow Tables. You get back the type
you passed in, with the results that library would give.
Sort, top-k and argsort
import pandas as pd
import kwker.frame as sf
df = pd.DataFrame({"region": ["eu", "us", "eu", "us", "eu"],
"price": [3.5, 1.0, None, 7.25, 2.0]})
print(sf.sort(df, ["region", "price"], descending=[False, True])) # ORDER BY region, price DESC; the NaN row last
print(sf.top_k(df, "price", 2, descending=True)) # ORDER BY price DESC LIMIT 2
order = sf.argsort(df, "price") # the row order, int64
The same calls work on Polars and pyarrow:
import polars as pl, pyarrow as pa
import kwker.frame as sf
pdf = pl.DataFrame({"k": [3, None, 1, 2], "v": ["c", "x", "a", "b"]})
print(sf.sort(pdf, "k")) # Polars: nulls first by default
tbl = pa.table({"k": [3, None, 1, 2]})
print(sf.sort(tbl, "k", nulls_last=True).column("k")) # pyarrow: nulls last by default
Key columns can be:
- integers, floats and booleans
- datetimes, durations and dates
- strings and binary, including Arrow string views
- categoricals, dictionaries and Enums: categoricals sort by category order in pandas and Polars, Enums by their order
- any other Arrow type, through a dense rank
Pass threads=4 for large frames.
Group-by
import pandas as pd
import kwker.frame as sf
df = pd.DataFrame({"user": ["b", "a", "b", "a", "c"], "amount": [10.0, 2.0, 5.0, 4.0, 1.0]})
print(sf.group_by(df, "user", {"n": "size", "total": ("amount", "sum"), "p50": ("amount", "median")}))
This computes SQL's GROUP BY ... ORDER BY the keys: one row per distinct key tuple, in ascending key order. The
operations are sum, mean, min, max, count (non-missing values), size (rows), first, last and median
(exact).
Arrow arrays
import pyarrow as pa
import kwker
arr = pa.array(["pear", None, "apple", "fig"])
print(kwker.arrow_argsort(arr)) # nulls at the end, like pyarrow.compute.sort_indices
print(kwker.arrow_top_k(arr, 2, order="descending"))
These calls read Arrow memory directly through the C Data Interface, with no conversion. They accept:
- primitives, temporal and decimal types
- strings, binary and string views
- dictionaries
- structs, lists and unions
- run-end encoded arrays
- chunked arrays
DuckDB
import duckdb
import kwker.duck as sd
con = duckdb.connect()
rel = con.sql("SELECT range % 7 AS g, random() AS x FROM range(10000)")
top = sd.top_k(rel, ["x"], 5, descending=True, connection=con) # a DuckDB relation back
grp = sd.group_by(rel, ["g"], {"n": "size", "mean_x": ("x", "mean")}, connection=con)
print(grp.fetchall()[:2])
kwker.duck returns a DuckDB relation. Pass arrow=True to get a pyarrow Table instead.
Missing values, NaN and equal keys
Each library keeps its own rules:
| Library | Missing values | NaN |
|---|---|---|
| pandas | NaN, None, NaT and pd.NA last in both directions (na_) |
missing |
| Polars | nulls first unless nulls_ |
a value above every number |
| pyarrow | nulls last | after every number, in both directions |
| DuckDB | NULLs last unless you ask otherwise | the largest value |
- Every sort is stable: rows with equal keys keep their input order. pandas' default
sort_valuesand DuckDB's own sort are not stable, so compare against pandas withkind="stable". - Group-by: pandas skips NaN and a sum of nothing is 0; in Polars NaN propagates through sums and means; in pyarrow a
sum of nothing is null.
dropnacontrols the null-key group, and in DuckDB NULL keys form a group. - Decimal columns with up to 18 digits are aggregated exactly. Sums are decimals with 38 digits; means are floats in
Polars and DuckDB and decimals in pyarrow and pandas. The full rules are in
help(kwker.frame.group_by).
Related
- Order and ranking: argsort and multi-column sorts on NumPy arrays.
- Groups, merges and sets: totals per key without a DataFrame.
- Strings: the string orders a table sort can use.