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 r² |
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
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"])