Skip to content

Corruptors

Corruptors make the feature matrix messier in specific, recorded ways. Each one also updates effective_feature_importances, so you can see how much of each feature's information it destroyed rather than guessing.

They never touch y. Label noise is a separate mechanism — see Label noise below.

Severity is a preset, not a number

Every corruptor takes severity="low", "medium", or "high". That is a string, and passing a float raises ValueError. When you want a specific value, set the underlying parameter instead:

from synthbench import MissingDataCorruptor

MissingDataCorruptor(severity="high")  # preset: 30% missing
MissingDataCorruptor(proportion=0.22)  # exactly 22% missing

Setting the parameter overrides whatever the severity preset would have chosen.

What each one does

Corruptor Parameter to set directly Effect on X Effect on importances
MeasurementNoiseCorruptor noise_level Adds Gaussian noise Scaled by Var(X) / (Var(X) + noise_level²)
OutlierCorruptor proportion Replaces values with extremes Scaled by 1 − proportion
MissingDataCorruptor proportion Inserts NaN Scaled by the realised missing rate
CollinearityCorruptor noise_std Adds correlated proxy columns Split between original and proxy by
CategoricalCorruptor n_bins Discretises into bins Discounted by 1 − 1/n_bins

Chaining them

from synthbench import (
    BenchPipeline,
    LinearDGP,
    MeasurementNoiseCorruptor,
    MissingDataCorruptor,
    OutlierCorruptor,
)

pipeline = BenchPipeline(
    LinearDGP(complexity="medium", task_type="regression", random_state=0),
    corruptors=[
        MeasurementNoiseCorruptor(noise_level=0.5),
        OutlierCorruptor(proportion=0.05),
        MissingDataCorruptor(proportion=0.1),
    ],
)
result = pipeline.run(n_samples=500, n_features=10, random_state=42)

print(result.metadata["corruptor_order"])
print(result.metadata["effective_feature_importances"])

Feature corruptors run in a fixed order — collinearity, categorical, measurement noise, outlier, missing data — regardless of how you list them, because these operations do not commute. Listing them out of order produces a UserWarning and the canonical order is used anyway. See BenchPipeline.

Missing data mechanisms

Which values go missing usually matters more than how many, so MissingDataCorruptor implements all three standard mechanisms via mechanism=. This is the difference between an imputation method looking good and looking honest.

"mcar" (the default) drops values uniformly at random, independent of everything. Mean imputation is unbiased here, which is why MCAR flatters imputation methods.

"mar" makes the drop probability a logistic function of an observed pivot column — column 0 unless you set pivot_col. The pivot itself is left intact unless you name it in columns. Missingness is predictable from data you still have, so a method that conditions on other features can recover.

"mnar" is self-masking: each column's values drive their own disappearance, with larger values more likely to vanish. Nothing observed explains the pattern, so every imputation method is biased and the size of that bias is what you are measuring.

import numpy as np

from synthbench import BenchPipeline, LinearDGP, MissingDataCorruptor

for mechanism in ("mcar", "mar", "mnar"):
    pipeline = BenchPipeline(
        LinearDGP(task_type="regression"),
        corruptors=[MissingDataCorruptor(proportion=0.15, mechanism=mechanism)],
    )
    result = pipeline.run(n_samples=500, n_features=10, random_state=0)
    print(mechanism, round(float(np.isnan(result.X).mean()), 3))

For MAR and MNAR the logistic offset is calibrated by bisection against the actual value distribution, so the realised missing rate matches proportion rather than drifting with the column's spread. A constant column cannot drive its own missingness, so those fall back to MCAR for that column.

from synthbench import MissingDataCorruptor

MissingDataCorruptor(proportion=0.2, mechanism="mar", pivot_col=2)

Label noise

LabelNoiseCorruptor modifies y and nothing else. It goes in label_corruptors=, not corruptors= — passing it to the wrong argument raises TypeError.

noise_rate is the fraction of labels corrupted. With two classes a flip is the complement. With more, the label is replaced by one of the other classes drawn uniformly — symmetric noise, and never a no-op, so the realised corrupted fraction is exactly noise_rate. metadata["label_noise"]["n_classes"] records what the flips were drawn against.

from synthbench import BenchPipeline, LinearDGP, LabelNoiseCorruptor

pipeline = BenchPipeline(
    LinearDGP(task_type="classification"),
    label_corruptors=[LabelNoiseCorruptor(noise_rate=0.1)],
)
result = pipeline.run(n_samples=500, random_state=42)

noise = result.metadata["label_noise"]
print(noise["noise_rate"])  # 0.1
print(len(noise["affected_indices"]))  # ~50 flipped labels

For regression, noise_std is the standard deviation of Gaussian noise added to every target value, and noise_rate is ignored.

from synthbench import BenchPipeline, LinearDGP, LabelNoiseCorruptor

pipeline = BenchPipeline(
    LinearDGP(task_type="regression"),
    label_corruptors=[LabelNoiseCorruptor(noise_std=0.5)],
)
result = pipeline.run(n_samples=500, random_state=42)

What gets recorded under metadata["label_noise"] depends on the task:

Key Classification Regression
noise_rate Fraction flipped None
noise_std None Standard deviation used
affected_indices Indices of flipped samples None — every sample is affected
n_classes Classes the flips were drawn against absent

Label noise raises the floor, and the metadata does not follow

bayes_error_analytic is computed by the DGP, before any label corruptor runs, so the reported value is the floor of the clean labels. Symmetric noise at rate r over K classes raises it to

floor_after = 1 − (1 − b)·(1 − r) − r·b/(K − 1)

where b is the reported floor. This is exact, not an approximation, because the noise preserves which class is most likely. At K = 2 it collapses to b·(1 − r) + (1 − b)·r. Apply it yourself when you use label noise and quote a floor.

Both at once

Feature and label corruptors combine freely:

from synthbench import (
    BenchPipeline,
    LabelNoiseCorruptor,
    LinearDGP,
    MeasurementNoiseCorruptor,
    MissingDataCorruptor,
)

pipeline = BenchPipeline(
    LinearDGP(task_type="classification"),
    corruptors=[
        MeasurementNoiseCorruptor(severity="low"),
        MissingDataCorruptor(proportion=0.10, mechanism="mar"),
    ],
    label_corruptors=[LabelNoiseCorruptor(noise_rate=0.05)],
)
result = pipeline.run(n_samples=500, n_features=10, random_state=7)

print(result.metadata["corruptor_order"])
print(result.metadata["label_corruptor_order"])