Monte Carlo studies in econometrics are often analytically transparent but empirically thin: functional forms, covariate distributions, error distributions, and treatment assignment rules are chosen for tractability rather than for resemblance to the empirical settings where estimators fail. Athey, Imbens, Metzger, and Munro (2024) propose using Wasserstein GANs to learn data-generating processes from real datasets and then use the fitted generator as a simulation design (Athey et al. 2024). This paper develops the same idea with denoising diffusion probabilistic models. The main claim is not that diffusion uniformly dominates GANs. The claim is that finite-sample econometric benchmarking should target the empirical geometry that drives estimator behavior–overlap, leverage, tails, treatment imbalance, and dependence–and that diffusion models are a promising engine for learning and perturbing that geometry because they replace adversarial fitting with supervised denoising. The cost is slower iterative sampling. The evidence below should be read as a runnable practice memo rather than a definitive benchmark.
Introduction
Suppose an applied study observes rows
\[
R_i = (Y_i, W_i, X_i), \qquad i=1,\ldots,n,
\]
where \(Y_i\) is an outcome, \(W_i \in \{0,1\}\) is a binary treatment or policy exposure, and \(X_i\) collects pre-treatment covariates. A traditional Monte Carlo design specifies a distribution \(P_0\) for \(R_i\) and then evaluates estimators under repeated draws from \(P_0\). The credibility of the exercise rests on whether \(P_0\) captures the parts of the empirical problem that matter for the estimators being compared: overlap, tail behavior, discreteness, collinearity, heteroskedasticity, treatment imbalance, and nonlinear dependence.
The AIMM paper reframes the problem as a learned simulation design. Let \(P_n\) be the empirical distribution of a real dataset. Fit a generator \(\hat G\) so that samples from \(\hat P_G\) look like draws from \(P_n\). Then use \(\hat P_G\) as the baseline simulation environment. This does not estimate structural primitives; it reduces arbitrary choices in simulation design while preserving the empirical geometry of the application.
This note separates two claims that are easy to conflate. The first is a generative-model comparison: WGANs and DDPMs are alternative ways to approximate a tabular row distribution. The second is a simulation-design claim: the object of an econometric Monte Carlo should be a realistic empirical environment plus controlled perturbations, not only a convenient analytic DGP. The second claim is the more important one. DDPMs matter here only insofar as they make that simulation-design workflow more stable and modular.
Learned Simulation Designs
A GAN has two networks. A generator maps noise \(Z\) into synthetic rows:
\[
\tilde R = G_\theta(Z), \qquad Z \sim P_Z.
\]
A discriminator or critic scores whether rows look real. In a Wasserstein GAN, the population objective is
For econometric simulation this has several attractions:
the generator is fast once trained;
it represents complicated row distributions without a parametric likelihood;
conditional generation can be implemented by feeding treatment, cohort, or market labels into the generator;
the Wasserstein critic gives a training signal with a distributional interpretation.
But the adversarial game is also the main cost. The generator improves only through the critic’s current gradients. If the critic is weak, the generator learns the wrong directions. If the critic is too strong or poorly regularized, training can become unstable. Mode collapse is a particularly bad failure for simulation studies because the generated sample can look locally plausible while missing subpopulations that determine estimator risk.
The optimizer choice matters because the problem is a saddle-point problem, not ordinary risk minimization. Simultaneous Adam is convenient, but the game can rotate around equilibria rather than converge. Optimistic Adam, mirror descent, and other game-aware updates are natural modifications because they use information about recent gradients to damp these rotations. That is a repair to the WGAN fitting problem, not a change in the underlying object being learned.
Denoising Diffusion Models
A denoising diffusion probabilistic model starts from the opposite direction. Instead of asking a generator to fool a critic, it defines a known corruption process that gradually turns data into noise. The model then learns to reverse that corruption.
Let \(x_0 \in \mathbb{R}^d\) denote a standardized data row. Choose a noise schedule
The model is a denoiser \(\epsilon_\theta(x_t,t,c)\), where \(c\) is optional context. In tabular simulations, \(c\) might be treatment status, market, cohort, site, panel time, or a policy environment. The simple DDPM training objective is
This is ordinary supervised learning. The label is the known noise used to corrupt the clean row. Ho, Jain, and Abbeel show how this objective arises from a variational bound and connects to denoising score matching (Ho, Jain, and Abbeel 2020).
with \(\sigma_t\) set by the chosen reverse variance. After reaching \(x_0\), invert the tabular preprocessing map and apply any column constraints: binary rounding, nonnegativity, bounded support, or category decoding.
Density Modeling Interpretation
The DDPM is a density model built from many noisy versions of the data. The forward process defines a family of smoothed densities \(p_t(x)\), where \(p_0\) is the data distribution and \(p_T\) is close to a standard Gaussian. The denoiser learns a vector field that points noisy observations back toward regions of high probability under \(p_t\).
This is the link to score-based generative modeling. Song, Sohl-Dickstein, Kingma, Kumar, Ermon, and Poole formulate the same idea in continuous time: add noise through a stochastic differential equation and sample backward using the estimated score field (Y. Song et al. 2021). DDPMs are one discrete-time implementation of that broader score-based idea.
The econometric analogy is useful. A likelihood model specifies \(p_\theta(x)\) and maximizes \(\sum_i \log p_\theta(X_i)\). A score-based model learns the local derivative \(\nabla_x \log p_t(x)\) along many noisy versions of the distribution. It does not need a normalized likelihood. For simulation, this is enough: if the learned reverse chain approximately transports Gaussian noise into the empirical row distribution, then it supplies draws for a Monte Carlo design.
Tabular Rows
The original DDPM success story is image generation, but the row-distribution problem is different. In image data, all coordinates are intensities with spatial structure. In an econometric row, coordinates may be continuous, binary, categorical, censored, top-coded, or logically constrained.
A practical tabular DDPM therefore needs a representation map
\[
z = S(x),
\]
where \(S\) standardizes continuous variables, encodes categorical variables, and records constraints needed to invert the transformation. Continuous variables can be diffused with Gaussian noise. Binary and categorical variables can be handled in several ways:
keep them as relaxed numeric variables during diffusion and round after sampling;
use separate output heads and postprocessing rules;
use discrete diffusion kernels designed for categorical state spaces.
The last option is conceptually cleaner. Multinomial diffusion and argmax flows explicitly model categorical variables (Hoogeboom et al. 2021). Structured denoising diffusion models in discrete state spaces extend the corruption process beyond uniform replacement kernels and provide a general D3PM framework (Austin et al. 2021). TabDDPM combines Gaussian diffusion for numerical features with multinomial diffusion for categorical features and shows that diffusion can be competitive for general tabular data (Kotelnikov et al. 2023).
For econometric simulation, the choice should be governed by the role of each variable. Rounding a binary demographic indicator may be acceptable in a first benchmark. Rounding the binary treatment \(W\) is usually not acceptable if the estimand conditions on the realized assignment mechanism. In that case, treatment should be fixed as context or generated by a separate assignment model.
Comparison with WGANs and Flows
The AIMM objective is to build realistic simulation designs from empirical datasets. Both WGANs and DDPMs are plausible tools. They differ mainly in what is hard.
Denoising loss plus downstream distribution checks
Mode coverage
Can be fragile when generator collapses
Usually strong because training sees noisy neighborhoods at all scales
Conditional designs
Feed context to generator and critic
Feed context to denoiser at every reverse step
Ablations
Possible but can destabilize the adversarial game
Natural: intervene on context, corruption path, or reverse constraints
This comparison explains why DDPMs are attractive for econometric practice. The fitting problem is closer to nonparametric regression than to adversarial equilibrium computation. Every training example supplies a known target \(\epsilon\). The model is trained across many local perturbations of each row, which tends to preserve support and neighborhood structure. For a simulation study where the cost of a missed subpopulation can be high, that stability is valuable.
The tradeoff is sampling cost. A WGAN can generate \(100{,}000\) rows with one forward pass per row. A DDPM may need tens or hundreds of denoising steps. Improved DDPMs learn reverse variances and improve the likelihood/sample-quality tradeoff (Nichol and Dhariwal 2021). DDIMs reduce the number of sampling steps by using a deterministic or partially deterministic non-Markovian sampler with the same training objective (J. Song, Meng, and Ermon 2021). In practice, a tabular simulation design can usually tolerate slower sampling because generating the synthetic benchmark is not the bottleneck relative to fitting many downstream estimators.
Normalizing Flows
Normalizing flows are another natural density-modeling candidate. A flow constructs an invertible map \(f_\theta\) such that
\[
z = f_\theta(x),
\qquad
z \sim p_Z,
\]
and evaluates the likelihood by the change-of-variables formula:
This gives exact likelihoods and direct sampling, which is attractive for statistical work (Papamakarios et al. 2021). The cost is architectural: \(f_\theta\) must be invertible and have a tractable Jacobian determinant. Those constraints can make flows awkward for mixed discrete-continuous tabular rows, although many useful constructions exist.
Diffusion models relax invertibility. They do not give exact likelihood as directly as a flow, although likelihood bounds and probability-flow ODEs are available in the broader score-based framework. For simulation design, exact likelihood is less important than faithful reproduction of the empirical row distribution and stable conditional sampling. That tilts the practical comparison toward DDPMs unless likelihood evaluation itself is part of the research design.
Trex API Surface
Trex exposes WGAN and DDPM simulators through the same minimal interface: fit on a matrix of transformed tabular rows, optionally condition on a context matrix, and sample synthetic transformed rows. The fitted rows are transformed back to data scale with TabularTransformer.
For the WGAN benchmark, the class is trex.TabularWGAN:
The extra method is log_prob, since exact or tractable likelihood evaluation is the main reason to add a flow baseline. The implementation choice would then be a conditional spline flow, masked autoregressive flow, or coupling flow depending on whether tabular likelihood or fast sampling is the priority.
Distance Metrics
Let \(R_1,\ldots,R_n \in \mathbb{R}^d\) denote observed rows and \(\tilde R_1,\ldots,\tilde R_m \in \mathbb{R}^d\) denote synthetic rows. All diagnostics below are computed after the same preprocessing convention used for the simulation exercise; in the Lalonde application, earnings are measured in thousands of dollars. Lower values indicate closer agreement.
For coordinate \(j\), the one-dimensional Wasserstein distance is
where \(\hat F_j\) and \(\hat G_j\) are the empirical CDFs of observed and synthetic column \(j\). The reported marginal_w1_mean and marginal_w1_max are
The reported marginal_ks_mean and marginal_ks_max average and maximize \(K_j\) across columns. The KS distance is scale-free, while Wasserstein distance preserves the units of each variable.
Because covariance is measured in original units, high-variance earnings columns can dominate it. The correlation diagnostic repeats the same calculation after converting covariance matrices into correlation matrices:
This is the cleaner summary of linear dependence across mixed-scale columns.
Finally, sliced Wasserstein averages one-dimensional Wasserstein distances over random projections. For a unit vector \(u \in \mathbb{S}^{d-1}\), project rows to \(u'R_i\) and \(u'\tilde R_i\). The population target is
The reported value approximates this expectation with random directions. This is useful in tabular simulation because it can detect joint-distribution failures that are invisible in marginal diagnostics, while remaining much cheaper than exact high-dimensional optimal transport.
Metrics for Estimator Geometry
These distances are generic generator diagnostics. They are not, by themselves, econometric loss functions. For estimator benchmarking, the question is which parts of the empirical distribution make estimators fail.
For treatment-effect estimators, several features matter:
overlap and common support in \(X\) across treatment states;
leverage points that dominate regression or balancing weights;
tail behavior in outcomes and pre-treatment earnings;
treatment imbalance and small treated or control cells;
dependence between treatment, covariates, and outcomes;
nonlinear nuisance-function difficulty in \(m_w(x)=\mathbb{E}[Y\mid W=w,X=x]\) and \(e(x)=\Pr(W=1\mid X=x)\).
The reported metrics proxy these features imperfectly. Marginal Wasserstein and KS distances detect column-level failures such as missed tails, mass at zero, or wrong binary shares. Mean and covariance distances catch first- and second-moment errors that often affect regression adjustment. Correlation distance summarizes linear dependence across columns. Sliced Wasserstein is the broadest row-geometry diagnostic because it sees random linear combinations of variables and can detect joint-distribution errors missed by marginal checks.
The metrics do not directly measure overlap, propensity-score calibration, leverage, or nuisance-function difficulty. A complete simulation-design benchmark should add estimator-specific diagnostics: propensity-score distributions, nearest-neighbor distances across treatment arms, maximum leverage, effective sample size for weighting estimators, and tail-sensitive outcome summaries. The present memo uses generic distances as a first screen and downstream ATT replication as one applied estimand check.
Simulation Protocol
A useful DDPM simulator for econometrics should make the target distribution explicit. The following workflow is close to the AIMM proposal but swaps the fitting engine:
Choose the empirical target: unconditional rows, rows conditional on treatment, rows within market, or a panel transition distribution.
Define a preprocessing map \(S\) and record all inverse transformations.
Decide which variables are generated and which are fixed as context.
Fit a denoiser with a transparent noise schedule and held-out diagnostics.
Generate synthetic samples with the same sample size, treatment shares, panel length, or market composition as the empirical design.
Report distances between real and generated data: marginal Wasserstein, KS distances, mean distance, covariance and correlation Frobenius norms, sliced Wasserstein, and downstream estimand replication.
Run estimator comparisons on many generated datasets.
Add ablations by changing one empirical feature at a time: overlap, tails, treatment imbalance, covariance, policy rules, or outcome noise.
The ablation step is where diffusion can be especially useful. Because generation is a gradual reverse process, one can condition, guide, or project intermediate draws. Examples include:
holding treatment fixed and generating only \((Y,X)\) conditional on \(W\);
matching marginal treatment shares exactly while allowing covariates and outcomes to vary;
projecting generated rows back into known support restrictions;
increasing or decreasing tail noise to stress-test robust estimators;
conditioning on policy environments to compare counterfactual regimes.
The relevant standard is practical: does the DDPM improve the credibility of finite-sample estimator comparisons?
Treatment as Target, Context, or Assignment
Treatment status plays different roles in different simulation designs. The distinction matters because the estimand changes when the simulated object changes.
In a joint row simulation, the target is
\[
P(Y,W,X).
\]
The generator draws complete rows, including treatment. This is useful when the assignment mechanism is part of the empirical environment one wants to reproduce. The risk is that rare treatments may be poorly learned, and the realized treatment share will vary across synthetic samples.
In a conditional row simulation, the target is
\[
P(Y,X \mid W),
\]
and the analyst supplies the treatment vector \(W_1,\ldots,W_n\). This is the design used in the Lalonde exercises below. It keeps the sample size and treatment count fixed, so differences across generators come from the learned conditional outcome/covariate distribution rather than from random variation in treatment assignment.
In an assignment-mechanism simulation, one separately models
This is closer to a causal DGP. It is also more demanding because misspecification in the assignment model changes overlap, treatment shares, and the interpretation of ATT or ATE exercises. A learned simulator can be used for any of these objects, but the paper must state which one is being used. In the empirical sections below, Iris fixes species labels as context, the held-out scikit benchmarks are unconditional row simulations, and the Lalonde designs fix treatment as context.
Empirical Illustration
Iris Sanity Check
The Iris dataset is too small and too clean to be an econometric benchmark. It is useful here because it lets us see the mechanics without hiding behind a large application. The row is
The exercise fixes the species counts and learns the conditional distribution of the four continuous measurements given species. This is the same design pattern one would use when a binary treatment indicator or site membership is part of the simulation design rather than a variable the generator is allowed to change.
The Iris example is deliberately modest. Its diagnostic role is to check whether small conditional generators preserve species-conditional clusters and produce plausible continuous measurements. The WGAN panel shows the adversarial baseline; the DDPM panel shows the same task fit through supervised denoising.
Held-Out Distance Benchmarks
For reporting generator quality, a held-out comparison is usually preferable to comparing synthetic rows against the same empirical rows used for fitting. The in-sample distance answers whether the generator can reproduce the training distribution. The held-out distance asks a more relevant question: after fitting on one sample, how close are generated rows to new rows from the same empirical source?
The following benchmark uses small numeric datasets bundled with scikit-learn. For each dataset, the models fit only the training rows. They then generate the same number of rows as the test split, and distances are computed against the held-out test rows. A train-resample baseline draws rows with replacement from the training set; it is not a learned generator, but it gives a useful finite-sample reference.
Held-out distances are noisy on these small datasets, but they are conceptually cleaner than in-sample distances. A generator should be judged against rows it did not see during fitting, and the train-resample baseline helps distinguish model failure from the ordinary sampling variability of a small empirical distribution.
Lalonde Designs
The AIMM paper uses WGANs to build simulation designs for treatment-effect estimators. The Lalonde setting is useful because it has two distinct empirical targets. The experimental target is the NSW randomized sample: treated and control units both come from the experiment. The observational target combines NSW treated units with a large CPS control pool. The second target is closer to the classic nonexperimental evaluation problem: controls come from a different empirical environment, treatment is rare, earnings are heavy-tailed and truncated, and covariate overlap is strained.
The exercise below evaluates both targets. In each case, earnings are scaled to thousands of dollars, the binary treatment indicator \(W\) is held fixed as context, and each generator learns the conditional distribution of \((Y,X)\) given \(W\). Thus the experimental and observational exercises differ in the empirical row distribution being learned, not in the simulation protocol.
The experimental sample has 445 rows, with 185 treated observations and 260 experimental controls. The observational sample has 16,177 rows, with the same 185 NSW treated observations and 15,992 CPS controls. The treated share is therefore about 41.6 percent in the experimental sample and 1.1 percent in the observational sample.
feature_columns = [column for column in LDW_COLUMNS if column != TREATMENT]feature_binary = [column for column in BINARY_COLUMNS if column in feature_columns]feature_nonnegative = [column for column in NONNEGATIVE_COLUMNS if column in feature_columns]def rebuild_lalonde(generated_features: np.ndarray, generated_t: np.ndarray) -> pd.DataFrame: out = pd.DataFrame(generated_features, columns=feature_columns) out.insert(0, TREATMENT, generated_t.reshape(-1).astype(float)) out = out[LDW_COLUMNS].astype(float)for col in [TREATMENT, *BINARY_COLUMNS]: out[col] = (np.clip(out[col], 0.0, 1.0) >=0.5).astype(float)for col in NONNEGATIVE_COLUMNS: out[col] = np.maximum(out[col], 0.0)return outdef fit_lalonde_design(design: str, df: pd.DataFrame, seed_offset: int) ->dict[str, object]: features = df[feature_columns].copy() context = df[[TREATMENT]].to_numpy(dtype=np.float32) transformer = TabularTransformer( column_names=feature_columns, binary_columns=feature_binary, nonnegative_columns=feature_nonnegative, ).fit(features) train_z = transformer.transform(features) lower, upper = transformer.transformed_bounds() batch_size =min(512, len(df)) wgan_model = TabularWGAN( hidden_dims=(64, 64), critic_hidden_dims=(64, 64), batch_size=batch_size, max_steps=450, critic_steps=2, lr=1e-4, optimizer="optimistic_adam", gp_weight=5.0, binary_dims=[feature_columns.index(col) for col in feature_binary], lower_bounds=lower, upper_bounds=upper, seed=SEED + seed_offset, device=DEVICE, ) ddpm_model = TabularDiffusion( hidden_dims=(64, 64), n_timesteps=80, batch_size=batch_size, max_steps=650, lr=1e-3, seed=SEED +1000+ seed_offset, device=DEVICE, ) start = time.perf_counter() wgan_model.fit(train_z, context=context) wgan_seconds = time.perf_counter() - start start = time.perf_counter() ddpm_model.fit(train_z, context=context) ddpm_seconds = time.perf_counter() - startdef synthesize(model: object, context_array: np.ndarray) -> pd.DataFrame: generated = model.sample(len(context_array), context=context_array).numpy()return rebuild_lalonde( transformer.inverse_transform(generated), context_array, )return {"design": design,"observed": df,"context": context,"transformer": transformer,"wgan": wgan_model,"ddpm": ddpm_model,"wgan_fake": synthesize(wgan_model, context),"ddpm_fake": synthesize(ddpm_model, context),"wgan_seconds": wgan_seconds,"ddpm_seconds": ddpm_seconds,"synthesize": synthesize, }lalonde_fits = { design: fit_lalonde_design(design, df, 100* k)for k, (design, df) inenumerate(lalonde_samples.items())}fit_times = pd.DataFrame([ {"design": design, "model": "WGAN-GP", "seconds": fit["wgan_seconds"]}for design, fit in lalonde_fits.items()] + [ {"design": design, "model": "DDPM", "seconds": fit["ddpm_seconds"]}for design, fit in lalonde_fits.items()])
The timing comparison is included only to show the practical cost of the two fitting routines in this small benchmark. It is not the main estimand of the exercise.
The ATT and distance tables separate two questions. The ATT table asks whether the downstream regression-adjustment estimand is similar. The distance table asks whether the generated rows resemble the empirical rows before any estimator is applied. The experimental design is a comparatively balanced benchmark. The observational design is a stress test with the same treated sample but a much larger and different control pool. A generator that looks acceptable in the experimental sample can still fail in the observational sample because the latter puts much more weight on overlap, earnings tails, and the dependence between treatment status and pre-treatment earnings.
The sliced Wasserstein distance is especially useful here because it is multivariate. It projects the full row vector onto many random directions and averages the one-dimensional Wasserstein distances. A generator can match each column separately while getting the joint geometry wrong; sliced Wasserstein is meant to catch that failure without solving a full high-dimensional optimal-transport problem. The Frobenius distances for covariance and correlation matrices provide a more classical second-moment version of the same check.
One synthetic dataset is only a first pass. A generator can match the ATT and still miss the row distribution. The distance table and scatter plot are therefore part of the simulation design, not decoration. Comparing the experimental and observational designs also clarifies what the synthetic-data task is supposed to preserve: randomized-sample balance in the first case, and nonexperimental control-pool geometry in the second.
Repeated Simulation Results
The single synthetic draw above answers whether one fitted generator can produce a plausible table for each Lalonde target. A simulation study also needs repeated draws from the fitted generator. The estimand and sample geometry are held fixed within each target: every synthetic dataset has the observed sample size and the observed treatment vector for that target. This isolates the fitted row distribution from changes in treatment prevalence.
where \(m \in \{\text{WGAN}, \text{DDPM}\}\) indexes the generator and \(w_i\) is the observed treatment assignment for row \(i\). Each replicate is evaluated by the same regression-adjustment ATT and by the same distribution distances used above.
The repeated draws turn the generator comparison into a Monte Carlo object. The ATT columns ask whether the downstream estimator sees a similar treatment-effect problem. The distance columns ask whether the synthetic tables reproduce the empirical distribution that makes the treatment-effect problem hard. The comparison should be read within each design. The experimental target tests whether the models can reproduce a randomized small-sample environment. The observational target tests whether they can reproduce the severe imbalance and covariate geometry of the CPS control design. Estimand replication alone can reward a generator for getting one target right for the wrong distributional reasons. Conversely, a generator with better row geometry can still produce a noisier downstream estimand in a small or rare-treatment design.
The observational CPS target has only 185 treated observations out of 16,177 rows. That imbalance is part of the empirical problem. A learned conditional generator lets us change this feature while keeping the fitted conditional row distributions fixed. The experimental target is already much more balanced, so the ablation is mainly informative for the observational design.
The ablation below draws a balanced sample with 500 treated and 500 controls from each fitted generator. It does not estimate a new population parameter. It asks how the regression-adjustment estimand behaves when the rare-treatment geometry is relaxed while the conditional outcome and covariate distributions are generated by the same fitted model.
This is the kind of ablation that learned simulation designs should make routine. The empirical target is used to fit the conditional row distribution; the analyst then changes one design feature and watches the downstream estimator respond. More substantive ablations would alter overlap, tail thickness, panel persistence, market concentration, or the outcome equation. The same implementation pattern applies: hold the fitted conditional generator fixed, change the design object deliberately, and report both distributional diagnostics and estimator consequences.
The balanced-sample exercise is a controlled perturbation of the simulation environment rather than an estimate of either original Lalonde estimand. The observed-share synthetic rows keep the empirical treatment share of their source design, while the balanced ablation has a treated share of 50 percent. The SE panel is useful because treatment-share changes mechanically alter precision even when the fitted conditional row distributions are held fixed. Large changes in the ablation table should be interpreted as sensitivity to the simulated design, not as evidence that the target estimand itself changed in the original data.
Limitations
Diffusion models do not remove judgment from simulation design. They move the judgment to preprocessing, conditioning, diagnostics, and ablation design. That is still progress, because those choices are visible and auditable.
There are also limits:
a DDPM trained on one empirical dataset cannot invent support that is absent from that dataset;
synthetic draws inherit sampling noise and measurement artifacts from the source data;
postprocessing can hide model failures if it is too aggressive;
small treatment cells remain hard because the conditional distribution is weakly learned;
privacy is not automatic and should not be claimed without a separate privacy analysis.
Finally, DDPMs are not always better than WGANs. If the table is small, smooth, and continuous, a WGAN or normalizing flow may be enough. If fast generation is essential, WGANs have a real advantage. If exact likelihood is required, flows are more natural. The evidence here is also limited: experimental and observational Lalonde variants, a few small scikit-learn datasets, a small number of repeated synthetic draws, and no systematic hyperparameter sensitivity. The case for DDPMs is strongest as a practical hypothesis for settings with complicated support, mixed variable types, and estimator-relevant dependence patterns that adversarial training may struggle to preserve.
Conclusion
The AIMM paper made an important methodological point: Monte Carlo studies can be made more credible by learning simulation designs from real applications. WGANs were a reasonable first tool for that job. DDPMs offer a cleaner fitting problem for the same econometric goal. They learn a denoising map across a controlled corruption path, connect directly to score-based density modeling, and appear well suited to conditional row generation and targeted ablation.
For econometric practice, the useful payoff is sharper finite-sample evidence: simulation designs that preserve the empirical geometry of the problem being studied, while allowing controlled interventions on the features that make estimators succeed or fail. The right next step is not to declare a universal generator winner, but to evaluate learned simulation designs by both distributional fidelity and estimator-specific stress tests across several empirical environments.
Appendix: Training-Step Sweep
The main examples fit each generator once. A useful diagnostic is to repeat the fit for progressively larger training budgets and compare generated samples to both the training rows and held-out rows. If train distances keep falling while test distances flatten or rise, the model is learning the empirical sample more than the underlying row distribution. If both distances improve, additional training is buying distributional fidelity rather than only memorization.
The exercise below uses the scikit-learn wine features. This is not an econometric stress test, but it is small enough to make the calculation transparent. Each row is standardized by a TabularTransformer fit on the training split. For each method, one model is initialized and then trained incrementally. At 50, 100, 200, 400, 800, 1600, and 3200 steps, the current checkpoint generates one synthetic sample with the same size as the training split and one with the same size as the held-out split. Distances are computed on the original feature scale. The horizontal axis should not be read as exactly equal compute across models: one WGAN step includes critic updates, while one DDPM step is one denoising-loss update. The seconds column gives cumulative fitting cost up to the checkpoint.
The train/test split in the plot is the important part. A single in-sample distance can make extra training look mechanically attractive. The held-out curve asks whether those extra optimization steps produce synthetic rows that resemble data not used in fitting. The WGAN and DDPM curves also separate two issues: distributional fidelity as optimization proceeds, and stability of the optimization problem itself. In this run, the useful diagnostic is not monotonicity at every point. The useful diagnostic is whether the test curve tracks the training curve closely or whether it begins to separate as the number of gradient steps increases.
References
Arjovsky, Martin, Soumith Chintala, and Leon Bottou. 2017. “Wasserstein Generative Adversarial Networks.” In Proceedings of the 34th International Conference on Machine Learning, 214–23.
Athey, Susan, Guido W. Imbens, Jonas Metzger, and Evan Munro. 2024. “Using Wasserstein Generative Adversarial Networks for the Design of Monte Carlo Simulations.”Journal of Econometrics.
Austin, Jacob, Daniel D. Johnson, Jonathan Ho, Daniel Tarlow, and Rianne van den Berg. 2021. “Structured Denoising Diffusion Models in Discrete State-Spaces.” In Advances in Neural Information Processing Systems.
Gulrajani, Ishaan, Faruk Ahmed, Martin Arjovsky, Vincent Dumoulin, and Aaron Courville. 2017. “Improved Training of Wasserstein GANs.” In Advances in Neural Information Processing Systems.
Ho, Jonathan, Ajay Jain, and Pieter Abbeel. 2020. “Denoising Diffusion Probabilistic Models.” In Advances in Neural Information Processing Systems.
Hoogeboom, Emiel, Didrik Nielsen, Priyank Jaini, Patrick Forré, and Max Welling. 2021. “Argmax Flows and Multinomial Diffusion: Learning Categorical Distributions.”arXiv Preprint arXiv:2102.05379.
Kotelnikov, Akim, Dmitry Baranchuk, Ivan Rubachev, and Artem Babenko. 2023. “TabDDPM: Modelling Tabular Data with Diffusion Models.” In Proceedings of the 40th International Conference on Machine Learning, 17564–79.
Nichol, Alexander Quinn, and Prafulla Dhariwal. 2021. “Improved Denoising Diffusion Probabilistic Models.” In Proceedings of the 38th International Conference on Machine Learning, 8162–71.
Papamakarios, George, Eric Nalisnick, Danilo Jimenez Rezende, Shakir Mohamed, and Balaji Lakshminarayanan. 2021. “Normalizing Flows for Probabilistic Modeling and Inference.”Journal of Machine Learning Research 22 (57): 1–64.
Song, Jiaming, Chenlin Meng, and Stefano Ermon. 2021. “Denoising Diffusion Implicit Models.” In International Conference on Learning Representations.
Song, Yang, Jascha Sohl-Dickstein, Diederik P. Kingma, Abhishek Kumar, Stefano Ermon, and Ben Poole. 2021. “Score-Based Generative Modeling Through Stochastic Differential Equations.” In International Conference on Learning Representations.
Source Code
---title: "Diffusion Models for Realistic Tabular Simulation"author: "Apoorva Lal"date: todaybibliography: references.bibformat: html: embed-resources: true page-layout: full toc: true toc-depth: 3 code-fold: true code-tools: true css: styles.cssexecute: echo: true warning: false message: false cache: truejupyter: torch---## AbstractMonte Carlo studies in econometrics are often analytically transparent but empirically thin: functional forms, covariate distributions, error distributions, and treatment assignment rules are chosen for tractability rather than for resemblance to the empirical settings where estimators fail. Athey, Imbens, Metzger, and Munro (2024) propose using Wasserstein GANs to learn data-generating processes from real datasets and then use the fitted generator as a simulation design [@athey2024wgan]. This paper develops the same idea with denoising diffusion probabilistic models. The main claim is not that diffusion uniformly dominates GANs. The claim is that finite-sample econometric benchmarking should target the empirical geometry that drives estimator behavior--overlap, leverage, tails, treatment imbalance, and dependence--and that diffusion models are a promising engine for learning and perturbing that geometry because they replace adversarial fitting with supervised denoising. The cost is slower iterative sampling. The evidence below should be read as a runnable practice memo rather than a definitive benchmark.## IntroductionSuppose an applied study observes rows$$R_i = (Y_i, W_i, X_i), \qquad i=1,\ldots,n,$$where $Y_i$ is an outcome, $W_i \in \{0,1\}$ is a binary treatment or policy exposure, and $X_i$ collects pre-treatment covariates. A traditional Monte Carlo design specifies a distribution $P_0$ for $R_i$ and then evaluates estimators under repeated draws from $P_0$. The credibility of the exercise rests on whether $P_0$ captures the parts of the empirical problem that matter for the estimators being compared: overlap, tail behavior, discreteness, collinearity, heteroskedasticity, treatment imbalance, and nonlinear dependence.The AIMM paper reframes the problem as a learned simulation design. Let $P_n$ be the empirical distribution of a real dataset. Fit a generator $\hat G$ so that samples from $\hat P_G$ look like draws from $P_n$. Then use $\hat P_G$ as the baseline simulation environment. This does not estimate structural primitives; it reduces arbitrary choices in simulation design while preserving the empirical geometry of the application.This note separates two claims that are easy to conflate. The first is a generative-model comparison: WGANs and DDPMs are alternative ways to approximate a tabular row distribution. The second is a simulation-design claim: the object of an econometric Monte Carlo should be a realistic empirical environment plus controlled perturbations, not only a convenient analytic DGP. The second claim is the more important one. DDPMs matter here only insofar as they make that simulation-design workflow more stable and modular.## Learned Simulation DesignsA GAN has two networks. A generator maps noise $Z$ into synthetic rows:$$\tilde R = G_\theta(Z), \qquad Z \sim P_Z.$$A discriminator or critic scores whether rows look real. In a Wasserstein GAN, the population objective is$$\min_\theta \max_{f \in \mathcal{F}_1}\left\{ \mathbb{E}_{X \sim P_n}[f(X)] - \mathbb{E}_{Z \sim P_Z}[f(G_\theta(Z))]\right\},$$where $\mathcal{F}_1$ is the class of 1-Lipschitz functions [@arjovsky2017wgan]. In practice, the Lipschitz constraint is enforced by weight clipping or a gradient penalty [@gulrajani2017wgangp].For econometric simulation this has several attractions:- the generator is fast once trained;- it represents complicated row distributions without a parametric likelihood;- conditional generation can be implemented by feeding treatment, cohort, or market labels into the generator;- the Wasserstein critic gives a training signal with a distributional interpretation.But the adversarial game is also the main cost. The generator improves only through the critic's current gradients. If the critic is weak, the generator learns the wrong directions. If the critic is too strong or poorly regularized, training can become unstable. Mode collapse is a particularly bad failure for simulation studies because the generated sample can look locally plausible while missing subpopulations that determine estimator risk.The optimizer choice matters because the problem is a saddle-point problem, not ordinary risk minimization. Simultaneous Adam is convenient, but the game can rotate around equilibria rather than converge. Optimistic Adam, mirror descent, and other game-aware updates are natural modifications because they use information about recent gradients to damp these rotations. That is a repair to the WGAN fitting problem, not a change in the underlying object being learned.## Denoising Diffusion ModelsA denoising diffusion probabilistic model starts from the opposite direction. Instead of asking a generator to fool a critic, it defines a known corruption process that gradually turns data into noise. The model then learns to reverse that corruption.Let $x_0 \in \mathbb{R}^d$ denote a standardized data row. Choose a noise schedule$$0 < \beta_1 < \cdots < \beta_T < 1,$$and define$$\alpha_t = 1-\beta_t,\qquad\bar\alpha_t = \prod_{s=1}^t \alpha_s.$$The forward corruption process is$$q(x_t \mid x_{t-1})=\mathcal{N}\left( \sqrt{\alpha_t}x_{t-1}, \beta_t I\right).$$By Gaussian algebra, one can sample $x_t$ directly from $x_0$:$$x_t=\sqrt{\bar\alpha_t}x_0+\sqrt{1-\bar\alpha_t}\epsilon,\qquad\epsilon \sim \mathcal{N}(0,I).$$The model is a denoiser $\epsilon_\theta(x_t,t,c)$, where $c$ is optional context. In tabular simulations, $c$ might be treatment status, market, cohort, site, panel time, or a policy environment. The simple DDPM training objective is$$\min_\theta\mathbb{E}_{x_0,t,\epsilon}\left[ \left\| \epsilon - \epsilon_\theta(x_t,t,c) \right\|_2^2\right].$$This is ordinary supervised learning. The label is the known noise used to corrupt the clean row. Ho, Jain, and Abbeel show how this objective arises from a variational bound and connects to denoising score matching [@ho2020ddpm].Sampling reverses the chain. Start with$$x_T \sim \mathcal{N}(0,I),$$and recursively apply$$\mu_\theta(x_t,t,c)=\frac{1}{\sqrt{\alpha_t}}\left( x_t - \frac{\beta_t}{\sqrt{1-\bar\alpha_t}} \epsilon_\theta(x_t,t,c)\right).$$Then draw$$x_{t-1}=\mu_\theta(x_t,t,c) + \sigma_t z_t,\qquadz_t \sim \mathcal{N}(0,I),$$with $\sigma_t$ set by the chosen reverse variance. After reaching $x_0$, invert the tabular preprocessing map and apply any column constraints: binary rounding, nonnegativity, bounded support, or category decoding.### Density Modeling InterpretationThe DDPM is a density model built from many noisy versions of the data. The forward process defines a family of smoothed densities $p_t(x)$, where $p_0$ is the data distribution and $p_T$ is close to a standard Gaussian. The denoiser learns a vector field that points noisy observations back toward regions of high probability under $p_t$.The score of a density is$$s_t(x) = \nabla_x \log p_t(x).$$For the Gaussian corruption step,$$\nabla_{x_t}\log q(x_t \mid x_0)=-\frac{x_t-\sqrt{\bar\alpha_t}x_0}{1-\bar\alpha_t}=-\frac{\epsilon}{\sqrt{1-\bar\alpha_t}}.$$Therefore a noise predictor implies a score estimate:$$s_\theta(x_t,t,c)\approx-\frac{\epsilon_\theta(x_t,t,c)} {\sqrt{1-\bar\alpha_t}}.$$This is the link to score-based generative modeling. Song, Sohl-Dickstein, Kingma, Kumar, Ermon, and Poole formulate the same idea in continuous time: add noise through a stochastic differential equation and sample backward using the estimated score field [@song2021sde]. DDPMs are one discrete-time implementation of that broader score-based idea.The econometric analogy is useful. A likelihood model specifies $p_\theta(x)$ and maximizes $\sum_i \log p_\theta(X_i)$. A score-based model learns the local derivative $\nabla_x \log p_t(x)$ along many noisy versions of the distribution. It does not need a normalized likelihood. For simulation, this is enough: if the learned reverse chain approximately transports Gaussian noise into the empirical row distribution, then it supplies draws for a Monte Carlo design.### Tabular RowsThe original DDPM success story is image generation, but the row-distribution problem is different. In image data, all coordinates are intensities with spatial structure. In an econometric row, coordinates may be continuous, binary, categorical, censored, top-coded, or logically constrained.A practical tabular DDPM therefore needs a representation map$$z = S(x),$$where $S$ standardizes continuous variables, encodes categorical variables, and records constraints needed to invert the transformation. Continuous variables can be diffused with Gaussian noise. Binary and categorical variables can be handled in several ways:- keep them as relaxed numeric variables during diffusion and round after sampling;- use separate output heads and postprocessing rules;- use discrete diffusion kernels designed for categorical state spaces.The last option is conceptually cleaner. Multinomial diffusion and argmax flows explicitly model categorical variables [@hoogeboom2021argmax]. Structured denoising diffusion models in discrete state spaces extend the corruption process beyond uniform replacement kernels and provide a general D3PM framework [@austin2021d3pm]. TabDDPM combines Gaussian diffusion for numerical features with multinomial diffusion for categorical features and shows that diffusion can be competitive for general tabular data [@kotelnikov2023tabddpm].For econometric simulation, the choice should be governed by the role of each variable. Rounding a binary demographic indicator may be acceptable in a first benchmark. Rounding the binary treatment $W$ is usually not acceptable if the estimand conditions on the realized assignment mechanism. In that case, treatment should be fixed as context or generated by a separate assignment model.## Comparison with WGANs and FlowsThe AIMM objective is to build realistic simulation designs from empirical datasets. Both WGANs and DDPMs are plausible tools. They differ mainly in what is hard.| Feature | WGAN simulation | DDPM simulation ||---|---|---|| Training problem | Minimax game between generator and critic | Supervised denoising objective || Main tuning risk | Critic strength, Lipschitz penalty, optimizer dynamics, mode collapse | Noise schedule, denoiser capacity, sampling steps, preprocessing || Sampling speed | Fast single generator pass | Slower iterative denoising || Diagnostics | Critic loss plus downstream distribution checks | Denoising loss plus downstream distribution checks || Mode coverage | Can be fragile when generator collapses | Usually strong because training sees noisy neighborhoods at all scales || Conditional designs | Feed context to generator and critic | Feed context to denoiser at every reverse step || Ablations | Possible but can destabilize the adversarial game | Natural: intervene on context, corruption path, or reverse constraints |This comparison explains why DDPMs are attractive for econometric practice. The fitting problem is closer to nonparametric regression than to adversarial equilibrium computation. Every training example supplies a known target $\epsilon$. The model is trained across many local perturbations of each row, which tends to preserve support and neighborhood structure. For a simulation study where the cost of a missed subpopulation can be high, that stability is valuable.The tradeoff is sampling cost. A WGAN can generate $100{,}000$ rows with one forward pass per row. A DDPM may need tens or hundreds of denoising steps. Improved DDPMs learn reverse variances and improve the likelihood/sample-quality tradeoff [@nichol2021improved]. DDIMs reduce the number of sampling steps by using a deterministic or partially deterministic non-Markovian sampler with the same training objective [@song2021ddim]. In practice, a tabular simulation design can usually tolerate slower sampling because generating the synthetic benchmark is not the bottleneck relative to fitting many downstream estimators.### Normalizing FlowsNormalizing flows are another natural density-modeling candidate. A flow constructs an invertible map $f_\theta$ such that$$z = f_\theta(x),\qquadz \sim p_Z,$$and evaluates the likelihood by the change-of-variables formula:$$\log p_\theta(x)=\log p_Z(f_\theta(x))+\log \left| \det \frac{\partial f_\theta(x)}{\partial x}\right|.$$This gives exact likelihoods and direct sampling, which is attractive for statistical work [@papamakarios2021flows]. The cost is architectural: $f_\theta$ must be invertible and have a tractable Jacobian determinant. Those constraints can make flows awkward for mixed discrete-continuous tabular rows, although many useful constructions exist.Diffusion models relax invertibility. They do not give exact likelihood as directly as a flow, although likelihood bounds and probability-flow ODEs are available in the broader score-based framework. For simulation design, exact likelihood is less important than faithful reproduction of the empirical row distribution and stable conditional sampling. That tilts the practical comparison toward DDPMs unless likelihood evaluation itself is part of the research design.### Trex API SurfaceTrex exposes WGAN and DDPM simulators through the same minimal interface: fit on a matrix of transformed tabular rows, optionally condition on a context matrix, and sample synthetic transformed rows. The fitted rows are transformed back to data scale with `TabularTransformer`.For the WGAN benchmark, the class is `trex.TabularWGAN`:```pythonTabularWGAN( hidden_dims=(128, 128, 128), critic_hidden_dims=None, noise_dim=None, batch_size=128, max_steps=1000, critic_steps=5, lr=1e-4, optimizer="adam", # or "optimistic_adam" gp_weight=5.0, generator_dropout=0.1, critic_dropout=0.0, binary_dims=(), lower_bounds=None, upper_bounds=None, seed=None, device=None,)wgan.fit(X, context=None)wgan.partial_fit(X, context=None, steps=100)fake_z = wgan.sample(n, context=None)```For the DDPM benchmark, the class is `trex.TabularDiffusion`:```pythonTabularDiffusion( hidden_dims=(128, 128, 128), time_dim=32, n_timesteps=100, beta_start=1e-4, beta_end=0.02, batch_size=128, max_steps=1000, lr=1e-3, dropout=0.0, seed=None, device=None,)ddpm.fit(X, context=None)ddpm.partial_fit(X, context=None, steps=100)fake_z = ddpm.sample(n, context=None)```A typical tabular call is:```pythontransformer = TabularTransformer( column_names=columns, binary_columns=binary_columns, nonnegative_columns=nonnegative_columns,).fit(df[columns])X = transformer.transform(df[columns])context = df[["t"]].to_numpy()ddpm = TabularDiffusion(max_steps=1000, seed=123, device="cuda")ddpm.fit(X, context=context)fake = transformer.inverse_transform( ddpm.sample(len(df), context=context).numpy())```This branch does not currently expose a `TabularFlow` class. A normalizing-flow simulator would fit the same interface naturally:```pythonflow.fit(X, context=None)fake_z = flow.sample(n, context=None)logp = flow.log_prob(X, context=None)```The extra method is `log_prob`, since exact or tractable likelihood evaluation is the main reason to add a flow baseline. The implementation choice would then be a conditional spline flow, masked autoregressive flow, or coupling flow depending on whether tabular likelihood or fast sampling is the priority.## Distance MetricsLet $R_1,\ldots,R_n \in \mathbb{R}^d$ denote observed rows and $\tilde R_1,\ldots,\tilde R_m \in \mathbb{R}^d$ denote synthetic rows. All diagnostics below are computed after the same preprocessing convention used for the simulation exercise; in the Lalonde application, earnings are measured in thousands of dollars. Lower values indicate closer agreement.For coordinate $j$, the one-dimensional Wasserstein distance is$$W_{1j}=\int_0^1\left| \hat F_{j}^{-1}(u) - \hat G_{j}^{-1}(u)\right|du,$$where $\hat F_j$ and $\hat G_j$ are the empirical CDFs of observed and synthetic column $j$. The reported `marginal_w1_mean` and `marginal_w1_max` are$$\frac{1}{d}\sum_{j=1}^d W_{1j},\qquad\max_{1 \leq j \leq d} W_{1j}.$$The Kolmogorov-Smirnov distance for coordinate $j$ is$$K_j=\sup_x\left| \hat F_j(x) - \hat G_j(x)\right|.$$The reported `marginal_ks_mean` and `marginal_ks_max` average and maximize $K_j$ across columns. The KS distance is scale-free, while Wasserstein distance preserves the units of each variable.The mean-distance diagnostic is$$\text{mean\_l2}=\left\| \bar R - \bar{\tilde R}\right\|_2.$$It is a location check. It can be small even when tails or dependence are wrong.Let $\hat\Sigma_R$ and $\hat\Sigma_{\tilde R}$ be empirical covariance matrices. The covariance diagnostic is$$\text{cov\_frobenius}=\left\| \hat\Sigma_R - \hat\Sigma_{\tilde R}\right\|_F.$$Because covariance is measured in original units, high-variance earnings columns can dominate it. The correlation diagnostic repeats the same calculation after converting covariance matrices into correlation matrices:$$\text{corr\_frobenius}=\left\| \hat C_R - \hat C_{\tilde R}\right\|_F.$$This is the cleaner summary of linear dependence across mixed-scale columns.Finally, sliced Wasserstein averages one-dimensional Wasserstein distances over random projections. For a unit vector $u \in \mathbb{S}^{d-1}$, project rows to $u'R_i$ and $u'\tilde R_i$. The population target is$$\text{SW}_1(R,\tilde R)=\mathbb{E}_{u \sim \operatorname{Unif}(\mathbb{S}^{d-1})}\left[ W_1(u'R, u'\tilde R)\right].$$The reported value approximates this expectation with random directions. This is useful in tabular simulation because it can detect joint-distribution failures that are invisible in marginal diagnostics, while remaining much cheaper than exact high-dimensional optimal transport.### Metrics for Estimator GeometryThese distances are generic generator diagnostics. They are not, by themselves, econometric loss functions. For estimator benchmarking, the question is which parts of the empirical distribution make estimators fail.For treatment-effect estimators, several features matter:- overlap and common support in $X$ across treatment states;- leverage points that dominate regression or balancing weights;- tail behavior in outcomes and pre-treatment earnings;- treatment imbalance and small treated or control cells;- dependence between treatment, covariates, and outcomes;- nonlinear nuisance-function difficulty in $m_w(x)=\mathbb{E}[Y\mid W=w,X=x]$ and $e(x)=\Pr(W=1\mid X=x)$.The reported metrics proxy these features imperfectly. Marginal Wasserstein and KS distances detect column-level failures such as missed tails, mass at zero, or wrong binary shares. Mean and covariance distances catch first- and second-moment errors that often affect regression adjustment. Correlation distance summarizes linear dependence across columns. Sliced Wasserstein is the broadest row-geometry diagnostic because it sees random linear combinations of variables and can detect joint-distribution errors missed by marginal checks.The metrics do not directly measure overlap, propensity-score calibration, leverage, or nuisance-function difficulty. A complete simulation-design benchmark should add estimator-specific diagnostics: propensity-score distributions, nearest-neighbor distances across treatment arms, maximum leverage, effective sample size for weighting estimators, and tail-sensitive outcome summaries. The present memo uses generic distances as a first screen and downstream ATT replication as one applied estimand check.## Simulation ProtocolA useful DDPM simulator for econometrics should make the target distribution explicit. The following workflow is close to the AIMM proposal but swaps the fitting engine:1. Choose the empirical target: unconditional rows, rows conditional on treatment, rows within market, or a panel transition distribution.2. Define a preprocessing map $S$ and record all inverse transformations.3. Decide which variables are generated and which are fixed as context.4. Fit a denoiser with a transparent noise schedule and held-out diagnostics.5. Generate synthetic samples with the same sample size, treatment shares, panel length, or market composition as the empirical design.6. Report distances between real and generated data: marginal Wasserstein, KS distances, mean distance, covariance and correlation Frobenius norms, sliced Wasserstein, and downstream estimand replication.7. Run estimator comparisons on many generated datasets.8. Add ablations by changing one empirical feature at a time: overlap, tails, treatment imbalance, covariance, policy rules, or outcome noise.The ablation step is where diffusion can be especially useful. Because generation is a gradual reverse process, one can condition, guide, or project intermediate draws. Examples include:- holding treatment fixed and generating only $(Y,X)$ conditional on $W$;- matching marginal treatment shares exactly while allowing covariates and outcomes to vary;- projecting generated rows back into known support restrictions;- increasing or decreasing tail noise to stress-test robust estimators;- conditioning on policy environments to compare counterfactual regimes.The relevant standard is practical: does the DDPM improve the credibility of finite-sample estimator comparisons?### Treatment as Target, Context, or AssignmentTreatment status plays different roles in different simulation designs. The distinction matters because the estimand changes when the simulated object changes.In a joint row simulation, the target is$$P(Y,W,X).$$The generator draws complete rows, including treatment. This is useful when the assignment mechanism is part of the empirical environment one wants to reproduce. The risk is that rare treatments may be poorly learned, and the realized treatment share will vary across synthetic samples.In a conditional row simulation, the target is$$P(Y,X \mid W),$$and the analyst supplies the treatment vector $W_1,\ldots,W_n$. This is the design used in the Lalonde exercises below. It keeps the sample size and treatment count fixed, so differences across generators come from the learned conditional outcome/covariate distribution rather than from random variation in treatment assignment.In an assignment-mechanism simulation, one separately models$$P(X), \qquad P(W\mid X), \qquad P(Y\mid W,X).$$This is closer to a causal DGP. It is also more demanding because misspecification in the assignment model changes overlap, treatment shares, and the interpretation of ATT or ATE exercises. A learned simulator can be used for any of these objects, but the paper must state which one is being used. In the empirical sections below, Iris fixes species labels as context, the held-out scikit benchmarks are unconditional row simulations, and the Lalonde designs fix treatment as context.## Empirical Illustration### Iris Sanity CheckThe Iris dataset is too small and too clean to be an econometric benchmark. It is useful here because it lets us see the mechanics without hiding behind a large application. The row is$$x_i =(\text{sepal length}, \text{sepal width}, \text{petal length}, \text{petal width}, \text{species}).$$The exercise fixes the species counts and learns the conditional distribution of the four continuous measurements given species. This is the same design pattern one would use when a binary treatment indicator or site membership is part of the simulation design rather than a variable the generator is allowed to change.```{python}#| label: setup#| echo: true#| output: falsefrom __future__ import annotationsimport sysimport timefrom pathlib import Pathimport matplotlib.pyplot as pltimport numpy as npimport pandas as pdimport torchfrom scipy.stats import wasserstein_distancefrom sklearn.datasets import load_breast_cancer, load_diabetes, load_iris, load_winefrom sklearn.model_selection import train_test_splitTREX_ROOT = Path("/home/alal/Desktop/code/econometrics/trex")ifstr(TREX_ROOT) notin sys.path: sys.path.insert(0, str(TREX_ROOT))from trex import TabularDiffusion, TabularTransformer, TabularWGAN, distribution_metricsSEED =20260526rng = np.random.default_rng(SEED)torch.manual_seed(SEED)DEVICE ="cuda"if torch.cuda.is_available() else"cpu"def one_hot(values: pd.Series | np.ndarray) -> np.ndarray: values = np.asarray(values, dtype=int) levels = np.sort(np.unique(values))return np.column_stack([(values == level).astype(float) for level in levels])def tidy_metrics(real: pd.DataFrame, fake: pd.DataFrame, columns: list[str], label: str) -> pd.DataFrame: out = distribution_metrics(real[columns].to_numpy(), fake[columns].to_numpy(), seed=SEED)return pd.DataFrame([{"sample": label, **out}])``````{python}#| label: iris-fitiris = load_iris(as_frame=True)iris_df = iris.frame.copy()iris_df.columns = ["sepal_length","sepal_width","petal_length","petal_width","species",]iris_df["species"] = iris_df["species"].astype(int)iris_features = ["sepal_length", "sepal_width", "petal_length", "petal_width"]iris_context = one_hot(iris_df["species"])iris_transformer = TabularTransformer( column_names=iris_features, nonnegative_columns=iris_features,).fit(iris_df[iris_features])iris_z = iris_transformer.transform(iris_df[iris_features])iris_lower, iris_upper = iris_transformer.transformed_bounds()iris_wgan = TabularWGAN( hidden_dims=(64, 64), critic_hidden_dims=(64, 64), batch_size=64, max_steps=500, critic_steps=2, lr=1e-4, optimizer="optimistic_adam", lower_bounds=iris_lower, upper_bounds=iris_upper, seed=SEED, device=DEVICE,)start = time.perf_counter()iris_wgan.fit(iris_z, context=iris_context)iris_wgan_seconds = time.perf_counter() - startiris_wgan_z = iris_wgan.sample(len(iris_df), context=iris_context).numpy()iris_wgan_fake = pd.DataFrame( iris_transformer.inverse_transform(iris_wgan_z), columns=iris_features,)iris_wgan_fake["species"] = iris_df["species"].to_numpy()iris_ddpm = TabularDiffusion( hidden_dims=(64, 64), n_timesteps=50, max_steps=700, batch_size=64, lr=1e-3, seed=SEED, device=DEVICE,)start = time.perf_counter()iris_ddpm.fit(iris_z, context=iris_context)iris_seconds = time.perf_counter() - startiris_fake_z = iris_ddpm.sample(len(iris_df), context=iris_context).numpy()iris_fake = pd.DataFrame( iris_transformer.inverse_transform(iris_fake_z), columns=iris_features,)iris_fake["species"] = iris_df["species"].to_numpy()iris_metrics = pd.concat( [ tidy_metrics( iris_df, iris_wgan_fake, [*iris_features, "species"],f"WGAN synthetic, {iris_wgan_seconds:.1f}s", ), tidy_metrics( iris_df, iris_fake, [*iris_features, "species"],f"DDPM synthetic, {iris_seconds:.1f}s", ), ], ignore_index=True,)iris_metrics[["sample","marginal_w1_mean","marginal_ks_mean","mean_l2","cov_frobenius","corr_frobenius","sliced_wasserstein",]].round(4).style.hide(axis="index")``````{python}#| label: iris-plot#| fig-width: 9#| fig-height: 3.4species_names =dict(enumerate(iris.target_names))fig, axes = plt.subplots(1, 3, figsize=(9, 3.4), sharex=True, sharey=True, constrained_layout=True)for ax, data, title in [ (axes[0], iris_df, "Observed Iris"), (axes[1], iris_wgan_fake, "WGAN synthetic"), (axes[2], iris_fake, "DDPM synthetic"),]:for species, group in data.groupby("species"): ax.scatter( group["sepal_length"], group["petal_length"], s=28, alpha=0.75, label=species_names[int(species)], ) ax.set_title(title) ax.set_xlabel("sepal length")axes[0].set_ylabel("petal length")axes[2].legend(frameon=False, loc="lower right")plt.show()```The Iris example is deliberately modest. Its diagnostic role is to check whether small conditional generators preserve species-conditional clusters and produce plausible continuous measurements. The WGAN panel shows the adversarial baseline; the DDPM panel shows the same task fit through supervised denoising.### Held-Out Distance BenchmarksFor reporting generator quality, a held-out comparison is usually preferable to comparing synthetic rows against the same empirical rows used for fitting. The in-sample distance answers whether the generator can reproduce the training distribution. The held-out distance asks a more relevant question: after fitting on one sample, how close are generated rows to new rows from the same empirical source?The following benchmark uses small numeric datasets bundled with scikit-learn. For each dataset, the models fit only the training rows. They then generate the same number of rows as the test split, and distances are computed against the held-out test rows. A train-resample baseline draws rows with replacement from the training set; it is not a learned generator, but it gives a useful finite-sample reference.```{python}#| label: sklearn-heldoutdef sklearn_frame(loader, name: str) -> pd.DataFrame: data = loader(as_frame=True) frame = data.frame.copy()if"target"in frame.columns: frame = frame.drop(columns=["target"]) frame.columns = [f"x{j}"for j inrange(frame.shape[1])] frame = frame.astype(float) frame.attrs["dataset_name"] = namereturn framesklearn_datasets = [ sklearn_frame(load_iris, "iris features"), sklearn_frame(load_wine, "wine features"), sklearn_frame(load_breast_cancer, "breast cancer features"), sklearn_frame(load_diabetes, "diabetes features"),]def fit_small_generators(train: pd.DataFrame, test: pd.DataFrame, seed: int) -> pd.DataFrame: columns =list(train.columns) transformer = TabularTransformer( column_names=columns, nonnegative_columns=columns, ).fit(train) z_train = transformer.transform(train) lower, upper = transformer.transformed_bounds() n_test =len(test) wgan_model = TabularWGAN( hidden_dims=(48, 48), critic_hidden_dims=(48, 48), batch_size=min(128, len(train)), max_steps=250, critic_steps=2, lr=1e-4, optimizer="optimistic_adam", lower_bounds=lower, upper_bounds=upper, seed=seed, device=DEVICE, ) ddpm_model = TabularDiffusion( hidden_dims=(48, 48), n_timesteps=50, batch_size=min(128, len(train)), max_steps=300, lr=1e-3, seed=seed, device=DEVICE, ) wgan_model.fit(z_train) ddpm_model.fit(z_train) train_resample = train.sample(n_test, replace=True, random_state=seed).reset_index(drop=True) wgan_fake = pd.DataFrame( transformer.inverse_transform(wgan_model.sample(n_test).numpy()), columns=columns, ) ddpm_fake = pd.DataFrame( transformer.inverse_transform(ddpm_model.sample(n_test).numpy()), columns=columns, ) rows = []for label, fake in [ ("train resample", train_resample), ("WGAN", wgan_fake), ("DDPM", ddpm_fake), ]: metrics = distribution_metrics( test[columns].to_numpy(), fake[columns].to_numpy(), n_projections=64, seed=seed, ) rows.append({"model": label, **metrics})return pd.DataFrame(rows)heldout_rows = []for k, frame inenumerate(sklearn_datasets): train, test = train_test_split(frame, test_size=0.30, random_state=SEED + k) result = fit_small_generators( train.reset_index(drop=True), test.reset_index(drop=True), SEED +100* k, ) result.insert(0, "dataset", frame.attrs["dataset_name"]) result.insert(1, "n_train", len(train)) result.insert(2, "n_test", len(test)) heldout_rows.append(result)heldout_table = pd.concat(heldout_rows, ignore_index=True)heldout_table[["dataset","n_train","n_test","model","marginal_w1_mean","marginal_ks_mean","mean_l2","corr_frobenius","sliced_wasserstein",]].round(4).style.hide(axis="index")``````{python}#| label: sklearn-heldout-plot#| fig-width: 8#| fig-height: 3.6fig, ax = plt.subplots(figsize=(8, 3.6), constrained_layout=True)plot_data = heldout_table.pivot(index="dataset", columns="model", values="sliced_wasserstein")plot_data = plot_data[["train resample", "WGAN", "DDPM"]]x = np.arange(len(plot_data))width =0.24for j, model inenumerate(plot_data.columns): ax.bar(x + (j -1) * width, plot_data[model], width=width, label=model)ax.set_xticks(x)ax.set_xticklabels(plot_data.index, rotation=20, ha="right")ax.set_ylabel("sliced Wasserstein to held-out test rows")ax.set_title("Held-out row-distribution distance")ax.legend(frameon=False, ncols=3)plt.show()```Held-out distances are noisy on these small datasets, but they are conceptually cleaner than in-sample distances. A generator should be judged against rows it did not see during fitting, and the train-resample baseline helps distinguish model failure from the ordinary sampling variability of a small empirical distribution.### Lalonde DesignsThe AIMM paper uses WGANs to build simulation designs for treatment-effect estimators. The Lalonde setting is useful because it has two distinct empirical targets. The experimental target is the NSW randomized sample: treated and control units both come from the experiment. The observational target combines NSW treated units with a large CPS control pool. The second target is closer to the classic nonexperimental evaluation problem: controls come from a different empirical environment, treatment is rare, earnings are heavy-tailed and truncated, and covariate overlap is strained.The exercise below evaluates both targets. In each case, earnings are scaled to thousands of dollars, the binary treatment indicator $W$ is held fixed as context, and each generator learns the conditional distribution of $(Y,X)$ given $W$. Thus the experimental and observational exercises differ in the empirical row distribution being learned, not in the simulation protocol.```{python}#| label: lalonde-load#| output: falseLDW_COLUMNS = ["t", "age", "education", "black", "hispanic","married", "nodegree", "re74", "re75", "re78",]EARNINGS_COLUMNS = ["re74", "re75", "re78"]BINARY_COLUMNS = ["black", "hispanic", "married", "nodegree"]NONNEGATIVE_COLUMNS = ["age", "education", "re74", "re75", "re78"]COVARIATES = ["age", "education", "black", "hispanic", "married", "nodegree", "re74", "re75"]OUTCOME ="re78"TREATMENT ="t"LOCAL_DATA = Path("data/original_data")LALONDE_SOURCES = {"NSW experimental": LOCAL_DATA /"nsw_experimental.feather","NSW/CPS observational": LOCAL_DATA /"cps_merged.feather",}missing_sources = [str(path) for path in LALONDE_SOURCES.values() ifnot path.exists()]if missing_sources:raiseFileNotFoundError(f"Missing local Lalonde data files: {missing_sources}")def load_lalonde_sample(path: Path) -> pd.DataFrame: out = pd.read_feather(path)[LDW_COLUMNS].astype(float) out[EARNINGS_COLUMNS] = out[EARNINGS_COLUMNS] /1000.0return outlalonde_samples = { name: load_lalonde_sample(path)for name, path in LALONDE_SOURCES.items()}lalonde_summary = pd.DataFrame([ {"design": name,"n": len(df),"n_treated": int(df[TREATMENT].sum()),"n_control": int(len(df) - df[TREATMENT].sum()),"treated_share": float(df[TREATMENT].mean()), }for name, df in lalonde_samples.items()])```The experimental sample has 445 rows, with 185 treated observations and 260 experimental controls. The observational sample has 16,177 rows, with the same 185 NSW treated observations and 15,992 CPS controls. The treated share is therefore about 41.6 percent in the experimental sample and 1.1 percent in the observational sample.```{python}#| label: lalonde-sample-summarylalonde_summary.round(4).style.hide(axis="index")``````{python}#| label: lalonde-estimand#| output: falsedef design_matrix(df: pd.DataFrame) -> np.ndarray:return np.column_stack([np.ones(len(df)), df[COVARIATES].to_numpy(dtype=float)])def regression_adjustment_att(df: pd.DataFrame) ->dict[str, float|int|str]: data = df[LDW_COLUMNS].astype(float).copy() t = data[TREATMENT].to_numpy(dtype=int) y = data[OUTCOME].to_numpy(dtype=float) X = design_matrix(data) treated = t ==1 control = t ==0if treated.sum() <5or control.sum() <= X.shape[1] +1:return {"att_thousand": np.nan,"att_dollars": np.nan,"se_thousand": np.nan,"se_dollars": np.nan,"n": len(data),"n_treated": int(treated.sum()),"n_control": int(control.sum()),"status": "insufficient support", } beta0 = np.linalg.lstsq(X[control], y[control], rcond=None)[0] treated_effects = y[treated] - X[treated] @ beta0 att =float(treated_effects.mean()) se =float(treated_effects.std(ddof=1) / np.sqrt(treated.sum()))return {"att_thousand": att,"att_dollars": 1000.0* att,"se_thousand": se,"se_dollars": 1000.0* se,"n": len(data),"n_treated": int(treated.sum()),"n_control": int(control.sum()),"status": "ok", }def result_row(design: str, name: str, df: pd.DataFrame) ->dict[str, float|int|str]: row = {"design": design, "sample": name, **regression_adjustment_att(df)} row["treated_share"] = row["n_treated"] / row["n"]return row``````{python}#| label: lalonde-fit#| output: falsefeature_columns = [column for column in LDW_COLUMNS if column != TREATMENT]feature_binary = [column for column in BINARY_COLUMNS if column in feature_columns]feature_nonnegative = [column for column in NONNEGATIVE_COLUMNS if column in feature_columns]def rebuild_lalonde(generated_features: np.ndarray, generated_t: np.ndarray) -> pd.DataFrame: out = pd.DataFrame(generated_features, columns=feature_columns) out.insert(0, TREATMENT, generated_t.reshape(-1).astype(float)) out = out[LDW_COLUMNS].astype(float)for col in [TREATMENT, *BINARY_COLUMNS]: out[col] = (np.clip(out[col], 0.0, 1.0) >=0.5).astype(float)for col in NONNEGATIVE_COLUMNS: out[col] = np.maximum(out[col], 0.0)return outdef fit_lalonde_design(design: str, df: pd.DataFrame, seed_offset: int) ->dict[str, object]: features = df[feature_columns].copy() context = df[[TREATMENT]].to_numpy(dtype=np.float32) transformer = TabularTransformer( column_names=feature_columns, binary_columns=feature_binary, nonnegative_columns=feature_nonnegative, ).fit(features) train_z = transformer.transform(features) lower, upper = transformer.transformed_bounds() batch_size =min(512, len(df)) wgan_model = TabularWGAN( hidden_dims=(64, 64), critic_hidden_dims=(64, 64), batch_size=batch_size, max_steps=450, critic_steps=2, lr=1e-4, optimizer="optimistic_adam", gp_weight=5.0, binary_dims=[feature_columns.index(col) for col in feature_binary], lower_bounds=lower, upper_bounds=upper, seed=SEED + seed_offset, device=DEVICE, ) ddpm_model = TabularDiffusion( hidden_dims=(64, 64), n_timesteps=80, batch_size=batch_size, max_steps=650, lr=1e-3, seed=SEED +1000+ seed_offset, device=DEVICE, ) start = time.perf_counter() wgan_model.fit(train_z, context=context) wgan_seconds = time.perf_counter() - start start = time.perf_counter() ddpm_model.fit(train_z, context=context) ddpm_seconds = time.perf_counter() - startdef synthesize(model: object, context_array: np.ndarray) -> pd.DataFrame: generated = model.sample(len(context_array), context=context_array).numpy()return rebuild_lalonde( transformer.inverse_transform(generated), context_array, )return {"design": design,"observed": df,"context": context,"transformer": transformer,"wgan": wgan_model,"ddpm": ddpm_model,"wgan_fake": synthesize(wgan_model, context),"ddpm_fake": synthesize(ddpm_model, context),"wgan_seconds": wgan_seconds,"ddpm_seconds": ddpm_seconds,"synthesize": synthesize, }lalonde_fits = { design: fit_lalonde_design(design, df, 100* k)for k, (design, df) inenumerate(lalonde_samples.items())}fit_times = pd.DataFrame([ {"design": design, "model": "WGAN-GP", "seconds": fit["wgan_seconds"]}for design, fit in lalonde_fits.items()] + [ {"design": design, "model": "DDPM", "seconds": fit["ddpm_seconds"]}for design, fit in lalonde_fits.items()])```The timing comparison is included only to show the practical cost of the two fitting routines in this small benchmark. It is not the main estimand of the exercise.```{python}#| label: fit-time-plot#| fig-width: 5#| fig-height: 2.4fig, ax = plt.subplots(figsize=(7.5, 2.8), constrained_layout=True)plot_times = fit_times.pivot(index="design", columns="model", values="seconds")x = np.arange(len(plot_times))width =0.34for j, model inenumerate(plot_times.columns): ax.bar(x + (j -0.5) * width, plot_times[model], width=width, label=model)ax.set_xticks(x)ax.set_xticklabels(plot_times.index, rotation=10, ha="right")ax.set_ylabel("seconds")ax.set_title("Generator fit time")ax.legend(frameon=False, ncols=2)plt.show()``````{python}#| label: lalonde-resultsatt_rows = []metric_rows = []for design, fit in lalonde_fits.items(): observed = fit["observed"] wgan_fake = fit["wgan_fake"] ddpm_fake = fit["ddpm_fake"] att_rows.extend([ result_row(design, "Observed", observed), result_row(design, "WGAN-GP synthetic", wgan_fake), result_row(design, "DDPM synthetic", ddpm_fake), ]) metric_rows.extend([ tidy_metrics(observed, wgan_fake, LDW_COLUMNS, "WGAN-GP synthetic").assign(design=design), tidy_metrics(observed, ddpm_fake, LDW_COLUMNS, "DDPM synthetic").assign(design=design), ])att_table = pd.DataFrame(att_rows)metric_table = pd.concat(metric_rows, ignore_index=True)att_table.round(4).style.hide(axis="index")``````{python}#| label: lalonde-metricsmetric_table[["design","sample","marginal_w1_mean","marginal_ks_mean","mean_l2","cov_frobenius","corr_frobenius","sliced_wasserstein",]].round(4).style.hide(axis="index")```The ATT and distance tables separate two questions. The ATT table asks whether the downstream regression-adjustment estimand is similar. The distance table asks whether the generated rows resemble the empirical rows before any estimator is applied. The experimental design is a comparatively balanced benchmark. The observational design is a stress test with the same treated sample but a much larger and different control pool. A generator that looks acceptable in the experimental sample can still fail in the observational sample because the latter puts much more weight on overlap, earnings tails, and the dependence between treatment status and pre-treatment earnings.The sliced Wasserstein distance is especially useful here because it is multivariate. It projects the full row vector onto many random directions and averages the one-dimensional Wasserstein distances. A generator can match each column separately while getting the joint geometry wrong; sliced Wasserstein is meant to catch that failure without solving a full high-dimensional optimal-transport problem. The Frobenius distances for covariance and correlation matrices provide a more classical second-moment version of the same check.```{python}#| label: lalonde-plot#| fig-width: 9#| fig-height: 3.4fig, axes = plt.subplots(len(lalonde_fits),3, figsize=(10.5, 5.8), sharex=True, sharey=True, constrained_layout=True,)for row, (design, fit) inenumerate(lalonde_fits.items()): plot_items = [ ("Observed", fit["observed"]), ("WGAN-GP", fit["wgan_fake"]), ("DDPM", fit["ddpm_fake"]), ]for col, (title, df) inenumerate(plot_items): ax = axes[row, col] sample = df.sample(min(4000, len(df)), random_state=SEED + row) ax.scatter(sample["re75"], sample["re78"], s=5, alpha=0.16) ax.set_title(f"{design}: {title}") ax.set_xlabel("1975 earnings, thousands") axes[row, 0].set_ylabel("1978 earnings, thousands")plt.show()```One synthetic dataset is only a first pass. A generator can match the ATT and still miss the row distribution. The distance table and scatter plot are therefore part of the simulation design, not decoration. Comparing the experimental and observational designs also clarifies what the synthetic-data task is supposed to preserve: randomized-sample balance in the first case, and nonexperimental control-pool geometry in the second.### Repeated Simulation ResultsThe single synthetic draw above answers whether one fitted generator can produce a plausible table for each Lalonde target. A simulation study also needs repeated draws from the fitted generator. The estimand and sample geometry are held fixed within each target: every synthetic dataset has the observed sample size and the observed treatment vector for that target. This isolates the fitted row distribution from changes in treatment prevalence.The repeated design is:$$\tilde R_{ib}^{(m)}\sim\hat P_m(\cdot \mid W = w_i),\qquadi=1,\ldots,n,\quadb=1,\ldots,B,$$where $m \in \{\text{WGAN}, \text{DDPM}\}$ indexes the generator and $w_i$ is the observed treatment assignment for row $i$. Each replicate is evaluated by the same regression-adjustment ATT and by the same distribution distances used above.```{python}#| label: repeated-simulationdef synthetic_from_fit(fit: dict[str, object], model_key: str, context_array: np.ndarray) -> pd.DataFrame:return fit["synthesize"](fit[model_key], context_array)def simulation_row( design: str, model_name: str, replicate: int, observed: pd.DataFrame, df: pd.DataFrame,) ->dict[str, float|int|str]: att = result_row(design, model_name, df) metrics = distribution_metrics( observed[LDW_COLUMNS].to_numpy(), df[LDW_COLUMNS].to_numpy(), n_projections=64, seed=SEED + replicate, ) observed_att = regression_adjustment_att(observed)["att_dollars"]return {"design": design,"model": model_name,"replicate": replicate,"att_dollars": att["att_dollars"],"se_dollars": att["se_dollars"],"att_error_dollars": att["att_dollars"] - observed_att,"sliced_wasserstein": metrics["sliced_wasserstein"],"marginal_ks_mean": metrics["marginal_ks_mean"],"corr_frobenius": metrics["corr_frobenius"],"cov_frobenius": metrics["cov_frobenius"], }N_REPLICATES =12simulation_rows = []for design, fit in lalonde_fits.items(): observed = fit["observed"] context = fit["context"]for b inrange(N_REPLICATES): simulation_rows.append( simulation_row(design, "WGAN-GP", b, observed, synthetic_from_fit(fit, "wgan", context)) ) simulation_rows.append( simulation_row(design, "DDPM", b, observed, synthetic_from_fit(fit, "ddpm", context)) )simulation_table = pd.DataFrame(simulation_rows)simulation_summary = ( simulation_table .groupby(["design", "model"]) .agg( att_mean_dollars=("att_dollars", "mean"), att_sd_dollars=("att_dollars", "std"), abs_att_error_mean=("att_error_dollars", lambda x: np.mean(np.abs(x))), sliced_wasserstein_mean=("sliced_wasserstein", "mean"), marginal_ks_mean=("marginal_ks_mean", "mean"), corr_frobenius_mean=("corr_frobenius", "mean"), cov_frobenius_mean=("cov_frobenius", "mean"), ) .reset_index())simulation_summary.round(4).style.hide(axis="index")```The repeated draws turn the generator comparison into a Monte Carlo object. The ATT columns ask whether the downstream estimator sees a similar treatment-effect problem. The distance columns ask whether the synthetic tables reproduce the empirical distribution that makes the treatment-effect problem hard. The comparison should be read within each design. The experimental target tests whether the models can reproduce a randomized small-sample environment. The observational target tests whether they can reproduce the severe imbalance and covariate geometry of the CPS control design. Estimand replication alone can reward a generator for getting one target right for the wrong distributional reasons. Conversely, a generator with better row geometry can still produce a noisier downstream estimand in a small or rare-treatment design.```{python}#| label: repeated-simulation-plot#| fig-width: 8#| fig-height: 3.6fig, axes = plt.subplots(len(lalonde_fits), 2, figsize=(9.5, 5.8), constrained_layout=True)for row, (design, group_design) inenumerate(simulation_table.groupby("design", sort=False)): observed_att = regression_adjustment_att(lalonde_samples[design])["att_dollars"]for model, group in group_design.groupby("model"): xpos =0if model =="WGAN-GP"else1 axes[row, 0].scatter( np.full(len(group), xpos) + rng.normal(0, 0.035, len(group)), group["att_dollars"], alpha=0.75, s=26, label=model, ) axes[row, 1].scatter( np.full(len(group), xpos) + rng.normal(0, 0.035, len(group)), group["sliced_wasserstein"], alpha=0.75, s=26, label=model, ) axes[row, 0].axhline(observed_att, color="black", lw=1, ls="--") axes[row, 0].set_title(f"{design}: ATT") axes[row, 0].set_ylabel("ATT, dollars") axes[row, 1].set_title(f"{design}: sliced Wasserstein") axes[row, 1].set_ylabel("distance")for ax in axes[row, :]: ax.set_xticks([0, 1]) ax.set_xticklabels(["WGAN", "DDPM"])plt.show()```### Treatment-Share AblationThe observational CPS target has only 185 treated observations out of 16,177 rows. That imbalance is part of the empirical problem. A learned conditional generator lets us change this feature while keeping the fitted conditional row distributions fixed. The experimental target is already much more balanced, so the ablation is mainly informative for the observational design.The ablation below draws a balanced sample with 500 treated and 500 controls from each fitted generator. It does not estimate a new population parameter. It asks how the regression-adjustment estimand behaves when the rare-treatment geometry is relaxed while the conditional outcome and covariate distributions are generated by the same fitted model.```{python}#| label: treatment-share-ablationbalanced_context = np.r_[np.ones((500, 1)), np.zeros((500, 1))].astype(np.float32)ablation_rows = []for design, fit in lalonde_fits.items(): ablation_rows.extend([ result_row(design, "WGAN-GP observed share", fit["wgan_fake"]), result_row(design, "DDPM observed share", fit["ddpm_fake"]), result_row( design,"WGAN-GP balanced 500/500", synthetic_from_fit(fit, "wgan", balanced_context), ), result_row( design,"DDPM balanced 500/500", synthetic_from_fit(fit, "ddpm", balanced_context), ), ])ablation_table = pd.DataFrame(ablation_rows)ablation_table.round(4).style.hide(axis="index")``````{python}#| label: treatment-share-plot#| fig-width: 10#| fig-height: 5.4ablation_plot = ablation_table.copy()fig, axes = plt.subplots(len(lalonde_fits),2, figsize=(10, 5.4), sharex="col", constrained_layout=True,)iflen(lalonde_fits) ==1: axes = np.array([axes])for row, (design, group) inenumerate(ablation_plot.groupby("design", sort=False)): group = group.sort_values("sample", ascending=True) y = np.arange(len(group)) axes[row, 0].barh(y, group["att_dollars"]) axes[row, 0].axvline( regression_adjustment_att(lalonde_samples[design])["att_dollars"], color="black", lw=1, ls="--", ) axes[row, 0].set_title(f"{design}: ATT") axes[row, 0].set_xlabel("dollars") axes[row, 1].barh(y, group["se_dollars"]) axes[row, 1].axvline( regression_adjustment_att(lalonde_samples[design])["se_dollars"], color="black", lw=1, ls="--", ) axes[row, 1].set_title(f"{design}: SE") axes[row, 1].set_xlabel("dollars")for ax in axes[row, :]: ax.set_yticks(y) ax.set_yticklabels(group["sample"])plt.show()```This is the kind of ablation that learned simulation designs should make routine. The empirical target is used to fit the conditional row distribution; the analyst then changes one design feature and watches the downstream estimator respond. More substantive ablations would alter overlap, tail thickness, panel persistence, market concentration, or the outcome equation. The same implementation pattern applies: hold the fitted conditional generator fixed, change the design object deliberately, and report both distributional diagnostics and estimator consequences.The balanced-sample exercise is a controlled perturbation of the simulation environment rather than an estimate of either original Lalonde estimand. The observed-share synthetic rows keep the empirical treatment share of their source design, while the balanced ablation has a treated share of 50 percent. The SE panel is useful because treatment-share changes mechanically alter precision even when the fitted conditional row distributions are held fixed. Large changes in the ablation table should be interpreted as sensitivity to the simulated design, not as evidence that the target estimand itself changed in the original data.## LimitationsDiffusion models do not remove judgment from simulation design. They move the judgment to preprocessing, conditioning, diagnostics, and ablation design. That is still progress, because those choices are visible and auditable.There are also limits:- a DDPM trained on one empirical dataset cannot invent support that is absent from that dataset;- synthetic draws inherit sampling noise and measurement artifacts from the source data;- postprocessing can hide model failures if it is too aggressive;- small treatment cells remain hard because the conditional distribution is weakly learned;- privacy is not automatic and should not be claimed without a separate privacy analysis.Finally, DDPMs are not always better than WGANs. If the table is small, smooth, and continuous, a WGAN or normalizing flow may be enough. If fast generation is essential, WGANs have a real advantage. If exact likelihood is required, flows are more natural. The evidence here is also limited: experimental and observational Lalonde variants, a few small scikit-learn datasets, a small number of repeated synthetic draws, and no systematic hyperparameter sensitivity. The case for DDPMs is strongest as a practical hypothesis for settings with complicated support, mixed variable types, and estimator-relevant dependence patterns that adversarial training may struggle to preserve.## ConclusionThe AIMM paper made an important methodological point: Monte Carlo studies can be made more credible by learning simulation designs from real applications. WGANs were a reasonable first tool for that job. DDPMs offer a cleaner fitting problem for the same econometric goal. They learn a denoising map across a controlled corruption path, connect directly to score-based density modeling, and appear well suited to conditional row generation and targeted ablation.For econometric practice, the useful payoff is sharper finite-sample evidence: simulation designs that preserve the empirical geometry of the problem being studied, while allowing controlled interventions on the features that make estimators succeed or fail. The right next step is not to declare a universal generator winner, but to evaluate learned simulation designs by both distributional fidelity and estimator-specific stress tests across several empirical environments.## Appendix: Training-Step SweepThe main examples fit each generator once. A useful diagnostic is to repeat the fit for progressively larger training budgets and compare generated samples to both the training rows and held-out rows. If train distances keep falling while test distances flatten or rise, the model is learning the empirical sample more than the underlying row distribution. If both distances improve, additional training is buying distributional fidelity rather than only memorization.The exercise below uses the scikit-learn wine features. This is not an econometric stress test, but it is small enough to make the calculation transparent. Each row is standardized by a `TabularTransformer` fit on the training split. For each method, one model is initialized and then trained incrementally. At 50, 100, 200, 400, 800, 1600, and 3200 steps, the current checkpoint generates one synthetic sample with the same size as the training split and one with the same size as the held-out split. Distances are computed on the original feature scale. The horizontal axis should not be read as exactly equal compute across models: one WGAN step includes critic updates, while one DDPM step is one denoising-loss update. The `seconds` column gives cumulative fitting cost up to the checkpoint.```{python}#| label: appendix-step-sweepstep_frame = sklearn_frame(load_wine, "wine features")step_train, step_test = train_test_split( step_frame, test_size=0.30, random_state=SEED +901,)step_train = step_train.reset_index(drop=True)step_test = step_test.reset_index(drop=True)step_columns =list(step_train.columns)step_transformer = TabularTransformer( column_names=step_columns, nonnegative_columns=step_columns,).fit(step_train)step_train_z = step_transformer.transform(step_train)step_lower, step_upper = step_transformer.transformed_bounds()step_grid = [50, 100, 200, 400, 800, 1600, 3200]def sample_with_seed(seed: int, draw_fn): cpu_state = torch.random.get_rng_state() cuda_state = torch.cuda.get_rng_state_all() if torch.cuda.is_available() elseNone torch.manual_seed(seed)try:return draw_fn()finally: torch.random.set_rng_state(cpu_state)if cuda_state isnotNone: torch.cuda.set_rng_state_all(cuda_state)def checkpoint_metrics( model_name: str, max_steps: int, seconds: float, fake_train_z: np.ndarray, fake_test_z: np.ndarray, seed_offset: int,) -> pd.DataFrame: fake_train = pd.DataFrame( step_transformer.inverse_transform(fake_train_z), columns=step_columns, ) fake_test = pd.DataFrame( step_transformer.inverse_transform(fake_test_z), columns=step_columns, ) rows = []for split, real, fake in [ ("train", step_train, fake_train), ("test", step_test, fake_test), ]: metrics = distribution_metrics( real[step_columns].to_numpy(), fake[step_columns].to_numpy(), n_projections=96, seed=SEED + seed_offset, ) rows.append({"model": model_name,"max_steps": max_steps,"split": split,"seconds": seconds,**metrics, })return pd.DataFrame(rows)def incremental_generator_trace(model_name: str, model, seed_base: int) -> pd.DataFrame: rows = [] current_steps =0 start = time.perf_counter()for checkpoint in step_grid: model.partial_fit(step_train_z, steps=checkpoint - current_steps) current_steps = checkpoint seconds = time.perf_counter() - start rows.append( checkpoint_metrics( model_name, checkpoint, seconds, sample_with_seed( SEED + seed_base + checkpoint,lambda: model.sample(len(step_train)).numpy(), ), sample_with_seed( SEED + seed_base +1000+ checkpoint,lambda: model.sample(len(step_test)).numpy(), ), seed_base + checkpoint, ) )return pd.concat(rows, ignore_index=True)step_ddpm = TabularDiffusion( hidden_dims=(64, 64), n_timesteps=50, batch_size=min(128, len(step_train)), max_steps=50, lr=1e-3, seed=SEED +2000, device=DEVICE,)step_wgan = TabularWGAN( hidden_dims=(64, 64), critic_hidden_dims=(64, 64), batch_size=min(128, len(step_train)), max_steps=50, critic_steps=2, lr=1e-4, optimizer="optimistic_adam", gp_weight=5.0, lower_bounds=step_lower, upper_bounds=step_upper, seed=SEED +3000, device=DEVICE,)step_sweep_table = pd.concat( [ incremental_generator_trace("DDPM", step_ddpm, 2000), incremental_generator_trace("WGAN-GP", step_wgan, 3000), ], ignore_index=True,)step_sweep_table[["model","max_steps","split","seconds","marginal_w1_mean","marginal_ks_mean","mean_l2","corr_frobenius","sliced_wasserstein",]].round(4).style.hide(axis="index")``````{python}#| label: appendix-step-sweep-plot#| fig-width: 10#| fig-height: 5.4fig, axes = plt.subplots(2, 2, figsize=(10, 5.4), sharex=True, constrained_layout=True)plot_metrics = [ ("sliced_wasserstein", "sliced Wasserstein"), ("marginal_w1_mean", "mean marginal Wasserstein"), ("marginal_ks_mean", "mean marginal KS"), ("corr_frobenius", "correlation Frobenius"),]styles = { ("DDPM", "train"): {"color": "tab:blue", "linestyle": "-", "marker": "o"}, ("DDPM", "test"): {"color": "tab:blue", "linestyle": "--", "marker": "o"}, ("WGAN-GP", "train"): {"color": "tab:orange", "linestyle": "-", "marker": "s"}, ("WGAN-GP", "test"): {"color": "tab:orange", "linestyle": "--", "marker": "s"},}for ax, (metric, title) inzip(axes.ravel(), plot_metrics):for (model, split), group in step_sweep_table.groupby(["model", "split"], sort=False): ax.plot( group["max_steps"], group[metric], label=f"{model}{split}",**styles[(model, split)], ) ax.set_xscale("log", base=2) ax.set_title(title) ax.set_xlabel("training steps") ax.set_ylabel("distance")axes[0, 0].legend(frameon=False)plt.show()```The train/test split in the plot is the important part. A single in-sample distance can make extra training look mechanically attractive. The held-out curve asks whether those extra optimization steps produce synthetic rows that resemble data not used in fitting. The WGAN and DDPM curves also separate two issues: distributional fidelity as optimization proceeds, and stability of the optimization problem itself. In this run, the useful diagnostic is not monotonicity at every point. The useful diagnostic is whether the test curve tracks the training curve closely or whether it begins to separate as the number of gradient steps increases.## References