Distributions and Conditional Generation
Two controls over the shape of generated values, and they compose.
distributions replaces the default — uniform within the column's declared domain — with a named statistical shape. Uniform amounts are the tell-tale sign of synthetic data: real money is heavy-tailed, real counts are Poisson-ish, real scores cluster.
conditions makes a column depend on other columns. Fuel settlements are large and shop settlements small; travel claims settle in days and property claims in months; weekend traffic arrives through a different channel. That structure is not in any single column's distribution — it is in the relationship between them.
Distributions
distributions={
"amount": {"distribution": "lognormal", "mean": 3.0, "sigma": 0.5},
"n_items": {"distribution": "poisson", "lam": 3.0},
"score": {"distribution": "normal", "loc": 50, "scale": 10, "min": 0, "max": 100},
}
Supported: uniform, normal, lognormal, exponential, poisson, triangular, beta. Parameters follow their NumPy names. Optional min/max clip the result, falling back to the column's declared domain; integer columns are rounded. Numerical columns only.
Conditions
conditions=[
{
"column": "amount",
"rules": [
{"when": {...}, "distribution": {...}}, # numerical outcome
{"when": {...}, "values": {...}}, # categorical outcome
],
"default": {"distribution": {...}}, # optional
},
]
| Key | Meaning |
|---|---|
column |
The column being conditioned. |
rules |
Evaluated in order — first match wins. Put the most specific rule first. |
when |
A mapping of other columns to a value, a list of values (is-in), or a calendar predicate. Multiple entries AND together. |
distribution |
Outcome for a numerical column — same form as a distributions entry. |
values |
Outcome as weighted values, for a column of any type. |
default |
Applied to rows matching no rule. Omit it and unmatched rows fall back to the column's base sampling. |
Calendar predicates
A when value over a date column may be a calendar predicate instead of an equality test, so seasonality needs no pre-computed flag column:
| Predicate | Meaning |
|---|---|
{"weekday_in": [5, 6]} |
Day of week, Monday 0 … Sunday 6. |
{"month_in": [12]} |
Calendar month, 1–12. |
{"day_in_month": [25]} |
Day of month — paydays, billing cycles. |
Multiple predicate keys in one mapping AND together. Unknown keys are rejected at fit() time.
Worked example
A stream of insurance claims: heavy-tailed amounts that depend on the policy type, fast-settling travel claims, and a reporting channel that shifts at weekends.
"""
Example: distributions and conditional generation — the SHAPE of the values.
Two related controls, no scaffold and no derived tables, so the effect of each
is visible on its own:
- `distributions` replaces the default "uniform within the declared domain"
with a named statistical shape per numerical column.
- `conditions` makes a column's sampling depend on OTHER columns: rules are
evaluated in order, first match wins, and unmatched rows fall back to
`default` or to the column's base sampling.
The table is a stream of insurance claims. Claim amounts are heavy-tailed and
depend on the policy type; travel claims settle fast; and the reporting channel
shifts at weekends — expressed with a CALENDAR PREDICATE on the claim date
rather than a pre-computed weekend flag.
Run with: python faker_distributions_conditions.py
"""
import pandas as pd
from ydata.metadata import Metadata
from ydata.metadata.builder import MetadataConfigurationBuilder
from ydata.synthesizers import FakerSynthesizer
columns = {
"claim_date": {"datatype": "date", "vartype": "date",
"min": "2026-01-01", "max": "2026-03-31",
"format": "%Y-%m-%d"},
"policy_type": {"datatype": "categorical", "vartype": "string",
"categories": {"auto": 50, "home": 30, "travel": 20}},
"channel": {"datatype": "categorical", "vartype": "string",
"categories": {"agent": 40, "online": 40, "app": 20}},
"claim_amount": {"datatype": "numerical", "vartype": "float",
"min": 50.0, "max": 250_000.0},
"days_to_settle": {"datatype": "numerical", "vartype": "int",
"min": 1, "max": 180},
}
metadata = Metadata(configuration_builder=MetadataConfigurationBuilder(columns))
synth = FakerSynthesizer(locale="en")
synth.fit(
metadata,
# ---------------------------------------------------------------------
# Unconditional shape: what the column looks like when no rule applies.
# Supported: uniform, normal, lognormal, exponential, poisson,
# triangular, beta. `min`/`max` clip (defaulting to the column's domain);
# integer columns are rounded.
# ---------------------------------------------------------------------
distributions={
"claim_amount": {"distribution": "lognormal", "mean": 7.0, "sigma": 1.0},
"days_to_settle": {"distribution": "poisson", "lam": 30},
},
# ---------------------------------------------------------------------
# Conditional shape: per-segment overrides.
# ---------------------------------------------------------------------
conditions=[
{ # weekend claims arrive through self-service, not through agents.
# `when` on a DATE column accepts a calendar predicate:
# weekday_in (0=Mon .. 6=Sun), month_in, day_in_month.
"column": "channel",
"rules": [
{"when": {"claim_date": {"weekday_in": [5, 6]}},
"values": {"app": 0.6, "online": 0.35, "agent": 0.05}},
],
# weekdays fall back to the declared category distribution
},
{ # amount magnitude per policy type; two `when` columns AND together
"column": "claim_amount",
"rules": [
{"when": {"policy_type": "home"},
"distribution": {"distribution": "lognormal", "mean": 9.2, "sigma": 0.9}},
{"when": {"policy_type": "auto", "channel": "agent"},
"distribution": {"distribution": "lognormal", "mean": 8.4, "sigma": 0.7}},
{"when": {"policy_type": "auto"},
"distribution": {"distribution": "lognormal", "mean": 7.6, "sigma": 0.7}},
{"when": {"policy_type": "travel"},
"distribution": {"distribution": "lognormal", "mean": 6.0, "sigma": 0.6}},
],
"default": {"distribution": {"distribution": "uniform", "low": 50, "high": 5_000}},
},
{ # travel claims settle in days, property claims in months
"column": "days_to_settle",
"rules": [
{"when": {"policy_type": "travel"},
"distribution": {"distribution": "poisson", "lam": 7}},
{"when": {"policy_type": "home"},
"distribution": {"distribution": "normal", "loc": 65, "scale": 20, "min": 5}},
],
"default": {"distribution": {"distribution": "poisson", "lam": 25}},
},
],
)
claims = synth.sample(5_000, random_state=42).to_pandas()
# ---------------------------------------------------------------------------
# What the configuration bought
# ---------------------------------------------------------------------------
by_type = claims.groupby("policy_type")["claim_amount"].median().round(0)
print("Median claim amount by policy type:")
print(by_type.to_string())
assert by_type["home"] > by_type["auto"] > by_type["travel"], \
"each policy type must land in its own band"
settle = claims.groupby("policy_type")["days_to_settle"].mean().round(1)
print("\nMean days to settle by policy type:")
print(settle.to_string())
assert settle["travel"] < settle["auto"] < settle["home"]
# the calendar predicate: the channel mix really does change at weekends
weekend = pd.to_datetime(claims["claim_date"]).dt.weekday >= 5
app_weekend = (claims.loc[weekend, "channel"] == "app").mean()
app_weekday = (claims.loc[~weekend, "channel"] == "app").mean()
print(f"\nShare of claims filed in the app: weekend {app_weekend:.2f} "
f"vs weekday {app_weekday:.2f}")
assert app_weekend > app_weekday * 2
# first match wins: auto claims through an agent are the larger auto claims
auto = claims[claims["policy_type"] == "auto"]
agent_median = auto[auto["channel"] == "agent"]["claim_amount"].median()
other_median = auto[auto["channel"] != "agent"]["claim_amount"].median()
print(f"Auto claims — agent median {agent_median:,.0f} vs "
f"self-service median {other_median:,.0f}")
assert agent_median > other_median
# the lognormal tail survives the domain clip
print(f"\nclaim_amount p50/p95/max: {claims['claim_amount'].quantile(0.5):,.0f} / "
f"{claims['claim_amount'].quantile(0.95):,.0f} / {claims['claim_amount'].max():,.0f}")
assert claims["claim_amount"].between(50.0, 250_000.0).all(), "values stay in the domain"
print("\nAll distribution and condition checks passed.")
Notes
What when may reference
Other metadata columns, scaffold dimensions (per-entity behaviour on a grid), include columns, or other conditioned columns. Dependencies are ordered automatically and cycles are rejected at fit() time.
Unique columns cannot be conditioned
Per-segment outcomes cannot guarantee uniqueness across segments, so conditions on a column marked unique are rejected at fit(). A distribution override on a unique column keeps the constraint enforced, falling back to duplicates with a warning only when the distribution's support is too small.
Order in the pipeline
scaffold → column sampling (with distributions) → conditioned columns → calculated features. A condition can therefore read a scaffold column, and a calculated feature can read a conditioned column, but not the reverse.
Both blocks are pure data and can live in a YAML configuration under the reserved distributions and conditions keys.
Related Materials
