Synthetic Data with Error Injection
This section demonstrates deterministic error injection — seeded, reproducible corruption applied as the last pipeline stage, for testing data-quality and reconciliation pipelines: spelled-out numbers in numeric columns (1200 → "one thousand, two hundred"), missing values, broken mappings, duplicated rows, and a corrupted derived summary that no longer reconciles with its detail.
Example Code
"""
Example: deterministic error injection with the FakerSynthesizer.
Perfect data doesn't test anything. This example generates a clean supermarket
daily-sales table plus a derived department summary, then injects seeded,
reproducible errors as the LAST pipeline stage — identities and aggregates are
computed on clean data, corruption exists only in the delivered output:
- format_violation "words": 1200.0 -> "one thousand, two hundred"
(the column is deliberately delivered as a string column — the untouched
numbers rendered as text next to the corrupted cells — downstream parsers
must survive exactly that)
- missing values at a controlled rate
- mapping_break: the strict sku -> department mapping violated in a
fraction of rows
- duplicate_rows: duplicated identifiers appended
- out_of_range ON THE DERIVED SUMMARY: the detail no longer reconciles to
the aggregate — the "unresolved mismatch" a data-quality or reconciliation
engine must alert on.
Everything is reproducible under random_state: the same run always corrupts
the same cells, so a DQ test suite can assert exactly which rows get flagged.
"""
import pandas as pd
from ydata.metadata import Metadata
from ydata.metadata.builder import MetadataConfigurationBuilder
from ydata.synthesizers import FakerSynthesizer
products = pd.DataFrame({
"sku": ["SKU-0001", "SKU-0002", "SKU-0003", "SKU-0004", "SKU-0005"],
"product_name": ["Semi-Skimmed Milk 2L", "Sourdough Loaf", "Bananas 1kg",
"Frozen Peas 750g", "Orange Juice 1L"],
"department": ["dairy", "bakery", "produce", "frozen", "beverages"],
})
columns = {
"units_sold": {"datatype": "numerical", "vartype": "int", "min": 1, "max": 500},
"revenue": {"datatype": "numerical", "vartype": "float", "min": 1.0, "max": 2000.0},
}
metadata = Metadata(configuration_builder=MetadataConfigurationBuilder(columns))
synth = FakerSynthesizer(locale="en")
synth.fit(
metadata,
table_name="daily_sales",
scaffold={
"dimensions": [
{"column": "sku", "from_table": "products",
"from_column": "sku", "include": ["product_name", "department"]},
{"column": "sales_date",
"date_range": {"start": "2026-03-01", "end": "2026-03-31", "freq": "D"}},
]
},
reference_data={"products": products},
derived_tables=[
{"name": "department_summary", "type": "aggregate",
"group_by": ["sales_date", "department"],
"aggregations": {
"total_revenue": {"column": "revenue", "agg": "sum"},
"total_units": {"column": "units_sold", "agg": "sum"},
}},
],
errors={
"daily_sales": {
"columns": {
"revenue": {
"format_violation": {"rate": 0.05, "styles": ["words", "currency"]},
"missing": 0.03,
},
"units_sold": {"out_of_range": {"rate": 0.02, "factor": 1000}},
},
"table": {
"duplicate_rows": 0.02,
"mapping_break": {"columns": ["sku", "department"], "rate": 0.03},
},
},
"department_summary": {
"columns": {"total_revenue": {"out_of_range": {"rate": 0.1, "factor": 100}}},
},
},
)
data = synth.sample(random_state=42)
sales = data["daily_sales"].to_pandas()
summary = data["department_summary"].to_pandas()
# ---------------------------------------------------------------------------
# What a data-quality pipeline should now find
# ---------------------------------------------------------------------------
# the whole column is text now; the corrupted cells are the ones that no
# longer parse as a plain number
text_cells = sales["revenue"].notna() & ~sales["revenue"].astype(str).str.fullmatch(r"-?\d+(\.\d+)?")
print("Representation errors (e.g. spelled-out numbers):")
print(sales.loc[text_cells, ["sku", "sales_date", "revenue"]].head(5).to_string())
assert text_cells.any() and pd.api.types.is_string_dtype(sales["revenue"])
print(f"\nMissing revenues injected: {sales['revenue'].isna().sum()}")
assert sales["revenue"].isna().any()
truth = dict(zip(products["sku"], products["department"]))
broken = sales[[truth[s] != d for s, d in zip(sales["sku"], sales["department"])]]
print(f"Broken sku->department mappings: {len(broken)}")
assert len(broken) >= 1
duplicates = sales.duplicated().sum()
print(f"Duplicated rows: {duplicates}")
assert duplicates >= 1
# absurd-but-numeric outliers (units x1000, far beyond the 1-500 domain)
outliers = pd.to_numeric(sales["units_sold"], errors="coerce") > 1000
print(f"Out-of-range unit counts: {outliers.sum()}")
assert outliers.any()
# reconciliation now fails on both fronts: the summary was corrupted AFTER
# derivation, and the corrupted detail (broken mappings, duplicates, text
# cells) no longer rolls up to the clean groups either — align on the
# summary's groups to count the disagreements
parseable_revenue = pd.to_numeric(sales["revenue"], errors="coerce")
recomputed = parseable_revenue.groupby([sales["sales_date"], sales["department"]]).sum()
reported = summary.set_index(["sales_date", "department"])["total_revenue"]
recomputed = recomputed.reindex(reported.index)
mismatches = (reported.round(2) != recomputed.round(2)).sum()
print(f"Date/department groups where the summary does NOT reconcile: {mismatches}")
assert mismatches >= 1
print("\nAll injected error classes are present and reproducible under random_state=42.")