Skip to content

synthetic

Synthetic data-generating processes for validation studies (spec 10.1).

All generators share an additive logistic base DGP with known shape functions; each study carves a controllable truth into it (density gap, missingness mechanism, pure-noise feature, interaction term).

SyntheticData dataclass

SyntheticData(
    X: DataFrame,
    y: ndarray,
    eta: ndarray,
    meta: Mapping[str, Any],
)

Generated design matrix, labels, true logit and DGP metadata.

make_additive

make_additive(
    n: int = 5000, *, seed: int = 0
) -> SyntheticData

Additive logistic base DGP with three known shape functions.

Source code in src/triadxai/synthetic.py
55
56
57
58
59
60
61
62
63
64
65
def make_additive(n: int = 5000, *, seed: int = 0) -> SyntheticData:
    """Additive logistic base DGP with three known shape functions."""
    rng = np.random.default_rng(seed)
    x0, x1, x2 = _draw(rng, n)
    eta = _base_eta(x0, x1, x2)
    return SyntheticData(
        X=pd.DataFrame({"x0": x0, "x1": x1, "x2": x2}),
        y=_labels(rng, eta),
        eta=eta,
        meta={"dgp": "additive", "center": _CENTER},
    )

make_density_gap

make_density_gap(
    n: int = 5000,
    *,
    gap: tuple[float, float] = (1.0, 2.5),
    keep_frac: float = 0.02,
    seed: int = 0
) -> SyntheticData

Base DGP with training mass carved out of a region of x0 (study 10.1.1).

Source code in src/triadxai/synthetic.py
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def make_density_gap(
    n: int = 5000,
    *,
    gap: tuple[float, float] = (1.0, 2.5),
    keep_frac: float = 0.02,
    seed: int = 0,
) -> SyntheticData:
    """Base DGP with training mass carved out of a region of x0 (study 10.1.1)."""
    rng = np.random.default_rng(seed)
    lo, hi = gap
    cols: list[np.ndarray] = []
    accepted = 0
    while accepted < n:
        x0, x1, x2 = _draw(rng, 2 * n)
        in_gap = (x0 > lo) & (x0 < hi)
        keep = ~in_gap | (rng.uniform(size=2 * n) < keep_frac)
        block = np.column_stack([x0[keep], x1[keep], x2[keep]])
        cols.append(block)
        accepted += len(block)
    data = np.concatenate(cols)[:n]
    x0, x1, x2 = data[:, 0], data[:, 1], data[:, 2]
    eta = _base_eta(x0, x1, x2)
    return SyntheticData(
        X=pd.DataFrame({"x0": x0, "x1": x1, "x2": x2}),
        y=_labels(rng, eta),
        eta=eta,
        meta={"dgp": "density_gap", "gap": gap, "keep_frac": keep_frac, "center": _CENTER},
    )

make_interacting

make_interacting(
    n: int = 5000,
    *,
    gap: tuple[float, float] = (1.0, 2.5),
    seed: int = 0
) -> SyntheticData

Non-additive DGP (x0*x1 interaction) with a density gap in x0.

Ground truth for the approximate-mode study: an unconstrained booster must model the interaction, and bag disagreement should concentrate the D channel inside the carved gap.

Source code in src/triadxai/synthetic.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
def make_interacting(
    n: int = 5000, *, gap: tuple[float, float] = (1.0, 2.5), seed: int = 0
) -> SyntheticData:
    """Non-additive DGP (x0*x1 interaction) with a density gap in x0.

    Ground truth for the approximate-mode study: an unconstrained booster
    must model the interaction, and bag disagreement should concentrate the
    D channel inside the carved gap.
    """
    base = make_density_gap(n, gap=gap, keep_frac=0.02, seed=seed)
    rng = np.random.default_rng(seed + 1)
    x0 = base.X["x0"].to_numpy()
    x1 = base.X["x1"].to_numpy()
    interaction = 1.0 * x0 * x1
    eta = base.eta + interaction
    return SyntheticData(
        X=base.X,
        y=_labels(rng, eta),
        eta=eta,
        meta={
            "dgp": "interacting",
            "gap": gap,
            "interaction_strength": 1.0,
            "center": _CENTER,
        },
    )

make_missingness

make_missingness(
    n: int = 5000,
    *,
    mechanism: str = "MNAR",
    missing_rate: float = 0.3,
    missing_effect: float = 1.0,
    seed: int = 0
) -> SyntheticData

Base DGP with MCAR / MAR / MNAR missingness injected into x0 (study 10.1.2).

MNAR plants missing_effect on the logit of masked rows (informative missingness); meta["eta_base"] retains the pre-effect logit so tests can recover the planted effect exactly.

Source code in src/triadxai/synthetic.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def make_missingness(
    n: int = 5000,
    *,
    mechanism: str = "MNAR",
    missing_rate: float = 0.3,
    missing_effect: float = 1.0,
    seed: int = 0,
) -> SyntheticData:
    """Base DGP with MCAR / MAR / MNAR missingness injected into x0 (study 10.1.2).

    MNAR plants ``missing_effect`` on the logit of masked rows (informative
    missingness); ``meta["eta_base"]`` retains the pre-effect logit so tests
    can recover the planted effect exactly.
    """
    rng = np.random.default_rng(seed)
    x0, x1, x2 = _draw(rng, n)
    eta_base = _base_eta(x0, x1, x2)
    if mechanism == "MCAR":
        p_miss = np.full(n, missing_rate)
    elif mechanism == "MAR":
        raw = expit(x1)
        p_miss = np.clip(missing_rate * raw / raw.mean(), 0.0, 1.0)
    elif mechanism == "MNAR":
        raw = expit(x0)
        p_miss = np.clip(missing_rate * raw / raw.mean(), 0.0, 1.0)
    else:
        raise ValueError(f"unknown mechanism {mechanism!r}; expected 'MCAR', 'MAR' or 'MNAR'")
    miss = rng.uniform(size=n) < p_miss
    eta = eta_base + (missing_effect * miss if mechanism == "MNAR" else 0.0)
    x0_observed = np.where(miss, np.nan, x0)
    return SyntheticData(
        X=pd.DataFrame({"x0": x0_observed, "x1": x1, "x2": x2}),
        y=_labels(rng, eta),
        eta=eta,
        meta={
            "dgp": "missingness",
            "mechanism": mechanism,
            "missing_rate": missing_rate,
            "missing_effect": missing_effect if mechanism == "MNAR" else 0.0,
            "eta_base": eta_base,
            "missing_mask": miss,
            "center": _CENTER,
        },
    )

make_noise_feature

make_noise_feature(
    n: int = 5000, *, seed: int = 0
) -> SyntheticData

Base DGP plus a pure-noise feature with zero coefficient (study 10.1.3).

Source code in src/triadxai/synthetic.py
144
145
146
147
148
149
150
151
152
153
154
155
def make_noise_feature(n: int = 5000, *, seed: int = 0) -> SyntheticData:
    """Base DGP plus a pure-noise feature with zero coefficient (study 10.1.3)."""
    rng = np.random.default_rng(seed)
    x0, x1, x2 = _draw(rng, n)
    noise = rng.normal(0, 1, n)
    eta = _base_eta(x0, x1, x2)
    return SyntheticData(
        X=pd.DataFrame({"x0": x0, "x1": x1, "x2": x2, "noise": noise}),
        y=_labels(rng, eta),
        eta=eta,
        meta={"dgp": "noise_feature", "noise_column": "noise", "center": _CENTER},
    )