kurveai/kurversc

Kurve Relational Signal Compression

Python

3

16 commits

updated Sep 20, 2026

See the code

README

KurveRSC

KurveRSC is an integrated relational representation and model-selection system. It uses GraphReduce as its relational feature engine, searches graph configurations across temporal frames, and selects them by downstream validation performance.

Kurve Research · KurveRSC Article · Technical Report · Citation · PyPI

KurveRSC materializes relational feature frames with different shapes, evaluates them jointly with a downstream learner, and selects one frozen graph configuration for final fitting.

KurveRSC searches the relational signal-compression space and fits the selected shape on full point-in-time data.

Thesis. Relational signal compression is a first-class optimization surface. Long-term progress will come not only from improving the downstream learner, but from learning which paths, time windows, reductions, and feature families should carry a database's task-relevant signal into that learner. Keeping this boundary explicit makes the learner pluggable: today CatBoost; tomorrow TabPFN-3 or another tabular or relational foundation model.

Kurve has pursued this direction from the outset: predictive performance depends on adapting the relational representation as well as the learner that consumes it. Recent independent results validate that longstanding bet from adjacent directions. KumoRFM-2 reports that task-specific fine-tuning improves its average SALT MRR from 0.83 in-context to 0.89, showing that even a strong relational foundation model benefits materially from task adaptation (KumoRFM-2). Prior Labs' TabPFN-Rel couples Deep Feature Synthesis with TabPFN-3 and obtains leading RelArena results, demonstrating the value of giving a foundation learner a separately constructed relational representation (RelArena and TabPFN-Rel). Neither system optimizes exactly the same object as KurveRSC, but both reinforce KurveRSC's original modular thesis: relational representation and downstream learning should be adapted jointly without permanently binding either layer to the other.

How KurveRSC differs from Deep Feature Synthesis

Deep Feature Synthesis generates a relational feature space; KurveRSC selects a relational program by measuring how well its complete feature frame works with the downstream learner.

DimensionDeep Feature SynthesisGraphReduceKurveRSC
Primary objectComposed feature definitionsExecutable table graph and node operationsSearch over complete GraphReduce programs
Learner roleNormally fitted after synthesisExternal to the execution engineIn the loop: AUROC or MAE scores every candidate frame
Task adaptationCaller chooses primitives and depth; later feature selection can remove columnsCaller configures one graph programSearch jointly chooses depth, families, annotations, budgets, and temporal policy
Final artifactFeature definitions and materialized tableReduced frame and operation lineageSelected configuration, frozen execution plan, schema, and fitted learner
InferenceRecompute the chosen definitionsRe-execute the configured graphReplay the learned plan with feature discovery disabled

Learner regularization over a wide DFS matrix can choose among columns that were generated, but it cannot recover paths, time windows, feature families, or propagation depths that were never materialized. KurveRSC makes those upstream choices part of validation-guided selection while retaining a replaceable downstream model. See the technical report's full comparison.

The KurveRSC technical report (PDF) explains the GraphReduce algorithm, relational feature families, learner-guided graph search, point-in-time guarantees, frozen-plan lifecycle, and evaluation protocol.

Quickstart

The high-level API is one function. Give fit an entity table, a label table, their join keys, the target, and an authoritative train/validation split:

pip install "kurversc[relbench]"  # omit [relbench] for ordinary tables
import kurversc

result = kurversc.fit(
    parent_node="customers.parquet",
    label_node="churn_labels.parquet",
    parent_key="customer_id",
    label_key="customer_id",
    target="churn",
    split_column="split",  # values: train / validation
)

print(result.best_config)         # highest validation ROC AUC or lowest MAE
print(result.recommended_config)  # simpler config when the gain is negligible
print(result.full_validation_score)
print(result.results)             # complete configuration-search audit trail

result is the fitted KurveRSC artifact: the selected GraphConfig, frozen GraphReduce feature-operation plan, downstream learner, feature schema, and validation metadata. Pass it to kurversc.predict(...) to replay the exact learned relational program at new cutoff dates.

RelArena performance

The latest fully completed 21-task reference profile (September 2, 2026) uses official RelBench v1 test splits through RelArena: full-data latest-cutoff graph search, three sequential reranking folds, one production cutoff, CatBoost, GraphReduce's fixed temporal periods, and no automatic text features. Classification reports test AUROC (higher is better); regression reports test MAE (lower is better).

KurveRSC RelArena default: complete latest-cutoff graph search, top-three reranking over three sequential full cutoff folds, a frozen selected graph plan, and final CatBoost fitting on one complete cutoff.

The reported reproducibility default prioritizes complete graph-configuration evidence while retaining only one materialized feature frame at a time.

DatasetTaskMetricKurveRSCTabPFN-Rel LocalWinner
rel-amazonuser-churnAUROC ↑0.7098730.702403KurveRSC
rel-amazonitem-churnAUROC ↑0.8264570.827857TabPFN-Rel Local
rel-amazonuser-ltvMAE ↓14.14189814.400940KurveRSC
rel-amazonitem-ltvMAE ↓42.37902447.768328KurveRSC
rel-avitouser-visitsAUROC ↑0.6741900.668811KurveRSC
rel-avitouser-clicksAUROC ↑0.6637180.614522KurveRSC
rel-avitoad-ctrMAE ↓0.0336580.031379TabPFN-Rel Local
rel-eventuser-repeatAUROC ↑0.7542780.769251TabPFN-Rel Local
rel-eventuser-ignoreAUROC ↑0.8319780.701376KurveRSC
rel-eventuser-attendanceMAE ↓0.2604200.239383TabPFN-Rel Local
rel-f1driver-dnfAUROC ↑0.7537550.714468KurveRSC
rel-f1driver-top3AUROC ↑0.6823590.792916TabPFN-Rel Local
rel-f1driver-positionMAE ↓3.9134333.761699TabPFN-Rel Local
rel-hmuser-churnAUROC ↑0.6963060.705690TabPFN-Rel Local
rel-hmitem-salesMAE ↓0.0318370.061362KurveRSC
rel-stackuser-engagementAUROC ↑0.9030120.905834TabPFN-Rel Local
rel-stackuser-badgeAUROC ↑0.8750810.863470KurveRSC
rel-stackpost-votesMAE ↓0.0633560.067957KurveRSC
rel-trialstudy-outcomeAUROC ↑0.7081410.730607TabPFN-Rel Local
rel-trialstudy-adverseMAE ↓41.33389642.591708KurveRSC
rel-trialsite-successMAE ↓0.4017550.385751TabPFN-Rel Local

KurveRSC wins 11 of 21 direct comparisons with TabPFN-Rel Local: 6–6 on classification and 5–4 on regression. On the complete 21-task matrix it is third overall by RelArena's bootstrapped Elo calculation at 1763.3, behind RT-PluRel and TabPFN-Rel API and ahead of TabPFN-Rel Local.

Aggregate RelArena leaderboard

This table includes every reproduced RelArena participant on the complete 21-task matrix. Elo is anchored to the global constant predictor at 1000; higher Elo and win rate are better, while lower mean rank and rescaled loss are better.

Elo rankMethodKindEloMean rankWin rateRescaled loss
1RT-PluRelsystem1857.52.95280.48%0.107555
2TabPFN-Rel APImodel1832.53.19078.10%0.148619
3KurveRSCsystem1763.33.90570.95%0.145282
4TabPFN-Rel Localmodel1733.04.23867.62%0.189932
5GraphSAGEmodel1662.25.04859.52%0.206977
6RelGTmodel1578.16.02449.76%0.321848
7RDBLearnmodel1563.66.19048.10%0.274152
8RelGNN-ESmodel1531.86.54844.52%0.305919
9LightGBM (entity-only)model1359.68.28627.14%0.546808
10Constant (per-entity)model1259.49.09519.05%0.618487
11Constant (global)model1000.010.5244.76%0.941930

Ranking by rescaled loss

RelArena's default aggregate orders methods by mean per-task min-max rescaled error. Lower is better; the Elo rank is retained to make the two orderings easy to compare.

Loss rankMethodKindRescaled lossElo rankElo
1RT-PluRelsystem0.10755511857.5
2KurveRSCsystem0.14528231763.3
3TabPFN-Rel APImodel0.14861921832.5
4TabPFN-Rel Localmodel0.18993241733.0
5GraphSAGEmodel0.20697751662.2
6RDBLearnmodel0.27415271563.6
7RelGNN-ESmodel0.30591981531.8
8RelGTmodel0.32184861578.1
9LightGBM (entity-only)model0.54680891359.6
10Constant (per-entity)model0.618487101259.4
11Constant (global)model0.941930111000.0

KurveRSC is also second overall by rescaled loss, behind RT-PluRel. Because systems use their own internal selection regimes, RelArena reports method kind explicitly: KurveRSC and RT-PluRel are systems, while the remaining learned participants are models under RelArena's standardized tuning interface.

Per-task results across all participants

These matrices combine KurveRSC's default-profile test results with the validation-selected, seed-zero test scores in RelArena's reproduced release artifact. Bold marks the best held-out score on each task. AUROC is maximized; MAE is minimized.

Classification — AUROC ↑
Dataset / taskKurveRSCRT-PluRelTabPFN-Rel APITabPFN-Rel LocalGraphSAGERelGTRDBLearnRelGNN-ESLightGBMConstant/entityConstant/globalOverall winner
rel-amazon/item-churn0.8264570.8326560.8279960.8278570.8305270.8238350.8195380.7855790.6622110.7288750.500000RT-PluRel
rel-amazon/user-churn0.7098730.7134600.7086490.7024030.7045960.7019240.6843750.6942810.5170560.6342050.500000RT-PluRel
rel-avito/user-clicks0.6637180.5833760.6751910.6145220.6086740.6443530.6787690.6675710.5641630.5041430.500000RDBLearn
rel-avito/user-visits0.6741900.6708870.6680260.6688110.6657580.6621420.6596240.6487310.5292820.6027030.500000KurveRSC
rel-event/user-ignore0.8319780.8475770.8786590.7013760.7587280.7815070.6643510.8053930.7771810.8399300.500000TabPFN-Rel API
rel-event/user-repeat0.7542780.7913770.7592910.7692510.7846260.7343580.7440840.7546120.7482950.7518050.500000RT-PluRel
rel-f1/driver-dnf0.7537550.7314600.7321720.7144680.7172350.7116670.7145510.7261060.7302980.6992580.500000KurveRSC
rel-f1/driver-top30.6823590.7588580.7714260.7929160.7259750.8108410.7800810.7588640.7388890.5565300.500000RelGT
rel-hm/user-churn0.6963060.7043560.7052150.7056900.6985250.6895310.6983520.6820250.5900810.6479720.500000TabPFN-Rel Local
rel-stack/user-badge0.8750810.8916120.8803860.8634700.8887480.5742860.7711470.6205840.5379950.7889560.500000RT-PluRel
rel-stack/user-engagement0.9030120.8967750.9059940.9058340.9056090.9067310.8586700.9050540.8118360.8267170.500000RelGT
rel-trial/study-outcome0.7081410.7234870.7647020.7306070.6862320.6684950.7212050.6574350.7150180.5000000.500000TabPFN-Rel API
Regression — MAE ↓
Dataset / taskKurveRSCRT-PluRelTabPFN-Rel APITabPFN-Rel LocalGraphSAGERelGTRDBLearnRelGNN-ESLightGBMConstant/entityConstant/globalOverall winner
rel-amazon/item-ltv42.37902443.02801246.76818147.76832849.24533748.67338648.99752752.45808455.75075065.35141964.233613KurveRSC
rel-amazon/user-ltv14.14189813.94301114.35821214.40094014.41532114.35212914.57754014.57522516.78468217.42346916.782979RT-PluRel
rel-avito/ad-ctr0.0336580.0348480.0310800.0313790.0389660.0364990.0341030.0426460.0412500.0412010.043067TabPFN-Rel API
rel-event/user-attendance0.2604200.2409490.2439440.2393830.2450180.2614930.2422150.2438580.2627980.2691520.263534TabPFN-Rel Local
rel-f1/driver-position3.9134333.8176993.7691813.7616994.0111124.7655293.8886494.2658874.1058294.1035094.399101TabPFN-Rel Local
rel-hm/item-sales0.0318370.0402580.0605070.0613620.0551530.0531680.0671320.0564930.0752950.0780330.076087KurveRSC
rel-stack/post-votes0.0633560.0634630.0678820.0679570.0648980.0679040.0677190.0679040.0660990.0693740.067904KurveRSC
rel-trial/site-success0.4017550.4103570.4126240.3857510.3248510.3701100.4858330.3404930.4375060.4411480.462222GraphSAGE
rel-trial/study-adverse41.33389632.65279139.75367442.59170844.31526144.12588744.02670546.27006444.57349557.53324757.533247RT-PluRel

See the technical report for the complete protocol and bootstrap uncertainty intervals.

Both node arguments accept a pandas DataFrame, CSV/Parquet path, or the name of a table/view on a supplied DuckDB connection. For explicit metadata, use Table and Labels:

result = kurversc.fit(
    parent_node=kurversc.Table(
        "users", name="users", key="Id", date="CreationDate"
    ),
    label_node=kurversc.Labels(
        "user_labels",
        key="user_id",
        target="will_return",
        timestamp="timestamp",
        split="split",
    ),
    tables=[
        kurversc.Table(
            "posts", name="posts", key="Id", date="CreationDate"
        ),
        kurversc.Table(
            "comments", name="comments", key="Id", date="CreationDate"
        ),
    ],
    relationships=[
        kurversc.Relationship(
            parent="users",
            child="posts",
            parent_key="Id",
            child_key="OwnerUserId",
        ),
        kurversc.Relationship(
            parent="posts",
            child="comments",
            parent_key="Id",
            child_key="PostId",
        ),
    ],
    connection=duckdb_connection,
)

When the target is a future aggregation over one of the graph's event tables, let GraphReduce generate it natively instead of supplying a materialized label table:

label_node=kurversc.GraphLabels(
    table="orders",
    field="id",
    operation="bool",
    period_days=365,
    train_cutoffs=("2023-01-01", "2024-01-01"),
    validation_cutoffs=("2025-01-01",),
    test_cutoffs=("2026-01-01",),
    target="will_order",
)

This executes GraphReduce's prep_for_labels() and automatic do_labels aggregation at every cutoff. Labels remains the correct interface for authoritative external targets such as official RelBench task tables.

Relationships are required when the compute graph contains feature tables: file names alone cannot determine foreign-key direction or whether a join is one-to-many. The two label/entity keys are also explicit so label attachment is never guessed.

What fit searches

The default search is deterministic and starts with the smallest base-only configuration. Before building a graph, KurveRSC profiles a small sample from every node and utility-ranks its source columns. Structural keys and cutoff dates are always retained. A cap therefore admits the strongest observed source columns instead of whichever columns happen to occur first in the physical schema.

result = kurversc.fit(
    ...,
    feature_family_max_columns=4,           # fixed columns per family
    feature_family_max_features_per_column=32,
    feature_propagation_max_functions_per_column=1,
    feature_ranking_rows=2_000,
    forward_search_beam_width=2,
    screening_rows=10_000,
    sample_rows=50_000,                     # confirmation fidelity
    confirmation_top_k=8,                  # diverse 50K candidates
    rerank_top_k=3,                         # full-data finalists
    rerank_cutoff_frames=3,                 # sequential walk-forward folds
    adaptive_depth_promotion=True,
    capability_pruning=True,
    search_max_features=8_000,
    random_state=42,                        # CatBoost and sampling seed
)

random_state is a reproducibility seed, not a trial count. KurveRSC uses the same deterministic seed for competing graph configurations so stochastic model behavior does not favor one shape over another.

The family lattice contains independent additions of temporal, sequence, conditional, and episode to base, including their combinations. It does not require a weak family to be present before a later family can be tested. Depth 3 is limited to combinations of base, temporal, and sequence; wider conditional and episode programs use depths 1 and 2.

At the default four-column budget, the forward beam executes at most 24 graph shapes: up to four adaptive base variants, then at most 8, 6, 4, and 2 survivors across the successive family levels. The complete 72-shape lattice remains in the audit trail with non-executed candidates marked pruned. A wider budget is opt-in: feature_family_max_column_options=(4, 8) adds another 72 potential records, but only the raw narrow-budget winner and the complexity-aware narrow-budget recommendation are promoted from four to eight source columns (with the next score-ranked shape filling the second slot when they are identical). That expanded funnel normally materializes at most 28 configurations rather than exhaustively running all 144 potential combinations.

default cap:       4 base + 8 singles + 6 pairs + 4 triples + 2 quadruples = 24
optional wide cap: top-2 complete narrow-cap shapes                          =  2
                                                                           ----
maximum materialized by the opt-in expanded funnel                          = 26

The default search is multi-fidelity. Beam-admitted configurations are first screened with at most 10,000 rows per node. Eight structurally diverse candidates are rebuilt and rescored with sample_rows: the raw and complexity-aware leaders plus representatives of available families, deeper propagation, and both annotation policies. The strongest three confirmed shapes are then reranked over three complete relational cutoff folds before the final graph program is selected. result.results, result.confirmation_results, and result.rerank_results expose the three audit trails separately.

Adaptive depth promotion evaluates both annotation policies at depth 1, promotes only the stronger policy to depth 2, and admits depth 3 only when the depth-2 gain exceeds both the task tolerance and validation uncertainty. Capability pruning removes families that cannot produce operations for the available graph schema. Finally, search_max_features uses the source-column audit and observed parent widths to reject a predicted feature explosion before its SQL is materialized. All three guards can be disabled independently.

Customize the stages with max_depth, auto_annotate_options, and feature_family_stages, or pass explicit graph_configs to override the frontier. Set feature_family_max_column_options=(4, 8) to opt into wider refinement, or include None as a tier to test an uncapped finalist. feature_family_max_features_per_column is a separate GraphReduce guardrail: it prevents a single temporal or categorical source from expanding into an unbounded number of derived SQL features. The propagation cap prevents each already-derived column from branching again at every graph hop while retaining its canonical continuation (max→max, min→min, sum→sum, count→sum, and avg→avg). Inspect result.feature_audit to see every source column's role, utility score, family rank, eligible budget tiers, and exclusion reason.

semantic uses automatic annotations when auto_annotate_features=True (or caller-supplied GraphReduce annotations). context requires peer-group keys; Table.context_keys supplies them directly, and the RelBench adapter derives them from foreign keys other than the edge currently being reduced.

Every candidate holds the remaining node policy fixed: GraphReduce's native 1/3/4/7/14/30/60/90/180/365/730-day time-series periods (plus the compute horizon when it exceeds 365 days) unless infer_ts_periods=True, categorical cardinality threshold 20, categorical top-k 5, automatic text features disabled, and annotation bounds 10 categorical columns, 4 gated numeric columns, and top-k 3. These settings are assigned to each node explicitly so they are effective with GraphReduce 1.10.

Optional TabPFN v3 estimator

CatBoost remains the default downstream estimator. Install the local TabPFN integration and select v3 explicitly with:

pip install "kurversc[relbench,tabpfn]"
result = kurversc.fit(
    **problem.fit_kwargs(),
    model_backend="tabpfn_v3",
    estimator_train_rows=10_000,
    model_params={
        "n_estimators": 2,
        "fit_mode": "low_memory",
    },
)

estimator_train_rows is applied consistently to sampled configuration screening, full-history finalist fitting, and final train-plus-validation fitting. Classification samples are stratified and deterministic. When the TabPFN backend is selected without an explicit cap, KurveRSC defaults it to 10,000 rows. Graph materialization remains independent of this estimator-only cap, and the fitted artifact can be replayed with kurversc.predict(...).

By default, each search source—including labels—is exposed to GraphReduce through a temporary DuckDB view capped at sample_rows. Set search_full_data=True to evaluate every candidate against complete source tables instead:

result = kurversc.fit(
    parent_node=parent,
    label_node=labels,
    tables=tables,
    relationships=relationships,
    sample_rows=50_000,       # still used for ordinary sampled searches
    search_full_data=True,    # disables row sampling during config search
    full_training_frames=3,   # cutoff dates used for final frame ensembling
    infer_ts_periods=True,
    auto_text_features=False,
)

search_full_data=True uses complete rows at every eligible search cutoff selected by search_training_frames for every configuration admitted by the forward funnel. The winning configuration is selected directly from those validation scores unless temporal reranking is enabled, and is then fit across the requested full-training cutoff dates. Adapters can attach a separate connected search_source while retaining their uncapped production source. A new graph is created for every candidate because GraphReduce execution mutates node state. If labels contain a timestamp, features are built at each label cutoff; otherwise labels are split randomly (or by split_column) and the current time is used as the feature cutoff.

Classification candidates use CatBoost and validation ROC AUC. Regression candidates use CatBoost and validation MAE. The highest-performing candidate is always retained as best_trial. KurveRSC also records feature count, feature/model time, and an estimated validation-metric standard error. Trials that add at least 2x as many features without improving beyond both the fixed 0.002 AUC / 0.5% relative MAE floor and the configured uncertainty threshold are marked in result.complexity_notes. recommended_trial is the lowest-feature candidate statistically indistinguishable from the raw winner; best_trial remains the unpenalized validation winner. Set complexity_uncertainty_multiplier=0 to use only the fixed tolerances.

By default, rerank the three strongest confirmed finalists over three walk-forward full-data cutoff folds:

result = kurversc.fit(
    ...,
    rerank_top_k=3,
    rerank_cutoff_frames=3,
    rerank_stability_penalty=0.25,
)

The reranker learns and scores one cutoff frame at a time, releases it, and then advances to the next fold. Classification maximizes mean ROC AUC minus the configured standard-deviation penalty; regression minimizes mean MAE plus that penalty. The raw stability-adjusted winner is selected; the complexity guard remains a screening and audit mechanism but cannot override this full-frame evidence. The audit trail is available as result.rerank_results. Set rerank_cutoff_frames=1 for a single full-data train-to-validation rerank; the stability penalty is then zero because there is only one score.

What the returned fitted model means

fit has a nine-stage lifecycle:

  1. Utility-rank source columns and record the capped feature-funnel audit.
  2. Build beam-admitted candidates from sampled source views, or from complete source rows when search_full_data=True.
  3. Rank candidates by one-frame validation ROC AUC or MAE and promote only the strongest graph shapes to broader source-column budgets.
  4. Confirm a structurally diverse bounded candidate set at medium fidelity.
  5. Rerank the three strongest confirmed candidates over sequential full-data walk-forward cutoff folds and select the raw stability-adjusted winner.
  6. Freeze its exact GraphReduce operation plan and training-only feature schema.
  7. Materialize one production cutoff at a time and fit an independent CatBoost model for that frame.
  8. Score validation with the training-frame ensemble, then add independently fitted validation-frame models to the final train-plus-validation ensemble.
  9. Replay the plan with GraphReduce(train=False) at test cutoffs and expose predictions as result.test_predictions. If an external Labels test split contains targets, KurveRSC also records a test score.

The resulting production artifact is result.fitted_model: selected GraphConfig, frozen execution plan, ordered feature schema, CatBoost model, and validation/test metadata. result.model returns its final CatBoost model; result.execution_plan returns the production GraphReduce plan. Validation and test never run feature inference or annotation again.

When infer_ts_periods=True, KurveRSC asks GraphReduce to infer relationship-specific event-cadence windows. Each dated node or relationship can then replace the initial [7, 30, 90] windows with compact, data-derived lookbacks spanning the configured compute horizon. KurveRSC stores those inferred periods inside the frozen execution plan and restores them during validation, outer refit, and prediction; replay never re-infers them.

Replay the fitted artifact on another timestamped entity frame with the same declarative graph metadata:

predictions = kurversc.predict(
    result,
    parent_node=parent,
    prediction_node=kurversc.Labels(
        scoring_rows, key="customer_id", timestamp="timestamp"
    ),
    tables=tables,
    relationships=relationships,
)

The output preserves prediction-row order and adds a prediction column.

Point-in-time production training can use many frames. By default, every configuration is screened on one frame at the latest eligible cutoff. With search_full_data=True, that frame uses all available rows. Supply all valid cutoffs through GraphLabels.train_cutoffs, or all timestamped rows through Labels, then choose the incremental production frame count:

result = kurversc.fit(
    ...,
    search_full_data=True,     # evaluate all candidates on complete rows
    full_training_frames=3,    # 3 evenly spaced available train cutoffs
)

full_training_frames=None (the default) uses every available training cutoff. These are point-in-time graph frames, not partitions of raw event tables: every frame sees the complete history allowed by its cutoff, and all frames replay the selected operation plan. KurveRSC releases each materialized feature frame before constructing the next one. Independent CatBoost models are combined as an ensemble, so the final fit never concatenates those wide frames in memory. When full_training_frames=1, KurveRSC always uses the latest eligible training cutoff. The search audit trail is available as result.results.

Official RelBench tasks

load_relbench_problem uses the production RelBench dataset, task tables, date keys, primary keys, and foreign keys without adding task-specific feature expressions:

import kurversc

problem = kurversc.load_relbench_problem(
    "rel-stack",
    "user-badge",
    sample_rows=10_000,
    max_train_timestamps=1,
    max_enrichment_columns=8,
)
result = kurversc.fit(**problem.fit_kwargs(), sample_rows=10_000)

For a full-data configuration search followed by a three-cutoff production fit, use:

problem = kurversc.load_relbench_problem(
    "rel-stack",
    "user-badge",
    sample_rows=50_000,
    search_full_data=True,
    max_train_timestamps=3,
)
result = kurversc.fit(
    **problem.fit_kwargs(),
    sample_rows=50_000,
    search_full_data=True,
    full_training_frames=3,
    infer_ts_periods=True,
    auto_text_features=False,
)

This runs the beam-admitted graph configurations on complete rows at the latest training cutoff, promotes only the strongest shapes to the wider source-column budget, and fits the selected configuration across three cutoff dates while retaining only one materialized feature frame.

Install the optional adapter with pip install "kurversc[relbench]". The object adapter relbench_problem_from_objects(...) accepts a task, an already-censored RelBench database, and its train/validation tables; RelArena uses this path so its official inner and outer database cutoffs remain authoritative.

Relational schemas do require keys. This adapter reads them from official RelBench metadata; for ordinary files or database tables, provide them with Table and Relationship. Self-referential/cyclic foreign keys are omitted because GraphReduce currently uses an acyclic DiGraph; every reachable acyclic foreign-key path is represented as its own node instance. Referenced dimension tables reached through association/event tables are joined with reduce=False; by default their projected feature attributes are capped at eight, excluding high-cardinality free text. Pass max_enrichment_columns=None to retain every dimension attribute.

For temporally meaningful relational evaluation, provide Labels.timestamp and date columns on event tables. A dated parent is always filtered with parent.date <= Labels.timestamp before feature inference and again through GraphReduce's do_filters_ops. If the parent is a genuinely timeless entity table, declare Table(..., timeless=True) explicitly; an omitted parent date is otherwise rejected for temporal labels. Without event dates, KurveRSC cannot distinguish historical features from future data.

Local customer example

examples/cust_data_future_order.py contains a complete run for /usr/local/lake/cust_data. It derives train and validation labels through GraphReduce for “places an order in the following 365 days,” declares the customer root as explicitly timeless, supplies every primary/foreign key and event date, and runs the default beam-pruned configuration funnel. The example enables the kurversc logger at INFO, showing every attempted configuration, its score/feature count/timing, and the selected configuration.

Citation

If you use KurveRSC in research, please cite the KurveRSC technical report:

@techreport{madrigal2026kurversc,
  title       = {KurveRSC: Validation-Guided Relational Signal Compression with a Downstream Learner in the Loop},
  author      = {Madrigal, Wes},
  institution = {Kurve AI},
  year        = {2026},
  month       = sep,
  url         = {https://github.com/kurveai/kurversc/blob/main/docs/kurversc-technical-report.pdf}
}

Contributors

wesmadrigal

16 commits

kurveai/kurversc

Kurve Relational Signal Compression

Python

3

16 commits

updated Sep 20, 2026

See the code

README

KurveRSC

KurveRSC is an integrated relational representation and model-selection system. It uses GraphReduce as its relational feature engine, searches graph configurations across temporal frames, and selects them by downstream validation performance.

Kurve Research · KurveRSC Article · Technical Report · Citation · PyPI

KurveRSC materializes relational feature frames with different shapes, evaluates them jointly with a downstream learner, and selects one frozen graph configuration for final fitting.

KurveRSC searches the relational signal-compression space and fits the selected shape on full point-in-time data.

Thesis. Relational signal compression is a first-class optimization surface. Long-term progress will come not only from improving the downstream learner, but from learning which paths, time windows, reductions, and feature families should carry a database's task-relevant signal into that learner. Keeping this boundary explicit makes the learner pluggable: today CatBoost; tomorrow TabPFN-3 or another tabular or relational foundation model.

Kurve has pursued this direction from the outset: predictive performance depends on adapting the relational representation as well as the learner that consumes it. Recent independent results validate that longstanding bet from adjacent directions. KumoRFM-2 reports that task-specific fine-tuning improves its average SALT MRR from 0.83 in-context to 0.89, showing that even a strong relational foundation model benefits materially from task adaptation (KumoRFM-2). Prior Labs' TabPFN-Rel couples Deep Feature Synthesis with TabPFN-3 and obtains leading RelArena results, demonstrating the value of giving a foundation learner a separately constructed relational representation (RelArena and TabPFN-Rel). Neither system optimizes exactly the same object as KurveRSC, but both reinforce KurveRSC's original modular thesis: relational representation and downstream learning should be adapted jointly without permanently binding either layer to the other.

How KurveRSC differs from Deep Feature Synthesis

Deep Feature Synthesis generates a relational feature space; KurveRSC selects a relational program by measuring how well its complete feature frame works with the downstream learner.

DimensionDeep Feature SynthesisGraphReduceKurveRSC
Primary objectComposed feature definitionsExecutable table graph and node operationsSearch over complete GraphReduce programs
Learner roleNormally fitted after synthesisExternal to the execution engineIn the loop: AUROC or MAE scores every candidate frame
Task adaptationCaller chooses primitives and depth; later feature selection can remove columnsCaller configures one graph programSearch jointly chooses depth, families, annotations, budgets, and temporal policy
Final artifactFeature definitions and materialized tableReduced frame and operation lineageSelected configuration, frozen execution plan, schema, and fitted learner
InferenceRecompute the chosen definitionsRe-execute the configured graphReplay the learned plan with feature discovery disabled

Learner regularization over a wide DFS matrix can choose among columns that were generated, but it cannot recover paths, time windows, feature families, or propagation depths that were never materialized. KurveRSC makes those upstream choices part of validation-guided selection while retaining a replaceable downstream model. See the technical report's full comparison.

The KurveRSC technical report (PDF) explains the GraphReduce algorithm, relational feature families, learner-guided graph search, point-in-time guarantees, frozen-plan lifecycle, and evaluation protocol.

Quickstart

The high-level API is one function. Give fit an entity table, a label table, their join keys, the target, and an authoritative train/validation split:

pip install "kurversc[relbench]"  # omit [relbench] for ordinary tables
import kurversc

result = kurversc.fit(
    parent_node="customers.parquet",
    label_node="churn_labels.parquet",
    parent_key="customer_id",
    label_key="customer_id",
    target="churn",
    split_column="split",  # values: train / validation
)

print(result.best_config)         # highest validation ROC AUC or lowest MAE
print(result.recommended_config)  # simpler config when the gain is negligible
print(result.full_validation_score)
print(result.results)             # complete configuration-search audit trail

result is the fitted KurveRSC artifact: the selected GraphConfig, frozen GraphReduce feature-operation plan, downstream learner, feature schema, and validation metadata. Pass it to kurversc.predict(...) to replay the exact learned relational program at new cutoff dates.

RelArena performance

The latest fully completed 21-task reference profile (September 2, 2026) uses official RelBench v1 test splits through RelArena: full-data latest-cutoff graph search, three sequential reranking folds, one production cutoff, CatBoost, GraphReduce's fixed temporal periods, and no automatic text features. Classification reports test AUROC (higher is better); regression reports test MAE (lower is better).

KurveRSC RelArena default: complete latest-cutoff graph search, top-three reranking over three sequential full cutoff folds, a frozen selected graph plan, and final CatBoost fitting on one complete cutoff.

The reported reproducibility default prioritizes complete graph-configuration evidence while retaining only one materialized feature frame at a time.

DatasetTaskMetricKurveRSCTabPFN-Rel LocalWinner
rel-amazonuser-churnAUROC ↑0.7098730.702403KurveRSC
rel-amazonitem-churnAUROC ↑0.8264570.827857TabPFN-Rel Local
rel-amazonuser-ltvMAE ↓14.14189814.400940KurveRSC
rel-amazonitem-ltvMAE ↓42.37902447.768328KurveRSC
rel-avitouser-visitsAUROC ↑0.6741900.668811KurveRSC
rel-avitouser-clicksAUROC ↑0.6637180.614522KurveRSC
rel-avitoad-ctrMAE ↓0.0336580.031379TabPFN-Rel Local
rel-eventuser-repeatAUROC ↑0.7542780.769251TabPFN-Rel Local
rel-eventuser-ignoreAUROC ↑0.8319780.701376KurveRSC
rel-eventuser-attendanceMAE ↓0.2604200.239383TabPFN-Rel Local
rel-f1driver-dnfAUROC ↑0.7537550.714468KurveRSC
rel-f1driver-top3AUROC ↑0.6823590.792916TabPFN-Rel Local
rel-f1driver-positionMAE ↓3.9134333.761699TabPFN-Rel Local
rel-hmuser-churnAUROC ↑0.6963060.705690TabPFN-Rel Local
rel-hmitem-salesMAE ↓0.0318370.061362KurveRSC
rel-stackuser-engagementAUROC ↑0.9030120.905834TabPFN-Rel Local
rel-stackuser-badgeAUROC ↑0.8750810.863470KurveRSC
rel-stackpost-votesMAE ↓0.0633560.067957KurveRSC
rel-trialstudy-outcomeAUROC ↑0.7081410.730607TabPFN-Rel Local
rel-trialstudy-adverseMAE ↓41.33389642.591708KurveRSC
rel-trialsite-successMAE ↓0.4017550.385751TabPFN-Rel Local

KurveRSC wins 11 of 21 direct comparisons with TabPFN-Rel Local: 6–6 on classification and 5–4 on regression. On the complete 21-task matrix it is third overall by RelArena's bootstrapped Elo calculation at 1763.3, behind RT-PluRel and TabPFN-Rel API and ahead of TabPFN-Rel Local.

Aggregate RelArena leaderboard

This table includes every reproduced RelArena participant on the complete 21-task matrix. Elo is anchored to the global constant predictor at 1000; higher Elo and win rate are better, while lower mean rank and rescaled loss are better.

Elo rankMethodKindEloMean rankWin rateRescaled loss
1RT-PluRelsystem1857.52.95280.48%0.107555
2TabPFN-Rel APImodel1832.53.19078.10%0.148619
3KurveRSCsystem1763.33.90570.95%0.145282
4TabPFN-Rel Localmodel1733.04.23867.62%0.189932
5GraphSAGEmodel1662.25.04859.52%0.206977
6RelGTmodel1578.16.02449.76%0.321848
7RDBLearnmodel1563.66.19048.10%0.274152
8RelGNN-ESmodel1531.86.54844.52%0.305919
9LightGBM (entity-only)model1359.68.28627.14%0.546808
10Constant (per-entity)model1259.49.09519.05%0.618487
11Constant (global)model1000.010.5244.76%0.941930

Ranking by rescaled loss

RelArena's default aggregate orders methods by mean per-task min-max rescaled error. Lower is better; the Elo rank is retained to make the two orderings easy to compare.

Loss rankMethodKindRescaled lossElo rankElo
1RT-PluRelsystem0.10755511857.5
2KurveRSCsystem0.14528231763.3
3TabPFN-Rel APImodel0.14861921832.5
4TabPFN-Rel Localmodel0.18993241733.0
5GraphSAGEmodel0.20697751662.2
6RDBLearnmodel0.27415271563.6
7RelGNN-ESmodel0.30591981531.8
8RelGTmodel0.32184861578.1
9LightGBM (entity-only)model0.54680891359.6
10Constant (per-entity)model0.618487101259.4
11Constant (global)model0.941930111000.0

KurveRSC is also second overall by rescaled loss, behind RT-PluRel. Because systems use their own internal selection regimes, RelArena reports method kind explicitly: KurveRSC and RT-PluRel are systems, while the remaining learned participants are models under RelArena's standardized tuning interface.

Per-task results across all participants

These matrices combine KurveRSC's default-profile test results with the validation-selected, seed-zero test scores in RelArena's reproduced release artifact. Bold marks the best held-out score on each task. AUROC is maximized; MAE is minimized.

Classification — AUROC ↑
Dataset / taskKurveRSCRT-PluRelTabPFN-Rel APITabPFN-Rel LocalGraphSAGERelGTRDBLearnRelGNN-ESLightGBMConstant/entityConstant/globalOverall winner
rel-amazon/item-churn0.8264570.8326560.8279960.8278570.8305270.8238350.8195380.7855790.6622110.7288750.500000RT-PluRel
rel-amazon/user-churn0.7098730.7134600.7086490.7024030.7045960.7019240.6843750.6942810.5170560.6342050.500000RT-PluRel
rel-avito/user-clicks0.6637180.5833760.6751910.6145220.6086740.6443530.6787690.6675710.5641630.5041430.500000RDBLearn
rel-avito/user-visits0.6741900.6708870.6680260.6688110.6657580.6621420.6596240.6487310.5292820.6027030.500000KurveRSC
rel-event/user-ignore0.8319780.8475770.8786590.7013760.7587280.7815070.6643510.8053930.7771810.8399300.500000TabPFN-Rel API
rel-event/user-repeat0.7542780.7913770.7592910.7692510.7846260.7343580.7440840.7546120.7482950.7518050.500000RT-PluRel
rel-f1/driver-dnf0.7537550.7314600.7321720.7144680.7172350.7116670.7145510.7261060.7302980.6992580.500000KurveRSC
rel-f1/driver-top30.6823590.7588580.7714260.7929160.7259750.8108410.7800810.7588640.7388890.5565300.500000RelGT
rel-hm/user-churn0.6963060.7043560.7052150.7056900.6985250.6895310.6983520.6820250.5900810.6479720.500000TabPFN-Rel Local
rel-stack/user-badge0.8750810.8916120.8803860.8634700.8887480.5742860.7711470.6205840.5379950.7889560.500000RT-PluRel
rel-stack/user-engagement0.9030120.8967750.9059940.9058340.9056090.9067310.8586700.9050540.8118360.8267170.500000RelGT
rel-trial/study-outcome0.7081410.7234870.7647020.7306070.6862320.6684950.7212050.6574350.7150180.5000000.500000TabPFN-Rel API
Regression — MAE ↓
Dataset / taskKurveRSCRT-PluRelTabPFN-Rel APITabPFN-Rel LocalGraphSAGERelGTRDBLearnRelGNN-ESLightGBMConstant/entityConstant/globalOverall winner
rel-amazon/item-ltv42.37902443.02801246.76818147.76832849.24533748.67338648.99752752.45808455.75075065.35141964.233613KurveRSC
rel-amazon/user-ltv14.14189813.94301114.35821214.40094014.41532114.35212914.57754014.57522516.78468217.42346916.782979RT-PluRel
rel-avito/ad-ctr0.0336580.0348480.0310800.0313790.0389660.0364990.0341030.0426460.0412500.0412010.043067TabPFN-Rel API
rel-event/user-attendance0.2604200.2409490.2439440.2393830.2450180.2614930.2422150.2438580.2627980.2691520.263534TabPFN-Rel Local
rel-f1/driver-position3.9134333.8176993.7691813.7616994.0111124.7655293.8886494.2658874.1058294.1035094.399101TabPFN-Rel Local
rel-hm/item-sales0.0318370.0402580.0605070.0613620.0551530.0531680.0671320.0564930.0752950.0780330.076087KurveRSC
rel-stack/post-votes0.0633560.0634630.0678820.0679570.0648980.0679040.0677190.0679040.0660990.0693740.067904KurveRSC
rel-trial/site-success0.4017550.4103570.4126240.3857510.3248510.3701100.4858330.3404930.4375060.4411480.462222GraphSAGE
rel-trial/study-adverse41.33389632.65279139.75367442.59170844.31526144.12588744.02670546.27006444.57349557.53324757.533247RT-PluRel

See the technical report for the complete protocol and bootstrap uncertainty intervals.

Both node arguments accept a pandas DataFrame, CSV/Parquet path, or the name of a table/view on a supplied DuckDB connection. For explicit metadata, use Table and Labels:

result = kurversc.fit(
    parent_node=kurversc.Table(
        "users", name="users", key="Id", date="CreationDate"
    ),
    label_node=kurversc.Labels(
        "user_labels",
        key="user_id",
        target="will_return",
        timestamp="timestamp",
        split="split",
    ),
    tables=[
        kurversc.Table(
            "posts", name="posts", key="Id", date="CreationDate"
        ),
        kurversc.Table(
            "comments", name="comments", key="Id", date="CreationDate"
        ),
    ],
    relationships=[
        kurversc.Relationship(
            parent="users",
            child="posts",
            parent_key="Id",
            child_key="OwnerUserId",
        ),
        kurversc.Relationship(
            parent="posts",
            child="comments",
            parent_key="Id",
            child_key="PostId",
        ),
    ],
    connection=duckdb_connection,
)

When the target is a future aggregation over one of the graph's event tables, let GraphReduce generate it natively instead of supplying a materialized label table:

label_node=kurversc.GraphLabels(
    table="orders",
    field="id",
    operation="bool",
    period_days=365,
    train_cutoffs=("2023-01-01", "2024-01-01"),
    validation_cutoffs=("2025-01-01",),
    test_cutoffs=("2026-01-01",),
    target="will_order",
)

This executes GraphReduce's prep_for_labels() and automatic do_labels aggregation at every cutoff. Labels remains the correct interface for authoritative external targets such as official RelBench task tables.

Relationships are required when the compute graph contains feature tables: file names alone cannot determine foreign-key direction or whether a join is one-to-many. The two label/entity keys are also explicit so label attachment is never guessed.

What fit searches

The default search is deterministic and starts with the smallest base-only configuration. Before building a graph, KurveRSC profiles a small sample from every node and utility-ranks its source columns. Structural keys and cutoff dates are always retained. A cap therefore admits the strongest observed source columns instead of whichever columns happen to occur first in the physical schema.

result = kurversc.fit(
    ...,
    feature_family_max_columns=4,           # fixed columns per family
    feature_family_max_features_per_column=32,
    feature_propagation_max_functions_per_column=1,
    feature_ranking_rows=2_000,
    forward_search_beam_width=2,
    screening_rows=10_000,
    sample_rows=50_000,                     # confirmation fidelity
    confirmation_top_k=8,                  # diverse 50K candidates
    rerank_top_k=3,                         # full-data finalists
    rerank_cutoff_frames=3,                 # sequential walk-forward folds
    adaptive_depth_promotion=True,
    capability_pruning=True,
    search_max_features=8_000,
    random_state=42,                        # CatBoost and sampling seed
)

random_state is a reproducibility seed, not a trial count. KurveRSC uses the same deterministic seed for competing graph configurations so stochastic model behavior does not favor one shape over another.

The family lattice contains independent additions of temporal, sequence, conditional, and episode to base, including their combinations. It does not require a weak family to be present before a later family can be tested. Depth 3 is limited to combinations of base, temporal, and sequence; wider conditional and episode programs use depths 1 and 2.

At the default four-column budget, the forward beam executes at most 24 graph shapes: up to four adaptive base variants, then at most 8, 6, 4, and 2 survivors across the successive family levels. The complete 72-shape lattice remains in the audit trail with non-executed candidates marked pruned. A wider budget is opt-in: feature_family_max_column_options=(4, 8) adds another 72 potential records, but only the raw narrow-budget winner and the complexity-aware narrow-budget recommendation are promoted from four to eight source columns (with the next score-ranked shape filling the second slot when they are identical). That expanded funnel normally materializes at most 28 configurations rather than exhaustively running all 144 potential combinations.

default cap:       4 base + 8 singles + 6 pairs + 4 triples + 2 quadruples = 24
optional wide cap: top-2 complete narrow-cap shapes                          =  2
                                                                           ----
maximum materialized by the opt-in expanded funnel                          = 26

The default search is multi-fidelity. Beam-admitted configurations are first screened with at most 10,000 rows per node. Eight structurally diverse candidates are rebuilt and rescored with sample_rows: the raw and complexity-aware leaders plus representatives of available families, deeper propagation, and both annotation policies. The strongest three confirmed shapes are then reranked over three complete relational cutoff folds before the final graph program is selected. result.results, result.confirmation_results, and result.rerank_results expose the three audit trails separately.

Adaptive depth promotion evaluates both annotation policies at depth 1, promotes only the stronger policy to depth 2, and admits depth 3 only when the depth-2 gain exceeds both the task tolerance and validation uncertainty. Capability pruning removes families that cannot produce operations for the available graph schema. Finally, search_max_features uses the source-column audit and observed parent widths to reject a predicted feature explosion before its SQL is materialized. All three guards can be disabled independently.

Customize the stages with max_depth, auto_annotate_options, and feature_family_stages, or pass explicit graph_configs to override the frontier. Set feature_family_max_column_options=(4, 8) to opt into wider refinement, or include None as a tier to test an uncapped finalist. feature_family_max_features_per_column is a separate GraphReduce guardrail: it prevents a single temporal or categorical source from expanding into an unbounded number of derived SQL features. The propagation cap prevents each already-derived column from branching again at every graph hop while retaining its canonical continuation (max→max, min→min, sum→sum, count→sum, and avg→avg). Inspect result.feature_audit to see every source column's role, utility score, family rank, eligible budget tiers, and exclusion reason.

semantic uses automatic annotations when auto_annotate_features=True (or caller-supplied GraphReduce annotations). context requires peer-group keys; Table.context_keys supplies them directly, and the RelBench adapter derives them from foreign keys other than the edge currently being reduced.

Every candidate holds the remaining node policy fixed: GraphReduce's native 1/3/4/7/14/30/60/90/180/365/730-day time-series periods (plus the compute horizon when it exceeds 365 days) unless infer_ts_periods=True, categorical cardinality threshold 20, categorical top-k 5, automatic text features disabled, and annotation bounds 10 categorical columns, 4 gated numeric columns, and top-k 3. These settings are assigned to each node explicitly so they are effective with GraphReduce 1.10.

Optional TabPFN v3 estimator

CatBoost remains the default downstream estimator. Install the local TabPFN integration and select v3 explicitly with:

pip install "kurversc[relbench,tabpfn]"
result = kurversc.fit(
    **problem.fit_kwargs(),
    model_backend="tabpfn_v3",
    estimator_train_rows=10_000,
    model_params={
        "n_estimators": 2,
        "fit_mode": "low_memory",
    },
)

estimator_train_rows is applied consistently to sampled configuration screening, full-history finalist fitting, and final train-plus-validation fitting. Classification samples are stratified and deterministic. When the TabPFN backend is selected without an explicit cap, KurveRSC defaults it to 10,000 rows. Graph materialization remains independent of this estimator-only cap, and the fitted artifact can be replayed with kurversc.predict(...).

By default, each search source—including labels—is exposed to GraphReduce through a temporary DuckDB view capped at sample_rows. Set search_full_data=True to evaluate every candidate against complete source tables instead:

result = kurversc.fit(
    parent_node=parent,
    label_node=labels,
    tables=tables,
    relationships=relationships,
    sample_rows=50_000,       # still used for ordinary sampled searches
    search_full_data=True,    # disables row sampling during config search
    full_training_frames=3,   # cutoff dates used for final frame ensembling
    infer_ts_periods=True,
    auto_text_features=False,
)

search_full_data=True uses complete rows at every eligible search cutoff selected by search_training_frames for every configuration admitted by the forward funnel. The winning configuration is selected directly from those validation scores unless temporal reranking is enabled, and is then fit across the requested full-training cutoff dates. Adapters can attach a separate connected search_source while retaining their uncapped production source. A new graph is created for every candidate because GraphReduce execution mutates node state. If labels contain a timestamp, features are built at each label cutoff; otherwise labels are split randomly (or by split_column) and the current time is used as the feature cutoff.

Classification candidates use CatBoost and validation ROC AUC. Regression candidates use CatBoost and validation MAE. The highest-performing candidate is always retained as best_trial. KurveRSC also records feature count, feature/model time, and an estimated validation-metric standard error. Trials that add at least 2x as many features without improving beyond both the fixed 0.002 AUC / 0.5% relative MAE floor and the configured uncertainty threshold are marked in result.complexity_notes. recommended_trial is the lowest-feature candidate statistically indistinguishable from the raw winner; best_trial remains the unpenalized validation winner. Set complexity_uncertainty_multiplier=0 to use only the fixed tolerances.

By default, rerank the three strongest confirmed finalists over three walk-forward full-data cutoff folds:

result = kurversc.fit(
    ...,
    rerank_top_k=3,
    rerank_cutoff_frames=3,
    rerank_stability_penalty=0.25,
)

The reranker learns and scores one cutoff frame at a time, releases it, and then advances to the next fold. Classification maximizes mean ROC AUC minus the configured standard-deviation penalty; regression minimizes mean MAE plus that penalty. The raw stability-adjusted winner is selected; the complexity guard remains a screening and audit mechanism but cannot override this full-frame evidence. The audit trail is available as result.rerank_results. Set rerank_cutoff_frames=1 for a single full-data train-to-validation rerank; the stability penalty is then zero because there is only one score.

What the returned fitted model means

fit has a nine-stage lifecycle:

  1. Utility-rank source columns and record the capped feature-funnel audit.
  2. Build beam-admitted candidates from sampled source views, or from complete source rows when search_full_data=True.
  3. Rank candidates by one-frame validation ROC AUC or MAE and promote only the strongest graph shapes to broader source-column budgets.
  4. Confirm a structurally diverse bounded candidate set at medium fidelity.
  5. Rerank the three strongest confirmed candidates over sequential full-data walk-forward cutoff folds and select the raw stability-adjusted winner.
  6. Freeze its exact GraphReduce operation plan and training-only feature schema.
  7. Materialize one production cutoff at a time and fit an independent CatBoost model for that frame.
  8. Score validation with the training-frame ensemble, then add independently fitted validation-frame models to the final train-plus-validation ensemble.
  9. Replay the plan with GraphReduce(train=False) at test cutoffs and expose predictions as result.test_predictions. If an external Labels test split contains targets, KurveRSC also records a test score.

The resulting production artifact is result.fitted_model: selected GraphConfig, frozen execution plan, ordered feature schema, CatBoost model, and validation/test metadata. result.model returns its final CatBoost model; result.execution_plan returns the production GraphReduce plan. Validation and test never run feature inference or annotation again.

When infer_ts_periods=True, KurveRSC asks GraphReduce to infer relationship-specific event-cadence windows. Each dated node or relationship can then replace the initial [7, 30, 90] windows with compact, data-derived lookbacks spanning the configured compute horizon. KurveRSC stores those inferred periods inside the frozen execution plan and restores them during validation, outer refit, and prediction; replay never re-infers them.

Replay the fitted artifact on another timestamped entity frame with the same declarative graph metadata:

predictions = kurversc.predict(
    result,
    parent_node=parent,
    prediction_node=kurversc.Labels(
        scoring_rows, key="customer_id", timestamp="timestamp"
    ),
    tables=tables,
    relationships=relationships,
)

The output preserves prediction-row order and adds a prediction column.

Point-in-time production training can use many frames. By default, every configuration is screened on one frame at the latest eligible cutoff. With search_full_data=True, that frame uses all available rows. Supply all valid cutoffs through GraphLabels.train_cutoffs, or all timestamped rows through Labels, then choose the incremental production frame count:

result = kurversc.fit(
    ...,
    search_full_data=True,     # evaluate all candidates on complete rows
    full_training_frames=3,    # 3 evenly spaced available train cutoffs
)

full_training_frames=None (the default) uses every available training cutoff. These are point-in-time graph frames, not partitions of raw event tables: every frame sees the complete history allowed by its cutoff, and all frames replay the selected operation plan. KurveRSC releases each materialized feature frame before constructing the next one. Independent CatBoost models are combined as an ensemble, so the final fit never concatenates those wide frames in memory. When full_training_frames=1, KurveRSC always uses the latest eligible training cutoff. The search audit trail is available as result.results.

Official RelBench tasks

load_relbench_problem uses the production RelBench dataset, task tables, date keys, primary keys, and foreign keys without adding task-specific feature expressions:

import kurversc

problem = kurversc.load_relbench_problem(
    "rel-stack",
    "user-badge",
    sample_rows=10_000,
    max_train_timestamps=1,
    max_enrichment_columns=8,
)
result = kurversc.fit(**problem.fit_kwargs(), sample_rows=10_000)

For a full-data configuration search followed by a three-cutoff production fit, use:

problem = kurversc.load_relbench_problem(
    "rel-stack",
    "user-badge",
    sample_rows=50_000,
    search_full_data=True,
    max_train_timestamps=3,
)
result = kurversc.fit(
    **problem.fit_kwargs(),
    sample_rows=50_000,
    search_full_data=True,
    full_training_frames=3,
    infer_ts_periods=True,
    auto_text_features=False,
)

This runs the beam-admitted graph configurations on complete rows at the latest training cutoff, promotes only the strongest shapes to the wider source-column budget, and fits the selected configuration across three cutoff dates while retaining only one materialized feature frame.

Install the optional adapter with pip install "kurversc[relbench]". The object adapter relbench_problem_from_objects(...) accepts a task, an already-censored RelBench database, and its train/validation tables; RelArena uses this path so its official inner and outer database cutoffs remain authoritative.

Relational schemas do require keys. This adapter reads them from official RelBench metadata; for ordinary files or database tables, provide them with Table and Relationship. Self-referential/cyclic foreign keys are omitted because GraphReduce currently uses an acyclic DiGraph; every reachable acyclic foreign-key path is represented as its own node instance. Referenced dimension tables reached through association/event tables are joined with reduce=False; by default their projected feature attributes are capped at eight, excluding high-cardinality free text. Pass max_enrichment_columns=None to retain every dimension attribute.

For temporally meaningful relational evaluation, provide Labels.timestamp and date columns on event tables. A dated parent is always filtered with parent.date <= Labels.timestamp before feature inference and again through GraphReduce's do_filters_ops. If the parent is a genuinely timeless entity table, declare Table(..., timeless=True) explicitly; an omitted parent date is otherwise rejected for temporal labels. Without event dates, KurveRSC cannot distinguish historical features from future data.

Local customer example

examples/cust_data_future_order.py contains a complete run for /usr/local/lake/cust_data. It derives train and validation labels through GraphReduce for “places an order in the following 365 days,” declares the customer root as explicitly timeless, supplies every primary/foreign key and event date, and runs the default beam-pruned configuration funnel. The example enables the kurversc logger at INFO, showing every attempted configuration, its score/feature count/timing, and the selected configuration.

Citation

If you use KurveRSC in research, please cite the KurveRSC technical report:

@techreport{madrigal2026kurversc,
  title       = {KurveRSC: Validation-Guided Relational Signal Compression with a Downstream Learner in the Loop},
  author      = {Madrigal, Wes},
  institution = {Kurve AI},
  year        = {2026},
  month       = sep,
  url         = {https://github.com/kurveai/kurversc/blob/main/docs/kurversc-technical-report.pdf}
}

Contributors

wesmadrigal

16 commits

Languages

Python

100.0%