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:
- The entity's archetype defines a master curve over the time window.
- The trajectory engine evaluates the curve → a position in
[0, 1]. - 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.
- The distribution (lognorm, beta, poisson, normal, gamma, weibull) shapes the value around the center implied by the position.
- Correlations are applied across metrics via a Cholesky copula at the same position.
- 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)andcreate_from_yaml(path)accept plain-language input (segments, metrics with type and polarity, archetype words likegrowthanddecline) 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_copymutations, import the public types fromplotsim.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.
seedis the master. The builder draws one fromsecretsif 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 (
csvby default, plusparquet,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¶
- Shaping metrics — choose distributions, baselines, and archetypes.
- Connecting metrics — correlations, causal lag, adstock decay, seasonality, time-varying phases.
- Designing tables — when auto-schema isn't enough.
- Running experiments — treatment/control, holdout splits, entity features.