Skip to content

SaaS

A B2B SaaS customer-success and revenue warehouse. Companies subscribe, onboard users, engage with the product, generate revenue, open support tickets, and eventually churn or expand. Plan tier is tracked with SCD2 versioning; revenue gets CDC audit columns; MRR carries heteroscedastic noise so large accounts vary more than small ones.

What it produces

Table Type Grain Rows (default) Notes
dim_date dim per_period 24 24 monthly periods, Jan 2023 – Dec 2024
dim_company dim per_entity (+ SCD2) 100–400 100 companies, expanded by SCD2 plan-tier moves
dim_user dim per_entity (sub-entity) 400 4 users per company
dim_support_category dim per_reference 8 8 ticket categories
fct_engagement fact per_entity_per_period 2,400 engagement, feature_adoption, active_seats
fct_revenue fact per_entity_per_period 2,400 MRR, expansion, discount_pct (CDC audit)
fct_support fact per_entity_per_period 2,400 tickets, NPS, churn_risk (FK to category)
evt_login event variable (proportional) ~24,000 scaled 10× engagement
evt_churn event variable (threshold) 15–30 churn_risk above 0.7 for 2 consecutive periods
<fct>_wide fact denormalized companion one per fact (FK'd dims left-joined)

100 entities across six segments: rapid_adopter (18), steady_grower (24), enterprise_steady (14), churning (18), expansion_play (12), at_risk (14). Approximate total row count: 40,000 (excluding wide companions).

Features exercised

  • Seasonality — Q4 budget flush (Nov–Dec, strength +0.30) and summer slowdown (Jun–Aug, strength -0.12).
  • Causal lagexpansion follows engagement with delay 2.
  • Correlationsmrr driven_by engagement, expansion related feature_adoption, churn_risk opposes engagement, plus explicit support_tickets -0.40 engagement and nps 0.55 engagement.
  • Multi-phase archetypesflat > decline @ 10, flat > growth @ 8, growth > spike_then_crash > flat @ 6 @ 12 as cohort stories.
  • Per-segment baselineshigh / mid / low per metric so enterprise_steady and churning sit in different bands of the same range.
  • Lifecycle stagestrial / active / engaged / power_user thresholds on engagement.
  • SCD2plan_tier on dim_company versioned by MRR thresholds (free / starter / growth / enterprise at 0.25, 0.55, 0.80).
  • Sub-entity dimdim_user with count: 4 (four users per company).
  • Reference dimdim_support_category with eight static categories.
  • CDCfct_revenue carries _inserted_at, _updated_at, _op audit columns.
  • Heteroscedastic noisescale_with_trajectory: true so large trajectories receive proportionally larger gaussian jitter.
  • Denormalized output<fct>_wide companion alongside each normalized fact.
  • Quality injection — null injection on engagement (3%), duplicate rows on logins (1.5%), late arrivals on revenue (2%).
  • Events — one proportional (evt_login) and one threshold (evt_churn).

Use it

from plotsim import load_template, generate_tables, write_tables

config = load_template("saas")
tables = generate_tables(config)
write_tables(tables, config, output_dir="./output")
plotsim template saas -o saas.yaml
plotsim run saas.yaml -o ./output

Common customizations

Switch off heteroscedastic noise

Default behavior scales gaussian jitter with each entity's trajectory position. Disable for uniform noise across the population:

from plotsim import load_template
from plotsim.types import NoiseConfig

config = load_template("saas")
config = config.model_copy(update={
    "noise": NoiseConfig(gaussian_sigma=0.04, outlier_rate=0.005, mcar_rate=0.0),
})
# In saas.yaml, replace the noise block:
noise:
  gaussian_sigma: 0.04
  outlier_rate:   0.005
  mcar_rate:      0.0
  # scale_with_trajectory: omitted, defaults to false

Add an A/B treatment on one segment

Carve steady_grower into a 50/50 treatment / control split:

# Re-author with create() instead of load_template — segments are immutable on the loaded config.
from plotsim import create

config = create(
    # ... same other fields ...
    segments=[
        # ... other segments unchanged ...
        {
            "name":      "steady_grower",
            "count":     24,
            "archetype": "accelerating",
            "treatment": {
                "fraction":      0.5,
                "lift_log_odds": 0.40,
                "start_period":  6,
                "target_metric": "expansion",
            },
        },
    ],
)
# In saas.yaml, add to the steady_grower segment:
segments:
  - name: steady_grower
    count: 24
    archetype: accelerating
    treatment:
      fraction:      0.5
      lift_log_odds: 0.40
      start_period:  6
      target_metric: expansion

Switch output to partitioned Parquet

Write per-year partitions for tables that carry the year column:

from plotsim.types import OutputConfig

config = config.model_copy(update={
    "output": OutputConfig(format="parquet", directory="./output", partition_by="year"),
})
output:
  format:       parquet
  directory:    ./output
  partition_by: year

Where it shines

  • Customer-success dashboards — fct_engagement joined to fct_support and fct_revenue reproduces the "engagement-up-MRR-up-tickets-down" story.
  • Churn-prediction training data — evt_churn as the label, all other facts as features. Use the manifest's treatment_cohorts section as the answer key when you add an A/B split.
  • Revenue-restatement pipelines — the CDC _op flips to "U" for any quality-corrupted row on fct_revenue.