kurversc 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,474 @@
1
+ Metadata-Version: 2.4
2
+ Name: kurversc
3
+ Version: 0.1.0
4
+ Summary: Validation-guided configuration search for GraphReduce
5
+ Author: KurveAI
6
+ Project-URL: Homepage, https://github.com/kurveai/kurversc
7
+ Project-URL: Repository, https://github.com/kurveai/kurversc
8
+ Project-URL: Documentation, https://github.com/kurveai/kurversc/blob/main/docs/kurversc-technical-report.md
9
+ Project-URL: Issues, https://github.com/kurveai/kurversc/issues
10
+ Requires-Python: <3.13,>=3.10
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: catboost<2,>=1.2
13
+ Requires-Dist: duckdb<1.3,>=1.2
14
+ Requires-Dist: graphreduce==1.10.13
15
+ Requires-Dist: numpy<2,>=1.24
16
+ Requires-Dist: pandas<3.1,>=1.5
17
+ Requires-Dist: pytz>=2024.1
18
+ Requires-Dist: scikit-learn<2,>=1.4
19
+ Requires-Dist: structlog>=23.1
20
+ Provides-Extra: test
21
+ Requires-Dist: pytest<9,>=8; extra == "test"
22
+ Provides-Extra: relbench
23
+ Requires-Dist: relbench<3,>=2.1; extra == "relbench"
24
+ Provides-Extra: tabpfn
25
+ Requires-Dist: tabpfn<9,>=8.5; extra == "tabpfn"
26
+
27
+ # KurveRSC
28
+
29
+ KurveRSC is an integrated relational representation and model-selection
30
+ system. It uses [GraphReduce](https://github.com/wesmadrigal/graphreduce) as
31
+ its relational feature engine, searches graph configurations
32
+ across temporal frames, and selects them by downstream validation performance.
33
+ Its main API is one function:
34
+
35
+ The [KurveRSC technical report](https://github.com/kurveai/kurversc/blob/main/docs/kurversc-technical-report.md)
36
+ ([PDF](https://github.com/kurveai/kurversc/blob/main/docs/kurversc-technical-report.pdf)) explains the
37
+ GraphReduce algorithm, relational feature families, learner-guided graph search,
38
+ point-in-time guarantees, frozen-plan lifecycle, and evaluation protocol. The
39
+ shorter [Kurve RSC white paper](https://github.com/kurveai/kurversc/blob/main/docs/kurversc-white-paper.md)
40
+ ([PDF](https://github.com/kurveai/kurversc/blob/main/docs/kurversc-white-paper.pdf)) introduces the central optimization idea.
41
+
42
+ ```bash
43
+ pip install "kurversc[relbench]" # omit [relbench] for ordinary tables
44
+ ```
45
+
46
+ ```python
47
+ import kurversc
48
+
49
+ result = kurversc.fit(
50
+ parent_node="customers.parquet",
51
+ label_node="churn_labels.parquet",
52
+ parent_key="customer_id",
53
+ label_key="customer_id",
54
+ target="churn",
55
+ split_column="split", # values: train / validation
56
+ )
57
+
58
+ print(result.best_config) # highest validation ROC AUC or lowest MAE
59
+ print(result.recommended_config) # simpler config when the gain is negligible
60
+ print(result.results)
61
+ ```
62
+
63
+ Both node arguments accept a pandas DataFrame, CSV/Parquet path, or the name of
64
+ a table/view on a supplied DuckDB connection. For explicit metadata, use
65
+ `Table` and `Labels`:
66
+
67
+ ```python
68
+ result = kurversc.fit(
69
+ parent_node=kurversc.Table(
70
+ "users", name="users", key="Id", date="CreationDate"
71
+ ),
72
+ label_node=kurversc.Labels(
73
+ "user_labels",
74
+ key="user_id",
75
+ target="will_return",
76
+ timestamp="timestamp",
77
+ split="split",
78
+ ),
79
+ tables=[
80
+ kurversc.Table(
81
+ "posts", name="posts", key="Id", date="CreationDate"
82
+ ),
83
+ kurversc.Table(
84
+ "comments", name="comments", key="Id", date="CreationDate"
85
+ ),
86
+ ],
87
+ relationships=[
88
+ kurversc.Relationship(
89
+ parent="users",
90
+ child="posts",
91
+ parent_key="Id",
92
+ child_key="OwnerUserId",
93
+ ),
94
+ kurversc.Relationship(
95
+ parent="posts",
96
+ child="comments",
97
+ parent_key="Id",
98
+ child_key="PostId",
99
+ ),
100
+ ],
101
+ connection=duckdb_connection,
102
+ )
103
+ ```
104
+
105
+ When the target is a future aggregation over one of the graph's event tables,
106
+ let GraphReduce generate it natively instead of supplying a materialized label
107
+ table:
108
+
109
+ ```python
110
+ label_node=kurversc.GraphLabels(
111
+ table="orders",
112
+ field="id",
113
+ operation="bool",
114
+ period_days=365,
115
+ train_cutoffs=("2023-01-01", "2024-01-01"),
116
+ validation_cutoffs=("2025-01-01",),
117
+ test_cutoffs=("2026-01-01",),
118
+ target="will_order",
119
+ )
120
+ ```
121
+
122
+ This executes GraphReduce's `prep_for_labels()` and automatic `do_labels`
123
+ aggregation at every cutoff. `Labels` remains the correct interface for
124
+ authoritative external targets such as official RelBench task tables.
125
+
126
+ Relationships are required when the compute graph contains feature tables:
127
+ file names alone cannot determine foreign-key direction or whether a join is
128
+ one-to-many. The two label/entity keys are also explicit so label attachment is
129
+ never guessed.
130
+
131
+ ## What `fit` searches
132
+
133
+ The default search is deterministic and starts with the smallest base-only
134
+ configuration. Before building a graph, KurveRSC profiles a small sample from
135
+ every node and utility-ranks its source columns. Structural keys and cutoff
136
+ dates are always retained. A cap therefore admits the strongest observed
137
+ source columns instead of whichever columns happen to occur first in the
138
+ physical schema.
139
+
140
+ ```python
141
+ result = kurversc.fit(
142
+ ...,
143
+ feature_family_max_columns=4, # fixed columns per family
144
+ feature_family_max_features_per_column=32,
145
+ feature_propagation_max_functions_per_column=1,
146
+ feature_ranking_rows=2_000,
147
+ forward_search_beam_width=2,
148
+ screening_rows=10_000,
149
+ sample_rows=50_000, # confirmation fidelity
150
+ confirmation_top_k=8, # diverse 50K candidates
151
+ rerank_top_k=3, # full-data finalists
152
+ rerank_cutoff_frames=3, # sequential walk-forward folds
153
+ adaptive_depth_promotion=True,
154
+ capability_pruning=True,
155
+ search_max_features=8_000,
156
+ random_state=42, # CatBoost and sampling seed
157
+ )
158
+ ```
159
+
160
+ `random_state` is a reproducibility seed, not a trial count. KurveRSC uses the
161
+ same deterministic seed for competing graph configurations so stochastic model
162
+ behavior does not favor one shape over another.
163
+
164
+ The family lattice contains independent additions of `temporal`, `sequence`,
165
+ `conditional`, and `episode` to `base`, including their combinations. It does
166
+ not require a weak family to be present before a later family can be tested.
167
+ Depth 3 is limited to combinations of `base`, `temporal`, and `sequence`; wider
168
+ conditional and episode programs use depths 1 and 2.
169
+
170
+ At the default four-column budget, the forward beam executes at most 24 graph
171
+ shapes: up to four adaptive base variants, then at most 8, 6, 4, and 2
172
+ survivors across the successive family levels. The complete 72-shape lattice remains in the audit
173
+ trail with non-executed candidates marked `pruned`. A wider budget is opt-in:
174
+ `feature_family_max_column_options=(4, 8)` adds another 72 potential records,
175
+ but only the raw narrow-budget winner and the complexity-aware narrow-budget
176
+ recommendation are promoted from four to eight source columns (with the next
177
+ score-ranked shape filling the second slot when they are identical). That
178
+ expanded funnel normally materializes at most 28 configurations rather than
179
+ exhaustively running all 144 potential combinations.
180
+
181
+ ```text
182
+ default cap: 4 base + 8 singles + 6 pairs + 4 triples + 2 quadruples = 24
183
+ optional wide cap: top-2 complete narrow-cap shapes = 2
184
+ ----
185
+ maximum materialized by the opt-in expanded funnel = 26
186
+ ```
187
+
188
+ The default search is multi-fidelity. Beam-admitted configurations are first
189
+ screened with at most 10,000 rows per node. Eight structurally diverse
190
+ candidates are rebuilt and rescored with `sample_rows`: the raw and
191
+ complexity-aware leaders plus representatives of available families, deeper
192
+ propagation, and both annotation policies. The strongest three confirmed
193
+ shapes are then reranked over three complete relational cutoff folds before the
194
+ final graph program is selected. `result.results`,
195
+ `result.confirmation_results`, and `result.rerank_results` expose the three
196
+ audit trails separately.
197
+
198
+ Adaptive depth promotion evaluates both annotation policies at depth 1,
199
+ promotes only the stronger policy to depth 2, and admits depth 3 only when the
200
+ depth-2 gain exceeds both the task tolerance and validation uncertainty.
201
+ Capability pruning removes families that cannot produce operations for the
202
+ available graph schema. Finally, `search_max_features` uses the source-column
203
+ audit and observed parent widths to reject a predicted feature explosion before
204
+ its SQL is materialized. All three guards can be disabled independently.
205
+
206
+ Customize the stages with `max_depth`, `auto_annotate_options`, and
207
+ `feature_family_stages`, or pass explicit `graph_configs` to override the
208
+ frontier. Set `feature_family_max_column_options=(4, 8)` to opt into wider
209
+ refinement, or include `None` as a tier to test an uncapped finalist.
210
+ `feature_family_max_features_per_column` is a separate GraphReduce guardrail:
211
+ it prevents a single temporal or categorical source from expanding into an
212
+ unbounded number of derived SQL features. The propagation cap prevents each
213
+ already-derived column from branching again at every graph hop while retaining
214
+ its canonical continuation (`max→max`, `min→min`, `sum→sum`, `count→sum`, and
215
+ `avg→avg`). Inspect `result.feature_audit` to see every source column's role,
216
+ utility score, family rank, eligible budget tiers, and exclusion reason.
217
+
218
+ `semantic` uses automatic annotations when `auto_annotate_features=True` (or
219
+ caller-supplied GraphReduce annotations). `context` requires peer-group keys;
220
+ `Table.context_keys` supplies them directly, and the RelBench adapter derives
221
+ them from foreign keys other than the edge currently being reduced.
222
+
223
+ Every candidate holds the remaining node policy fixed: GraphReduce's native
224
+ 1/3/4/7/14/30/60/90/180/365/730-day time-series periods (plus the compute
225
+ horizon when it exceeds 365 days) unless `infer_ts_periods=True`, categorical
226
+ cardinality threshold 20, categorical top-k 5, automatic text features disabled, and annotation
227
+ bounds 10 categorical columns, 4 gated numeric columns, and top-k 3. These settings are assigned to
228
+ each node explicitly so they are effective with GraphReduce 1.10.
229
+
230
+ ### Optional TabPFN v3 estimator
231
+
232
+ CatBoost remains the default downstream estimator. Install the local TabPFN
233
+ integration and select v3 explicitly with:
234
+
235
+ ```bash
236
+ pip install "kurversc[relbench,tabpfn]"
237
+ ```
238
+
239
+ ```python
240
+ result = kurversc.fit(
241
+ **problem.fit_kwargs(),
242
+ model_backend="tabpfn_v3",
243
+ estimator_train_rows=10_000,
244
+ model_params={
245
+ "n_estimators": 2,
246
+ "fit_mode": "low_memory",
247
+ },
248
+ )
249
+ ```
250
+
251
+ `estimator_train_rows` is applied consistently to sampled configuration
252
+ screening, full-history finalist fitting, and final train-plus-validation
253
+ fitting. Classification samples are stratified and deterministic. When the
254
+ TabPFN backend is selected without an explicit cap, KurveRSC defaults it to
255
+ 10,000 rows. Graph materialization remains independent of this estimator-only
256
+ cap, and the fitted artifact can be replayed with `kurversc.predict(...)`.
257
+
258
+ By default, each search source—including labels—is exposed to GraphReduce
259
+ through a temporary DuckDB view capped at `sample_rows`. Set
260
+ `search_full_data=True` to evaluate every candidate against complete source
261
+ tables instead:
262
+
263
+ ```python
264
+ result = kurversc.fit(
265
+ parent_node=parent,
266
+ label_node=labels,
267
+ tables=tables,
268
+ relationships=relationships,
269
+ sample_rows=50_000, # still used for ordinary sampled searches
270
+ search_full_data=True, # disables row sampling during config search
271
+ full_training_frames=3, # cutoff dates used for final frame ensembling
272
+ infer_ts_periods=True,
273
+ auto_text_features=False,
274
+ )
275
+ ```
276
+
277
+ `search_full_data=True` uses complete rows at every eligible search cutoff
278
+ selected by `search_training_frames` for every configuration admitted by the
279
+ forward funnel. The winning
280
+ configuration is selected
281
+ directly from those validation scores unless temporal reranking is enabled,
282
+ and is then fit across the requested full-training cutoff dates. Adapters can
283
+ attach a separate connected `search_source` while retaining their uncapped
284
+ production source. A
285
+ new graph is created for every candidate because GraphReduce execution mutates
286
+ node state. If labels contain a timestamp, features are built at each label
287
+ cutoff; otherwise labels are split randomly (or by `split_column`) and the
288
+ current time is used as the feature cutoff.
289
+
290
+ Classification candidates use CatBoost and validation ROC AUC. Regression
291
+ candidates use CatBoost and validation MAE. The highest-performing candidate
292
+ is always retained as `best_trial`. KurveRSC also records feature count,
293
+ feature/model time, and an estimated validation-metric standard error. Trials
294
+ that add at least 2x as many features without improving beyond both the fixed
295
+ 0.002 AUC / 0.5% relative MAE floor and the configured uncertainty threshold
296
+ are marked in `result.complexity_notes`. `recommended_trial` is the
297
+ lowest-feature candidate statistically indistinguishable from the raw winner;
298
+ `best_trial` remains the unpenalized validation winner. Set
299
+ `complexity_uncertainty_multiplier=0` to use only the fixed tolerances.
300
+
301
+ By default, rerank the three strongest confirmed finalists over three
302
+ walk-forward full-data cutoff folds:
303
+
304
+ ```python
305
+ result = kurversc.fit(
306
+ ...,
307
+ rerank_top_k=3,
308
+ rerank_cutoff_frames=3,
309
+ rerank_stability_penalty=0.25,
310
+ )
311
+ ```
312
+
313
+ The reranker learns and scores one cutoff frame at a time, releases it, and
314
+ then advances to the next fold. Classification maximizes mean ROC AUC minus
315
+ the configured standard-deviation penalty; regression minimizes mean MAE plus
316
+ that penalty. The raw stability-adjusted winner is selected; the complexity
317
+ guard remains a screening and audit mechanism but cannot override this
318
+ full-frame evidence. The audit trail is available as `result.rerank_results`.
319
+ Set `rerank_cutoff_frames=1` for a single full-data train-to-validation rerank;
320
+ the stability penalty is then zero because there is only one score.
321
+
322
+ ## What the returned fitted model means
323
+
324
+ `fit` has a nine-stage lifecycle:
325
+
326
+ 1. Utility-rank source columns and record the capped feature-funnel audit.
327
+ 2. Build beam-admitted candidates from sampled source views, or from complete
328
+ source rows when `search_full_data=True`.
329
+ 3. Rank candidates by one-frame validation ROC AUC or MAE and promote only the
330
+ strongest graph shapes to broader source-column budgets.
331
+ 4. Confirm a structurally diverse bounded candidate set at medium fidelity.
332
+ 5. Rerank the three strongest confirmed candidates over sequential full-data
333
+ walk-forward cutoff folds and select the raw stability-adjusted winner.
334
+ 6. Freeze its exact GraphReduce operation plan and training-only feature schema.
335
+ 7. Materialize one production cutoff at a time and fit an independent CatBoost
336
+ model for that frame.
337
+ 8. Score validation with the training-frame ensemble, then add independently
338
+ fitted validation-frame models to the final train-plus-validation ensemble.
339
+ 9. Replay the plan with `GraphReduce(train=False)` at test cutoffs and expose
340
+ predictions as `result.test_predictions`. If an external `Labels` test
341
+ split contains targets, KurveRSC also records a test score.
342
+
343
+ The resulting production artifact is `result.fitted_model`: selected
344
+ `GraphConfig`, frozen execution plan, ordered feature schema, CatBoost model,
345
+ and validation/test metadata. `result.model` returns its final CatBoost model;
346
+ `result.execution_plan` returns the production GraphReduce plan. Validation
347
+ and test never run feature inference or annotation again.
348
+
349
+ When `infer_ts_periods=True`, KurveRSC asks GraphReduce to infer
350
+ relationship-specific event-cadence windows. Each dated node or relationship
351
+ can then replace the initial `[7, 30, 90]` windows with compact, data-derived
352
+ lookbacks spanning the configured compute horizon. KurveRSC stores those
353
+ inferred periods inside the frozen execution plan and restores them during
354
+ validation, outer refit, and prediction; replay never re-infers them.
355
+
356
+ Replay the fitted artifact on another timestamped entity frame with the same
357
+ declarative graph metadata:
358
+
359
+ ```python
360
+ predictions = kurversc.predict(
361
+ result,
362
+ parent_node=parent,
363
+ prediction_node=kurversc.Labels(
364
+ scoring_rows, key="customer_id", timestamp="timestamp"
365
+ ),
366
+ tables=tables,
367
+ relationships=relationships,
368
+ )
369
+ ```
370
+
371
+ The output preserves prediction-row order and adds a `prediction` column.
372
+
373
+ Point-in-time production training can use many frames. By default, every
374
+ configuration is screened on one frame at the latest eligible cutoff. With
375
+ `search_full_data=True`, that frame uses all available rows. Supply all valid
376
+ cutoffs through `GraphLabels.train_cutoffs`, or all timestamped rows through
377
+ `Labels`, then choose the incremental production frame count:
378
+
379
+ ```python
380
+ result = kurversc.fit(
381
+ ...,
382
+ search_full_data=True, # evaluate all candidates on complete rows
383
+ full_training_frames=3, # 3 evenly spaced available train cutoffs
384
+ )
385
+ ```
386
+
387
+ `full_training_frames=None` (the default) uses every available training
388
+ cutoff. These are point-in-time graph frames, not partitions of raw event
389
+ tables: every frame sees the complete history allowed by its cutoff, and all
390
+ frames replay the selected operation plan. KurveRSC releases each materialized
391
+ feature frame before constructing the next one. Independent CatBoost models are
392
+ combined as an ensemble, so the final fit never concatenates those wide frames
393
+ in memory. When
394
+ `full_training_frames=1`, KurveRSC always uses the latest eligible training
395
+ cutoff. The search audit trail is available as `result.results`.
396
+
397
+ ## Official RelBench tasks
398
+
399
+ `load_relbench_problem` uses the production RelBench dataset, task tables,
400
+ date keys, primary keys, and foreign keys without adding task-specific feature
401
+ expressions:
402
+
403
+ ```python
404
+ import kurversc
405
+
406
+ problem = kurversc.load_relbench_problem(
407
+ "rel-stack",
408
+ "user-badge",
409
+ sample_rows=10_000,
410
+ max_train_timestamps=1,
411
+ max_enrichment_columns=8,
412
+ )
413
+ result = kurversc.fit(**problem.fit_kwargs(), sample_rows=10_000)
414
+ ```
415
+
416
+ For a full-data configuration search followed by a three-cutoff production
417
+ fit, use:
418
+
419
+ ```python
420
+ problem = kurversc.load_relbench_problem(
421
+ "rel-stack",
422
+ "user-badge",
423
+ sample_rows=50_000,
424
+ search_full_data=True,
425
+ max_train_timestamps=3,
426
+ )
427
+ result = kurversc.fit(
428
+ **problem.fit_kwargs(),
429
+ sample_rows=50_000,
430
+ search_full_data=True,
431
+ full_training_frames=3,
432
+ infer_ts_periods=True,
433
+ auto_text_features=False,
434
+ )
435
+ ```
436
+
437
+ This runs the beam-admitted graph configurations on complete rows at the latest
438
+ training cutoff, promotes only the strongest shapes to the wider source-column
439
+ budget, and fits the selected configuration across three cutoff dates while
440
+ retaining only one materialized feature frame.
441
+
442
+ Install the optional adapter with `pip install "kurversc[relbench]"`. The object
443
+ adapter `relbench_problem_from_objects(...)` accepts a task, an already-censored
444
+ RelBench database, and its train/validation tables; RelArena uses this path so
445
+ its official inner and outer database cutoffs remain authoritative.
446
+
447
+ Relational schemas do require keys. This adapter reads them from official
448
+ RelBench metadata; for ordinary files or database tables, provide them with
449
+ `Table` and `Relationship`. Self-referential/cyclic foreign keys are omitted
450
+ because GraphReduce currently uses an acyclic `DiGraph`; every reachable
451
+ acyclic foreign-key path is represented as its own node instance. Referenced
452
+ dimension tables reached through association/event tables are joined with
453
+ `reduce=False`; by default their projected feature attributes are capped at
454
+ eight, excluding high-cardinality free text. Pass
455
+ `max_enrichment_columns=None` to retain every dimension attribute.
456
+
457
+ For temporally meaningful relational evaluation, provide `Labels.timestamp`
458
+ and date columns on event tables. A dated parent is always filtered with
459
+ `parent.date <= Labels.timestamp` before feature inference and again through
460
+ GraphReduce's `do_filters_ops`. If the parent is a genuinely timeless entity
461
+ table, declare `Table(..., timeless=True)` explicitly; an omitted parent date
462
+ is otherwise rejected for temporal labels. Without event dates, KurveRSC
463
+ cannot distinguish historical features from future data.
464
+
465
+ ## Local customer example
466
+
467
+ [`examples/cust_data_future_order.py`](examples/cust_data_future_order.py)
468
+ contains a complete run for `/usr/local/lake/cust_data`. It derives train and
469
+ validation labels through GraphReduce for “places an order in the following
470
+ 365 days,” declares
471
+ the customer root as explicitly timeless, supplies every primary/foreign key
472
+ and event date, and runs the default beam-pruned configuration funnel. The example enables
473
+ the `kurversc` logger at `INFO`, showing every attempted configuration, its
474
+ score/feature count/timing, and the selected configuration.