For the complete documentation index, see llms.txt. This page is also available as Markdown.

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.000000

adjacency 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, not rootcause — only the import name is rootcause, and pip 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:

Question
Verb
Twin kinds

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 with CASE 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:

Constructor
Meaning

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

Last updated