> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kaireonai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Counterfactual Training

> Decision-boundary data augmentation for the gradient-boosted model trainer that generates synthetic neighbors of marginal training rows

## What it does

The **counterfactual trainer** is a pre-train hook that sharpens the
decision boundary of the `gradient_boosted` model by augmenting the
training set with synthetic rows near low-confidence predictions. It
runs entirely in TypeScript before the existing remote-GBM training
call, so the Python ml-worker stays unchanged.

The augmenter produces an enriched training set + a deterministic
`summary` describing how many synthetic rows were added and which
feature columns were perturbed.

## Honest limits

* **Numeric only.** Boolean / categorical features are held as-is on
  synthetic rows. Perturbation rules for them are undefined — silently
  perturbing them would corrupt training data.
* **Deterministic.** mulberry32 RNG seeded by `options.seed` (default
  7\) so two runs with the same options produce the same synthetic
  data.
* **Bounded budget.** `maxSynthetic` defaults to 10\_000. The function
  surfaces a `summary.syntheticRowsAdded` so operators can audit how
  much data was added per training pass.
* **Not a feature-store substitute.** This augments the training set at
  call time. It does not modify any persisted dataset.

## Opt-in via model config

Augmentation is off by default (training time + cost rise proportionally
to `syntheticPerRow × marginalCount`). It is enabled **per gradient-boosted
model** through that model's `config.counterfactualAugmentation` object —
not a tenant-level setting:

```jsonc theme={null}
{
  "config": {
    "counterfactualAugmentation": {
      "enabled": true,           // default false
      "marginalBand": 0.1,       // decision-boundary half-width
      "syntheticPerRow": 4,      // K synthetic neighbors per marginal row
      "maxSynthetic": 10000,     // hard cap
      "perturbStdFraction": 0.5, // gaussian noise as a fraction of feature σ
      "seed": 7                  // deterministic RNG seed
    }
  }
}
```

`trainGBMFromOutcomes` in `lib/scoring/train.ts` reads
`config.counterfactualAugmentation`. It runs the augmenter only when
`enabled === true` **and the model already has trained trees**
(`modelState.trees`) — the scorer needs a prior model, so augmentation is
skipped on cold-start. With the flag off (or on the first train) it
bypasses the augmenter entirely and passes the raw training set straight
to `trainGBMRemote`. The augmentation `summary` is persisted to
`modelState.augmentation` for auditability.

## API surface

The augmenter is a pure TS function. There is no HTTP endpoint — it
runs inline at training time. Callers use it like:

```ts theme={null}
import { augmentWithCounterfactuals } from "@/lib/ml/counterfactual-trainer";
import { trainGBMRemote } from "@/lib/ml-worker-client";
import { scoreGradientBoosted } from "@/lib/scoring/gradient-boosted";

// scoreGradientBoosted(modelState, predictors, features) → { score, ... }
const scorer = (features) =>
  scoreGradientBoosted(currentModel.modelState, predictors, features).score;
const { augmented, summary } = augmentWithCounterfactuals({
  request,
  scorer,
  options: { marginalBand: 0.1, syntheticPerRow: 4, seed: 7 },
});
console.log("counterfactual augmentation:", summary);

const result = await trainGBMRemote(augmented);
```

## Algorithm — what it does, what it doesn't

What the augmenter does, step by step:

1. Score every row of the labeled training set with the *current*
   `gradient_boosted` model via the supplied `scorer`.
2. Identify **marginal rows** — predicted probability in
   `[0.5 - marginalBand .. 0.5 + marginalBand]` (default band 0.1).
3. For each marginal row, generate **K synthetic neighbors** by
   perturbing each numeric feature with gaussian noise scaled to the
   observed feature standard deviation (default `0.5σ`, K=4).
4. Append the synthetic rows to the training set and return the
   augmented set with a reproducibility `summary`.

What the augmenter does **not** do:

* It does not perturb boolean or categorical features. Those are held
  as-is on synthetic rows.
* It does not perform binned-Bayes predictor grouping or online
  incremental updates. The augmenter only widens the training set;
  the gradient-boosted trainer itself does the learning.
* It does not vary the learning-rate schedule per row. The augmented
  set is fed to the remote GBM trainer with the same hyperparameters
  as any other training pass.
* It does not modify any persisted dataset. Augmentation happens at
  call time only.

Every synthetic row is reproducible from the seed, the input set, and
the scorer.
