std.dataframe
Polars-backed analytical dataframes, expression plans and CSV/Parquet files. The optional, separately compiled engine is reused by every Plenty program. See installation and deployment and the tutorial. The daily sales report application combines ingestion, checked catalogue joins, row quarantine and typed exports.
Use clist[T] for a lightweight mutable columnar collection with a
compile-time class schema and Plenty arithmetic. Use std.dataframe for
analytical queries and file-backed datasets with runtime schemas. Neither
replaces ordinary lists.
import std.dataframe as df
def main() -> Result[(), df.DataFrameError | AllocError | IoError]:
data = df.frame([
df.column_str("station", ["north", "south", "north", "south"]?)?,
df.column_f64("reading", [12.0, 30.0, 18.0, 24.0]?)?
]?)?
recent = df.col("reading")?.gt(df.lit(15.0)?)?
keys = [df.col("station")?]?
averages = [df.col("reading")?.mean()?.alias("average")?]?
result = data.lazy()?.filter(recent)?.group_by(keys, averages)?.sort(
[df.SortKey(df.col("station")?)]?
)?.collect()?
print(result.values_str("station")?)?
print(result.values_f64("average")?)?
Ok(())
["north", "south"]
[18.0, 27.0]
Construction and extraction
column_i64(name: &str, values: &list[i64]) and
column_f64(name: &str, values: &list[f64]) return
Result[Column, DataFrameError]. Each makes one native call and copies the
borrowed numeric values into a native contiguous column. Names are copied;
inputs remain usable. No input pointer survives the call.
frame(columns: &list[Column]) -> Result[DataFrame, DataFrameError]
shares the columns’ native buffers. Duplicate names and unequal lengths fail.
An empty column list creates a zero-row, zero-column frame. Zero-length
typed columns retain their types. This engine supports at most
2**32 - 1 rows.
DataFrame.height() returns Result[i64, DataFrameError].
get_i64(name, row) and get_f64(name, row) return
Result[Option[i64], DataFrameError] and
Result[Option[f64], DataFrameError]. Bounds, missing names and mismatched
types fail; a valid null cell returns Ok(Nothing).
values_i64(name) and values_f64(name) return a Plenty list in a
Result. They allocate initialized Plenty storage, then copy the whole
column in one native call. A column containing any nulls fails with
DataFrameError.Null; use values_i64_nullable or values_f64_nullable
to preserve missing values, or fill_null when replacement is appropriate.
Strings, booleans, binary payloads, nullable input/output and schema inspection are described
in Column types and schemas. No implicit casts or
zero-copy borrowing occur at the Plenty boundary.
Logical Date and Datetime columns add checked temporal
interchange, parsing, formatting and calendar expressions.
Expression plans
Every operation below returns a Result[..., DataFrameError].
The signature index gives exact declarations.
| Operation | Result value |
|---|---|
col(name), lit(f64), lit_i64(i64), lit_str(text), lit_bool(value), lit_binary(payload: &[u8]), null_f64() | Expr |
when(predicate, then_value, otherwise), expr.cast(dtype, strict=True) | Expr |
expr.str_lower/str_upper/str_len(), str_contains/str_starts_with/str_ends_with(pattern) | Expr |
expr.n_unique(), expr.over(partition_by) | Expr |
expr.add/sub/mul/div/gt/eq/lt/ge/le(&other) | Expr |
expr.and_(&other), or_(&other), fill_null(&other) | Expr |
expr.sum/count/mean/min/max/is_null() | Expr |
expr.alias(name) | Expr |
frame.lazy(), scan_parquet(path) | Query |
query.filter(&predicate) | Query |
query.with_columns(&expressions), select(&expressions) | Query |
query.group_by(&keys, &aggregates) | Query |
query.join(&other, &left_keys, &right_keys, how=JoinKind.Inner, suffix="_right", nulls_equal=False) | Query |
query.join_asof(&other, "key", "key", strategy=AsOfStrategy.Backward, tolerance=Nothing, allow_exact_matches=True, suffix="_right") | Query |
query.sort(&keys), slice(offset, length), limit(count) | Query |
query.collect(), collect_streaming() | DataFrame |
scan_csv(path, ...) | Query |
frame.write_csv(path, ...), query.sink_parquet(path) | () |
frame.write_parquet(path) | () |
See expression contracts for conditional evaluation, explicit casts, string operations, distinct counts and partitioned windows.
Expressions describe native Polars operations; they are not opaque Plenty
callbacks. Polars can optimize a complete plan, push projections/filters
into Parquet scans, and use its worker pool. A scan is lazy; errors can
arise during construction or collection. with_columns replaces matching
names or adds new columns. Expressions in one call see the input schema;
chain calls when a later expression depends on a newly derived column.
Selection projects or aggregates. Grouping supports multiple keys and
aggregates, but does not promise group order. Add sort to the plan when
an application needs a particular order.
Owners and plans are immutable through this public API. Methods borrow their receivers; resulting plans clone native handles and share buffers. Dropping the original columns, frame or expressions does not invalidate a derived query. Each handle is freed by the engine that allocated it. There are no public raw pointers or user-created native handles.
Joins, ordering and row windows
For sorted integer keys, as-of joins attach the previous, next or nearest right row with an optional distance bound. Unlike equality joins, they produce at most one match for each left row.
These operations return new immutable query plans; neither input is consumed or mutated. They execute inside Polars, with no Plenty row callbacks or intermediate conversion into Plenty lists. Input owners, key expressions and sort specifications may be dropped after constructing the resulting plan.
JoinKind offers Inner, Left, Full, Semi and Anti equality joins.
Inner/left/full retain both sets of key columns; semi/anti are membership
queries returning only left columns. See equality join contracts
for duplicate/null matching, key retention, suffix collisions and validation.
SortKey(expression: Expr, descending: bool = False, nulls_last: bool = True)
owns its expression; constructing the wrapper itself is infallible.
query.sort(keys: &list[SortKey]) orders lexicographically, with the first
key most significant. Every key has independent direction and null placement.
Null placement is independent of direction: nulls_last=True puts nulls last
even in descending order. NaN follows Polars value ordering. Equal keys retain
their input order; that does not make ties from an unordered join/group
deterministic. Include an additional identifying key when that matters.
An empty sort list fails with Schema.
slice(offset: i64, length: i64) selects a row window.
The second argument is a count, unlike Plenty list slice(start, stop).
Negative offsets count from the end. The requested interval is intersected
with the available rows: slice(-2, 10) keeps the last two rows, while
slice(-100, 2) on a three-row frame is empty. Offsets beyond the end also
produce empty frames. length must be in 0..4294967295 inclusive;
negative or larger lengths fail immediately with DataFrameError.Bounds.
limit(count) is slice(0, count), with the same length checks.
Sort first when a page or top-N result needs a defined ordering.
See CSV and streaming I/O for file options, direct sinks, create-new output behavior and buffering limits.
Semantics and limits
Polars 0.55.2 semantics apply inside expressions, not Plenty scalar
semantics. Schemas are checked at runtime. Input/extraction supports
all Plenty numeric widths (i8/i16/i32/i64, u8/u16/u32/u64, f32/f64),
str, bool, Binary (std.bytes.Bytes), Date and naive/UTC Datetime,
including nullable columns.
Lists and structs have column-oriented construction and extraction,
native access expressions and structured schema inspection. Other unexposed native
types may pass through Parquet queries and appear in schema inspection, but cannot
be extracted through this API. Paths are UTF-8 strings. There is no automatic
lambda translation, mutation API, cloud credential configuration or stable
serialization of plans.
Null is missing data, distinct from floating-point NaN. Comparisons with
null yield null, which filtering drops. Polars orders NaN above finite
numbers: NaN > 50.0 may keep a row that a Plenty scalar comparison would
drop. Aggregations generally skip nulls; count counts non-null values and
returns i64. Floating-point sums may differ with execution order and
parallelism. Group order is unspecified. Native integer arithmetic and
reductions are not Plenty’s checked arithmetic: overflow must not be
assumed to return a recoverable error. Validate input ranges where needed.
DataFrameError retains native messages in Schema, Query, Io,
NativePanic, Null, and Bounds. Polars errors, including some file
errors, use Query; direct filesystem errors use Io.
Offset-table lengths and packed string byte totals are checked on the Plenty
side; an unrepresentable extent returns
DataFrameError.Allocation(AllocError.CapacityOverflow) before allocating the
affected table or advancing the total. This does not change the native engine’s
row limit or its allocation-failure policy. Copying a native
error message can itself fail with Allocation(AllocError) or
Message(IoError). Plenty-side list allocation is recoverable.
Native Rust/Polars allocation failure can abort the process.
Caught Rust panics become errors where unwinding is available; this does
not make arbitrary native failures recoverable.
POLARS_MAX_THREADS configures the engine’s global pool before first use.
It is independent of Plenty executors. Avoid oversubscribing both pools
when running many analytical queries concurrently.
Acknowledgements
- Wraps Polars 0.55.2, built into the optional dataframe engine library (MIT).