Skip to content

fix: prevent data leakage — fit preprocessing after train/test split - #934

Merged
ooples merged 12 commits into
masterfrom
fix/data-leakage-fit-after-split
Mar 9, 2026
Merged

ooples merged 12 commits into
masterfrom
fix/data-leakage-fit-after-split

Conversation

@ooples

@ooples ooples commented Mar 7, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Fixes fix: data leakage — preprocessing pipeline fitted before train/test split #929: The preprocessing pipeline (e.g., StandardScaler) was fitted on the ENTIRE dataset before the train/test split, causing data leakage — test/validation statistics (mean, std dev) leaked into the training pipeline
  • Restructured BuildSupervisedInternalAsync() to split data FIRST, then FitTransform only on training data, and Transform (not FitTransform) validation/test data
  • The federated learning path is unchanged — it correctly treats all data as training data

What Changed

src/AiModelBuilder.cs — BuildSupervisedInternalAsync():

  • Moved DataSplitter.Split() call BEFORE preprocessing pipeline fitting
  • Training data: _preprocessingPipeline.FitTransform(XTrain) — learns statistics from training set only
  • Validation/test data: _preprocessingPipeline.Transform(XVal) / Transform(XTest) — applies training-fitted statistics
  • Federated path (usePartitionedFederatedData): unchanged, still FitTransform on all data (correct since all is training)

tests/.../DataLeakagePreventionIntegrationTests.cs — 5 new integration tests:

  1. Verifies preprocessing pipeline is fitted only on training data
  2. Verifies model trains successfully with preprocessing
  3. Verifies the fitted pipeline stored in AiModelResult can transform new data
  4. Verifies the no-preprocessing path still works correctly
  5. Verifies scaler statistics differ between train-only fit and all-data fit (proves no leakage)

Test plan

  • All 5 new DataLeakagePreventionIntegrationTests pass
  • All 24 existing LinearRegressionIntegrationTests pass (no regressions)
  • dotnet build src/AiDotNet.csproj -f net10.0 succeeds
  • dotnet build tests/AiDotNet.Tests/AiDotNetTests.csproj -f net10.0 succeeds

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Prevented data leakage by ensuring resampling/preparation and preprocessing are fitted only on training splits; added row-alignment and total-sample validation across splits and federated partitions.
  • New Features

    • AutoML and supervised flows now split data first, apply training-only preprocessing, and support explicit partitioned-federated aggregation with integrity checks; inference paths maintain alignment.
  • Documentation

    • Registry clarified as a compatibility accessor; builders are primary and concurrency caveats documented.
  • Tests

    • Added integration tests verifying training-only fitting, transform correctness, end-to-end predictions, and no-preprocessing fallback.

In BuildSupervisedInternalAsync(), the preprocessing pipeline (e.g.,
StandardScaler) was fitted on the ENTIRE dataset before the train/test
split. This leaked test/validation statistics (mean, std dev) into the
training pipeline, causing artificially inflated metrics.

The fix restructures the method:
1. Split data into train/val/test FIRST using DataSplitter.Split()
2. FitTransform preprocessing pipeline on training data ONLY
3. Transform (not FitTransform) validation and test data

The federated learning path is unchanged — it correctly uses all data
as training data, so FitTransform on everything remains correct.

Fixes #929

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 7, 2026 13:34
@vercel

vercel Bot commented Mar 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
aidotnet_website Ready Ready Preview, Comment Mar 9, 2026 3:18am
aidotnet-playground-api Ready Ready Preview, Comment Mar 9, 2026 3:18am

@coderabbitai

coderabbitai Bot commented Mar 7, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Splits data before preprocessing to prevent leakage: fit preprocessing only on the training partition and transform val/test; adds partitioned-federated aggregation and per-client/total-sample validations; demotes global PreprocessingRegistry to a compatibility shim; adds integration tests verifying training-only fitting and end-to-end behavior.

Changes

Cohort / File(s) Summary
Core builder logic
src/AiModelBuilder.cs
Reordered supervised build to call DataSplitter before any Fit operations; perform FitTransform on training partition and Transform on validation/test. Added partitioned-federated aggregation with per-client ranges and total-sample checks; propagate PreprocessingInfo. BLOCKING: review for any TODO/stubs/simplified implementations introduced by control-flow changes.
Preprocessing registry docs
src/Preprocessing/PreprocessingRegistry.cs
Demoted registry to compatibility convenience, added explicit concurrency warnings and guidance to use per-AiModelBuilder pipelines (via PreprocessingInfo); updated examples and remarks.
Integration tests
tests/AiDotNet.Tests/IntegrationTests/Regression/DataLeakagePreventionIntegrationTests.cs
Added 5 integration tests validating that preprocessing (e.g., scaler) is fitted only on training split, transforms produce expected shapes/values, end-to-end predict works with preprocessing, and no-preprocessing path remains functional; includes deterministic dataset helper.

Sequence Diagram(s)

sequenceDiagram
    participant User as "User"
    participant Builder as "AiModelBuilder"
    participant Splitter as "DataSplitter"
    participant Preproc as "PreprocessingPipeline"
    participant Trainer as "Trainer"
    participant Clients as "Federated Clients"

    User->>Builder: BuildSupervisedInternalAsync(data, config)
    Builder->>Splitter: Split(data) -> XTrain,XVal,XTest,yTrain,yVal,yTest
    alt non-federated & pipeline configured
        Builder->>Preproc: FitTransform(XTrain)
        Preproc-->>Builder: preprocessedXTrain
        Builder->>Preproc: Transform(XVal), Transform(XTest)
        Preproc-->>Builder: preprocessedXVal, preprocessedXTest
    else partitioned federated data
        Clients->>Builder: Provide per-client partitions
        Builder->>Builder: Aggregate client partitions -> aggregatedTrain, record per-client ranges
        Builder->>Preproc: FitTransform(aggregatedTrain)
        Preproc-->>Builder: preprocessedAggregatedTrain
        Builder->>Builder: Validate per-client ranges & total sample counts
    end
    Builder->>Trainer: Train(modelSpec, preprocessedXTrain, yTrain)
    Trainer-->>Builder: trainedModel
    Builder-->>User: AiModelResult(trainedModel, PreprocessingInfo)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

Poem

Split the rows, let training keep its sight,
Fit on what's its own, not on hidden light.
Federated shards counted, ranges tied and checked,
Pipelines travel with models, no stats misconnected.
Tests hum: no leakage — the pipeline's respect.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the primary fix: preventing data leakage by fitting preprocessing after the train/test split, which is the core change throughout the codebase.
Linked Issues check ✅ Passed The PR fully addresses issue #929 by reordering BuildSupervisedInternalAsync to split data first, fit preprocessing only on training data, and transform validation/test with the fitted pipeline, eliminating data leakage as required.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing data leakage: reordering split/preprocessing in AiModelBuilder, adding integration tests to verify the fix, and updating PreprocessingRegistry documentation. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/data-leakage-fit-after-split

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes supervised-training data leakage by ensuring the preprocessing pipeline is fitted after the train/validation/test split (fit on train only; transform val/test), and adds integration tests intended to prevent regressions.

Changes:

  • Refactors AiModelBuilder.BuildSupervisedInternalAsync() to split first, then FitTransform on XTrain and Transform on XVal/XTest.
  • Keeps the partitioned federated-learning path fitting on all data (since all data is training data there).
  • Adds DataLeakagePreventionIntegrationTests covering preprocessing fit/transform behavior and inference-time transform via stored pipeline.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
src/AiModelBuilder.cs Reorders split vs preprocessing to prevent leakage; adjusts preprocessing flow for supervised vs partitioned federated paths.
tests/AiDotNet.Tests/IntegrationTests/Regression/DataLeakagePreventionIntegrationTests.cs Adds integration tests for leakage prevention and pipeline persistence/transform.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/AiModelBuilder.cs
Comment thread src/AiModelBuilder.cs
Comment thread src/AiModelBuilder.cs
Comment thread src/AiModelBuilder.cs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/AiModelBuilder.cs (1)

2052-2086: ⚠️ Potential issue | 🟠 Major

Client-range partitioning is unsafe without a row-order guarantee.

This branch only verifies X/Y counts after preprocessing, then reuses the original StartRow/SampleCount ranges when CreateFederatedClientPartitionsFromClientRanges slices the transformed dataset. A transformer that reorders rows without changing the sample count will silently assign examples to the wrong client. Either carry stable row IDs through preprocessing and validate the output order, or reject partitioned-federated preprocessing stages unless they explicitly guarantee per-client row-order preservation. Based on learnings: Federated preprocessing invariant: When using IFederatedClientDataLoader in PredictionModelBuilder.BuildSupervisedInternalAsync (src/PredictionModelBuilder.cs), any preprocessing must preserve per-client row ordering and total sample counts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/AiModelBuilder.cs` around lines 2052 - 2086, The code currently applies
_preprocessingPipeline across the aggregated dataset and only checks row counts,
which allows row-reordering transformers to misassign examples to clients; fix
by either (A) propagating stable row IDs through preprocessing and validating
they remain in the original per-client order before calling
CreateFederatedClientPartitionsFromClientRanges (ensure
ConversionsHelper.ConvertToMatrix/ConvertToVector preserve/return row IDs and
compare them to the original StartRow/SampleCount ranges created for
CreateFederatedClientPartitionsFromClientRanges), or (B) forbid global
preprocessing when using IFederatedClientDataLoader in
PredictionModelBuilder.BuildSupervisedInternalAsync unless the pipeline
explicitly advertises it is order-preserving (add a flag/interface on the
pipeline and throw an InvalidOperationException from
AiModelBuilder.BuildSupervisedInternalAsync if _preprocessingPipeline != null
and not order-preserving). Include references to _preprocessingPipeline,
PreprocessingInfo<T,TInput,TOutput>,
CreateFederatedClientPartitionsFromClientRanges,
ConversionsHelper.ConvertToMatrix/ConvertToVector and IFederatedClientDataLoader
when implementing the check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/AiModelBuilder.cs`:
- Around line 2031-2039: BuildSupervisedInternalAsync still fits preprocessing
on the full dataset in two places (_dataPreparationPipeline.FitResample(...)
before splitting and the AutoML branch where
_preprocessingPipeline.FitTransform(autoMLPreparedX) runs before
DataSplitter.Split), which leaks validation/test statistics; change both flows
to split raw data first via DataSplitter.Split into XTrain/yTrain and XVal/yVal
(and XTest if used), run Fit/Resample only on XTrain/yTrain (e.g., call
_dataPreparationPipeline.FitResample on the training partition and
_preprocessingPipeline.Fit on XTrain), then call Transform on validation/test
partitions with the training-fitted pipeline; update the AutoML branch to
prepare and fit transforms only on its training partition (use
autoMLPreparedXTrain) before model selection/evaluation so all paths
consistently avoid leaking test/validation data.
- Around line 2042-2048: The CS8600/CS8604 suppression is left active for the
rest of AiModelBuilder.cs; narrow its scope by adding a matching `#pragma` warning
restore CS8600, CS8604 immediately after the four local declarations (TInput
XVal, TOutput yVal, TInput XTest, TOutput yTest) in the method around those
lines (or alternatively replace the suppression by making the validation/test
slot variables explicitly nullable or initialized via explicit non-null checks),
so that only that small block around those declarations disables the warnings
rather than the entire file.

In
`@tests/AiDotNet.Tests/IntegrationTests/Regression/DataLeakagePreventionIntegrationTests.cs`:
- Around line 25-236: Add a test exercising the federated path to ensure
preprocessing preserves per-client ordering and total sample counts: create a
minimal IFederatedClientDataLoader fixture (multiple clients with known row
counts and deterministic per-client row order), call
PredictionModelBuilder.BuildSupervisedInternalAsync (via
AiModelBuilder.BuildAsync) with a StandardScaler preprocessing pipeline, and
assert that the resulting PreprocessingInfo.Pipeline is fitted and that
transforming each client's data preserves row ordering and that the sum of
transformed rows equals the original total sample count; also include a negative
check comparing a scaler fitted on ALL concatenated data to ensure
per-client-fitted behavior differs if the implementation is wrong.
- Around line 27-106: The two tests
BuildAsync_WithPreprocessing_FitsOnlyOnTrainingData and
BuildAsync_WithPreprocessing_ModelTrainsSuccessfully are too weak (only
non-null/IsFitted checks) and won't detect preprocessing-fit-on-all-data
leakage; update the first test to compute training-only feature means (from rows
0..trainEnd) and overall feature means, then retrieve the fitted StandardScaler
parameters from result.PreprocessingInfo (mean/scale) and assert they match the
training-only means (and differ from overall means) to prove the scaler was
fitted on train only; for the second test either collapse into a single smoke
test with a clearer name or replace the trivial IsFitted assertion with a
concrete numeric check (e.g., transform a held-out test sample with
result.PreprocessingInfo + model and assert predictions follow expected linear
relationship) so both tests assert specific expected values rather than just
non-null.
- Around line 108-138: Update the test
BuildAsync_WithPreprocessing_PipelineInResultCanTransformNewData to assert
concrete properties of the transformed output rather than only non-null: after
calling preprocessingInfo.Pipeline.Transform(newData) verify the transformed
Matrix<double> has the same row/column dimensions as newData and assert at least
one expected scaled value for a known input (e.g., compute the expected
standardized value for newData[0,0] using the training data/statistics from
CreateLinearDataset or by applying the same StandardScaler logic) so the test
checks both shape preservation and a specific numeric result.
- Around line 164-233: The test currently only verifies the pipeline differs
from a scaler fit on ALL data; instead, update
BuildAsync_WithPreprocessing_ScalerStatisticsReflectTrainingDataOnly to
deterministically obtain the exact training subset and assert the pipeline was
fitted only on that training data: configure the data splitting used by
AiModelBuilder (or DataLoaders.FromMatrixVector) to a deterministic
split/no-shuffle or expose the training indices, then create a
StandardScaler<double> and FitTransform it on that exact training subset (using
the same Matrix<double> rows), and finally assert that
result.PreprocessingInfo.Pipeline.Transform(testPoint) equals the scaler fitted
on that exact training subset (compare per-feature within Tolerance) and not
merely unequal to the all-data fit; reference the test method
BuildAsync_WithPreprocessing_ScalerStatisticsReflectTrainingDataOnly,
AiModelBuilder.ConfigurePreprocessing, StandardScaler<double>,
DataLoaders.FromMatrixVector, and result.PreprocessingInfo.Pipeline to locate
where to change the test.

---

Outside diff comments:
In `@src/AiModelBuilder.cs`:
- Around line 2052-2086: The code currently applies _preprocessingPipeline
across the aggregated dataset and only checks row counts, which allows
row-reordering transformers to misassign examples to clients; fix by either (A)
propagating stable row IDs through preprocessing and validating they remain in
the original per-client order before calling
CreateFederatedClientPartitionsFromClientRanges (ensure
ConversionsHelper.ConvertToMatrix/ConvertToVector preserve/return row IDs and
compare them to the original StartRow/SampleCount ranges created for
CreateFederatedClientPartitionsFromClientRanges), or (B) forbid global
preprocessing when using IFederatedClientDataLoader in
PredictionModelBuilder.BuildSupervisedInternalAsync unless the pipeline
explicitly advertises it is order-preserving (add a flag/interface on the
pipeline and throw an InvalidOperationException from
AiModelBuilder.BuildSupervisedInternalAsync if _preprocessingPipeline != null
and not order-preserving). Include references to _preprocessingPipeline,
PreprocessingInfo<T,TInput,TOutput>,
CreateFederatedClientPartitionsFromClientRanges,
ConversionsHelper.ConvertToMatrix/ConvertToVector and IFederatedClientDataLoader
when implementing the check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8e851d3d-db47-4da6-a6a5-8b870354e85d

📥 Commits

Reviewing files that changed from the base of the PR and between 640251b and d178825.

📒 Files selected for processing (2)
  • src/AiModelBuilder.cs
  • tests/AiDotNet.Tests/IntegrationTests/Regression/DataLeakagePreventionIntegrationTests.cs

Comment thread src/AiModelBuilder.cs Outdated
Comment thread src/AiModelBuilder.cs Outdated
…istry sync

- Scope #pragma warning disable CS8600/CS8604 with matching restore
- Restore PreprocessingRegistry.Current in all ConfigurePreprocessing overloads
- Update PreprocessingRegistry docs to reflect thread-safety and usage pattern
- Rewrite tests to actually detect leakage: verify scaler produces different
  output than all-data scaler, verify transform shape and non-identity
- Add end-to-end predict test with preprocessing
- Add test verifying no-preprocessing path still works with predict

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (6)
src/AiModelBuilder.cs (2)

2039-2047: ⚠️ Potential issue | 🔴 Critical

Leakage is still possible in the remaining full-dataset fit paths.

The split-before-fit logic below fixes the plain supervised branch, but this method still learns from validation/test rows earlier via _dataPreparationPipeline.FitResample(...) / FitResampleTensor(...) on the full dataset, and the AutoML branch still calls _preprocessingPipeline.FitTransform(autoMLPreparedX) before its split. That means the stated leakage fix is still incomplete.

Concrete fix direction
-// fit row-changing preparation before splitting
-var (prepX, prepY) = _dataPreparationPipeline.FitResample(...);
-...
-(XTrain, yTrain, XVal, yVal, XTest, yTest) = DataSplitter.Split(... preparedX, preparedY, ...);
+// split raw data first
+(XTrain, yTrain, XVal, yVal, XTest, yTest) = DataSplitter.Split(... x, y, ...);
+
+// fit row-changing preparation on training only
+if (_dataPreparationPipeline != null && _dataPreparationPipeline.Count > 0)
+{
+    (XTrain, yTrain) = FitResampleTrainingOnly(XTrain, yTrain);
+}
+
+// then fit preprocessing on XTrain and transform XVal/XTest

Apply the same ordering in the AutoML branch as well.

As per coding guidelines: "Every PR must contain production-ready code." and "Incomplete features: Half-implemented patterns where some code paths work but others silently do nothing."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/AiModelBuilder.cs` around lines 2039 - 2047, The current change prevents
leakage for the plain supervised path but still fits preprocessing on the whole
dataset in other paths; update the AutoML and full-dataset fit paths to follow
the same split-then-fit ordering: move calls to
_dataPreparationPipeline.FitResample and FitResampleTensor and any call to
_preprocessingPipeline.FitTransform(autoMLPreparedX) so they are executed only
on training partitions after you perform the train/validation/test split (or
skip split only for true federated-client-all-training mode); ensure the AutoML
branch uses the same split-before-fit flow as the supervised branch and rewire
any variables it expects (e.g., autoMLPreparedX -> autoMLTrainX/autoMLValX) so
no preprocessing Fit/FitTransform is run on validation/test rows.

2050-2057: ⚠️ Potential issue | 🟠 Major

Scope the nullability suppression to the placeholder declarations only.

TInput and TOutput are unconstrained here, so default can still be null, and the #pragma warning disable CS8600, CS8604 stays active until Line 2813. That hides unrelated nullability diagnostics across almost the entire method.

Suggested change
-// These generic types are always value types (Matrix<T>, Vector<T>) at runtime,
-// so default produces valid zero-initialized values, not null. The suppression covers
-// both the declarations and all downstream usage sites in this method.
-#pragma warning disable CS8600, CS8604 // Generic type defaults - T is always a value type at runtime
-TInput XVal = default;
-TOutput yVal = default;
-TInput XTest = default;
-TOutput yTest = default;
+// Validation/test slots are optional in the federated branch.
+TInput XVal = default!;
+TOutput yVal = default!;
+TInput XTest = default!;
+TOutput yTest = default!;
...
-#pragma warning restore CS8600, CS8604

If downstream sites still warn after this, make those slots explicitly nullable or branch-specific instead of suppressing the whole method.

This checks the suppression span and whether the repository actually treats these generics as reference-capable. Expected result: the disable begins near Line 2053, the restore is only at Line 2813, and the repo contains evidence that the “always value types” comment is not safe.

#!/bin/bash
set -euo pipefail

file="src/AiModelBuilder.cs"

printf '%s\n' '-- suppression block --'
sed -n '2048,2058p' "$file"

printf '\n%s\n' '-- restore location --'
sed -n '2808,2814p' "$file"

printf '\n%s\n' '-- Matrix/Vector declarations in repo --'
rg -nP --type=cs '^\s*(public|internal)\s+(readonly\s+)?(partial\s+)?(struct|class)\s+(Matrix|Vector)<'

printf '\n%s\n' '-- reference-type AiModelBuilder usages/examples --'
rg -nP --type=cs 'AiModelBuilder<[^>]+,\s*string\s*,|AiModelBuilder<[^>]+,[^>]+,\s*string\s*>' src tests || true

Also applies to: 2813-2813

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/AiModelBuilder.cs` around lines 2050 - 2057, The nullability suppression
(`#pragma` warning disable CS8600, CS8604) currently spans the entire method;
narrow it so it only applies to the placeholder declarations for TInput and
TOutput: keep the pragma immediately above the four default assignments (TInput
XVal, TOutput yVal, TInput XTest, TOutput yTest) and add a matching `#pragma`
warning restore directly after those declarations; remove the broad suppression
that extends to Line 2813 and instead address any downstream warnings by making
specific variables nullable or adding targeted, minimal suppressions at the
usage sites (reference the TInput/TOutput placeholder declarations to locate
where to move the restore).
tests/AiDotNet.Tests/IntegrationTests/Regression/DataLeakagePreventionIntegrationTests.cs (4)

150-188: ⚠️ Potential issue | 🔴 Critical

PredictEndToEndWorks never verifies prediction correctness.

Right now this only proves the pipeline returns finite numbers. For an end-to-end regression test, use a dataset with known coefficients—or return them from CreateLinearDataset—and assert concrete predictions for fixed inputs within tolerance.

As per coding guidelines, tests must “Assert specific expected values, not just non-null.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tests/AiDotNet.Tests/IntegrationTests/Regression/DataLeakagePreventionIntegrationTests.cs`
around lines 150 - 188, Update the test
BuildAsync_WithPreprocessing_PredictEndToEndWorks to assert concrete prediction
values: modify CreateLinearDataset (or overload it) to generate and return the
true coefficient vector and intercept for the synthetic linear model, then after
building the pipeline (AiModelBuilder.BuildAsync) use those known coefficients
to compute expected outputs for the fixed newData and compare against
result.Predict(newData) with a numeric tolerance (e.g., using Assert.InRange or
Math.Abs difference). Ensure you reference CreateLinearDataset to obtain the
true parameters and use result.Predict for the actual predictions, replacing the
current only-finite checks with assertions that each predicted value is within
tolerance of the analytically computed expected value.

25-272: ⚠️ Potential issue | 🟠 Major

The federated preprocessing branch is still untested.

All five cases go through DataLoaders.FromMatrixVector, so the path the PR explicitly promises to preserve never gets exercised here. Please add one federated regression case that proves preprocessing keeps per-client row ordering intact and preserves the total sample count after transform.

Based on learnings, “When using IFederatedClientDataLoader in PredictionModelBuilder.BuildSupervisedInternalAsync (src/PredictionModelBuilder.cs), any preprocessing must preserve per-client row ordering and total sample counts.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tests/AiDotNet.Tests/IntegrationTests/Regression/DataLeakagePreventionIntegrationTests.cs`
around lines 25 - 272, Add a new integration test (e.g.,
BuildAsync_WithPreprocessing_FederatedPreservesOrderingAndCount) that exercises
the federated preprocessing branch by using an IFederatedClientDataLoader
implementation instead of DataLoaders.FromMatrixVector; arrange multiple clients
each with known row sequences and counts, configure the AiModelBuilder with a
StandardScaler via ConfigurePreprocessing, call BuildAsync so
PredictionModelBuilder.BuildSupervisedInternalAsync runs the federated path,
then assert that result.PreprocessingInfo.Pipeline.IsFitted is true and that
transforming each client's data preserves per-client row ordering and that the
sum of transformed row counts equals the total input samples (use unique markers
or sequential values per row to verify ordering and equality of counts).

27-92: ⚠️ Potential issue | 🔴 Critical

This still doesn’t prove the scaler was fit on the exact training split.

Comparing only against an all-data scaler leaves a false-positive gap: a buggy implementation that fits on train + validation would still differ from allDataScaler and pass. Rebuild the exact deterministic training partition and assert equality with a StandardScaler<double> fit on those rows, then keep the inequality check against the all-data fit as the secondary guard.

As per coding guidelines, good tests should “Assert specific expected values, not just non-null.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tests/AiDotNet.Tests/IntegrationTests/Regression/DataLeakagePreventionIntegrationTests.cs`
around lines 27 - 92, The test must prove the scaler was fitted on the exact
training rows — reconstruct the deterministic training partition used by
AiModelBuilder and assert the pipeline equals a scaler fit on those rows.
Recreate the train indices using the same splitter/settings used by the builder
(e.g., call DataSplitter.Split or the same split routine/seed used by
AiModelBuilder), fit a new StandardScaler<double> on x[trainIndices,*], and
Assert that trainFittedPipeline.Transform(testPoint) equals that scaler's
Transform(testPoint); keep the existing inequality check against allDataScaler
as a secondary guard. Locate symbols:
BuildAsync_WithPreprocessing_ScalerStatisticsReflectTrainingDataOnly,
AiModelBuilder.ConfigurePreprocessing, result.PreprocessingInfo?.Pipeline,
StandardScaler<double>, and DataSplitter.Split (or the project’s split function)
to implement this change.

95-148: ⚠️ Potential issue | 🔴 Critical

TransformProducesCorrectShape is still a smoke test.

Shape preservation plus “some value changed” will pass for a lot of broken transforms. This should assert one or more exact standardized values for a fixed input, ideally by fitting a reference scaler on the same deterministic training rows and comparing the transformed cells directly.

As per coding guidelines, tests must “Assert specific expected values, not just non-null.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tests/AiDotNet.Tests/IntegrationTests/Regression/DataLeakagePreventionIntegrationTests.cs`
around lines 95 - 148, The test
BuildAsync_WithPreprocessing_TransformProducesCorrectShape is currently a weak
smoke test; replace the vague "some value changed" check with deterministic
assertions by reproducing the StandardScaler fit on the same training rows (the
CreateLinearDataset output used by the builder) to compute expected standardized
values and then compare specific transformed cells from
preprocessingInfo.Pipeline.Transform(newData) against those expected values (use
Tolerance for floating comparison). Locate the fit used by the builder (the
ConfigurePreprocessing pipeline / StandardScaler<double>), fit an independent
StandardScaler on the same training matrix x, transform the same newData with
that reference scaler, and assert equality for one or more specific cells (e.g.,
transformed[0,0] and transformed[1,2]) to ensure exact standardized behavior
rather than just shape or any-change checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Preprocessing/PreprocessingRegistry.cs`:
- Around line 16-19: The static mutable registry (PreprocessingRegistry.Current
/ field _preprocessingPipeline) allows cross-build contamination; remove/stop
using the global accessor in the active path by eliminating reads from
PreprocessingRegistry.Current inside Transform() and FitTransform(), instead
thread-scope the fitted pipeline on the builder and propagate the fitted
pipeline via PreprocessingInfo (or a builder-owned property) to all consumers;
update callers of Transform(), FitTransform(), and any code that previously read
PreprocessingRegistry.Current to accept the fitted pipeline/PreprocessingInfo as
an explicit parameter so no concurrent build ever reads a shared static.

---

Duplicate comments:
In `@src/AiModelBuilder.cs`:
- Around line 2039-2047: The current change prevents leakage for the plain
supervised path but still fits preprocessing on the whole dataset in other
paths; update the AutoML and full-dataset fit paths to follow the same
split-then-fit ordering: move calls to _dataPreparationPipeline.FitResample and
FitResampleTensor and any call to
_preprocessingPipeline.FitTransform(autoMLPreparedX) so they are executed only
on training partitions after you perform the train/validation/test split (or
skip split only for true federated-client-all-training mode); ensure the AutoML
branch uses the same split-before-fit flow as the supervised branch and rewire
any variables it expects (e.g., autoMLPreparedX -> autoMLTrainX/autoMLValX) so
no preprocessing Fit/FitTransform is run on validation/test rows.
- Around line 2050-2057: The nullability suppression (`#pragma` warning disable
CS8600, CS8604) currently spans the entire method; narrow it so it only applies
to the placeholder declarations for TInput and TOutput: keep the pragma
immediately above the four default assignments (TInput XVal, TOutput yVal,
TInput XTest, TOutput yTest) and add a matching `#pragma` warning restore directly
after those declarations; remove the broad suppression that extends to Line 2813
and instead address any downstream warnings by making specific variables
nullable or adding targeted, minimal suppressions at the usage sites (reference
the TInput/TOutput placeholder declarations to locate where to move the
restore).

In
`@tests/AiDotNet.Tests/IntegrationTests/Regression/DataLeakagePreventionIntegrationTests.cs`:
- Around line 150-188: Update the test
BuildAsync_WithPreprocessing_PredictEndToEndWorks to assert concrete prediction
values: modify CreateLinearDataset (or overload it) to generate and return the
true coefficient vector and intercept for the synthetic linear model, then after
building the pipeline (AiModelBuilder.BuildAsync) use those known coefficients
to compute expected outputs for the fixed newData and compare against
result.Predict(newData) with a numeric tolerance (e.g., using Assert.InRange or
Math.Abs difference). Ensure you reference CreateLinearDataset to obtain the
true parameters and use result.Predict for the actual predictions, replacing the
current only-finite checks with assertions that each predicted value is within
tolerance of the analytically computed expected value.
- Around line 25-272: Add a new integration test (e.g.,
BuildAsync_WithPreprocessing_FederatedPreservesOrderingAndCount) that exercises
the federated preprocessing branch by using an IFederatedClientDataLoader
implementation instead of DataLoaders.FromMatrixVector; arrange multiple clients
each with known row sequences and counts, configure the AiModelBuilder with a
StandardScaler via ConfigurePreprocessing, call BuildAsync so
PredictionModelBuilder.BuildSupervisedInternalAsync runs the federated path,
then assert that result.PreprocessingInfo.Pipeline.IsFitted is true and that
transforming each client's data preserves per-client row ordering and that the
sum of transformed row counts equals the total input samples (use unique markers
or sequential values per row to verify ordering and equality of counts).
- Around line 27-92: The test must prove the scaler was fitted on the exact
training rows — reconstruct the deterministic training partition used by
AiModelBuilder and assert the pipeline equals a scaler fit on those rows.
Recreate the train indices using the same splitter/settings used by the builder
(e.g., call DataSplitter.Split or the same split routine/seed used by
AiModelBuilder), fit a new StandardScaler<double> on x[trainIndices,*], and
Assert that trainFittedPipeline.Transform(testPoint) equals that scaler's
Transform(testPoint); keep the existing inequality check against allDataScaler
as a secondary guard. Locate symbols:
BuildAsync_WithPreprocessing_ScalerStatisticsReflectTrainingDataOnly,
AiModelBuilder.ConfigurePreprocessing, result.PreprocessingInfo?.Pipeline,
StandardScaler<double>, and DataSplitter.Split (or the project’s split function)
to implement this change.
- Around line 95-148: The test
BuildAsync_WithPreprocessing_TransformProducesCorrectShape is currently a weak
smoke test; replace the vague "some value changed" check with deterministic
assertions by reproducing the StandardScaler fit on the same training rows (the
CreateLinearDataset output used by the builder) to compute expected standardized
values and then compare specific transformed cells from
preprocessingInfo.Pipeline.Transform(newData) against those expected values (use
Tolerance for floating comparison). Locate the fit used by the builder (the
ConfigurePreprocessing pipeline / StandardScaler<double>), fit an independent
StandardScaler on the same training matrix x, transform the same newData with
that reference scaler, and assert equality for one or more specific cells (e.g.,
transformed[0,0] and transformed[1,2]) to ensure exact standardized behavior
rather than just shape or any-change checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c13c6717-e3e5-4491-aaa3-ddd4a7422f4a

📥 Commits

Reviewing files that changed from the base of the PR and between d178825 and f838e2e.

📒 Files selected for processing (3)
  • src/AiModelBuilder.cs
  • src/Preprocessing/PreprocessingRegistry.cs
  • tests/AiDotNet.Tests/IntegrationTests/Regression/DataLeakagePreventionIntegrationTests.cs

Comment thread src/Preprocessing/PreprocessingRegistry.cs Outdated
FitResample (SMOTE, outlier removal, augmentation) was applied to the
full dataset before splitting, allowing synthetic samples derived from
test/validation data to leak into training. Now FitResample runs after
the split on training data only in both standard and AutoML paths.
Also fixed AutoML preprocessing to FitTransform on training only and
Transform on validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/AiModelBuilder.cs (1)

2088-2102: ⚠️ Potential issue | 🟠 Major

Sample-count checks alone don't protect federated client boundaries.

CreateFederatedClientPartitionsFromClientRanges() later slices by the original StartRow/SampleCount. A preprocessing step that reorders rows but preserves count will pass these checks and silently assign samples to the wrong clients. This path needs an order-preservation contract, or the preprocessing/data-preparation has to run per client before aggregation.

Based on learnings, Federated preprocessing invariant: When using IFederatedClientDataLoader in PredictionModelBuilder.BuildSupervisedInternalAsync (src/PredictionModelBuilder.cs), any preprocessing must preserve per-client row ordering and total sample counts. The code now enforces X/Y alignment and total count checks after PreprocessData when federated client ranges are used, failing fast with a clear exception if violated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/AiModelBuilder.cs` around lines 2088 - 2102, The current checks in
BuildSupervisedInternalAsync only validate global X/Y alignment and total sample
count after PreprocessData but do not guarantee per-client row ordering, which
can let reordered-but-count-preserving preprocessing misassign samples when
CreateFederatedClientPartitionsFromClientRanges slices by original
StartRow/SampleCount; fix by enforcing an order-preservation contract: either
run preprocessing per-client before aggregating (i.e., apply PreprocessData for
each IFederatedClientDataLoader partition), or add a post-preprocess validation
that for each federated client range (using the original StartRow/SampleCount
from IFederatedClientDataLoader) the corresponding rows in preprocessedMatrix
remain contiguous and in the same relative order (and throw a clear
InvalidOperationException referencing PreprocessData,
CreateFederatedClientPartitionsFromClientRanges, and
PredictionModelBuilder.BuildSupervisedInternalAsync if any client’s rows are
missing/reordered); ensure error messages name
preprocessedMatrix/preprocessedVector and expectedFederatedSampleCount so
callers know to switch to per-client preprocessing or disable reordering
filters.
♻️ Duplicate comments (1)
src/AiModelBuilder.cs (1)

2043-2047: ⚠️ Potential issue | 🟠 Major

Don't leave validation/test slots unset in the partitioned federated flow.

When usePartitionedFederatedData is true, these locals stay at default, but the HPO block later still builds OptimizationInputData from them before the federated branch takes over. That makes the warning suppression hide a real runtime path, not just noise. Either short-circuit HPO for partitioned federated builds or keep the federated path from sharing validation/test locals at all.

Possible guard
+        if (usePartitionedFederatedData &&
+            _hyperparameterOptimizer is not null &&
+            _hyperparameterSearchSpace is not null)
+        {
+            throw new InvalidOperationException(
+                "Hyperparameter optimization is not supported with partitioned federated data because validation/test splits are not materialized in that flow.");
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/AiModelBuilder.cs` around lines 2043 - 2047, The locals XVal, yVal,
XTest, yTest are left as default when usePartitionedFederatedData is true, but
later the HPO block still constructs OptimizationInputData from them causing a
real runtime path; update the flow so that when usePartitionedFederatedData is
true you either (a) short-circuit/skip the HPO/OptimizationInputData
construction entirely, or (b) ensure the federated branch provides valid
validation/test values before building OptimizationInputData. Concretely, modify
the logic around usePartitionedFederatedData and the HPO block (the code that
constructs OptimizationInputData and runs HPO) so it is not executed with
uninitialized XVal/yVal/XTest/yTest, or route federated validation/test data
into those variables before use.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/AiModelBuilder.cs`:
- Around line 2111-2115: The supervised split is hardcoded to shuffle:true which
reintroduces temporal leakage for time-series tasks; update the
DataSplitter.Split call in AiModelBuilder (the block that creates
XTrain,yTrain,XVal,yVal,XTest,yTest) to reuse the same shuffle policy used
earlier for AutoML time-series branches (i.e. set shuffle = false when the task
is TimeSeriesForecasting or TimeSeriesAnomalyDetection, or reuse the previously
computed shuffle flag/variable), so the final training/evaluation uses a
non-shuffled chronological split for time-series builds instead of always true.

---

Outside diff comments:
In `@src/AiModelBuilder.cs`:
- Around line 2088-2102: The current checks in BuildSupervisedInternalAsync only
validate global X/Y alignment and total sample count after PreprocessData but do
not guarantee per-client row ordering, which can let
reordered-but-count-preserving preprocessing misassign samples when
CreateFederatedClientPartitionsFromClientRanges slices by original
StartRow/SampleCount; fix by enforcing an order-preservation contract: either
run preprocessing per-client before aggregating (i.e., apply PreprocessData for
each IFederatedClientDataLoader partition), or add a post-preprocess validation
that for each federated client range (using the original StartRow/SampleCount
from IFederatedClientDataLoader) the corresponding rows in preprocessedMatrix
remain contiguous and in the same relative order (and throw a clear
InvalidOperationException referencing PreprocessData,
CreateFederatedClientPartitionsFromClientRanges, and
PredictionModelBuilder.BuildSupervisedInternalAsync if any client’s rows are
missing/reordered); ensure error messages name
preprocessedMatrix/preprocessedVector and expectedFederatedSampleCount so
callers know to switch to per-client preprocessing or disable reordering
filters.

---

Duplicate comments:
In `@src/AiModelBuilder.cs`:
- Around line 2043-2047: The locals XVal, yVal, XTest, yTest are left as default
when usePartitionedFederatedData is true, but later the HPO block still
constructs OptimizationInputData from them causing a real runtime path; update
the flow so that when usePartitionedFederatedData is true you either (a)
short-circuit/skip the HPO/OptimizationInputData construction entirely, or (b)
ensure the federated branch provides valid validation/test values before
building OptimizationInputData. Concretely, modify the logic around
usePartitionedFederatedData and the HPO block (the code that constructs
OptimizationInputData and runs HPO) so it is not executed with uninitialized
XVal/yVal/XTest/yTest, or route federated validation/test data into those
variables before use.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f5950163-0315-48d3-9fb2-3fbbca8482af

📥 Commits

Reviewing files that changed from the base of the PR and between f838e2e and d3e5f5b.

📒 Files selected for processing (1)
  • src/AiModelBuilder.cs

Comment thread src/AiModelBuilder.cs
Updated PreprocessingRegistry docs to clearly warn about concurrent build
safety issues. Applied same time-series non-shuffle policy to the final
supervised training split that AutoML already uses.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (3)
src/Preprocessing/PreprocessingRegistry.cs (1)

10-16: ⚠️ Potential issue | 🔴 Critical

Blocking: documenting the singleton hazard is not a production fix.

The warning is accurate, but Current, Transform(), and FitTransform() still keep the process-global pipeline on the active path, and the Current remarks below still call it thread-safe. Parallel builds can still observe another builder’s fitted transformer, so this remains cross-build contamination/data leakage, not just a docs issue.

Concrete direction
 public static class PreprocessingRegistry<T, TInput>
 {
+    [Obsolete("Use the builder-owned preprocessing pipeline / AiModelResult.PreprocessingInfo instead of process-global state.")]
     public static IDataTransformer<T, TInput, TInput>? Current
     {
         get
         {
             lock (_lock)
@@
         }
     }

+    [Obsolete("Pass the fitted pipeline explicitly; do not use process-global preprocessing state.")]
     public static TInput Transform(TInput input)
     {
         var pipeline = Current;
         if (pipeline != null && pipeline.IsFitted)
         {
             return pipeline.Transform(input);
         }
         return input;
     }

+    [Obsolete("Pass the fitted pipeline explicitly; do not use process-global preprocessing state.")]
     public static TInput FitTransform(TInput input)

Then migrate active callers to take the fitted pipeline explicitly from AiModelBuilder / PreprocessingInfo so this registry is compatibility-only until it can be removed.

As per coding guidelines, "Users should ONLY interact with AiModelBuilder.cs and AiModelResult.cs" and blocking issues include "Incomplete features" and "Every PR must contain production-ready code."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Preprocessing/PreprocessingRegistry.cs` around lines 10 - 16, The
PreprocessingRegistry singleton still exposes and mutates a process-global
pipeline via Current, Transform(), and FitTransform(), causing cross-build
contamination; update callers so the registry is strictly compatibility-only and
no longer holds or returns the active pipeline: remove or stop using the global
_preprocessingPipeline in PreprocessingRegistry by changing Current, Transform,
and FitTransform to no-op or to throw a clear compatibility-only exception, and
update all call sites to take the fitted pipeline explicitly from AiModelBuilder
(use its instance-level _preprocessingPipeline) or from PreprocessingInfo passed
in the result; ensure any code that previously relied on
PreprocessingRegistry.Current or its Transform/FitTransform now receives the
pipeline via AiModelBuilder/PreprocessingInfo to prevent any shared state across
parallel builds.
src/AiModelBuilder.cs (2)

1816-1820: ⚠️ Potential issue | 🟠 Major

This still shuffles manual time-series builds.

shuffleBeforeSplit is derived only from _autoMLOptions?.TaskFamilyOverride. Direct time-series model builds, and custom ConfigureAutoML(IAutoMLModel<...>) flows where _autoMLOptions stays null, still fall back to shuffle: true, so chronological evaluation is still wrong in those paths. Compute this once from the actual task/model being trained and reuse that helper in both branches.

Also applies to: 2114-2117

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/AiModelBuilder.cs` around lines 1816 - 1820, The shuffle logic currently
only checks _autoMLOptions?.TaskFamilyOverride so manual/time-series builds (or
ConfigureAutoML flows where _autoMLOptions is null) still get shuffle:true; add
a small helper (e.g., IsTimeSeriesTask or ComputeShuffleBeforeSplit) that
determines whether the current training target is a time-series task by
inspecting the actual task/model being trained (not just _autoMLOptions), then
use that helper to compute shuffleBeforeSplit and pass it into
DataSplitter.Split<T,TInput,TOutput> in both places (the block around
DataSplitter.Split and the other occurrence at the later branch) so
chronological splits are consistently unshuffled for time-series models.

2040-2047: ⚠️ Potential issue | 🟠 Major

The nullability suppression is still too broad.

Line 2040's rationale is incorrect: TInput and TOutput are unconstrained type parameters, so default can still be null. Keeping CS8600/CS8604 disabled until Line 2841 hides nullable-flow problems across the rest of BuildSupervisedInternalAsync, not just the optional validation/test placeholders. Replace this with explicit optional slots, or at minimum narrow the suppression to the smallest possible block.

Verify the scope and missing type constraints with:

#!/bin/bash
set -euo pipefail

file="src/AiModelBuilder.cs"

printf '%s\n' '-- generic declaration (no class/struct constraints) --'
sed -n '139,145p' "$file"

printf '\n%s\n' '-- pragma disable site --'
sed -n '2040,2048p' "$file"

printf '\n%s\n' '-- pragma restore site --'
sed -n '2840,2842p' "$file"

Also applies to: 2841-2841

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/AiModelBuilder.cs`:
- Around line 1816-1820: The shuffle logic currently only checks
_autoMLOptions?.TaskFamilyOverride so manual/time-series builds (or
ConfigureAutoML flows where _autoMLOptions is null) still get shuffle:true; add
a small helper (e.g., IsTimeSeriesTask or ComputeShuffleBeforeSplit) that
determines whether the current training target is a time-series task by
inspecting the actual task/model being trained (not just _autoMLOptions), then
use that helper to compute shuffleBeforeSplit and pass it into
DataSplitter.Split<T,TInput,TOutput> in both places (the block around
DataSplitter.Split and the other occurrence at the later branch) so
chronological splits are consistently unshuffled for time-series models.

In `@src/Preprocessing/PreprocessingRegistry.cs`:
- Around line 10-16: The PreprocessingRegistry singleton still exposes and
mutates a process-global pipeline via Current, Transform(), and FitTransform(),
causing cross-build contamination; update callers so the registry is strictly
compatibility-only and no longer holds or returns the active pipeline: remove or
stop using the global _preprocessingPipeline in PreprocessingRegistry by
changing Current, Transform, and FitTransform to no-op or to throw a clear
compatibility-only exception, and update all call sites to take the fitted
pipeline explicitly from AiModelBuilder (use its instance-level
_preprocessingPipeline) or from PreprocessingInfo passed in the result; ensure
any code that previously relied on PreprocessingRegistry.Current or its
Transform/FitTransform now receives the pipeline via
AiModelBuilder/PreprocessingInfo to prevent any shared state across parallel
builds.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9448b4cf-fb40-480b-9739-3a60449ac1e8

📥 Commits

Reviewing files that changed from the base of the PR and between d3e5f5b and 928128f.

📒 Files selected for processing (2)
  • src/AiModelBuilder.cs
  • src/Preprocessing/PreprocessingRegistry.cs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/AiModelBuilder.cs Outdated
When _dataPreparationPipeline runs (resampling/outlier removal) it modifies
XTrain/yTrain. The else branch (no preprocessing) was incorrectly using
preparedX/preparedY (pre-preparation) instead of XTrain/yTrain, losing
training-only preparation results for downstream logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
src/AiModelBuilder.cs (2)

2040-2047: ⚠️ Potential issue | 🟠 Major

CS8600/CS8604 are still suppressed for almost the entire method.

TInput/TOutput are unconstrained here, and later paths in this class handle reference-shaped datasets (TInput[], IEnumerable<TInput>), so default is not guaranteed non-null. Keeping the pragma active until Line 2841 hides unrelated nullability warnings across the rest of BuildSupervisedInternalAsync; make these slots explicitly nullable and restore immediately after the declarations.

Also applies to: 2841-2841

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/AiModelBuilder.cs` around lines 2040 - 2047, The pragma suppressions
around BuildSupervisedInternalAsync are too broad; change the four locals XVal,
yVal, XTest, and yTest to be explicitly nullable (e.g., TInput? XVal, TOutput?
yVal, etc.) so their types reflect possible null for unconstrained generics, and
then restore/remove the `#pragma` warning disable CS8600, CS8604 immediately after
those declarations so the suppression no longer spans the rest of the method;
keep all other logic unchanged but ensure subsequent code handles the nullable
locals or uses null-forgiving only where safe.

1816-1820: ⚠️ Potential issue | 🟠 Major

Non-AutoML time-series builds still shuffle.

Both split sites derive shuffleBeforeSplit only from _autoMLOptions?.TaskFamilyOverride. A caller that configures a forecasting or time-series anomaly model directly via ConfigureModel() never sets that flag, so chronological data can still be randomized and leak future points into training.

Also applies to: 2114-2117

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/AiModelBuilder.cs` around lines 1816 - 1820, The shuffle flag is only
keyed off _autoMLOptions?.TaskFamilyOverride so direct ConfigureModel()
time-series builds still shuffle and leak future data; update the
shuffleBeforeSplit condition to also check the explicit model configuration set
by ConfigureModel (e.g., the field/property that records the configured task
family such as _modelOptions or _configuredTaskFamily) and return false when
that configured task family is TimeSeriesForecasting or
TimeSeriesAnomalyDetection; apply the same change at both locations that compute
shuffleBeforeSplit (the site feeding DataSplitter.Split<T, TInput, TOutput> and
the other occurrence noted) so chronological splits are preserved for any
time-series build whether set via AutoML or ConfigureModel().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/AiModelBuilder.cs`:
- Around line 2051-2072: The federated branch in AiModelBuilder (the block using
_dataPreparationPipeline.FitResample / FitResampleTensor and
_preprocessingPipeline.FitTransform that writes preparedX/preparedY ->
preprocessedX) can silently break per-client row ordering; update this code to
either (a) apply FitResample/FitTransform per-client using the original
clientRanges from IFederatedClientDataLoader and rebuild clientRanges from the
per-client outputs, or (b) detect and reject pipelines that are not guaranteed
order-preserving by adding a validation step after FitResample/FitTransform
which verifies per-client sample counts and exact row ordering (e.g. a stable
index/sequence check) and throws when violated; reference
_dataPreparationPipeline, FitResample, FitResampleTensor,
_preprocessingPipeline, FitTransform, preparedX/preparedY, preprocessedX,
clientRanges, and PredictionModelBuilder.BuildSupervisedInternalAsync when
implementing the per-client processing or the validation rejection path.
- Around line 390-393: The code assigns the shared static
PreprocessingRegistry<T, TInput>.Current from AiModelBuilder (the
_preprocessingPipeline in ConfigurePreprocessing), which allows races and
pipeline leakage across models; to fix, stop writing the global: remove or guard
the assignment to PreprocessingRegistry<T, TInput>.Current and instead propagate
_preprocessingPipeline via instance-level injection into consumers (e.g., change
DocumentNeuralNetworkBase to accept an IPreprocessingPipeline instance or pass
_preprocessingPipeline into its inference methods), or if modifying consumers is
not possible, use a tightly-scoped set/restore pattern with a lock around any
inference that must read the static (save old = Current; Current =
_preprocessingPipeline; run inference; Current = old) to ensure no cross-model
swaps. Ensure all references to ConfigurePreprocessing and
DocumentNeuralNetworkBase are updated to use the injected/parameterized pipeline
rather than relying on the static Current.

---

Duplicate comments:
In `@src/AiModelBuilder.cs`:
- Around line 2040-2047: The pragma suppressions around
BuildSupervisedInternalAsync are too broad; change the four locals XVal, yVal,
XTest, and yTest to be explicitly nullable (e.g., TInput? XVal, TOutput? yVal,
etc.) so their types reflect possible null for unconstrained generics, and then
restore/remove the `#pragma` warning disable CS8600, CS8604 immediately after
those declarations so the suppression no longer spans the rest of the method;
keep all other logic unchanged but ensure subsequent code handles the nullable
locals or uses null-forgiving only where safe.
- Around line 1816-1820: The shuffle flag is only keyed off
_autoMLOptions?.TaskFamilyOverride so direct ConfigureModel() time-series builds
still shuffle and leak future data; update the shuffleBeforeSplit condition to
also check the explicit model configuration set by ConfigureModel (e.g., the
field/property that records the configured task family such as _modelOptions or
_configuredTaskFamily) and return false when that configured task family is
TimeSeriesForecasting or TimeSeriesAnomalyDetection; apply the same change at
both locations that compute shuffleBeforeSplit (the site feeding
DataSplitter.Split<T, TInput, TOutput> and the other occurrence noted) so
chronological splits are preserved for any time-series build whether set via
AutoML or ConfigureModel().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 39eeefa7-8e64-4db6-92f4-59909a504895

📥 Commits

Reviewing files that changed from the base of the PR and between 928128f and a655066.

📒 Files selected for processing (1)
  • src/AiModelBuilder.cs

Comment thread src/AiModelBuilder.cs Outdated
Comment thread src/AiModelBuilder.cs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/AiModelBuilder.cs Outdated
Comment thread src/AiModelBuilder.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

ooples and others added 2 commits March 8, 2026 12:03
…alues

Replace clustered bimodal data with linearly-spaced unique values (0, 10, 20, ..., 990)
to guarantee any 70/30 shuffle split produces significantly different scaler statistics
from the full dataset, eliminating flaky test failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ing field! patterns

Replaces default! with proper initialization (NumOps.Zero, throw, or nullable types),
null! with nullable parameters or removed dead code, and field! with extracted local
variables with null-coalescing throw expressions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove static PreprocessingRegistry writes from ConfigurePreprocessing
methods, aligning with master's instance-based pipeline approach.
Take master's deprecation docs for PreprocessingRegistry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 8, 2026 23:13
@vercel

vercel Bot commented Mar 8, 2026

Copy link
Copy Markdown

Deployment failed with the following error:

Resource is limited - try again in 15 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/franklins-projects-02a0b5a0?upgradeToPro=build-rate-limit

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/AiModelBuilder.cs Outdated
Comment thread src/Finance/Forecasting/Foundation/TOTEM.cs Outdated
Comment thread src/Finance/Forecasting/Foundation/TOTEM.cs Outdated
Comment thread src/AiModelBuilder.cs Outdated
ooples and others added 2 commits March 8, 2026 19:52
- Fix exception text to reference ConfigureModel() instead of WithModel()
- Remove nullable T? from TOTEM._lastCommitmentLoss field, initialize
  with NumOps.Zero in both constructors to avoid ambiguous nullability
- Fix incorrect comment about Matrix<T>/Vector<T> being value types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace EqualityComparer<T>.Default.Equals with _numOps.Equals to
fix CS8604 null reference error on net471 and follow codebase patterns.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 9, 2026 00:24
@vercel

vercel Bot commented Mar 9, 2026

Copy link
Copy Markdown

Deployment failed with the following error:

Resource is limited - try again in 14 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/franklins-projects-02a0b5a0?upgradeToPro=build-rate-limit

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/AiModelBuilder.cs
Comment thread src/AiModelBuilder.cs Outdated
Comment thread src/Helpers/StatisticsHelper.cs
- Narrow CS8600/CS8604 pragma scopes to specific declaration and call
  sites instead of spanning 800+ lines
- Clarify StatisticsHelper comment about default(T) vs NumOps.Zero
- Snapshot original values before Transform in test to prevent
  in-place mutation from invalidating the comparison

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ooples
ooples merged commit 89d3f33 into master Mar 9, 2026
35 of 39 checks passed
@ooples
ooples deleted the fix/data-leakage-fit-after-split branch March 9, 2026 10:36

This branch was successfully deployed

2 active deployments
Preview – aidotnet_website — 2d907f5e Deployed Mar 9, 2026 by vercel[bot]
Preview – aidotnet-playground-api — 2d907f5e Deployed Mar 9, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: data leakage — preprocessing pipeline fitted before train/test split

3 participants