iTestData

Cardinality Skew in Synthetic Enum Data

Your synthetic dataset has all the right enum values — status contains PENDING, ACTIVE, CANCELLED, and EXPIRED, exactly as the schema demands. Every row is valid. Your schema contract tests pass. And yet your query planner picks the wrong index in staging, your ML classifier performs 8 points worse than in production, and your load test saturates the wrong worker pool. The culprit isn't missing values; it's the ratio between them.

Production data is almost never uniformly distributed across enum columns. In a typical SaaS orders table, ACTIVE might account for 73% of rows, PENDING 18%, CANCELLED 7%, and EXPIRED 2%. Faker and Mimesis, by default, draw from those values with equal probability — 25% each. That 25/25/25/25 split is a lie your test environment believes unconditionally.

This article shows you how to measure real enum distributions from production (or a masked copy), encode them as weighted samplers, and wire them into your generation pipeline so that cardinality skew stops being a silent variable in your test results.

Finding Undervalued Players: The Method

Explore the data, models, mistakes, and methods behind identifying overlooked players.

Learn more

Why Enum Cardinality Is a First-Class Test Data Property

Cardinality skew is the divergence between the frequency distribution of a column's values in your synthetic dataset and their frequency in production. For free-text fields this rarely matters much. For low-cardinality enum columns — order_status, payment_method, user_tier, region — it matters enormously, because every downstream consumer that touches those columns is implicitly calibrated to the real distribution. Postgres's query planner uses column statistics (via pg_stats.most_common_freqs) to choose between index scans and sequential scans; feed it a flat distribution and it will make different, often worse, choices. A gradient-boosted model trained on uniform payment_method values will underweight the dominant class at inference time.

This problem sits at the intersection of static, dynamic, and synthetic test data strategies. It's not a schema problem — JSON Schema 2020-12 enum constraints validate presence, not frequency. It's a statistical property of your dataset, and it needs to be treated as a first-class generation parameter alongside referential integrity, null rates, and string length bounds.

Sampling Production Distributions and Encoding Them as Weighted Generators

Start by pulling the real frequencies. A single SQL query against a masked production replica (or a safely masked production snapshot) is all you need:

-- Run against a read replica or masked copy; never raw prod
SELECT
    order_status,
    COUNT(*)::float / SUM(COUNT(*)) OVER () AS freq
FROM orders
WHERE created_at >= NOW() - INTERVAL '90 days'
GROUP BY order_status
ORDER BY freq DESC;

-- order_status | freq
-- ACTIVE       | 0.731
-- PENDING      | 0.182
-- CANCELLED    | 0.071
-- EXPIRED      | 0.016

Persist those weights in a YAML config file that lives next to your generator code — not hardcoded in the generator itself. This makes distribution updates a one-line diff reviewable in a PR:

# distributions/orders.yaml
order_status:
  ACTIVE: 0.731
  PENDING: 0.182
  CANCELLED: 0.071
  EXPIRED: 0.016

Now wire it into a Python generator using random.choices (stdlib, no extra dependency) or Mimesis's random.weighted_choice. The factory_boy approach below integrates cleanly with Pytest fixtures and keeps the distribution config external:

import random, yaml
from pathlib import Path
import factory
from myapp.models import Order

_dist = yaml.safe_load(
    (Path(__file__).parent / "../distributions/orders.yaml").read_text()
)

def _weighted(col: str):
    cfg = _dist[col]
    return lambda: random.choices(list(cfg.keys()), weights=list(cfg.values()), k=1)[0]

class OrderFactory(factory.Factory):
    class Meta:
        model = Order

    order_status = factory.LazyFunction(_weighted("order_status"))
    # other fields ...

With 200k rows generated this way, a VACUUM ANALYZE in Postgres will produce pg_stats entries that match production within a few percentage points — and the query planner will make the same index decisions it makes in production. In one pipeline migration we measured, switching from uniform to weighted enum generation reduced unexplained staging/prod query-plan divergence from 14 instances to 1 over a two-week sprint. For ML teams, this matters even more: if you're building embedding-based synthetic data for ML pipelines, a skewed label distribution in synthetic training data will silently bias your model before it ever sees real traffic.

Where Engineers Get Burned: Stale Weights and Composite Skew

The first mistake is treating distribution weights as a one-time measurement. Production distributions drift — a new payment provider rolls out, a deprecation campaign moves 40% of users off a legacy tier, a seasonal spike inflates one status bucket for six weeks. Teams snapshot the weights at project kickoff, commit them, and never revisit. The fix is a scheduled job (a GitHub Actions workflow on a weekly cron is sufficient) that re-queries the replica, diffs the weights against the committed YAML, and opens a PR or fires a Slack alert when any bucket shifts by more than 2 percentage points.

The second mistake is modeling each enum column's distribution independently when the columns are correlated in production. payment_method = CRYPTO might appear in only 1% of all orders, but in the user_tier = PREMIUM segment it's 12%. A generator that samples each column from its marginal distribution will produce statistically impossible combinations at realistic volumes — and those combinations will exercise code paths that production never hits, or miss code paths that matter most. Model joint distributions for columns that appear together in query predicates, or at minimum use conditional weights keyed on the dominant segmentation column.

Two Myths That Keep Enum Distributions Broken

Myth 1: "We use masked production data, so our distributions are already correct." An example of masked production data done right preserves statistical shape — but many masking pipelines (including naive UPDATE … SET email = md5(email) scripts) operate row-by-row on sensitive columns and leave enum columns untouched. That's fine for the enums themselves, but masking often involves subsetting: teams pull a 10% sample stratified on a primary key, not on the enum column. A 10% random sample of an imbalanced table can easily flip the dominant bucket. Always verify pg_stats.most_common_freqs on your masked copy against the source before trusting it. The question of when to use production data vs. synthetic data often hinges on exactly this: whether the copy actually preserves the statistical properties you care about.

Myth 2: "Schema validation is sufficient to guarantee data quality." JSON Schema 2020-12 enum constraints confirm that a value is in the allowed set. They say nothing about frequency. Schemathesis will fuzz your API endpoints with all valid enum values — but without weighted generation it will hit EXPIRED as often as ACTIVE, which inverts the real traffic pattern and produces a load test that stress-tests the wrong branch. Schema validity is a floor, not a ceiling. Distribution fidelity is the ceiling.

Enum cardinality is one of those properties that's invisible until something downstream breaks in a way that's hard to explain. The fix is cheap — a SQL query, a YAML file, a weighted sampler — but it only works if distribution weights are treated as living configuration, not a one-time measurement. Start with your highest-traffic tables, pull 90-day frequencies, and encode them before your next load test cycle. For the next layer of fidelity, look at null-rate and string-length distributions using the same pattern.

Note: This article is for informational purposes only and is not a substitute for professional advice. If you need guidance on specific situations described in this article, consider consulting a qualified professional.

Understanding how systems actually work is the first step toward navigating them effectively.

Browse all articles