Working with Digital Twins
This guide covers the full twin lifecycle: inspecting a discovered graph, encoding domain knowledge, training, asking a trained twin every question the platform can answer, sampling raw draws, and moving trained twins between environments. Outputs shown are real transcripts.
The causal graph
Discovery returns a Graph. Its edges are a DataFrame, and the adjacency matrix comes labelled:
>>> graph = rc.discover(df)
>>> graph.edges
cause effect strength fixed
0 leads revenue 0.957978 False
1 marketing_spend leads 0.864212 False
2 seasonality leads 0.373061 False
>>> graph.adjacency()
marketing_spend seasonality leads revenue
marketing_spend 0.0 0.0 0.864212 0.000000
seasonality 0.0 0.0 0.373061 0.000000
leads 0.0 0.0 0.000000 0.957978
revenue 0.0 0.0 0.000000 0.000000adjacency also takes values="sign" or values="bool", and to_numpy() and to_networkx() convert onward (networkx needs pip install "rootcause-sdk[graph]").
The distribution is
rootcause-sdk, notrootcause— only the import name isrootcause, andpip install "rootcause[graph]"reaches an unrelated PyPI project.
Domain knowledge
Encode what you know with two verbs. pin fixes an edge as present, forbid fixes it as absent, and both write into the version's fixed subgraph that discovery and training honour:
Training
train blocks until the model is fitted. Calling it on an already trained version returns the twin unchanged with a note: the platform retrains through new versions, not by re-fitting in place.
Retraining is two verbs. new_version derives a fresh, untrained version — configuration and causal graph inherited from the base, every training output reset — and retrain is new_version plus train in one call:
Version numbers never collide: the bumped component skips past any label already taken. To rebuild a direct-mode model from scratch (after an engine fix, or a corrupt artifact), rc.discover(df, force=True) remains the recovery path.
In platform mode you rarely train at all; a twin someone trained in the UI is ready to query:
Keeping a trained model current
New rows landing in the twin's backing source do not require a retrain. update() folds them into the trained model incrementally — seconds, not minutes — and reports what happened rather than failing:
The three statuses are the contract: committed (new rows folded in), up_to_date (nothing new since the last update), and 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. twin.update_eligibility answers the same question read-only, so an orchestrator can decide without starting a job. The full monthly-refresh pattern, including the Airflow shape, is in Temporal and Panel Twins.
Asking a trained twin a question
Every simulation family the platform's New Simulation wizard offers has a verb here, and each one blocks until the run completes. Which verb a twin accepts depends on what kind of twin it is, and the SDK refuses the wrong one before submitting anything rather than letting the platform answer with a 422:
What will this specific case do?
predict
static, multi-environment static
What happens over time?
forecast
temporal, multi-environment temporal
What if we change X?
intervene
every kind
Why does this happen?
explain
every kind
What should we change?
optimise
every kind
How do I reach a goal?
score
static
Why is this variable broken?
root_cause
every kind
Is anything broken at all?
anomalies
every kind
intervene, forecast and score have sections of their own further down; the rest are covered here.
Prediction
predict answers for the rows you hand it: one prediction per input record, with an uncertainty interval around each. Leave the target columns out of the input: those are what the model answers with.
The row column is the position of the input record each prediction answers for, so the frame joins straight back onto the one you asked about. A second target adds a variable column instead of dropping a series, and confidence= sets the interval width.
Targets are inferred from the version's variable roles when you leave targets off. Prediction reads one row at a time, so it is a static-twin verb: a temporal twin projects forward with forecast instead, and says so rather than guessing.
Explanation
explain asks the model why, and the mode follows from what you name, so you rarely pass mode yourself:
The result carries a ranked driver list with effect sizes, confidence intervals, dose-response curves for numeric causes, and the split between direct and indirect pathways. Panel twins take environments= to narrow which environments are explained.
Optimization
optimise searches for the actions that best move your objectives. Objectives are measured by SQL over the sampled frame, which is registered as df, data, and dataset; decision_vars is the set of levers the optimizer is allowed to touch:
rc.objective takes either spelling of maximise/minimise, plus unit= and weight= for trading several objectives off against each other. Add variable_constraints= to bound how far a lever may move, metric_constraints= for guardrails every plan must respect, and max_changes= to cap how many variables one plan may touch.
A temporal twin optimizes over a horizon and needs horizon=; a static one optimizes a single period and refuses it. Panel twins take environments=.
A categorical outcome has to be counted, not averaged.
SELECT AVG("Churn")over a text column is not a number, and the run fails inside the engine rather than at submission. Count the category you care about withCASE WHEN, as above.
Diagnosis
Two verbs, and which one you want depends on whether you already know what is wrong. root_cause traces one named variable upstream to what actually broke it:
anomalies scans every variable instead, and diagnoses whatever it flags:
Both take target_fpr= to set detection sensitivity as a false-positive rate (lower flags less), and both need the observations you want diagnosed: there is no scanning the training data by default. On a temporal twin root_cause takes a timestep= to diagnose and anomalies takes a start_step/end_step window.
Panel twins 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:
When there is no verb for it
ask runs the platform's own scenario generator over a plain-English question and executes whatever it produces, which reaches the families that have no dedicated verb yet:
result.scenario is what the translator built, so it doubles as the way to discover a scenario shape you then send yourself.
Batch scoring
Point the trained model at rows and ask what it would take to change each one's outcome. For every row, the counterfactual engine finds the smallest set of changes that reaches the target — a risk register with an action column:
Static trained twins only; a non-variable column (like customer above) becomes the row label. max_changes= caps how much each counterfactual may touch, and constraints= locks variables the business cannot move.
Sweeps
Instead of pinning a variable to one value, sweep it across a grid with rc.range and read the full dose-response curve back:
Each point also carries effectStd and a confidenceInterval.
One rc.range per scenario; every other intervention in it is pinned, so the curve reads as the effect of that one dial in a fixed context.
Raw sampling
Every simulation the platform offers is built on conditional sampling from the fitted model. The SDK exposes that primitive directly, so you can compute your own estimands instead of waiting for a packaged analysis:
Apply interventions before sampling with do=, and compare against baseline:
The 20 percent push propagates through the chain the graph discovered: marketing lifts leads, leads lift revenue, and seasonality is untouched because nothing points at it.
Note: seeds are reproducible across every twin family. Panel twins sample each environment independently and derive stable per-environment child seeds from your seed, so backtest comparisons are deterministic. Pass environments=["uk", "france"] to narrow a panel twin; the returned frame gains an environment column.
Intervention values
A bare value means "set to exactly this". The constructors cover the rest:
rc.set(120)
set the variable to 120
rc.pct(+15)
relative change of +15 percent
rc.add(-5)
relative change of -5 units
rc.prob("yes", 0.8)
set a category's probability to 0.8
rc.adjust_prob("yes", +10)
shift a category's probability by 10 percentage points
rc.members(include=["Alice"], size=4)
set-valued column membership
Conditions scope any intervention to a subpopulation: where={"region": "EMEA"} for equality, or where={"income": ("<", 5000)} with any of == != > < >= <=.
Interventions with metrics
intervene runs the full simulation machinery server side and blocks for the result. Interventions measure their effect through metrics; the simplest form names outcome columns and gets mean-of-column metrics:
Full control uses SQL metrics over the sampled frame, which is registered under the table names df, data, and dataset:
Calling intervene with neither outcomes nor metrics raises immediately with guidance, before any job is submitted:
Forecasts
Temporal and panel-temporal twins forecast. Target variables are inferred from the version's variable roles when unambiguous, or passed explicitly. Temporal and Panel Twins covers forecasting in depth, including attribution and backtest anchoring:
Portable twins
Twin exports carry the trained model parameters, so a .rctwin file round-trips to a runnable model:
Compute always stays on the platform; the file makes the model portable between environments, not the algorithms.
Cleaning up
twin.delete() removes a twin permanently — fitted models, every version, the run history, and the record itself; running workflows are cancelled first. Sources and datasets answer the same verb. There is no undo, which makes the iterate-and-discard loop explicit:
Deletion needs the matching scope on your key (digital-twins:delete, sources:delete, datasets:delete).
Next steps
Temporal and Panel Twins: time series, environments, scheduled interventions, forecast attribution
Last updated

