Skip to content

How plotsim works

A short mental model: every value in every table can be traced to one trajectory position. Once that clicks, the rest of the system follows.

The trajectory-first contract

For every entity at every time period:

  1. The entity's archetype defines a master curve over the time window.
  2. The trajectory engine evaluates the curve → a position in [0, 1].
  3. Every metric for that entity at that period is derived from that position:
    • Positive-polarity metrics (engagement, revenue): high position → high value.
    • Negative-polarity metrics (churn risk, support tickets): high position → low value.
  4. The distribution (lognorm, beta, poisson, normal, gamma, weibull) shapes the value around the center implied by the position.
  5. Correlations are applied across metrics via a Cholesky copula at the same position.
  6. Noise is the last step.

This means if engagement drops for one entity at period 14, MRR also drops, support tickets rise, and churn risk rises — not because the engine post-hoc forced a correlation, but because they all read from the same trajectory position that dropped at period 14.

The three layers

Plotsim runs in three layers, in this order:

Layer 1 — Trajectories. One position curve per entity, computed before any metric values exist. The curve is the entity's archetype's master curve, modulated by per-entity overrides like inflection_month and (in the builder) arrival distributions that govern when each entity actually starts.

Layer 2 — Metrics. For each (entity, period) cell, the engine samples a value from the metric's distribution centered at the trajectory position. Causal lag is applied here (a metric reads its driver's effective position from T - delay periods earlier), seasonality multiplies the center, and correlations couple metric draws via a shared Gaussian copula.

Layer 3 — Tables. Dimensions are built first (no behavioral data), then facts (one row per entity-period for the main grain), then events (variable row count driven by metric values or threshold crossings). Every fact and event column either reads a metric, references a dim, or emits text/keys via a deterministic source.

Builder versus engine

Two layers of abstraction sit between the user and the generation engine:

  • Builder — the public API. create(**kwargs) and create_from_yaml(path) accept plain-language input (segments, metrics with type and polarity, archetype words like growth and decline) and translate it into a fully-specified engine config. This is the supported surface.
  • Engine — the lower-level config the builder produces. It carries every numeric knob the engine actually consumes. Most callers don't touch it directly; for typed annotations or model_copy mutations, import the public types from plotsim.types. Everything in these docs uses the builder.

Both surfaces produce the same output for the same input. The builder exists so that "one growth segment, one decline segment" is two lines of YAML rather than fifty.

Determinism

Same seed in, same output out. Always.

  • seed is the master. The builder draws one from secrets if you don't set it; the chosen seed is recorded in the manifest so you can reproduce.
  • Arrival distributions, treatment assignments, faker draws, noise, outliers, MCAR, and quality-injection draws all flow through RNG streams seeded from the master seed.
  • Treatment assignment is salted independently from arrival draws, so changing how entities arrive does not shift which entities land in the treatment arm.

Two generation modes (serial and vectorized) consume RNG in different orders and produce statistically equivalent but not byte-identical output. The builder defaults to auto — vectorized when the largest single-archetype entity group reaches fifty entities, serial below that.

What you get on disk

A write_tables call (or plotsim run) writes:

  • One file per table in the chosen format (csv by default, plus parquet, jsonl, sql).
  • config.yaml — the resolved engine config, suitable for re-running.
  • validation_report.txt — every check the engine ran on the output.
  • manifest.json — ground-truth records: archetype assignments, trajectory samples, event firings, SCD events, treatment cohorts, correlation adjustments, quality injections, holdout cutoff, seasonal decomposition, regression pairs, variance partitions, and per-archetype GP kernel fits.

The manifest is the answer key. If you injected outliers, the outlier_injections section names them. If you ran an A/B test, the treatment_cohorts section names who was in which arm. If you set a holdout split, the holdout section pins the cutoff period.

Where to go next