Temporal and Panel Twins
Time series and multi-environment data get their own twin kinds with their own machinery: lagged dependencies, latent influence detection, one model per environment, forecasts that explain themselves, and interventions scheduled in time. This guide runs a panel end to end; outputs shown are real transcripts.
temporal
time=
one time series with lagged causal structure
multi-environment-static
entity=
the same system observed across environments
multi-environment-temporal
time= + entity=
a panel: many environments, each a time series
Everything from Working with Digital Twins applies unchanged; this page covers what these kinds add, including the names the other simulation families take here and the two arguments that only exist because there is a time axis.
A panel in long format
One row per store per month. time= names the timestamp column, entity= the environment column:
>>> panel.head()
month store price demand revenue
0 2024-01-01 london 21.70 68.4 152.0
1 2024-02-01 london 21.74 50.2 107.6
2 2024-03-01 london 22.06 44.8 101.2
3 2024-04-01 london 20.16 54.6 109.7
4 2024-05-01 london 20.43 50.8 102.7
>>> graph = rc.discover(panel, time="month", entity="store")
>>> graph.edges
cause effect strength fixed
0 Unknown influence 1 demand 1.000000 False
1 Unknown influence 1 revenue 1.000000 False
2 demand revenue 0.964863 False
3 month price 0.842668 False
4 month demand 0.716051 False
5 month revenue 0.652976 False
6 store revenue 1.000000 FalseThree things in this graph do not exist for static twins:
A latent influence.
Unknown influence 1is a hidden common cause the engine detected in the data but could not name. It is real model structure, not a column; it cannot be intervened on directly.Time as a cause. The
monthedges carry trend and seasonality into the variables they touch.The environment as a cause. The
storeedge says the environments genuinely differ, beyond what the other variables explain.
One model per environment
Panel twins hold a model per environment. Sampling narrows with environments=, the returned frame carries an environment column, and a seed derives stable per-environment child seeds, so comparisons are deterministic:
The environments really are heterogeneous: Berlin runs at more than double London's demand under the same prices, exactly the per-store scale the store -> revenue edge announced.
Forecasts that explain themselves
forecast runs per environment. environments= narrows which, aggregate= ("sum", "avg", "min", "max") adds a combined series, and origin_timestamp (ms epoch) anchors the start, which is how a backtest aligns a forecast against months the twin never saw:
Every step carries an attribution: how much of the prediction is trend, season, and each causal parent, with lags named:
That demand_lag1 entry is the lagged dependency discovery found: last month's demand carrying into this month's revenue.
Interventions scheduled in time
Temporal and panel interventions happen at moments, not in the abstract. rc.at wraps any intervention value with when it applies and for how long:
rc.at(spec, persistent=True)
applies from the first step onwards
rc.at(spec, duration_steps=6)
applies for six steps, then reverts
rc.at(spec, timestamp=1782864000000)
starts at a specific moment (ms epoch)
Without rc.at, an intervention on a temporal twin is persistent: it applies from the first step onwards, indefinitely. With it, you express ramps, windows, and permanent policy changes. Everything composes with where= conditions and the metric machinery from Working with Digital Twins.
Working with a subset of environments
twin.env(...) pins a handle to some of the panel's environments. Its graph re-aggregates the causal adjacency over just those environments — edges carry agreementRate, the share of the subset's environments in which discovery found the relationship — and every simulation on the handle is scoped automatically:
Note what's gone next to the full graph above: the store -> revenue edge. Within a two-store slice there is less environment-driven variation to explain — the subset's adjacency is genuinely different structure, not a filter on the full graph. adjacency(agreement_threshold=...) turns the edge-survival knob (default 0.5), and combos() shows the exact environments the handle resolved to.
Simulations on the handle run only in the subset — same verbs, pre-scoped. A price cut in London and Berlin, leaving Paris untouched:
Over REST this is POST .../versions/{vId}/graph/slice — the subset can also be defined by column values or per-environment stat filters, not just exact combos.
Saving a subset as an environment group
A handle from twin.env(...) lives as long as the Python object does. save() puts it on the twin as a named environment group, which lives as long as the twin does:
What gets stored is the rule, not the answer. Naming environments stores the exact combos; a where= subset stores the filter, so it re-selects as the data moves — "high revenue" next quarter means whichever stores are high-revenue then, not the ones that were today:
Groups belong to the twin, not to a version, so they survive retraining, and they are the same groups the platform's environment picker lists — a group saved from a notebook is in the dropdown by the time you switch tabs. Next session it comes back by name:
A group is an EnvSubset with a memory: the same graph, environments, sample, intervene and forecast, scoped to what the group means on this version right now. Membership is resolved server-side on first use and cached on the handle, so eu.graph and eu.forecast(...) agree with each other.
Simulations differ in one way that matters. A twin.env(...) handle expands to a list of environment names and sends those; a group is sent by id, and the platform freezes what it resolved onto the run:
That snapshot is the run's provenance: it says which group the run covered and what the group meant at submit time. Editing or deleting the group afterwards never rewrites it, and droppedEnvKeys names members this version could not honour.
Edits are twin-level, so they apply to every version at once. update() takes the same vocabulary as env():
Because the rule outlives the data it was written against, a group can stop fitting. Two outcomes, and they are not the same thing: a group that resolves cleanly and matches nothing is empty — the rule is fine, the environments moved on — while a group naming a column this version does not have raises with the reason it cannot be evaluated at all.
The other families, on a temporal or panel twin
Forecasting and scheduled interventions are what these kinds are usually reached for, but the rest of the simulation families work here too, under names of their own. The SDK picks the name from the twin's kind, so the call is the same one you would write against a static twin:
predict
prediction
not available
prediction
not available
forecast
not available
forecast
not available
panel_forecast
explain
explanation
temporal_explanation
panel_explanation
panel_explanation
optimise
optimisation
temporal_optimisation
panel_optimisation
panel_optimisation
root_cause
root_cause_analysis
temporal_root_cause_analysis
static_panel_root_cause_analysis
panel_root_cause_analysis
anomalies
anomaly_detection
temporal_anomaly_detection
static_panel_anomaly_detection
panel_anomaly_detection
Prediction is the one that does not carry over. It answers for a row of inputs, which a series does not have; a temporal twin projects forward with forecast instead, and says so rather than guessing:
A multi-environment static panel is the exception: it has rows, so it predicts, and the scenario is plain prediction with no panel variant.
Three arguments only exist because there is a time axis, and passing one to a static twin is refused rather than dropped:
horizon is required for a temporal optimization, which plans across steps rather than picking one setting; it is optional on a panel and refused on a static twin.
Diagnosing environments
root_cause and anomalies need the observations you want diagnosed. On a panel twin you can either share one set of rows across every environment, by passing a flat list, or give each environment its own by passing a mapping:
The mapping becomes panelSamples on the scenario, keyed by environment. A flat list becomes samples, which the engine shares across the environments in scope.
Every family is scoped by a subset or a group
The environment handles from the two sections above carry the whole verb set, not just sample, intervene and forecast. A subset pins the environments by name; a saved group is sent by id, so the platform still freezes onto the run exactly what the group resolved to:
Monthly refresh: assimilate instead of retrain
When next month's rows arrive, the model doesn't need rebuilding. Extend the twin's source with the new rows and fold them into the fitted model with update() — seconds, not a training run. It finishes with a status, never an error: committed (rows folded in), up_to_date (nothing new), or retrain_required (the model can't take these rows incrementally — result.reasons says why; call twin.retrain()).
Static and temporal twins assimilate out of the box. Among panel twins, only multi-environment-temporal ones can assimilate, and only when they opt into the v2 panel engine in the twin builder; a multi-environment-static twin has no v2 option and always needs a retrain. London's series as its own temporal twin:
Running update() again with nothing new in the source is how a scheduled job stays honest — the second call is a cheap no-op:
The refreshed model forecasts onwards from the assimilated months — note the timestamps start after the two new rows, not before them:
The production shape of this loop is a monthly job: sync or extend the source, call twin.update(), branch on the status — retrain_required triggers twin.retrain() instead of a page at 3am. In Airflow, that's three tasks; pass webhook_url= to update() if you'd rather be poked than poll, and check twin.update_eligibility first when you want the decision without starting a job.
Run it yourself
The transcript above is the temporal-panel example notebook, end to end:
Next steps
Interactive Apps in Notebooks: the twin console works on panel twins too
Python API Reference: full signatures for every verb on
Twin,EnvSubsetandGroup
Last updated

