polars_online¶
Streaming / online regression models for Polars.
Three entry points share one Rust core (see docs/PLAN.md):
ModelBank, chunk-fed, memory O(state) not O(data) – and as a plan,lf.online.fit_predict(specs), aLazyFramethat streams (df.online.fit_predict(specs)for a frame in memory);run(), or theonlineCLI: parquet, ipc, csv or ndjson in and out;the expression namespace,
pl.col("y").online.<model>(...), for a frame in memory only: polars calls it with the whole column in either engine, so every use warns (InMemoryExpressionWarning,polars_online._expr).
Errors follow one contract throughout, and each docstring says which of it
applies: a file that cannot be read or written is the OSError subclass
for what went wrong (FileNotFoundError, PermissionError, …), with
the path in the message; a value that is refused – a spec parameter, a
config key, what a column holds – is ValueError naming the spec and
the parameter or column; a wrong type is TypeError; a spec name or
position a bank has not got is KeyError or IndexError; a bank fed
from two threads at once is RuntimeError. Inside a polars plan the same
messages arrive as polars’ ComputeError. A refused chunk never changes
a bank, and a failed run never replaces an output or a state file.
- class polars_online.ModelBank(specs: Iterable[dict[str, Any]])[source]¶
Bases:
objectRuns a list of specs over ordered chunks, one state per (spec, group).
Chunks must arrive in stream order within each group (the clock must not run backwards unless the spec’s reset semantics say so). Feed
LazyFrame.collect_batches()for out-of-core streams.specsare the dicts thepolars_online.specbuilders make.ValueErrorwhen there are none, when two share a name, or when a dict is not a spec (the message names the field).A bank is one ordered stream, so it is not for two threads at once: a method that finds the bank in use on another thread raises
RuntimeErrorsaying so rather than interleave with it (fit_predict()releases the GIL while it works).predict()learns nothing, so any number ofpredictcalls may overlap; only afit_predictin flight refuses them, and they refuse it.- property specs: list[dict[str, Any]]¶
The specs this bank runs, as the dicts the builders made.
A state file carries them, so a bank loaded from one reports its specs without being told what they are – every field, including the ones left at their default. That is what makes a state file self-describing: with
groups(),output_fields()and the four diagnostic tables, a file can be walked by a caller who knows nothing about it in advance.A copy, and read-only. The bank’s behaviour comes from the Rust state built at construction, not from this list, so a mutation here could only disagree with it – and used to, silently: editing
bank.specs[0]["features"]in place leftcoef()labelling coefficients from a spec the bank was not running. Assigning tobank.specsraisesAttributeError, and mutating what it returns changes nothing.
- rows_seen() int[source]¶
Rows fed so far, over every chunk and group – skipped rows and dropped groups included, so not the sum of
groups().
- groups(spec: str | int | None = None) DataFrame[source]¶
The groups the bank holds state for: one row per (spec, group).
groupis the key as a string –""for a spec without agroupcolumn, null for rows whose key was null, as insolve_failures().rows_processedcounts the group’s rows that the null policy did not skip, andlast_clockis its last clock value (null before the first row, or on a row-count clock). State lives untildrop_groups()removes it, so this is how a long-running bank finds the groups that have gone quiet:stale = bank.groups().filter(pl.col("last_clock") < now - 30 * 86400) bank.drop_groups(stale["group"])
spec, a name or a position, narrows the table to one spec:KeyErrorfor a name the bank has not got (the message lists the names),IndexErrorfor a position it has not got.
- drop_groups(keys: Iterable[str | None], spec: str | int | None = None) int[source]¶
Forget the state of these groups, in every spec or in one, and return how many streams were dropped. Keys are as
groups()reports them; a key the bank does not hold is not an error, it just drops nothing. A dropped group starts cold if it appears again, exactly as a never-seen one would; nothing else in the bank changes, androws_seen()still counts the rows it was fed.specis as forgroups()(KeyError/IndexErrorfor one the bank has not got).
- fit_predict(df: DataFrame) DataFrame[source]¶
One chunk in; the chunk plus one struct column per spec out.
The struct is named after the spec (
out["m"]; its fields areoutput_fields()), andpredin it is out-of-sample: computed from the state before the row updates it. Chunk boundaries never change the numbers, only the cadence at whichcoefis reported.Raises
TypeErrorfor anything but aDataFrame(aLazyFrameis told to collect, or to feedfit_predict_batches()), andValueError– naming the spec and the column – when a column a spec reads (target, feature, clock, session, weight, group) is not in the frame; a target, feature, clock or weight column is not numeric (a datetime clock is refused rather than read as its epoch integer: cast it to the unithalflifeandmax_dclockare in); the clock has a null or non-finite value; a weight is negative (null skips the row); a spec is named like an input column, which the struct would replace; or a group’s clock runs backwards underon_clock_reset="error"(the other policies absorb it). A refused chunk leaves the bank exactly as it was, so the corrected chunk can be fed.RuntimeErrorwhen the bank is in use on another thread (class docstring).
- predict(df: DataFrame) DataFrame[source]¶
Score a frame against the bank as it stands, learning nothing.
Every row gets the struct
fit_predict()would give it as the next row of its group’s stream, computed from the current state; the bank is left exactly as it was, so the call is safe from any number of threads at once and row order does not matter. This is the serving side of a trained bank:loadonce,predictper request, andfit_predictthe rows later, in order, once their targets arrive.The frame needs each spec’s feature and clock columns. Target columns are optional – present, they give
residand the standardized residual; absent, those are null. The session column is optional and feedssession_gap; a weight column is not read. A trend model (holt) extrapolates over the clock distance from the row it last learned, capped bymax_dclock, and akalmanwithrevert_halflifeshrinks its coefficients over that distance exactly as the nextfit_predictrow would; the other coefficient models predict from their current coefficients regardless of the clock.Per field:
n_eff,lam_selected,sigma, the residual quantiles, autocorrelation and the metrics are the values the bank holds, frozen;coefis filled on the last accepted row (the same coefficients score every row);driftnever fires; rows of a group the bank has never seen, or without usable features, are null throughout, as a skipped row is infit_predict.Raises what
fit_predict()raises for the same frame – a missing or non-numeric column, a bad clock value – except that a missing target is not an error, andRuntimeErroronly when afit_predictis in flight on another thread.
- fit_predict_batches(batches: Iterable[DataFrame]) Iterable[DataFrame][source]¶
Lazily map
fit_predict()over an iterator of chunks: each is fed as the generator reaches it, solf.collect_batches()streams through the bank one chunk at a time. Whateverfit_predictraises for a chunk, this raises there; the chunks before it have been learned from.
- coef(spec: str | int | None = None, group: str | None = None) DataFrame[source]¶
The coefficients behind a fit: one row per (spec, group, instance, position), so a bank loaded from a state file answers “what are the betas?” without a row of data:
bank = po.ModelBank.load("state.bin") betas = bank.coef() # every spec that has coefficients just_one = bank.coef("ols") # or name one wide = betas.pivot("term", index=["group", "instance"], values="coef")
Like
last_row(),summary()anddescribe(), it takes every spec by default and leads with aspeccolumn, so the frames of several banks stack with a plainconcat. Sweeping this way skips a spec that has no coefficients – anew_cov, which emits statistics, or aseqtest, which emits evidence – since the question was “the coefficients in this bank” and those have none. Naming one of them still raises, because then the question was about that spec. A bank with no coefficients at all gives an empty frame.The values are what the output’s
coeffield reported on the last row each stream learned from: the fit after that row, which the next row’spredis computed from. Laid out bypolars_online.spec.coef_index():group,instanceAs
groups()andgram()report them: the key as a string (""for a spec without agroupcolumn) and the decay instance’s field suffix ("@h500", or""for a single one).n_effThe accumulated weight behind the fit – what the next row’s
n_efffield reports. The solve schedule (solve_every, default halflife/50 or every row forhalflife=inf, andmax_rows_between_solves) decides when a stream first solves, notmin_periods:predwaits formin_periods,coefdoes not, so a fit withn_effbelow it is over fewer rows than the spec asks for (a solve over fewer rows than terms is a jittered one, counted bysolve_failures()).position,target,ridge,feature_set,lambda,termcoef_index()’s columns:positionindexes the flatcoeflist,termis"intercept", a feature name, or"level"/"trend"forholt.coefThe value, in the features’ original units; null until the stream’s first solve, as
coefis on those rows.
specandgroupare as forgram():KeyError/IndexErrorfor a spec the bank has not got, and a group it has never seen gives an empty frame with the same columns.ValueErrorfor a namedew_covspec, which emits statistics, not coefficients, and for a namedseqtestspec, which emits evidence.
- last_row(spec: str | int | None = None, group: str | None = None) DataFrame[source]¶
The output struct as it stood on the last row each stream learned from: one row per (spec, group), the struct’s fields unnested after
specandgroup.It is the row
fit_predict()reported for that row, field for field –pred,resid,sigma, the metrics, the residual quantiles,n_eff, andcoefwhen the row carried it (a chunk’s last row does;coef()has the coefficients whichever row was last). It travels with the state, so a bank loaded from a file says how each model was doing without its output frame, and a directory of fits compares without keeping the last row of every output:fits = sorted(Path("fits").glob("*.bin")) table = pl.concat( [po.ModelBank.load(f).last_row().with_columns(file=pl.lit(f.name)) for f in fits], how="diagonal_relaxed", ) table.sort("ic_y", descending=True) # with emit_metrics=True
spec, a name or a position, narrows the table to one spec (KeyError/IndexErrorfor one the bank has not got, asgroups());groupto one group, and a group the bank has never seen gives an empty frame. Specs with different fields are stackeddiagonal_relaxed, so a field one spec has not got is null on its rows. A group with no learned row yet – every row skipped so far, or a state file written before 0.2.0 – is a row of nulls.predict()does not move it, and a chunk that ends in skipped rows leaves the row before them.
- summary(spec: str | int | None = None, group: str | None = None) DataFrame[source]¶
What each stream has been fed: one row per (spec, group).
Counts and ranges over every row routed to the group since its state began – undecayed, so they say what the model was trained on rather than what it still remembers – and kept in the state file, so a bank loaded from a file says it too. The columns:
spec,groupAs
groups()reports them.rows_fedRows routed to the group, skipped or not.
rows_processedRows the models saw: every feature and the weight usable.
rows_skippedrows_fed - rows_processed: a null, NaN, infinite or out-of-bound feature or weight.rows_learnedProcessed rows with a positive weight and, for a model with targets, at least one usable target.
rows_zero_weightProcessed rows with weight 0 (the clock moved; nothing learned).
weight_sumSum of the processed rows’ weights (1 per row without a weight column).
clock_min,clock_max,last_clockThe clock range fed and the last value; null on a row-count clock.
session_changesRows whose session differed from the previous row’s.
clock_backwardsRows whose clock fell below the previous row’s within a session (what
on_clock_resetdecided about).resetsRows at which
session_gap="reset"oron_clock_reset="reset_state"restarted the stream.
specnarrows to one spec (KeyError/IndexErrorfor one the bank has not got),groupto one group; a group never seen gives an empty frame. A state file written before 0.2.0 carries no summary: its groups reportspec,group,rows_processedandlast_clock, and nulls elsewhere – for good, since a count that began at the load would read as the whole history.predict()moves none of it, and feeding the same rows in one chunk or a thousand gives the same numbers to the bit.
- describe(spec: str | int | None = None, group: str | None = None) DataFrame[source]¶
Per-column statistics of what each stream has been fed: one row per (spec, group, input column), in spec order – features, then targets, then the weight column.
columnandrole("feature","target","weight") name the column;countandnull_countpartition the rows fed (a value counts when finite and within the input bound, as the models take it, and is a null otherwise – polars nulls, NaN, infinities and magnitudes beyond the bound alike);mean,std(sample,ddof=1; null below two values),minandmaxare over the counted values, undecayed and in row order, so chunking cannot move them. An unsupervised model lists no targets, anew_classlabel column has its counts only, and a comparison’s target is the difference of residuals it tests, named as the spec names it.specandgroupnarrow the frame as insummary(). A state file written before 0.2.0 lists its columns with every number null; seesummary().
- gram(spec: str | int, group: str | None = None) list[dict[str, Any]][source]¶
The EW accumulators behind a spec’s fit, per group and instance.
Returns one dict per (group, decay instance) with:
group,instanceThe group key as
groups()reports it (""for a spec without agroupcolumn,Nonefor a null key) and the instance’s field suffix ("@h500", or""for a single instance).columns,targetsWhat the axes mean: the spec’s features, with
"intercept"first when the spec has one (thetermnames ofpolars_online.spec.coef_index()), and the target names the per-target arrays are indexed by.targetsis empty forew_cov, which learns from none. They are what makes the mapping self-describing, sopolars_online.gramcan take a column by name.n_effAccumulated weight behind these moments.
n_kishKish’s effective sample size,
n_eff**2 / sum(w**2)– the number of equally weighted rows these moments are worth, and what a standard error computed from them divides by.n_effcounts weight, not rows, so it is not a sample size:(1 + lam) / (1 - lam)is the Kish size of an exponentially weighted window, whatever the halflife’s units.Nonebefore the first row.It is scale-free: decay divides
n_effandsum(w**2)by the same factor, son_kishdoes not shrink when a stream goes quiet. It says how many rows these moments average, not how old they are –n_effandtarget_weightsare what say that.meansEW column means, shape
(k,).comomentsCentered co-moments, shape
(k, k)– the EW analogue of a centeredX'X / n. Centered is what makes it accurate at large offsets (E11b).cross_momentsPer-target uncentered cross-moments
E[z*y], shape(n_targets, k). Empty forew_cov.target_weightsPer-target accumulated weight, shape
(n_targets,). Differs fromn_effwhen targets have different null patterns.target_means,target_varsPer-target EW mean and centered variance of the target itself, shape
(n_targets,), in the same arithmetic ascomoments– a target’s variance here is the variance anew_covover that column would report, to the bit. Empty forew_cov.target_n_kishPer-target Kish effective sample size,
target_weights**2 / sum(w**2)over that target’s rows;nanfor a target that has not seen a weighted row. Empty forew_cov.lags,lag_comomentsThe lags an
ew_cov(lags=[...])accumulates at, in the order given, and their cross-moments as an(L, k, k)array:lag_comoments[l][a][b]isE_w[d_a(t) * d_b(t - lags[l])], both deviations against the mean before the row (ENHANCEMENTS E56). BothNonefor a spec withoutlags. The matrix is not symmetric for a lag above zero – a leading b is not b leading a – and lag 0 would becomomentsexactly.
The target moments are what makes the export a complete sufficient statistic (ENHANCEMENTS E45). With the cross-moments alone there is no residual variance, no
R^2, no information criterion and no standard error to be had from a saved Gram, because every one of them needsVar[y]:beta = solve(raw, cross_moments[t]) # the model's own fit resid_var = target_vars[t] - beta[1:] @ comoments[1:, 1:] @ beta[1:] r2 = 1 - resid_var / target_vars[t]
Under a
windoweverything here is the window’s – the accumulators the fitcoefreports was solved from, sopo.gram.solveon it is that fit – except the target moments, which the window’s snapshots do not carry:target_means,target_varsandtarget_n_kishareNonethere rather than the whole history’s (review 2026-09-12, S19).A state saved before task 38 has none of them:
n_kish,target_means,target_varsandtarget_n_kishareNonethere, for that state’s whole remaining life. The weight sums behind them cannot be replayed, and asum(w**2)accumulated from the resume point against ann_efffrom the whole stream would report an effective size too large by the length of the history – a wrong number whereNoneis the true answer.The two moment forms differ, and mixing them gives a silently wrong answer rather than an error, so the bridging identity is worth stating plainly:
raw = comoments + np.outer(means, means) raw @ beta[t] == cross_moments[t] # up to the ridge term
Values are in the features’ original units. The intercept, when the spec has one, is column 0: a constant 1, so it has zero variance in
comomentsandraw[0] == means.Only models that keep a co-moment matrix report –
ewridge,lassoandew_cov. The others yield nothing:rlsandkalmantrack an inverse, and the gradient models keep no second moment at all.Why this exists (ENHANCEMENTS E30): the accumulators are the expensive part, and they are already exact, centered, decayed on the model’s own clock with session and
max_dclockhandling, and resumable. Anyone wanting to do something other than our solve with them – a custom penalty, an information criterion,cond(G), a scree plot, forward stepwise, orthogonal matching pursuit, or simply to check a fit by hand – previously had to recomputeX'Xfrom raw data in a second pass. These come from one pass over data that is never materialized, at every point in the stream rather than one, and they are the same matrices the deployed model solves against.This is not a speed claim: for a single batch Gram over materialized data, BLAS
dgemmis blocked, vectorized, and comfortably faster.specis a spec name or position (KeyError/IndexErrorfor one the bank has not got, as forgroups());groupnarrows the list to one group. A group the bank has never seen gives an empty list, as does a model that keeps no co-moments – neither is an error.Requires numpy, which is not a dependency of this package – polars does not require it either, and one optional accessor is no reason to put it on every install.
pip install polars-online[numpy]adds it; without it the call raisesModuleNotFoundErrorsaying so.
- marginal(spec: str | int, group: str | None = None) DataFrame[source]¶
The pairs a
marginalspec keeps: one row per (group, instance, feature, target), in spec order – groups sorted, targets in spec order, features in spec order within each.groupandinstanceAs
gram()reports them (""for a spec without agroupcolumn;""for a single decay instance, else the field suffix such as"@h500").feature,targetThe pair’s columns, by name.
n_effThe target’s accumulated weight
W_t: rows where the target was present, weighted and decayed. Differs from the struct’sn_effwhen targets have different null patterns.n_kishW_t^2 / Q_twithQ_tthe accumulated squared weight: the Kish effective sample size, the number of equally weighted rows that carry the same information. Null before the target’s first row.mean_x,var_x,mean_y,var_y,covThe pair’s EW moments – population form, over the decayed weights, so
varis never negative.corr,beta,tcov / sqrt(var_x var_y),cov / var_xandcorr * sqrt((n_kish - 2) / (1 - corr^2)); null untiln_effreaches the target’smin_periods, and null where undefined (a constant column,n_kish <= 2).
With
lags(E66), four more list columns and four numbers:lagcorr_xx,lagcorr_yyEach series’ own autocorrelation at the configured lags.
lagcorr_xy,lagcorr_yxThe feature now against the target
lrows back, and the reverse. A feature whoselagcorr_yx[0]exceeds itscorrleads the target; one whoselagcorr_xy[0]does follows it.n_serial,t_serialn_kishdivided by Bartlett’s serial-dependence factor, and the statistic against it. Null withoutserial_rule.phi_x,phi_yThe per-row decays fitted under
serial_rule="geometric"; null otherwise, and null when fewer than two kept lags are positive.
With
binsorbin_edges(E67), the nonlinear view. These columns are present whenever the spec asked for bins, holding empty lists and nulls until the edges are fixed:bin_edgesThe feature’s interior edges, fixed once and never moved. Ragged: a feature keeps only the bins it can support, so a binary feature has two bins and a constant one has a single bin.
bin_n,bin_mean_y,bin_var_yThe target’s weight, mean and variance inside each bin – the feature’s response curve. One more entry than
bin_edges, since the outer two bins are open. A bin no row has landed in hasbin_n = 0and null for the two moments.split_gain,split_atThe fraction of the target’s variance removed by the best single cut of the feature, and where that cut falls. Directly comparable with
corr ** 2, sosplit_gain - corr ** 2is the nonlinear surplus.split_gain_tThe
tacorrwould need to match that gain, againstn_serialwhere there is one. Optimistic, because the cut was chosen by maximising over the candidates – a ranking, not a p-value.
The pairs are read from the state, so a bank loaded from a file reports them as the bank that saved it would, and feeding the rows in one chunk or a thousand gives the same numbers to the bit. A group the bank has never seen gives an empty frame; a spec that is not a
marginalis refused (ValueError) – anew_cov’s moments are read withgram().specis a name or position (KeyError/IndexErrorfor one the bank has not got).
- closed_groups(spec: str | int | None = None, *, drop: bool = True) DataFrame[source]¶
The groups that have finished and not yet been read (docs/ENHANCEMENTS.md E54), oldest first, as one long frame.
A spec with
group_closeemits a group’s accumulators at the moment the bank can prove no further row will join it – a key smaller than the largest one fed so far under"monotone", or a session that has ended under"session"– and then drops the stream. That is what keeps a bank over an unbounded key space bounded: without it, every key ever seen stays in memory.One row per (group, decay instance), with the common columns
spec,group,instance,sessionWhich stream closed.
sessionis the value of the span that ended undergroup_close = "session", and null under"monotone".n_eff,n_kishAs
gram()reports them, at the moment of the close.rows_fed,rows_learned,clock_min,clock_maxThe span’s own
summary()counts and clock range.
and then a block per kind, present when any spec of the bank closes groups and is of that kind, null on the rows of other kinds:
columns,means,comoments,targets,target_means,target_vars,target_weights,target_n_kishandcross_momentsfor a kind that keeps accumulators;coeffor every kind that reports one;eig_valsandeig_vecsfor anew_covwithpca;pair_*for amarginal.The
pair_*block ismarginal()’s frame turned on its side:pair_featureandpair_targetname the pairs, and every othermarginalcolumn becomespair_<column>holding one entry per pair in the same order. A column that is a list per pair there – thelagcorr_*family withlags,bin_edges,bin_n,bin_mean_yandbin_var_ywithbins– is a list of lists here, and those blocks follow the same rule as the rest: present when any closingmarginalasked for them, null on the rows of one that did not. A nested list has no CSV form, so a closed frame with them is for parquet or the frame itself.comomentsis the upper triangle with the diagonal, row by row (k(k+1)/2numbers), andcross_momentsis row-major(n_targets, k).polars_online.gram.from_row()expands both and hands back exactly whatgram()would have returned for that group – field for field and bit for bit, since one builder makes them both. The exact solve on a closed group is then one line:po.gram.solve(po.gram.from_row(row))
eig_vecsare signed for continuity with the previous closed row of the same (spec, instance) – under"monotone"the previous group, and under"session"the same group’s previous close, never the previous chunk – so a component’s sign is stable along the sequence of closes and a sign flip between two rows is a real rotation rather than an eigensolver’s arbitrary choice.drop(the default) removes what it returns from the queue, which is what a driver draining per chunk wants;drop=Falsepeeks. The streams are dropped when they close, never when this is called: a bank’s memory must not depend on the caller polling. What is undrained is saved with the state, so a driver that saves between chunks does not lose rows silently.specnarrows the frame to one spec’s rows (a name or a position;KeyError/IndexErrorfor one the bank has not got).predict()never closes anything.
- solve_failures() dict[str, dict[str | None, int]][source]¶
Jittered or failed matrix factorizations so far, per spec and group.
A solve never returns NaN silently (docs/PLAN.md section 7): a near-singular system is retried with escalating diagonal jitter, and total failure keeps the previous coefficients. Both cases are counted here, so a nonzero value means the inputs are degenerate (constant or collinear features, or far too few observations for the feature count), not that anything crashed. Models that do not factorize – rls, kalman, ftrl – always report 0.
bocpdcounts rows here too: a row whose predictive could not be evaluated reports nulls and leaves the run-length posterior where it stands, and the count is what makes a run of them visible.
- output_fields() dict[str, list[str]][source]¶
Spec name -> the field names of its output struct, in order –
polars_online.spec.output_fields()for every spec in the bank. The fields are fixed by the spec, so this is the output schema before any row is fed.
- save(path: str | Path) None[source]¶
Versioned msgpack state; loads on any supported OS.
Written to a temporary sibling and renamed into place, so an interrupted save leaves the previous state where it was rather than truncating it (docs/IMPROVEMENTS.md C6). The rename is preceded by a filesystem sync, which is what a resumable file costs: ~4 ms on macOS, against ~0.5 ms for serializing 500 groups. Save every chunk and the sync dominates; save every hundredth and it disappears.
Raises the
OSErrorfor what went wrong –FileNotFoundErrorfor a directory that is not there,PermissionErrorfor one that cannot be written – with the path in the message; the file, if it existed, is untouched.RuntimeErrorwhile afit_predictis in flight on another thread.
- save_bytes() bytes[source]¶
What
save()writes, as bytes – for a store that is not a file (load_bytes()reads them back). This is also what pickle andcopy.deepcopycarry.RuntimeErrorwhile afit_predictis in flight on another thread.
- to_json(*, pretty: bool = True) str[source]¶
The state as JSON: everything
save()writes, in a form that can be read without this library.An export, not a second state format –
load()reads msgpack and only msgpack. Use it to look at a state, diff two of them, or hand one to something that is not Python.It refuses rather than lying.
serde_jsonwritesNaNand±infasnulland says nothing about it, where msgpack round-trips all three, so every export is read back and re-encoded and the msgpack must match the real state byte for byte. A state holding a value JSON cannot carry raisesValueErrornaming the problem instead of returning a file that is quietly wrong.RuntimeErrorwhile afit_predictis in flight on another thread, assave()does.
- save_json(path: str | Path, *, pretty: bool = True) None[source]¶
Write
to_json()topath.Plain
write_text, notsave()’s atomic rename: this is an export of a state that lives elsewhere, so a half-written one costs nothing but a re-run.
- classmethod load(path: str | Path, specs: Iterable[dict[str, Any]] | None = None) ModelBank[source]¶
A bank from a file
save()wrote, on this or any other OS.The file carries the specs, so none need be given; passing
specsasserts they are the file’s, which is how a resuming job checks that the state it found is the state of the bank it is about to run.Raises
FileNotFoundError(or theOSErrorfor what went wrong) when the file cannot be read, andValueErrorwhen it can but is not a bank this build loads: not a bank state file at all, written by a newer build (the file’s format or state schema version is above this build’s), orspecsdiffer from the file’s.
- classmethod load_bytes(data: bytes, specs: Iterable[dict[str, Any]] | None = None) ModelBank[source]¶
load()from the bytessave_bytes()gave, with the samespecscheck and the sameValueErrorfor bytes that are not a bank this build loads.
- polars_online.run(config: dict[str, Any] | str | Path | None = None, *, input: str | PathLike[str] | LazyFrame | DataFrame | Iterable[DataFrame] | None = None, output: str | PathLike[str] | None = None, no_output: bool = False, specs: Iterable[dict[str, Any]] | None = None, chunk_rows: int | None = None, load_state: str | PathLike[str] | None = None, save_state: str | PathLike[str] | None = None, closed_groups: str | PathLike[str] | None = None, predict: bool | None = None, input_format: str | None = None, output_format: str | None = None, keep_columns: Iterable[str] | None = None, progress: Callable[[int, int], object] | None = None) dict[str, int][source]¶
Stream rows through a model bank and write them out with its columns.
inputis a path (parquet, ipc, csv or ndjson, told from the extension or named byinput_format; globs and cloud URLs aspl.scan_*takes them), aLazyFrame(any query: the scan is polars’, with whatever options it needs), aDataFrame, or any iterable ofDataFrames in stream order – chunks from a database cursor, a socket, a generator.outputis a path in any of the four formats, told the same way. Leave it out – and out of the config – for a run whose product is its state: the per-row output of an accumulator-only spec is n_eff and nothing else, which over a billion rows is 8 GB of file written so it can be deleted (ENHANCEMENTS E50).save_stateis then required, since a run that writes nothing and saves nothing has done nothing.no_output=Truesays the same thing over a config that names an output, and is what the CLI’s--no-outputsets. An output that is written is written through a temporary and renamed into place, so a run that fails leaves the previous file where it was. CSV cannot hold the bank’s struct columns, so there each spec’s struct is flattened to<spec>.<field>columns and a list field (coef) becomes a JSON string –pl.col("ridge.coef").str.json_decode(pl.List(pl.Float64))reads it back.configis a dict, a path to a TOML file, orNoneto build the config from the keyword arguments. Keywords override whatever the config supplies, so a checked-in TOML can be reused with a different input:po.run("bank.toml", input="today.csv", output="today-out.parquet")
Returns
{"rows": ..., "chunks": ...}. Chunking never changes the numbers – it only trades memory for overhead – sochunk_rows(the reader’s chunk; frames passed in directly are taken as they come) is purely a resource knob. On data sorted by group, a chunk should span several groups: the bank fits groups in parallel within a chunk.keep_columnsselects input columns before the bank sees them (and before the scan reads them).progress(rows, chunks)is called after each chunk; raising in it stops the run without publishing the output.closed_groupswrites the groups that finished during the run to a sidecar file beside the output, in the format its extension names (ENHANCEMENTS E54; seeModelBank.closed_groups()for the schema). It needs a spec withgroup_closeand refusespredict, which closes nothing. The file is written once, at the end, through a temporary renamed into place – beforesave_state, so a state file always has the closed rows that go with it.outputmay be left out at the same time: that is the accumulate-only pass whose product is the closed groups. A run in which nothing closed writes an empty frame with the schema.predict=Truescores instead of learning: every row gets what the bank loaded fromload_statepredicts for it as it stands (ModelBank.predict()), and the bank is not updated – so it needsload_stateand refusessave_state. One TOML can serve both runs: the keyword drops the config’ssave_state, which belongs to the learning run, unlesssave_state=is passed alongside it.What is wrong with the call or the config is
ValueError, before a row is read: no input; no specs; a spec the bank refuses (ModelBank); a key the config, a spec or its model has not got, named with the keys there are (a misspelt key is never kept at its default in silence);chunk_rowsbelow 1; a format that cannot be told from a path’s extension, or that is not one of the four;predict=Truewithoutload_state, or withsave_state; an iterable that produced no frames; aload_statethat is not a bank this build loads or whose specs are notspecs; and a TOML that does not parse (tomllib.TOMLDecodeError).TypeErrorfor aconfigthat is none of the three, aprogressthat is not callable, or an item ofinputthat is not aDataFrame. A file fails as theOSErrorfor what went wrong, with the path in the message: aconfigorinputthat is not there (the scan is polars’, so itsFileNotFoundError), aload_statethat cannot be read, anoutputwhose directory is not there, and asave_statewhose directory is not – found out before the run, since after it the output would be written and the state lost. A column the specs read that the input has not got, or thatkeep_columnsdropped, is the bank’sValueError(akeep_columnsname the input has not got is polars’ColumnNotFoundError); a value the bank refuses – a null clock, a negative weight, a clock running backwards – is itsValueErrormid-run. Whatever stops the run – the bank, the writer,progressor the iterable raising (both come through as themselves) – leaves the previousoutputwhere it was andsave_stateunwritten: the state is saved last, after the output is in place, so a state file always has an output to go with it.
- polars_online.fit_predict(frame: LazyFrame, specs: Iterable[dict[str, Any]] | None = None, *, load_state: str | PathLike[str] | None = None, save_state: str | PathLike[str] | None = None, closed_groups: str | PathLike[str] | None = None, chunk_rows: int | None = None) LazyFrame[source]¶
- polars_online.fit_predict(frame: DataFrame, specs: Iterable[dict[str, Any]] | None = None, *, load_state: str | PathLike[str] | None = None, save_state: str | PathLike[str] | None = None, closed_groups: str | PathLike[str] | None = None, chunk_rows: int | None = None) DataFrame
frame.online.fit_predict(...)as a plain function, so that a type checker can see it.A
LazyFramegives a plan that streams the rows through a bank when it runs (LazyFrameOnlineNamespace.fit_predict()); aDataFramegives the frame with the bank’s columns (DataFrameOnlineNamespace.fit_predict()).load_statestarts the bank from a saved one andsave_statewrites where it ends up;chunk_rowsis the plan’s read chunk, and a frame already in memory is fitted in one call.TypeErrorfor aframethat is neither; otherwise raises what the namespace method does.
- polars_online.predict(frame: LazyFrame, bank: ModelBank | str | PathLike[str], *, chunk_rows: int | None = None) LazyFrame[source]¶
- polars_online.predict(frame: DataFrame, bank: ModelBank | str | PathLike[str], *, chunk_rows: int | None = None) DataFrame
frame.online.predict(bank)as a plain function, so that a type checker can see it.Scores the rows against
bankas it stands and learns nothing: a plan from aLazyFrame(LazyFrameOnlineNamespace.predict()), a frame from aDataFrame(DataFrameOnlineNamespace.predict()).TypeErrorfor aframethat is neither; otherwise raises what the namespace method does.
- polars_online.unnest(frame: LazyFrame, specs: Iterable[dict[str, Any]] | ModelBank | str | PathLike[str]) LazyFrame[source]¶
- polars_online.unnest(frame: DataFrame, specs: Iterable[dict[str, Any]] | ModelBank | str | PathLike[str]) DataFrame
frame.online.unnest(specs)as a plain function, so that a type checker can see it.Takes each spec’s struct column apart into columns, the
coeflists as one named column per coefficient: a plan from aLazyFrame(LazyFrameOnlineNamespace.unnest()), a frame from aDataFrame(DataFrameOnlineNamespace.unnest()).TypeErrorfor aframethat is neither; otherwise raises what the namespace method does.
- polars_online.online(expr: Expr) OnlineNamespace[source]¶
expr.onlineas a plain function, so that a type checker can see it.A registered namespace is attached to
pl.Exprat runtime, so to a type checkerpl.col("y").onlineis an attribute that does not exist. This returns the same namespace, with its methods and their typed keywords (docs/IMPROVEMENTS.md U4) visible:df.with_columns(po.online(pl.col("y")).ewridge(features=["x0"], halflife=10.0))
- class polars_online.InMemoryExpressionWarning[source]¶
Bases:
UserWarningIssued by every
pl.col(...).online.<model>(...)call: the expression form runs on the whole column at once.Polars calls a stateful user expression once with its whole column, in either engine, so in a plan over a file this form is O(data) where
lf.online.fit_predict(specs)is O(chunk) – the same model, the same numbers, and only one of them streams (module docstring). The warning is aUserWarning, shown by default wherever the call is made; aDeprecationWarningwould be hidden outside__main__, which is the one place – a pipeline module – where it matters. Using the expression on a frame that is in memory anyway is fine; say so once:warnings.filterwarnings("ignore", category=po.InMemoryExpressionWarning)
- polars_online.native_version()¶
Version of the compiled extension, checked against the Python package version.
- polars_online.schema_version()¶
State-file schema version (see online_core::SCHEMA_VERSION).
- polars_online.thread_pool_size()¶
Size of the bank’s thread pool.
The pool is built at the first bank call from
POLARS_ONLINE_MAX_THREADS(unset: one thread per core) and never resized; this builds it if nothing has yet. Polars’ own pool,POLARS_MAX_THREADS, is separate –pl.thread_pool_size()reports that one.Raises
ValueErrorwhen the variable is set to anything but a non-negative integer.