TCPFN — MCP Tool Reference
Temporal Causal Foundation Model. These tools are exposed over the MCP streamable-HTTP protocol — they are not REST endpoints. All actions are advisory; no autonomous actuation.
MCP · streamable-HTTP · 9 toolsQuickstart — Hello World
A five-minute first run that answers a real causal question end-to-end, using a small sample dataset we ship with the server. No data of your own required — just your connector URL and API key. By the end you will have watched TCPFN recover a causal structure it was never told, and name the one lever that actually moves the number you care about.
explanation and diagnostics block for what it actually returned.
The sample dataset
A factory's hourly utility log — download it from
/api/sample.csv
(799 rows, 5 columns). We generated it with a known causal
structure so you can check TCPFN's answers against the truth. TCPFN is
not told any of this — it infers it from the numbers alone:
| Planted cause → effect (lag 1) | Meaning | What TCPFN should conclude |
|---|---|---|
heater_power → room_temp |
A running heater warms the room a beat later. |
A clear edge. |
heater_power → power_bill |
The heater shows up on the next hour's bill — a real lever, but the smaller one. |
An edge; moderate effect. |
fan_speed → power_bill |
The fan drives the bill about twice as hard as the heater. |
An edge; the largest effect — the counterintuitive answer. |
random_noise → (nothing) |
A meter that records pure noise, correlated with nothing. |
No edge. The decoy TCPFN must ignore. |
The lesson to watch for: you would guess the heater is the biggest driver of the power bill — but the data fingers the fan. That gap between intuition and evidence is exactly what these tools exist to close.
Set up (once)
Grab the file and export your connector URL + API key (both are in your onboarding email). Every step below is a plain REST call — if you use a Claude / ChatGPT connector instead, skip the cURL and just ask in natural language (the italic prompt under each step).
export BASE_URL="https://YOUR-SERVICE-URL" # your connector URL
export KEY="<YOUR_API_KEY>" # from your onboarding email
# download the sample dataset (799 rows)
curl -s -o utility_bill.csv "$BASE_URL/api/sample.csv"The cURL steps attach utility_bill.csv as a file upload
(multipart/form-data): the CSV is the file part and the
tool arguments are a JSON string in params.
Step 1 · Is the data usable?
check_data — cheap, no model inference. Always the sane first call:
it confirms the dataset clears the statistical floor before you spend a real
inference on it.
Connector prompt: "Use tcpfn check_data on
$BASE_URL/api/sample.csv with target power_bill."
curl -X POST "$BASE_URL/api/tools/check_data" \
-H "Authorization: Bearer $KEY" \
-F file=@utility_bill.csv \
-F 'params={"target":"power_bill"}'What you should see: n_samples: 799,
n_variables: 5, and suitable_for_full_graph_discovery: true,
with empty prior_coverage, warnings and
collinear_pairs — a clean bill of health. (On your own data this is where
TCPFN would flag short series, collinear columns, or patterns outside its training
prior before you spend an inference on them.)
Step 2 · What causes what?
discover — the causal graph. This is the "aha": TCPFN reconstructs
the planted structure from the raw series.
Connector prompt: "Run tcpfn discover on that dataset with target power_bill and explain the graph."
curl -X POST "$BASE_URL/api/tools/discover" \
-H "Authorization: Bearer $KEY" \
-F file=@utility_bill.csv \
-F 'params={"target":"power_bill","threshold":0.1}'What you should see: edges for
heater_power → room_temp,
heater_power → power_bill and
fan_speed → power_bill — and no edge touching
random_noise. That's the whole planted graph recovered, decoy correctly
dropped. Read the explanation field for the plain-language version.
Step 3 · Can I trust an effect estimate?
is_identifiable — the firewall. Before you believe a number, ask
whether the data can even separate cause from confounding.
Connector prompt: "Is the effect of heater_power on power_bill identifiable in this data?"
curl -X POST "$BASE_URL/api/tools/is_identifiable" \
-H "Authorization: Bearer $KEY" \
-F file=@utility_bill.csv \
-F 'params={"cause":"heater_power","effect":"power_bill"}'What you should see: identifiable: true — no
necessary condition is violated, so this is the green light to estimate the effect.
The consistency number alongside it is a heterogeneity
diagnostic, not a confidence score: on this dataset it comes back
negative (the per-unit effects vary more than their mean size),
which does not weaken the verdict.
Step 4 · How big is it?
estimate_effect — the number. Quantifies how much the bill moves per
unit of the heater.
Connector prompt: "Estimate the effect of heater_power on power_bill."
curl -X POST "$BASE_URL/api/tools/estimate_effect" \
-H "Authorization: Bearer $KEY" \
-F file=@utility_bill.csv \
-F 'params={"cause":"heater_power","effect":"power_bill"}'What you should see: a positive ate_mean with
identifiable: true — raising the heater raises the bill. Hold onto this
number; the next step puts it in context.
Step 5 · Which lever should I pull?
rank_interventions — the payoff. Rank the candidates by their predicted
effect on the bill and let the data pick the winner.
Connector prompt: "Rank heater_power, fan_speed and random_noise by their effect on power_bill."
curl -X POST "$BASE_URL/api/tools/rank_interventions" \
-H "Authorization: Bearer $KEY" \
-F file=@utility_bill.csv \
-F 'params={"target":"power_bill","candidates":["heater_power","fan_speed","random_noise"]}'What you should see: fan_speed ranked first
— roughly twice the effect of heater_power (which lands second),
and random_noise at the bottom. Note why the decoy is last: not
because its number is small (it isn't — the ATE of a pure decoy is noise, and here it
lands between the two real levers), but because it comes back
identifiable: false, and the ranking puts every unidentifiable candidate
below every identifiable one. That's the whole point: the intuitive lever (the heater)
is real but not the biggest, the surprising one (the fan) is, and the decoy is dismissed
on grounds that a big number can't override. You just tested every core capability
against a known answer.
The five steps above answer "what causes what" and "what should I change." The remaining tool,
what_should_i_measure, answers a different question —
what is blocking a confident answer: which variable to log or intervene on
next to resolve a causal ambiguity. See its entry in the reference below. (It is
deliberately left out of this scripted first run while its uncertainty head is being
recalibrated, so this walkthrough only ever shows results you can trust.)
How to call these tools
TCPFN runs as an MCP server (FastMCP, streamable-HTTP).
A client opens a session and invokes a tool with the JSON-RPC method
tools/call. There is one HTTP endpoint (the MCP mount); the
tools below are selected by params.name.
Base URL & endpoints
On the HTTP (streamable-HTTP) transport the server exposes three paths.
Default local address http://127.0.0.1:8080
(the Docker image binds 0.0.0.0:8080).
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /mcp | required | MCP JSON-RPC endpoint — every tool call goes here. |
GET | /health | public | Liveness check → |
GET | /docs | public | This reference page. |
Authentication
The MCP endpoint /mcp accepts two schemes — pick by client type:
OAuth for interactive MCP clients (Claude.ai / ChatGPT custom
connectors, MCP SDKs), and a static API key for scripts and the
REST API.
OAuth 2.1 — for MCP clients (recommended). The TCPFN server is
its own OAuth 2.1 Authorization & Resource Server, so a self-hosted
deployment needs no external identity provider. A remote MCP client discovers the
endpoints from the server's metadata, runs the standard
authorization-code + PKCE flow, and sends the resulting access token as
Authorization: Bearer <access_token> on every /mcp
request. You don't assemble this by hand — paste the server URL and client
credentials into the connector's "Add custom connector" dialog and it runs the flow
for you (auto-approved: there is no per-user login, so the client secret + PKCE are
the gate).
| What you provide | Where it comes from |
|---|---|
Server URL | Your deployment's public HTTPS URL
( |
Client ID + Client secret | A pair you choose
(e.g. two |
Discovery + flow endpoints (the client calls these for you):
/.well-known/oauth-authorization-server,
/.well-known/oauth-protected-resource, /authorize,
/token. Access tokens are stateless JWTs (~1 h); refresh tokens
~30 days; scope tcpfn. OAuth turns on server-side when
TCPFN_OAUTH_CLIENT_ID, TCPFN_OAUTH_CLIENT_SECRET and
TCPFN_MCP_PUBLIC_URL are all set.
API key — for scripts & REST (dual auth).
The same /mcp (and the REST API below) also accept a static bearer key,
so CLI, cURL, and server-to-server callers work unchanged:
Authorization: Bearer <YOUR_API_KEY>The key is set server-side via TCPFN_MCP_API_KEY
(constant-time compared). If it is unset and OAuth is off, auth is disabled and
a warning is logged — dev only. /health and /docs never
require auth.
Connect with an MCP client (recommended)
These are MCP tools, so the easiest way to call them is an MCP client (Claude Desktop, Cursor, or an MCP SDK) — the client performs the session handshake for you. Point it at the published package:
{
"mcpServers": {
"tcpfn": {
"command": "uvx",
"args": ["--from", "tcpfn", "tcpfn-mcp"],
"env": { "TCPFN_MODEL_HF_REPO": "ProfitOpsAI/tcpfn" }
}
}
}Claude Desktop uses the same shape at
~/Library/Application Support/Claude/claude_desktop_config.json. Then just
ask, e.g. "Use the tcpfn discover tool on my CSV." See
docs/MCP_QUICKSTART.md for stdio vs HTTP client setup.
Raw HTTP (cURL / Postman)
Start the HTTP transport with
tcpfn-mcp --transport streamable-http --host 127.0.0.1 --port 8080.
The health check is a plain GET (no auth):
curl http://127.0.0.1:8080/health
# → {"status":"ok","service":"tcpfn-mcp"}For calling the tools from cURL / Postman, the easiest path is the plain
REST API (no session, no SSE) — the same five tools as JSON-in / JSON-out
endpoints under /api/tools/:
curl -X POST http://127.0.0.1:8080/api/tools/discover \
-H "Authorization: Bearer $TCPFN_MCP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"data_path":"gs://plant-data/line1.parquet","target":"downtime","threshold":0.5}'Interactive REST reference (schemas + a working "Send"):
/api/docs · OpenAPI spec:
/api/openapi.json. Same Authorization: Bearer auth; the REST tool
endpoints require it, /api/docs is public.
/mcp endpoint itself is session-based (for MCP clients):
initialize → Mcp-Session-Id → tools/call, with
Accept: application/json, text/event-stream and SSE responses. So for raw
scripting prefer the REST API above (or an MCP SDK). The per-tool Request examples
below show the JSON-RPC tools/call body that travels inside an MCP
session. Full handshake details: docs/MCP_QUICKSTART.md.
Data
Every tool takes a data source — a data_path
(gs://, s3://, an https:// URL, or a local path) or an
inline data_csv string. See docs/MCP_DATA_SOURCES.md.
is_identifiable before acting on an
estimate_effect result?estimate_effect always returns an effect size (ATE) —
but a number coming back doesn't mean it's causally trustworthy. The ATE is
only valid if the effect is identifiable from your data: i.e. the
causal structure lets the model separate the true cause → effect
impact from confounding and mere correlation. If a hidden common cause drives
both variables, the estimate is biased and acting on it (changing the cause
expecting the predicted effect) can be wrong or costly.
is_identifiable is the firewall: it returns
identifiable (bool) — true exactly when no necessary condition
(independence / precedence / deconfounding) is violated, the same test
estimate_effect applies, so the two can never disagree — plus a
consistency number and a plain-language explanation.
consistency measures how uniform the effect is across units; it is
not bounded to [0, 1], goes negative when the per-unit spread exceeds
the mean effect size, and does not decide the verdict. If it comes back
not identifiable, treat the estimate_effect ATE as unreliable and
don't act on it — that's the line between an advisory number and a real-world
intervention. (Use what_should_i_measure to learn what to measure
or intervene on to make it identifiable.)
Diagnostics & troubleshooting
Every tool response carries a diagnostics block, and every
error carries a stable code plus a one-line
remediation. Together they make the tools
self-troubleshooting: when a result looks wrong or comes
back empty, the answer for what to do next is in the response itself —
you rarely need to guess or re-ask blind.
The diagnostics block
Present on every successful tool result. Read it first when a result is surprising — the model may have analysed different data than you uploaded, or filtered everything out for a reason it will state plainly.
| Field | Type | What it tells you |
|---|---|---|
data_report | DataReport | What the model actually analysed vs. what was uploaded:
|
empty_result_reason | string | null | Set only when the tool returned no edges / no actions / no
rankings. Says why — e.g. "0 edges at threshold 0.1; the
strongest pair scored 0.07". |
warnings | array<string> | Data- or result-quality flags (short series, unsigned edges, unnormalised target-scoped strengths, …). Empty = no known concerns. |
suggested_next_calls | array<string> | Concrete next tool calls to diagnose or improve the result, e.g. "retry discover with threshold=0.05" or "call check_data". |
The self-service loop
When a result is empty or looks wrong, work down this list — each step reads a field the tools already returned:
| # | Situation | Do this |
|---|---|---|
| 1 | Any surprising result |
Read |
| 2 |
|
Check |
| 3 | Result still looks wrong |
Call |
| 4 | An expected edge is missing |
Retry |
| 5 | Before acting on an edge |
Check its |
Errors
Failed calls raise a structured error rather than a bare string: a stable
code (safe to branch on) and a remediation sentence
describing the fix. Handle the code; surface the
remediation to the operator.
The full field list for each block appears in the per-tool
Returns tables below (diagnostics,
edges_at_thresholds, debug, stability),
and check_data has its own section. This page is generated from the
schemas, so those tables never drift from the code.
discover
Discover the causal graph from a time-series CSV/Parquet file.
Returns ranked causal edges with strength, lag, and effect sign. Use this first when investigating a new dataset.
Input
| Parameter | Type | Default | Description | |
|---|---|---|---|---|
data_csv | string | optional | Inline CSV text. Rows are timesteps, columns are variables. Header row required. Use this when the data lives on the client side (Claude Desktop chat upload, ChatGPT file) and the MCP server is remote. Subject to TCPFN_MCP_MAX_INLINE_CSV_MB (default 50 MB) and the deployment's HTTP request body limit. | |
| ExampleSay the utility_bill readings are in a spreadsheet on your laptop and the TCPFN server runs in the cloud. Paste the CSV text — the heater_power, room_temp, fan_speed, power_bill and random_noise columns with their header row — straight into the call, and nothing has to be uploaded or hosted first. | ||||
data_path | string | optional | Local filesystem path OR remote URL pointing at a CSV / Parquet file. Rows are timesteps, columns are variables. Supported URL schemes: https:// (any signed URL), gs:// (GCS via container service account), s3:// (S3 via container IAM role), azure:// (Blob Storage via DefaultAzureCredential). Local paths still work for stdio servers and mounted-volume deployments. | |
| ExampleSay the same utility_bill readings already sit in a bucket your server can reach. Point data_path at them instead of pasting the rows, and every later call re-reads the file rather than sending the data again. | ||||
variable_names | array<string> | optional | Optional subset of columns to analyze. Defaults to all numeric columns in the file, in their original order. | |
| ExampleSuppose your export carries a meter_id column and a duplicate copy of power_bill alongside the real readings. List just heater_power, room_temp, fan_speed and power_bill here and the rest sit the analysis out — so a copy of the bill is never discovered as a 'cause' of the bill. | ||||
model_version | enum(v2.1, v2.2, v3) | optional | TCPFN weight version. v2.1 is best for general discovery / RCA; v2.2 for 12+ hour industrial lags. When omitted, the deployment default (TCPFN_MODEL_VERSION env, else v2.1) is used. | |
| ExampleNormally leave this out and let the deployment choose. Pin it when you are running the utility_bill analysis repeatedly and want every run answered by the same weights, so a change to the deployment cannot move your results mid-comparison. | ||||
auto_fit | boolean | optional | If True, automatically apply safe reductions (drop constant/NaN columns, uniformly downsample rows) when the dataset would exceed device memory budget. Will not drop informative columns — refuses if column subsetting would be needed. Defaults to TCPFN_MCP_AUTO_FIT env var (off by default). | |
| ExampleSuppose your utility_bill export covers years of readings and is too large for the machine. Turn this on and the call thins the rows and drops columns that never move — a flat sensor, say — instead of refusing outright, and fit_report tells you exactly what it did. | ||||
threshold | number | optional | 0.1 | Edge strength threshold; edges below this are dropped. Default 0.1 (same as the CLI). If the result is empty, check `edges_at_thresholds` in the output to see how many edges exist at looser thresholds before concluding the graph is empty; raise the threshold to keep only the strongest edges. |
| ExampleSuppose you run discover on the utility_bill data and get nothing back. Rather than concluding the heater and fan do not matter, lower the threshold and the weaker links reappear — the setting decides how much evidence an edge needs, not whether the edge exists. Raise it when you only want the few strongest drivers. | ||||
target | string | optional | If set, restrict discovery to causes of this single variable. Much faster than full graph discovery. Caveat: target-scoped strengths are on the raw effect-magnitude scale (not normalised to [0,1]), so thresholds are not comparable with full-graph runs. | |
| ExampleSet this to power_bill when the bill is the only thing you care about. You get the causes of the bill — heater_power and fan_speed — without also mapping how the heater affects room_temp, which is much faster on a wide file. | ||||
include_debug | boolean | optional | False | If True, include a `debug` payload with per-variable signal statistics (std, lag-1 autocorrelation, sharp-change count), the intermediate score matrices, and per-pair judgment components. Use when an expected edge is missing: a variable with high autocorrelation and few sharp changes is too slow-varying for the model to score. |
| ExampleSuppose you expected fan_speed to show up as a cause of power_bill and it did not. Turn this on and the response also reports what the fan's signal looked like, which separates 'the fan was scored and lost' from 'the fan never varied enough to be scored at all'. | ||||
stability | StabilityInput | boolean | optional | Robustness check: re-run discovery on temporal subsamples and report per-edge `support` (fraction of runs in which the edge re-appeared). DEFAULT (null) = automatic: it runs unless the main pass shows the extra passes would exceed the server's compute budget, in which case the output says so (`stability.performed=false`, a warning) instead of silently skipping. Pass a StabilityInput to force it with your settings, true for the defaults, or false to disable for exploratory work — but do not act on an edge that was never re-checked. | |
| ExampleBefore you tell the plant to change the fan, ask for this. The analysis is re-run on several stretches of the utility_bill history and each edge reports how often it reappeared — a fan_speed link found in every stretch is a different proposition from one that shows up only in a good month. | ||||
Provide exactly one data source: data_csv (inline CSV text) or data_path (local path or https:// / gs:// / s3:// / azure:// URL). Both are marked optional individually because the constraint is between them — omitting both is a 422. Over multipart/form-data, the uploaded file IS the data source, so neither field is sent.
Returns (DiscoverOutput)
| Field | Type | Description |
|---|---|---|
edges | array<CausalEdge> | Discovered causal edges, ranked by strength (strongest first). |
| ExampleThis is the answer: heater_power and fan_speed appear as causes of power_bill, each with the delay before it shows up and whether it pushes the bill up or down. random_noise is absent, which is the point — it never caused anything. | ||
variables | array<string> | Variables analysed, in input order. |
| ExampleThe columns that took part. If you expected room_temp to appear somewhere in the graph and it is missing from this list, it never reached the analysis at all — the data report will say why. | ||
n_samples | integer | Number of timesteps (rows) used. |
| ExampleHow many utility_bill readings were actually used. If you uploaded years of data and this comes back small, most of the history did not survive loading, and the graph rests on less evidence than you expected. | ||
model_version | string | TCPFN weight version that produced this result. |
| ExampleWhich weights answered. Keep it beside any utility_bill result you circulate: if a later run disagrees, this is what tells you whether you are comparing two answers or two models. | ||
regime | string | How the data was handled: 'temporal' (panel/natural-experiment path) or 'cross_sectional' (native i.i.d. readout). Auto-detected from lag-1 autocorrelation — no input controls this; it is reported so you can see which path produced the graph. On the 'cross_sectional' path edges carry strength + effect_sign but no p_null/p_ident (the judgment head is temporal-only), and a classical LiNGAM cross-check may appear in diagnostics. |
| ExampleTells you which engine ran. Time-ordered utility_bill readings take the temporal path, where a lag means real elapsed time. If your rows arrived sorted by meter or aggregated into monthly totals, the order is gone and the cross-sectional path answers instead — the edges then describe what moves together, not what comes first. | ||
explanation | string | Plain-language summary of the discovered graph. |
| ExampleThe graph written out in a sentence — heater_power and fan_speed drive power_bill — for pasting into a ticket or a note to someone who will not open the JSON. | ||
confidence | number | Overall confidence in the returned graph, in [0, 1]: the mean of per-edge `evidence` over kept edges — the model's own judgment (p_ident × (1 − p_null)) DISCOUNTED by every data-side check the edge failed or was not re-confirmed by (necessary-condition gate factor, stability support). 0.0 when no edges were kept. When the stability check did not run the value carries no re-check evidence and the diagnostics say so. |
| ExampleOne number for the whole utility_bill graph, useful for comparing this run against another. Before acting on the fan specifically, read that edge's own numbers rather than this summary. | ||
edges_at_thresholds | object | How many edges discover would RETURN at reference thresholds (e.g. {'0.1': 9, '0.3': 3, '0.5': 0}) — counted after the necessary-condition gate, mediator pruning and direction reconciliation, so they match the edge list, at no extra cost. If your threshold returned 0 edges but looser thresholds show edges, the signal exists but is weak — retry lower. Equal counts at every threshold mean nothing more appears at any cut-off: look for a long-lag or regime-change warning instead. |
| ExampleRead this the moment the edge list looks too short. It says how many links between the heater, the fan and the bill would be RETURNED at each setting — after the same checks the list went through — so an empty result turns into 'my threshold was strict' rather than 'nothing causes the bill'. If the count is the same at every setting, lowering it will not help: look for a long-lag or regime-change warning. | ||
elapsed_seconds | number | Server-side compute time, in seconds. |
| ExampleHow long the server took. Useful when you move from the five utility_bill columns to a real plant feed with hundreds, and need to know whether the call still fits inside a dashboard refresh. | ||
fit_report | FitReport | null | Present only when auto_fit reduced the data; describes what was changed. |
| ExampleEmpty in the normal case. If it is filled in, your utility_bill data was thinned to fit the machine before the analysis ran, and this is where you find out what was left out. | ||
diagnostics | Diagnostics | null | Self-troubleshooting block: data report, warnings, empty-result reason. |
| ExampleThe place to look when something seems off — say power_bill has no causes at all. It carries what was analysed versus what you sent, any warnings, the reason a result was empty, and the calls to make next. | ||
stability | StabilityReport | null | Always present on the temporal path: whether the stability check ran (and if not, why); per-edge support is on each edge. |
| ExamplePresent when you asked for the robustness check. It records how the utility_bill history was re-sampled, which is what makes each edge's reappearance rate meaningful. | ||
debug | object | null | Present when include_debug=true: per-variable signal stats, intermediate score matrices, per-pair judgment components. |
| ExamplePresent when you asked for it, and there for one question: why is the edge I expected missing? It shows what each of the utility_bill columns looked like to the model before any edge was scored. | ||
Types marked | null may be absent or null in a response — check before dereferencing.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "discover",
"arguments": {
"data_path": "https://<your-tcpfn-host>/api/sample.csv",
"target": "power_bill",
"include_debug": true
}
}
}
{
"edges": [
{
"cause": "fan_speed",
"effect": "power_bill",
"strength": 1.8486513319912061,
"lag": 1,
"lag_window": [
25,
12
],
"effect_sign": 1,
"p_null": 0.01507568359375,
"p_ident": 0.9853515625,
"raw_magnitude": 10.302619934082031,
"support": 1.0,
"veto": null,
"veto_detail": null,
"evidence": 0.9704967141151428,
"orientation_confidence": null,
"evidence_windows": null,
"evidence_agreement": null,
"lag_caveat": null,
"direction_undecided": null,
"reverse_lag": null,
"reverse_strength": null,
"block_support": 1.0,
"block_note": "present in all 3 assessable contiguous blocks with one sign",
"closed_loop": null,
"closed_loop_note": null,
"mediation_note": null
},
{
"cause": "heater_power",
"effect": "power_bill",
"strength": 0.7891331014054239,
"lag": 1,
"lag_window": [
25,
12
],
"effect_sign": 1,
"p_null": 0.0025615692138671875,
"p_ident": 0.99755859375,
"raw_magnitude": 4.289557456970215,
"support": 1.0,
"veto": null,
"veto_detail": null,
"evidence": 0.9950032783672214,
"orientation_confidence": null,
"evidence_windows": null,
"evidence_agreement": null,
"lag_caveat": null,
"direction_undecided": null,
"reverse_lag": null,
"reverse_strength": null,
"block_support": 0.3333333333333333,
"block_note": "present in 1 of 3 assessable contiguous blocks; not detectable in rows 0-266, rows 266-532",
"closed_loop": null,
"closed_loop_note": null,
"mediation_note": null
}
],
"variables": [
"heater_power",
"room_temp",
"fan_speed",
"power_bill",
"random_noise"
],
"n_samples": 799,
"model_version": "v2.1",
"regime": "temporal",
"explanation": "Discovered 2 causes of 'power_bill'",
"confidence": 0.9827499962411821,
"edges_at_thresholds": {
"0.05": 2,
"0.1": 2,
"0.2": 2,
"0.3": 2,
"0.5": 2
},
"elapsed_seconds": 3.63640066300286,
"fit_report": null,
"diagnostics": {
"data_report": {
"n_rows_original": 799,
"n_cols_original": 5,
"rows_used": 799,
"cols_used": 5,
"dropped_columns": [],
"columns_used": [
"heater_power",
"room_temp",
"fan_speed",
"power_bill",
"random_noise"
],
"sampling_interval": null,
"sampling_interval_seconds": null
},
"switches": {
"TCPFN_HYBRID_PRUNE": "0",
"TCPFN_EXOGENEITY_SCREEN": "0",
"TCPFN_HYBRID_K": "3",
"TCPFN_HYBRID_ALPHA": "0.01",
"TCPFN_EXOGENEITY_LOOKBACK": "5",
"TCPFN_CI_REFINE": "1",
"TCPFN_SHAPE_EDGES": "1",
"TCPFN_DISC_N_LAGS": "3",
"TCPFN_DISC_LONG_LAG_WINDOW": "1",
"TCPFN_COVARIATE_SELECTION": "by_index",
"TCPFN_XS_HONESTY_GATES": "1"
},
"warnings": [
{
"code": "general",
"severity": "warning",
"message": "target-scoped strengths are on the raw effect-magnitude scale (not normalised to [0,1]); do not compare thresholds with full-graph runs.",
"field": null
},
{
"code": "general",
"severity": "warning",
"message": "NO DISCRIMINATIVE SIGNAL: 4 of 4 scored pairs have an effect indistinguishable from a shuffled-treatment control (median placebo ratio 1.19). The model likely has no real dose-response signal on this data (out-of-distribution / regime mismatch) \u2014 treat the ranking as unreliable. Consider a checkpoint calibrated for this regime, or check_data for suitability.",
"field": null
},
{
"code": "general",
"severity": "warning",
"message": "necessary-condition gate: demoted edge(s) that fail a checkable requirement for direct causation: room_temp \u2192 power_bill (confounded: assoc 0.33\u21920.05 given heater_power; strength 0.57\u21920.19); random_noise \u2192 power_bill (independent: assoc 0.076; strength 0.41\u21920.29). independent = no association at any lag (decoy signature); reversed = the claimed effect leads the claimed cause; confounded = association vanishes given a common cause; shared_clock = association vanishes once a shared cycle/trend (time itself) is removed \u2014 two series on the same rhythm; underpowered_for_marginal_test = the association sits too close to the independence floor to decide either way (demoted the same, but no confounding claim is made). schedule_indicator = an endpoint is a fixed schedule (a deterministic function of the clock) \u2014 refused outright. Disable with TCPFN_DISCOVER_INDEPENDENCE_GATE=0.",
"field": null
}
],
"errors": [],
"empty_result_reason": null,
"suggested_next_calls": []
},
"stability": {
"performed": true,
"mode": "auto",
"n_subsamples": 3,
"subsample_fraction": 0.7,
"skipped_reason": null,
"note": "Each edge's `support` is the fraction of subsample runs in which it re-appeared above the threshold. The rule is: a real relationship appears in EVERY slice (support = 1.0). Anything below that appeared in only some stretches of the data \u2014 treat it as data-window-sensitive, whatever the number. The windows are 70% of the file at evenly spaced starts, so they all overlap its middle: support 1.0 does NOT mean the relationship held throughout. Each edge's block_support / block_note (non-overlapping thirds) answers that."
},
"debug": {
"n_variables": 5,
"n_timesteps": 799,
"variables": [
"heater_power",
"room_temp",
"fan_speed",
"power_bill",
"random_noise"
],
"config": {
"T_pre": 20,
"T_post": 10,
"tie_tolerance": 0.15,
"asymmetry_weight": 0.0,
"threshold": 0.1,
"tool": "discover"
},
"matrices": {
"raw_cate": {
"shape": [
5,
5
],
"data": [
[
0.0,
0.0,
0.0,
0.7891331014054239,
0.0
],
[
0.0,
0.0,
0.0,
0.5733570319094569,
0.0
],
[
0.0,
0.0,
0.0,
1.8486513319912061,
0.0
],
[
0.0,
0.0,
0.0,
0.0,
0.0
],
[
0.0,
0.0,
0.0,
0.4076606291524655,
0.0
]
]
},
"last_lags": {
"shape": [
5,
5
],
"data": [
[
0.0,
0.0,
0.0,
5.0,
0.0
],
[
0.0,
0.0,
0.0,
0.0,
0.0
],
[
0.0,
0.0,
0.0,
1.0,
0.0
],
[
0.0,
0.0,
0.0,
0.0,
0.0
],
[
0.0,
0.0,
0.0,
2.0,
0.0
]
]
},
"final": {
"shape": [
5,
5
],
"data": [
[
0.0,
0.0,
0.0,
0.7891331014054239,
0.0
],
[
0.0,
0.0,
0.0,
0.5733570319094569,
0.0
],
[
0.0,
0.0,
0.0,
1.8486513319912061,
0.0
],
[
0.0,
0.0,
0.0,
0.0,
0.0
],
[
0.0,
0.0,
0.0,
0.4076606291524655,
0.0
]
]
}
},
"counters": {
"n_unidentifiable": 0,
"pair_drop_counts": {}
},
"signal_stats": [
{
"name": "heater_power",
"index": 0,
"std": 16.987435625763514,
"autocorr_lag1": 0.6073879119622362,
"sharp_change_count": 158
},
{
"name": "room_temp",
"index": 1,
"std": 7.285835864859985,
"autocorr_lag1": 0.5869305966629326,
"sharp_change_count": 148
},
{
"name": "fan_speed",
"index": 2,
"std": 22.88786267021232,
"autocorr_lag1": 0.6289269808214812,
"sharp_change_count": 153
},
{
"name": "power_bill",
"index": 3,
"std": 5.408623357627738,
"autocorr_lag1": 0.6191331841862169,
"sharp_change_count": 159
},
{
"name": "random_noise",
"index": 4,
"std": 28.825055177869064,
"autocorr_lag1": 0.011028893081771378,
"sharp_change_count": 111
}
],
"per_pair": [
{
"cause": "fan_speed",
"effect": "power_bill",
"strength": 1.8486513319912061,
"raw_magnitude": 10.302619934082031,
"placebo_ratio": 0.9039553071901717,
"p_null": 0.01507568359375,
"p_ident": 0.9853515625,
"lag": 1,
"sign": 1
},
{
"cause": "heater_power",
"effect": "power_bill",
"strength": 0.7891331014054239,
"raw_magnitude": 4.289557456970215,
"placebo_ratio": 1.1467163095354815,
"p_null": 0.0025615692138671875,
"p_ident": 0.99755859375,
"lag": 5,
"sign": -1
},
{
"cause": "random_noise",
"effect": "power_bill",
"strength": 0.2892013746956809,
"raw_magnitude": 2.2244412899017334,
"placebo_ratio": 2.0357452955702064,
"p_null": 0.00441741943359375,
"p_ident": 0.99560546875,
"lag": 2,
"sign": 1
},
{
"cause": "room_temp",
"effect": "power_bill",
"strength": 0.18693560483698188,
"raw_magnitude": 3.1304993629455566,
"placebo_ratio": 1.243046447040753,
"p_null": 0.00453948974609375,
"p_ident": 0.9951171875,
"lag": 0,
"sign": 1
}
],
"per_pair_truncated": false
}
}
Over streamable-HTTP the response arrives wrapped in the
JSON-RPC envelope {"jsonrpc":"2.0","id":1,"result":{…}}.
estimate_effect
Estimate the average causal effect of one variable on another.
Pair with ``is_identifiable`` first if you'll act on the answer.
Input
| Parameter | Type | Default | Description | |
|---|---|---|---|---|
data_csv | string | optional | Inline CSV text. Rows are timesteps, columns are variables. Header row required. Use this when the data lives on the client side (Claude Desktop chat upload, ChatGPT file) and the MCP server is remote. Subject to TCPFN_MCP_MAX_INLINE_CSV_MB (default 50 MB) and the deployment's HTTP request body limit. | |
| ExampleSay the utility_bill readings are in a spreadsheet on your laptop and the TCPFN server runs in the cloud. Paste the CSV text — the heater_power, room_temp, fan_speed, power_bill and random_noise columns with their header row — straight into the call, and nothing has to be uploaded or hosted first. | ||||
data_path | string | optional | Local filesystem path OR remote URL pointing at a CSV / Parquet file. Rows are timesteps, columns are variables. Supported URL schemes: https:// (any signed URL), gs:// (GCS via container service account), s3:// (S3 via container IAM role), azure:// (Blob Storage via DefaultAzureCredential). Local paths still work for stdio servers and mounted-volume deployments. | |
| ExampleSay the same utility_bill readings already sit in a bucket your server can reach. Point data_path at them instead of pasting the rows, and every later call re-reads the file rather than sending the data again. | ||||
variable_names | array<string> | optional | Optional subset of columns to analyze. Defaults to all numeric columns in the file, in their original order. | |
| ExampleSuppose your export carries a meter_id column and a duplicate copy of power_bill alongside the real readings. List just heater_power, room_temp, fan_speed and power_bill here and the rest sit the analysis out — so a copy of the bill is never discovered as a 'cause' of the bill. | ||||
model_version | enum(v2.1, v2.2, v3) | optional | TCPFN weight version. v2.1 is best for general discovery / RCA; v2.2 for 12+ hour industrial lags. When omitted, the deployment default (TCPFN_MODEL_VERSION env, else v2.1) is used. | |
| ExampleNormally leave this out and let the deployment choose. Pin it when you are running the utility_bill analysis repeatedly and want every run answered by the same weights, so a change to the deployment cannot move your results mid-comparison. | ||||
auto_fit | boolean | optional | If True, automatically apply safe reductions (drop constant/NaN columns, uniformly downsample rows) when the dataset would exceed device memory budget. Will not drop informative columns — refuses if column subsetting would be needed. Defaults to TCPFN_MCP_AUTO_FIT env var (off by default). | |
| ExampleSuppose your utility_bill export covers years of readings and is too large for the machine. Turn this on and the call thins the rows and drops columns that never move — a flat sensor, say — instead of refusing outright, and fit_report tells you exactly what it did. | ||||
cause | string | required | Variable name to intervene on. | |
| ExampleThe lever you are weighing up. Set it to fan_speed to ask what the fan does to the bill, then run it again with heater_power to compare the two. It works best on something that actually changed in your history — a setting that was moved now and then, not one that has sat still all year. | ||||
effect | string | required | Variable name whose response to estimate. | |
| ExampleThe outcome you want to move: power_bill. If a decision depends on the answer, ask is_identifiable about the same pair first. | ||||
Provide exactly one data source: data_csv (inline CSV text) or data_path (local path or https:// / gs:// / s3:// / azure:// URL). Both are marked optional individually because the constraint is between them — omitting both is a 422. Over multipart/form-data, the uploaded file IS the data source, so neither field is sent.
Returns (EstimateEffectOutput)
| Field | Type | Description |
|---|---|---|
cause | string | Cause variable name (echoed from input). |
| ExampleThe lever you asked about, echoed back so a saved result still makes sense months later. | ||
effect | string | Effect variable name (echoed from input). |
| ExampleThe outcome you asked about, echoed back so a saved result still makes sense months later. | ||
ate_mean | number | null | Average treatment effect across test units (cross-fitted over all natural experiments) — the model's do-contrast response in the OUTCOME's units to an impulse dose of the cause (a ±2σ swing that then mean-reverts), averaged over the forecast horizon. NOT a per-unit structural coefficient: its scale carries the cause's σ, persistence and the horizon — see `scale.per_unit_effect` for the MEASURED dY/dX coefficient. The RELATIVE ranking of candidates is reliable; see the magnitude_unreliable diagnostic when consistency is low. PROVENANCE: ate_mean is the sign-CORRECTED summary (lead-lag evidence can override the model contrast's noisy sign — flagged via the sign_corrected diagnostic), while any per-unit cate arrays in the response are the RAW model outputs; do not recompute the headline sign by averaging them. Null ONLY when the deployment sets TCPFN_SUPPRESS_UNIDENTIFIABLE_ATE=1 and the pair failed a necessary condition (identifiable=false) — no number is safer than a confidently wrong one under feedback control. |
| ExampleThe headline: what happens to power_bill when fan_speed is pushed. Use it to rank the fan against the heater, which is what it is reliable for. For the plainer statement — one unit more fan gives this much more bill — read scale instead. | ||
ate_std | number | null | Std of CATE across test units (cross-unit spread). Null only under TCPFN_SUPPRESS_UNIDENTIFIABLE_ATE=1 when identifiable=false. |
| ExampleHow much the fan's effect on the bill varied from one occasion to another. A large spread means the fan matters on average but not dependably every time, which is worth saying out loud before someone plans around it. | ||
scale | EffectScale | null | Unit conversion of ate_mean to an approximate per-unit coefficient (dY/dX). None when the conversion is degenerate (e.g. a rare-event cause with no dose variance). |
| ExampleConverts the headline into the form an engineer expects: turn the fan up by one unit and the bill moves by this much. Reach for this whenever you have to state the effect in the plant's own units. | ||
event_study | EventStudy | null | During-vs-local-baseline evidence, present only for a sparse event-shaped cause (see EventStudy). For such causes this is the stronger ruler: the windowed ate_mean above compares busy-vs-quiet periods and can carry the opposite sign. |
| ExamplePresent when the cause is event-shaped (a gate, an outage flag). READ THIS FIRST for such causes: the headline ate_mean compares busy periods against quiet ones and can carry the opposite sign; this block measures what actually happened around the real events. | ||
consistency | number | 1.0 = effect is uniform across units; lower = heterogeneous; negative when the spread across units exceeds the mean effect magnitude. Not bounded to [0, 1] and not a confidence score — see `identifiable` for whether the effect can be trusted at all. |
| ExampleDescribes how alike the fan's effect was across occasions, nothing more. It is not a score to threshold on — whether the answer can be trusted is what identifiable tells you. | ||
n_units | integer | Number of test units the effect was averaged over. |
| ExampleHow many separate occasions in the utility_bill history the answer rests on. A handful means a few episodes are carrying the whole estimate, so treat the size as indicative. | ||
horizon_steps | integer | Forecast horizon (in samples) the effect is measured across. |
| ExampleHow far ahead the bill was followed after the fan moved. It defines the claim: the effect over the next stretch of readings, not instantly and not forever. | ||
identifiable | boolean | Whether the effect is identifiable from this data (see is_identifiable). |
| ExampleThe gate. If this is false, the number beside it should not drive a decision however large it looks — which is exactly what happens when you ask about random_noise, the decoy in the sample. | ||
explanation | string | Plain-language summary of the estimated effect. |
| ExampleThe result in a sentence with its caveats already attached — the safest thing to forward to someone who will not read the rest. | ||
confidence | number | Confidence in the estimate, in [0, 1]. |
| ExampleHow much to trust this estimate. A firm 'yes the fan matters' alongside a shaky size is a normal outcome: act on the direction, collect more history before quoting the amount. | ||
elapsed_seconds | number | Server-side compute time, in seconds. |
| ExampleHow long the server took, for sizing a batch of these across many meters. | ||
fit_report | FitReport | null | Present only when auto_fit reduced the data; describes what was changed. |
| ExampleFilled in when the utility_bill data was thinned first, in which case the effect was measured on a coarser clock than your raw readings. | ||
diagnostics | Diagnostics | null | Self-troubleshooting block: data report, warnings, empty-result reason. |
| ExampleWhere to look when the number surprises you — in particular the warning that says the ranking of fan against heater is sound but the size is not. | ||
Types marked | null may be absent or null in a response — check before dereferencing.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "estimate_effect",
"arguments": {
"data_path": "https://<your-tcpfn-host>/api/sample.csv",
"cause": "fan_speed",
"effect": "power_bill"
}
}
}
{
"cause": "fan_speed",
"effect": "power_bill",
"ate_mean": 3.8616590245564772,
"ate_std": 4.22291240856285,
"scale": {
"per_unit_effect": 0.21286132899036783,
"per_unit_source": "adjusted_lagged_slope",
"per_unit_lag": 1,
"marginal_slope": 0.21847564675564152,
"marginal_lag": 1,
"closed_loop_detected": false,
"model_ate_conversion": null,
"sign_agreement": true,
"dose_slope": null,
"cause_persistence": 0.6289269808214812,
"horizon_decay": null
},
"event_study": null,
"consistency": 0.0,
"n_units": 15,
"horizon_steps": 10,
"identifiable": true,
"explanation": "Estimated ATE = 3.862 across 15 test units LOW CONFIDENCE (0.00): 'fan_speed' lacks strong natural experiments (sharp level changes) in this data, so the effect SIZE is uncertain \u2014 trust the direction more than the magnitude, and gather data with clearer changes in 'fan_speed' before acting on the number.",
"confidence": 0.0,
"elapsed_seconds": 0.18052232699119486,
"fit_report": null,
"diagnostics": {
"data_report": {
"n_rows_original": 799,
"n_cols_original": 5,
"rows_used": 799,
"cols_used": 5,
"dropped_columns": [],
"columns_used": [
"heater_power",
"room_temp",
"fan_speed",
"power_bill",
"random_noise"
],
"sampling_interval": null,
"sampling_interval_seconds": null
},
"switches": {
"TCPFN_HYBRID_PRUNE": "0",
"TCPFN_EXOGENEITY_SCREEN": "0",
"TCPFN_HYBRID_K": "3",
"TCPFN_HYBRID_ALPHA": "0.01",
"TCPFN_EXOGENEITY_LOOKBACK": "5",
"TCPFN_CI_REFINE": "1",
"TCPFN_SHAPE_EDGES": "1",
"TCPFN_DISC_N_LAGS": "3",
"TCPFN_DISC_LONG_LAG_WINDOW": "1",
"TCPFN_COVARIATE_SELECTION": "by_index",
"TCPFN_XS_HONESTY_GATES": "1"
},
"warnings": [
{
"code": "low_consistency",
"severity": "warning",
"message": "effect consistency is low (0.00) \u2014 the effect varies a lot across units, which can mean heterogeneity OR confounding. Call is_identifiable before acting on this ATE.",
"field": "ate_mean"
},
{
"code": "magnitude_unreliable",
"severity": "warning",
"message": "MAGNITUDE NOTE \u2014 ate_mean is the response to an IMPULSE dose of the cause (a \u00b12\u03c3 swing that then mean-reverts), averaged over the forecast horizon \u2014 NOT a per-unit coefficient. Its scale carries the cause's \u03c3, persistence and the horizon; the MEASURED per-unit coefficient is scale.per_unit_effect=+0.2129 (slope at lag 1, measured from the data; validated median 1.00x of planted truth). Cross-unit consistency is low (0.00), so treat the magnitude as approximate; the RELATIVE ranking of candidates is the reliable signal.",
"field": "ate_mean"
}
],
"errors": [],
"empty_result_reason": null,
"suggested_next_calls": []
}
}
Over streamable-HTTP the response arrives wrapped in the
JSON-RPC envelope {"jsonrpc":"2.0","id":1,"result":{…}}.
is_identifiable
Check whether a causal effect is identifiable from the data.
The firewall between advisory and dangerous — refusing to act under unidentifiability is the right call.
Input
| Parameter | Type | Default | Description | |
|---|---|---|---|---|
data_csv | string | optional | Inline CSV text. Rows are timesteps, columns are variables. Header row required. Use this when the data lives on the client side (Claude Desktop chat upload, ChatGPT file) and the MCP server is remote. Subject to TCPFN_MCP_MAX_INLINE_CSV_MB (default 50 MB) and the deployment's HTTP request body limit. | |
| ExampleSay the utility_bill readings are in a spreadsheet on your laptop and the TCPFN server runs in the cloud. Paste the CSV text — the heater_power, room_temp, fan_speed, power_bill and random_noise columns with their header row — straight into the call, and nothing has to be uploaded or hosted first. | ||||
data_path | string | optional | Local filesystem path OR remote URL pointing at a CSV / Parquet file. Rows are timesteps, columns are variables. Supported URL schemes: https:// (any signed URL), gs:// (GCS via container service account), s3:// (S3 via container IAM role), azure:// (Blob Storage via DefaultAzureCredential). Local paths still work for stdio servers and mounted-volume deployments. | |
| ExampleSay the same utility_bill readings already sit in a bucket your server can reach. Point data_path at them instead of pasting the rows, and every later call re-reads the file rather than sending the data again. | ||||
variable_names | array<string> | optional | Optional subset of columns to analyze. Defaults to all numeric columns in the file, in their original order. | |
| ExampleSuppose your export carries a meter_id column and a duplicate copy of power_bill alongside the real readings. List just heater_power, room_temp, fan_speed and power_bill here and the rest sit the analysis out — so a copy of the bill is never discovered as a 'cause' of the bill. | ||||
model_version | enum(v2.1, v2.2, v3) | optional | TCPFN weight version. v2.1 is best for general discovery / RCA; v2.2 for 12+ hour industrial lags. When omitted, the deployment default (TCPFN_MODEL_VERSION env, else v2.1) is used. | |
| ExampleNormally leave this out and let the deployment choose. Pin it when you are running the utility_bill analysis repeatedly and want every run answered by the same weights, so a change to the deployment cannot move your results mid-comparison. | ||||
auto_fit | boolean | optional | If True, automatically apply safe reductions (drop constant/NaN columns, uniformly downsample rows) when the dataset would exceed device memory budget. Will not drop informative columns — refuses if column subsetting would be needed. Defaults to TCPFN_MCP_AUTO_FIT env var (off by default). | |
| ExampleSuppose your utility_bill export covers years of readings and is too large for the machine. Turn this on and the call thins the rows and drops columns that never move — a flat sensor, say — instead of refusing outright, and fit_report tells you exactly what it did. | ||||
cause | string | required | Cause variable name. | |
| ExampleThe lever you are thinking of pulling — fan_speed, before you ask the plant to change it. | ||||
effect | string | required | Effect variable name. | |
| ExampleThe outcome you expect it to move: power_bill. | ||||
Provide exactly one data source: data_csv (inline CSV text) or data_path (local path or https:// / gs:// / s3:// / azure:// URL). Both are marked optional individually because the constraint is between them — omitting both is a 422. Over multipart/form-data, the uploaded file IS the data source, so neither field is sent.
Returns (IsIdentifiableOutput)
| Field | Type | Description |
|---|---|---|
cause | string | Cause variable name (echoed from input). |
| ExampleThe lever you asked about, echoed back. | ||
effect | string | Effect variable name (echoed from input). |
| ExampleThe outcome you asked about, echoed back. | ||
identifiable | boolean | True if the cause→effect effect is identifiable from this data. |
| ExampleThe gate between an interesting number and a change you actually make. True means the fan genuinely precedes the bill and no other column you recorded explains the connection away. It cannot rule out something you never measured — that is what what_should_i_measure is for. | ||
closed_loop_note | string | null | Set when the pair is the regulated process of a closed control loop: identifiable, but the raw lead-lag sign is the controller's — read estimate_effect's scale.per_unit_effect (history-adjusted). |
| ExampleSet when the pair is the regulated process of a control loop: identifiable, but the raw lead-lag sign is the controller's (the thermostat heats when the room is cold), so read estimate_effect's scale.per_unit_effect, the history-adjusted number, not a raw correlation. | ||
consistency | number | How UNIFORM the estimated effect is across test units — 1 - std(CATE)/mean(|CATE|). 1.0 = the same effect on every unit; lower = more spread; NEGATIVE when the spread exceeds the mean magnitude (routine for rare-event causes with few natural experiments). It is NOT bounded to [0, 1] and it does NOT decide the verdict: `identifiable` is true exactly when no necessary condition (independence / precedence / deconfounding) is violated, which is the same test estimate_effect applies. Read this as a heterogeneity diagnostic, not a confidence score. |
| ExampleReports how alike the effect was across occasions, and nothing about whether to trust it. Do not put a cut-off on this number; read identifiable. | ||
explanation | string | Plain-language reasoning behind the verdict. |
| ExampleWhich checks were made and how they came out. When the answer is no, this names the reason — the two never move together, the bill moves first, or something like room_temp accounts for both — and each points somewhere different. | ||
confidence | number | Confidence in the verdict, in [0, 1]. |
| ExampleHow firm the verdict is. Passing these checks is not proof of causation, so even a clean pass on the fan sits at a measured level rather than a certain one. | ||
elapsed_seconds | number | Server-side compute time, in seconds. |
| ExampleHow long the server took. | ||
fit_report | FitReport | null | Present only when auto_fit reduced the data; describes what was changed. |
| ExampleFilled in when the utility_bill data was thinned before the check ran. | ||
diagnostics | Diagnostics | null | Self-troubleshooting block: data report, warnings, empty-result reason. |
| ExampleWhere the reason appears when the question could not be answered at all — usually a fan setting that barely changed across the whole history. | ||
Types marked | null may be absent or null in a response — check before dereferencing.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "is_identifiable",
"arguments": {
"data_path": "https://<your-tcpfn-host>/api/sample.csv",
"cause": "fan_speed",
"effect": "power_bill"
}
}
}
{
"cause": "fan_speed",
"effect": "power_bill",
"identifiable": true,
"closed_loop_note": null,
"consistency": 0.08293091156075949,
"explanation": "Identifiable given the observed variables: 'fan_speed' passes independence, temporal precedence, and deconfounding against every other measured variable, so no observed common cause explains the 'fan_speed'->'power_bill' association away. (An unobserved confounder cannot be ruled out from data alone.)",
"confidence": 0.6,
"elapsed_seconds": 0.18817737000063062,
"fit_report": null,
"diagnostics": {
"data_report": {
"n_rows_original": 799,
"n_cols_original": 5,
"rows_used": 799,
"cols_used": 5,
"dropped_columns": [],
"columns_used": [
"heater_power",
"room_temp",
"fan_speed",
"power_bill",
"random_noise"
],
"sampling_interval": null,
"sampling_interval_seconds": null
},
"switches": {
"TCPFN_HYBRID_PRUNE": "0",
"TCPFN_EXOGENEITY_SCREEN": "0",
"TCPFN_HYBRID_K": "3",
"TCPFN_HYBRID_ALPHA": "0.01",
"TCPFN_EXOGENEITY_LOOKBACK": "5",
"TCPFN_CI_REFINE": "1",
"TCPFN_SHAPE_EDGES": "1",
"TCPFN_DISC_N_LAGS": "3",
"TCPFN_DISC_LONG_LAG_WINDOW": "1",
"TCPFN_COVARIATE_SELECTION": "by_index",
"TCPFN_XS_HONESTY_GATES": "1"
},
"warnings": [
{
"code": "low_consistency",
"severity": "warning",
"message": "identifiable, but the effect SIZE is uncertain (consistency 0.08) \u2014 trust the direction; treat the magnitude as order-of-magnitude only.",
"field": "ate_mean"
}
],
"errors": [],
"empty_result_reason": null,
"suggested_next_calls": []
}
}
Over streamable-HTTP the response arrives wrapped in the
JSON-RPC envelope {"jsonrpc":"2.0","id":1,"result":{…}}.
what_should_i_measure
Propose actions to resolve causal uncertainty in the dataset.
The "what don't I know?" capability — tells you which variables to measure or intervene on to disambiguate the causal structure. It does NOT rank business levers by impact; that is ``rank_interventions``.
Worked example — a plant's power bill. ``rank_interventions`` with target="power_bill" and candidates=["heater_power", "fan_speed"] tells you *which lever to pull*: e.g. fan_speed moves the bill twice as much as heater_power. ``what_should_i_measure`` answers a different question — *what is blocking a confident answer*: if discovery can't tell whether heater_power → room_temp or the reverse, this tool says "log the thermostat setpoint", because that missing measurement is what would resolve the ambiguity.
Rule of thumb: use ``rank_interventions`` to decide what to change; use ``what_should_i_measure`` to decide what to measure or test next.
Input
| Parameter | Type | Default | Description | |
|---|---|---|---|---|
data_csv | string | optional | Inline CSV text. Rows are timesteps, columns are variables. Header row required. Use this when the data lives on the client side (Claude Desktop chat upload, ChatGPT file) and the MCP server is remote. Subject to TCPFN_MCP_MAX_INLINE_CSV_MB (default 50 MB) and the deployment's HTTP request body limit. | |
| ExampleSay the utility_bill readings are in a spreadsheet on your laptop and the TCPFN server runs in the cloud. Paste the CSV text — the heater_power, room_temp, fan_speed, power_bill and random_noise columns with their header row — straight into the call, and nothing has to be uploaded or hosted first. | ||||
data_path | string | optional | Local filesystem path OR remote URL pointing at a CSV / Parquet file. Rows are timesteps, columns are variables. Supported URL schemes: https:// (any signed URL), gs:// (GCS via container service account), s3:// (S3 via container IAM role), azure:// (Blob Storage via DefaultAzureCredential). Local paths still work for stdio servers and mounted-volume deployments. | |
| ExampleSay the same utility_bill readings already sit in a bucket your server can reach. Point data_path at them instead of pasting the rows, and every later call re-reads the file rather than sending the data again. | ||||
variable_names | array<string> | optional | Optional subset of columns to analyze. Defaults to all numeric columns in the file, in their original order. | |
| ExampleSuppose your export carries a meter_id column and a duplicate copy of power_bill alongside the real readings. List just heater_power, room_temp, fan_speed and power_bill here and the rest sit the analysis out — so a copy of the bill is never discovered as a 'cause' of the bill. | ||||
model_version | enum(v2.1, v2.2, v3) | optional | TCPFN weight version. v2.1 is best for general discovery / RCA; v2.2 for 12+ hour industrial lags. When omitted, the deployment default (TCPFN_MODEL_VERSION env, else v2.1) is used. | |
| ExampleNormally leave this out and let the deployment choose. Pin it when you are running the utility_bill analysis repeatedly and want every run answered by the same weights, so a change to the deployment cannot move your results mid-comparison. | ||||
auto_fit | boolean | optional | If True, automatically apply safe reductions (drop constant/NaN columns, uniformly downsample rows) when the dataset would exceed device memory budget. Will not drop informative columns — refuses if column subsetting would be needed. Defaults to TCPFN_MCP_AUTO_FIT env var (off by default). | |
| ExampleSuppose your utility_bill export covers years of readings and is too large for the machine. Turn this on and the call thins the rows and drops columns that never move — a flat sensor, say — instead of refusing outright, and fit_report tells you exactly what it did. | ||||
Provide exactly one data source: data_csv (inline CSV text) or data_path (local path or https:// / gs:// / s3:// / azure:// URL). Both are marked optional individually because the constraint is between them — omitting both is a 422. Over multipart/form-data, the uploaded file IS the data source, so neither field is sent.
Returns (WhatShouldIMeasureOutput)
| Field | Type | Description |
|---|---|---|
actions | array<DisambiguationAction> | Proposed actions, ranked by expected gain. |
| ExampleWhat to start recording or test next. This answers a different question from ranking levers: not 'fan or heater' but 'what am I missing' — if the thermostat setting were logged alongside room_temp, an ambiguity in the graph would resolve itself. | ||
n_actions | integer | Number of actions returned. |
| ExampleHow many suggestions came back. None is not a failure — the reason field may well be saying the utility_bill picture is already clear and more instrumentation would not help. | ||
explanation | string | Plain-language summary of the recommendation. |
| ExampleThe recommendation in a sentence, ready to forward. | ||
confidence | number | Confidence in the recommendation, in [0, 1]. |
| ExampleHow firm the suggestion is. Treat a low value as a prompt for your own judgement about the plant rather than a list to work through. | ||
elapsed_seconds | number | Server-side compute time, in seconds. |
| ExampleHow long the server took. | ||
fit_report | FitReport | null | Present only when auto_fit reduced the data; describes what was changed. |
| ExampleFilled in when the utility_bill data was thinned before the analysis. | ||
diagnostics | Diagnostics | null | Self-troubleshooting block: data report, warnings, empty-result reason. |
| ExampleWhere an empty list is explained, and it separates two opposite situations: nothing would help, or the picture was too tangled to advise on. | ||
Types marked | null may be absent or null in a response — check before dereferencing.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "what_should_i_measure",
"arguments": {
"data_path": "https://<your-tcpfn-host>/api/sample.csv"
}
}
}
{
"actions": [],
"n_actions": 0,
"explanation": "Proposed 0 actions to resolve causal uncertainty",
"confidence": 0.0,
"elapsed_seconds": 0.915492394939065,
"fit_report": null,
"diagnostics": {
"data_report": {
"n_rows_original": 799,
"n_cols_original": 5,
"rows_used": 799,
"cols_used": 5,
"dropped_columns": [],
"columns_used": [
"heater_power",
"room_temp",
"fan_speed",
"power_bill",
"random_noise"
],
"sampling_interval": null,
"sampling_interval_seconds": null
},
"switches": {
"TCPFN_HYBRID_PRUNE": "0",
"TCPFN_EXOGENEITY_SCREEN": "0",
"TCPFN_HYBRID_K": "3",
"TCPFN_HYBRID_ALPHA": "0.01",
"TCPFN_EXOGENEITY_LOOKBACK": "5",
"TCPFN_CI_REFINE": "1",
"TCPFN_SHAPE_EDGES": "1",
"TCPFN_DISC_N_LAGS": "3",
"TCPFN_DISC_LONG_LAG_WINDOW": "1",
"TCPFN_COVARIATE_SELECTION": "by_index",
"TCPFN_XS_HONESTY_GATES": "1"
},
"warnings": [],
"errors": [],
"empty_result_reason": "the discovered graph has 3 edge(s) but none were flagged uncertain \u2014 the causal structure is confidently resolved, and no additional measurement would reduce causal uncertainty. This is a good outcome, not an error.",
"suggested_next_calls": [
"proceed to rank_interventions or estimate_effect on the edges returned by discover"
]
}
}
Over streamable-HTTP the response arrives wrapped in the
JSON-RPC envelope {"jsonrpc":"2.0","id":1,"result":{…}}.
rank_interventions
Rank candidate intervention variables by predicted effect on a target.
Unfiltered ranking — identifiability + SOP + safety gates live in ``RecommendationAgent``, not in this tool. Use this when you want a quick "which of these 10 things should I try?" answer.
Worked example — a plant's power bill. With target="power_bill" and candidates=["heater_power", "fan_speed"], this tool tells you *which lever to pull*: e.g. fan_speed moves the bill twice as much as heater_power. It does NOT tell you what is missing — if discovery can't tell whether heater_power → room_temp or the reverse, resolving that is ``what_should_i_measure``'s job (it would say "log the thermostat setpoint").
Rule of thumb: ``rank_interventions`` = what to change; ``what_should_i_measure`` = what to measure or test next.
Input
| Parameter | Type | Default | Description | |
|---|---|---|---|---|
data_csv | string | optional | Inline CSV text. Rows are timesteps, columns are variables. Header row required. Use this when the data lives on the client side (Claude Desktop chat upload, ChatGPT file) and the MCP server is remote. Subject to TCPFN_MCP_MAX_INLINE_CSV_MB (default 50 MB) and the deployment's HTTP request body limit. | |
| ExampleSay the utility_bill readings are in a spreadsheet on your laptop and the TCPFN server runs in the cloud. Paste the CSV text — the heater_power, room_temp, fan_speed, power_bill and random_noise columns with their header row — straight into the call, and nothing has to be uploaded or hosted first. | ||||
data_path | string | optional | Local filesystem path OR remote URL pointing at a CSV / Parquet file. Rows are timesteps, columns are variables. Supported URL schemes: https:// (any signed URL), gs:// (GCS via container service account), s3:// (S3 via container IAM role), azure:// (Blob Storage via DefaultAzureCredential). Local paths still work for stdio servers and mounted-volume deployments. | |
| ExampleSay the same utility_bill readings already sit in a bucket your server can reach. Point data_path at them instead of pasting the rows, and every later call re-reads the file rather than sending the data again. | ||||
variable_names | array<string> | optional | Optional subset of columns to analyze. Defaults to all numeric columns in the file, in their original order. | |
| ExampleSuppose your export carries a meter_id column and a duplicate copy of power_bill alongside the real readings. List just heater_power, room_temp, fan_speed and power_bill here and the rest sit the analysis out — so a copy of the bill is never discovered as a 'cause' of the bill. | ||||
model_version | enum(v2.1, v2.2, v3) | optional | TCPFN weight version. v2.1 is best for general discovery / RCA; v2.2 for 12+ hour industrial lags. When omitted, the deployment default (TCPFN_MODEL_VERSION env, else v2.1) is used. | |
| ExampleNormally leave this out and let the deployment choose. Pin it when you are running the utility_bill analysis repeatedly and want every run answered by the same weights, so a change to the deployment cannot move your results mid-comparison. | ||||
auto_fit | boolean | optional | If True, automatically apply safe reductions (drop constant/NaN columns, uniformly downsample rows) when the dataset would exceed device memory budget. Will not drop informative columns — refuses if column subsetting would be needed. Defaults to TCPFN_MCP_AUTO_FIT env var (off by default). | |
| ExampleSuppose your utility_bill export covers years of readings and is too large for the machine. Turn this on and the call thins the rows and drops columns that never move — a flat sensor, say — instead of refusing outright, and fit_report tells you exactly what it did. | ||||
target | string | required | Variable whose value we want to drive. | |
| ExampleThe number you are trying to move: power_bill. Everything that comes back is expressed as an effect on it. | ||||
candidates | array<string> | required | Cause variables to evaluate as candidate intervention points. | |
| ExampleThe levers you could really pull — heater_power and fan_speed, not every column in the file. Slipping random_noise into the list is a cheap sanity check: a decoy should come back rejected, and in the sample it does. | ||||
stability | StabilityInput | boolean | optional | Re-rank on temporal subsamples to measure how firmly the #1 lever holds (`top_lever_confidence` = fraction of subsample runs in which it stayed #1). DEFAULT (null) = automatic: runs unless the main pass shows the extra passes would exceed the server's compute budget, in which case `stability.performed=false` says so and top_lever_confidence is null. Pass a StabilityInput to force it with your settings, true for the defaults, false to disable. | |
| ExampleLeave it at the default and the ranking is re-run on several stretches of the utility_bill history, so top_lever_confidence can say whether fan_speed led every time or only in a good month. Set false only for quick exploration — then the #1 spot has not been re-checked and the output says so. | ||||
Provide exactly one data source: data_csv (inline CSV text) or data_path (local path or https:// / gs:// / s3:// / azure:// URL). Both are marked optional individually because the constraint is between them — omitting both is a 422. Over multipart/form-data, the uploaded file IS the data source, so neither field is sent.
Returns (RankInterventionsOutput)
| Field | Type | Description |
|---|---|---|
target | string | Target variable being driven (echoed from input). |
| ExampleThe number being driven, echoed back. | ||
rankings | array<CandidateRanking> | Sorted identifiable-first, then by |ate_mean| descending. The identifiable block comes first on purpose: an unidentifiable candidate can carry a large |ate_mean| (a decoy variable often does) and must not outrank a real, weaker lever. Compare magnitudes only WITHIN the identifiable block. |
| ExampleThe payoff — your candidate levers put in order as advice, with the ones that survived the trust checks placed above the ones that did not. On the utility_bill data the fan comes out ahead of the heater, which is the useful surprise. | ||
equivalence_classes | object | Functionally-tied candidate groups, keyed by class id. The members of one class carry ONE underlying signal (copies, transforms, mixes of each other) and split its effect in the ranking — read each class as a single cause. Which member is the physically-measured one cannot be determined from observation; only an intervention can settle it. |
| ExampleGroups of candidates that are the same signal wearing different names — a sensor and its square root, say. Each group's effect is SPLIT across its members, so a real cause can look weak simply because it appears twice. Read each group as one lever; which member is the physical sensor is a question for the plant, not the data. | ||
rejected_leakage | object | Candidates excluded BEFORE ranking because they are functional restatements of the target itself (column -> evidence). Reporting the target's own copy as a cause is circular; these are never ranked, deliberately and loudly. |
| ExampleCandidates thrown out before ranking because they restate the target itself — a noisy copy of the bill offered as a cause of the bill. Such a column would win every ranking it enters, which is exactly why it must never enter one. The evidence string says how it was caught. | ||
derived_mixes | object | Candidates reconstructible as a combination of the OTHER candidates (column -> evidence), e.g. an average of several sensors. They carry no information of their own — their effect restates their components'. A mix whose attributed components are all ranked themselves is refused as a lever (identifiable=false, reason on the row); one whose components are not available stays ranked, as it may carry the only actionable form of its signal. Prefer component candidates as levers either way. |
| ExampleCandidates that are arithmetic on the OTHER candidates — an average of three sensors, say. They stay in the ranking so nothing disappears silently, but pulling this 'lever' means pulling its components; act on those instead. | ||
top_lever | string | null | The #1 identifiable lever by estimated effect — always named when at least one candidate is identifiable; None only when nothing could be ranked. How firmly it is #1 is NOT a yes/no: read `top_lever_confidence` (held #1 in what fraction of data subsamples) and `top_lever_cross_check` (does a model-free ordering of the same levers agree). The two answer different questions — luck, and a model that is consistently wrong — and `top_lever_note` states both in one sentence. |
| ExampleThe headline answer: the lever with the largest estimated effect that passed every check. Always named when anything could be ranked — how firmly it is #1 is the next two fields, not a yes/no. Quote it together with top_lever_note. | ||
top_lever_confidence | number | null | Fraction of subsample re-rankings in which top_lever stayed #1 (stability). 1.0 = held in every stretch of the data; 0.33 = held in one of three, i.e. the order between the leaders is data-window-sensitive. null when the stability check did not run (see `stability.skipped_reason`) or only one candidate was identifiable. This measures sampling luck only — a systematic model error survives every slice; that is what the cross-check is for. |
| ExampleHow often fan_speed stayed #1 when the ranking was re-run on different stretches of the utility_bill history: 1.0 = every stretch, 0.33 = one in three, so the order between the leaders depends on which month you look at. Null means the re-check did not run (the stability block says why). This catches luck; it cannot catch a model that is wrong the same way every time — that is what the cross-check is for. | ||
top_lever_cross_check | LeverCrossCheck | null | An independent, model-free ordering of the same levers computed from the data alone (each lever's standardised effect on the target at its best lag, all levers fitted together). `agrees` is whether it puts the same lever (or the same equivalence class) first. When it disagrees, the #1 position is contested by the data itself — do not act on top_lever without an experiment, whatever top_lever_confidence says. |
| ExampleA second opinion that does not use the model at all: the same levers ordered by plain lagged regression on the data. If it agrees, two independent methods name the same #1. If it puts heater_power first while the model says fan_speed, the #1 spot is genuinely contested — an experiment settles it, the tool will not. | ||
top_lever_note | string | null | One sentence combining top_lever, its confidence and the cross-check — the line to quote. |
| ExampleThe one sentence to paste into the summary: the lever, how often it held #1, and whether the second opinion agrees. | ||
top_group | array<string> | The levers that contested the #1 spot: every candidate that was #1 in some subsample, plus the model-free #1 when it disagrees. Empty or [top_lever] when the leader is uncontested. |
| ExampleEveryone who contested the #1 spot — levers that led in some stretch of the data, plus the data-only #1 when it differs. [fan_speed] alone means the leader was never challenged. | ||
n_tied_top | integer | Size of top_group when it has more than one member, else 0. Kept for compatibility; top_lever_confidence and the cross-check carry the substance. |
| ExampleSize of top_group when more than one lever contested #1; 0 when the leader was unchallenged. Kept for older readers — top_lever_confidence and the cross-check carry the substance. | ||
stability | StabilityReport | null | Whether the subsample re-ranking ran (and if not, why). |
| ExampleWhether the re-ranking on other stretches of the data actually ran; when it did not, top_lever_confidence is null and the reason is here. | ||
unranked_likely_drivers | array<UnrankedDriver> | Candidates too rare to rank by effect size whose event timing nevertheless shows they likely drive the target (elevation follows their events at >=2x chance). Treat as a GROUP of likely drivers alongside the ranking — their internal order is not established. |
| ExampleEasy to overlook, and it matters. A cause that fires only rarely cannot be sized like the fan, yet its timing can still show it drives the bill. Such a lever appears here and nowhere else — a reader who skips this section will conclude it does not matter. | ||
n_candidates | integer | Number of candidate variables evaluated. |
| ExampleHow many levers you submitted. | ||
n_ranked | integer | May be < n_candidates if some failed to estimate. |
| ExampleHow many could actually be sized. Fewer than you submitted means some produced no estimate — check the list above before assuming they are irrelevant. | ||
explanation | string | Plain-language summary of the ranking. |
| ExampleThe ordering in a sentence, ready for a summary slide. | ||
confidence | number | Confidence in the ranking, in [0, 1]. |
| ExampleHow firm the ordering is as a whole — the top-ranked lever's sign_certainty. It answers 'is the leader really pushing the bill the way we think', not 'is the leader really ahead of #2'; the second question belongs to top_lever. | ||
elapsed_seconds | number | Server-side compute time, in seconds. |
| ExampleHow long the server took. Every candidate is estimated separately, so a long list costs more — another reason to submit real levers rather than every column. | ||
fit_report | FitReport | null | Present only when auto_fit reduced the data; describes what was changed. |
| ExampleFilled in when the utility_bill data was thinned before the ranking. | ||
diagnostics | Diagnostics | null | Self-troubleshooting block: data report, warnings, empty-result reason. |
| ExampleThe caveats that qualify the ordering, and the reason if nothing could be ranked. | ||
Types marked | null may be absent or null in a response — check before dereferencing.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "rank_interventions",
"arguments": {
"data_path": "https://<your-tcpfn-host>/api/sample.csv",
"target": "power_bill",
"candidates": [
"heater_power",
"room_temp",
"fan_speed",
"random_noise"
]
}
}
}
{
"target": "power_bill",
"rankings": [
{
"cause": "fan_speed",
"ate_mean": 3.8616590245564772,
"ate_std": 4.22291240856285,
"consistency": 0.0,
"sign_certainty": 0.9996023898622671,
"identifiable": true,
"reliability_note": null,
"distinguishable_from_next": false,
"equivalence_class": null
},
{
"cause": "heater_power",
"ate_mean": 1.906293697357178,
"ate_std": 5.673852779995192,
"consistency": 0.0,
"sign_certainty": 0.8068236523731855,
"identifiable": true,
"reliability_note": null,
"distinguishable_from_next": null,
"equivalence_class": null
},
{
"cause": "random_noise",
"ate_mean": 3.4414266642402205,
"ate_std": 4.961172570153613,
"consistency": 0.0,
"sign_certainty": 0.9957646990764921,
"identifiable": false,
"reliability_note": "independent: random_noise is ~marginally independent of power_bill at every lag (max|correlation| 0.076 < 0.107); a direct cause implies some dependence \u2014 this effect estimate is likely a variance artifact, not causal.",
"distinguishable_from_next": null,
"equivalence_class": null
},
{
"cause": "room_temp",
"ate_mean": -2.400410405327292,
"ate_std": 4.568357312660014,
"consistency": 0.0,
"sign_certainty": 0.9697236487999223,
"identifiable": false,
"reliability_note": "confounded: the association between room_temp and power_bill falls below the independence floor once heater_power's recent history is accounted for (0.326 \u2192 0.051 < 0.155); heater_power is a likely common cause of both \u2014 this effect is confounded, not direct.",
"distinguishable_from_next": null,
"equivalence_class": null
}
],
"equivalence_classes": {},
"rejected_leakage": {},
"derived_mixes": {},
"top_lever": "fan_speed",
"top_lever_confidence": 0.6666666666666666,
"top_lever_cross_check": {
"agrees": true,
"data_top": "fan_speed",
"data_ordering": [
{
"cause": "fan_speed",
"score": 0.9297602127650145,
"lag": 1
},
{
"cause": "heater_power",
"score": 0.35310743390248844,
"lag": 1
}
],
"note": "A model-free ordering of the same levers (standardised lagged least squares, no model) also puts fan_speed first."
},
"top_lever_note": "fan_speed is the strongest identifiable lever by estimated effect; held #1 in 2 of 3 data subsamples; the model-free ordering agrees.",
"top_group": [
"fan_speed",
"heater_power"
],
"n_tied_top": 2,
"stability": {
"performed": true,
"mode": "auto",
"n_subsamples": 3,
"subsample_fraction": 0.7,
"skipped_reason": null,
"note": "top_lever_confidence = fraction of subsample re-rankings in which top_lever stayed #1."
},
"unranked_likely_drivers": [],
"n_candidates": 4,
"n_ranked": 4,
"explanation": "Ranked 4 candidate interventions on 'power_bill'",
"confidence": 0.9996023898622671,
"elapsed_seconds": 3.064401308001834,
"fit_report": null,
"diagnostics": {
"data_report": {
"n_rows_original": 799,
"n_cols_original": 5,
"rows_used": 799,
"cols_used": 5,
"dropped_columns": [],
"columns_used": [
"heater_power",
"room_temp",
"fan_speed",
"power_bill",
"random_noise"
],
"sampling_interval": null,
"sampling_interval_seconds": null
},
"switches": {
"TCPFN_HYBRID_PRUNE": "0",
"TCPFN_EXOGENEITY_SCREEN": "0",
"TCPFN_HYBRID_K": "3",
"TCPFN_HYBRID_ALPHA": "0.01",
"TCPFN_EXOGENEITY_LOOKBACK": "5",
"TCPFN_CI_REFINE": "1",
"TCPFN_SHAPE_EDGES": "1",
"TCPFN_DISC_N_LAGS": "3",
"TCPFN_DISC_LONG_LAG_WINDOW": "1",
"TCPFN_COVARIATE_SELECTION": "by_index",
"TCPFN_XS_HONESTY_GATES": "1"
},
"warnings": [
{
"code": "general",
"severity": "warning",
"message": "necessary-condition firewall: ranked below the identifiable candidates because they cannot be a direct cause of power_bill: random_noise (independent); room_temp (confounded). ate_mean is reported unchanged \u2014 only the ranking and the identifiable flag carry the constraint.",
"field": null
},
{
"code": "top_lever_unstable",
"severity": "warning",
"message": "the #1 lever changed across data subsamples: fan_speed held #1 in 2 of 3 re-rankings (others: heater_power). The order between these levers is data-window-sensitive.",
"field": null
}
],
"errors": [],
"empty_result_reason": null,
"suggested_next_calls": []
}
}
Worked example — derived / decoy columns
Same tool on a hostile panel: utility_bill_derived plants transforms of real sensors (fan_speed_sqrt, heater_power_^2, room_temp_sin), an average of three (avg_of_3) and a noisy copy of the target (power_bill_linear) among the candidates. Watch four things: rejected_leakage — the target's copy is excluded before ranking; equivalence_classes — each transform is grouped with its sensor, and a group splits one underlying effect; derived_mixes — the average carries no signal of its own; and the #1 verdict — top_lever is named, top_lever_confidence says how often it held #1 across data subsamples, and top_lever_cross_check shows whether a model-free ordering of the same levers agrees (on this panel: read top_lever_note — one sentence with both). The dataset ships with the container — fetch it at /api/sample-derived.csv.
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "rank_interventions",
"arguments": {
"data_path": "https://<your-tcpfn-host>/api/sample-derived.csv",
"target": "power_bill",
"candidates": [
"heater_power",
"room_temp",
"fan_speed",
"random_noise",
"avg_of_3",
"heater_power_^2",
"room_temp_sin",
"fan_speed_sqrt",
"power_bill_linear"
]
}
}
}
{
"target": "power_bill",
"rankings": [
{
"cause": "heater_power_^2",
"ate_mean": 3.9878851209368023,
"ate_std": 4.400761255104026,
"consistency": 0.0,
"sign_certainty": 0.999302647365917,
"identifiable": true,
"reliability_note": null,
"distinguishable_from_next": false,
"equivalence_class": "class_2"
},
{
"cause": "fan_speed_sqrt",
"ate_mean": 2.625341078440348,
"ate_std": 3.387768173152773,
"consistency": 0.0,
"sign_certainty": 0.9973122086605741,
"identifiable": true,
"reliability_note": null,
"distinguishable_from_next": false,
"equivalence_class": "class_1"
},
{
"cause": "heater_power",
"ate_mean": 2.544257043202718,
"ate_std": 6.544209501951928,
"consistency": 0.0,
"sign_certainty": 0.8678655618446807,
"identifiable": true,
"reliability_note": null,
"distinguishable_from_next": false,
"equivalence_class": "class_2"
},
{
"cause": "fan_speed",
"ate_mean": 1.7184426053365072,
"ate_std": 2.674152431559673,
"consistency": 0.0,
"sign_certainty": 0.9871834243804759,
"identifiable": true,
"reliability_note": null,
"distinguishable_from_next": false,
"equivalence_class": "class_1"
},
{
"cause": "avg_of_3",
"ate_mean": 1.0753651440143586,
"ate_std": 3.62567169993655,
"consistency": 0.0,
"sign_certainty": 0.7645316679873463,
"identifiable": true,
"reliability_note": null,
"distinguishable_from_next": null,
"equivalence_class": null
},
{
"cause": "random_noise",
"ate_mean": 2.1757262398214907,
"ate_std": 5.652528666970876,
"consistency": 0.0,
"sign_certainty": 0.8874948093909519,
"identifiable": false,
"reliability_note": "independent: random_noise is ~marginally independent of power_bill at every lag (max|correlation| 0.076 < 0.107); a direct cause implies some dependence \u2014 this effect estimate is likely a variance artifact, not causal.",
"distinguishable_from_next": null,
"equivalence_class": null
},
{
"cause": "room_temp",
"ate_mean": -1.799842436173383,
"ate_std": 5.724303474328228,
"consistency": 0.0,
"sign_certainty": 0.8051594844716619,
"identifiable": false,
"reliability_note": "confounded: the association between room_temp and power_bill falls below the independence floor once heater_power's recent history is accounted for (0.326 \u2192 0.051 < 0.155); heater_power is a likely common cause of both \u2014 this effect is confounded, not direct.",
"distinguishable_from_next": null,
"equivalence_class": "class_3"
},
{
"cause": "room_temp_sin",
"ate_mean": -0.9676934778690338,
"ate_std": 4.148228343098752,
"consistency": 0.0,
"sign_certainty": 0.649239377920232,
"identifiable": false,
"reliability_note": "independent: room_temp_sin is ~marginally independent of power_bill at every lag (max|correlation| 0.070 < 0.106); a direct cause implies some dependence \u2014 this effect estimate is likely a variance artifact, not causal.",
"distinguishable_from_next": null,
"equivalence_class": "class_3"
}
],
"equivalence_classes": {
"class_1": [
"fan_speed",
"fan_speed_sqrt"
],
"class_2": [
"heater_power",
"heater_power_^2"
],
"class_3": [
"room_temp",
"room_temp_sin"
]
},
"rejected_leakage": {
"power_bill_linear": "linear R2=0.921 vs target@lag0"
},
"derived_mixes": {
"avg_of_3": "reconstructible as a linear mix of fan_speed, heater_power, room_temp (R2>=0.999; a minimal derivable-from set, not necessarily the formula)"
},
"top_lever": "heater_power_^2",
"top_lever_confidence": 0.0,
"top_lever_cross_check": {
"agrees": false,
"data_top": "fan_speed",
"data_ordering": [
{
"cause": "fan_speed",
"score": 0.8904099783650529,
"lag": 1
},
{
"cause": "heater_power",
"score": 0.36966119997529384,
"lag": 1
},
{
"cause": "fan_speed_sqrt",
"score": 0.041957891764358275,
"lag": 1
},
{
"cause": "heater_power_^2",
"score": 0.014982426154308115,
"lag": 1
},
{
"cause": "avg_of_3",
"score": 0.0023972498927546273,
"lag": 1
}
],
"note": "A model-free ordering of the same levers (standardised lagged least squares, no model) puts fan_speed first, not heater_power_^2. The two rulers disagree on the #1 lever \u2014 treat the order as unresolved and settle it with an experiment; do not act on heater_power_^2 as the strongest lever."
},
"top_lever_note": "heater_power_^2 is the strongest identifiable lever by estimated effect; held #1 in 0 of 3 data subsamples; the model-free ordering puts fan_speed first instead.",
"top_group": [
"heater_power_^2",
"fan_speed_sqrt",
"fan_speed",
"heater_power"
],
"n_tied_top": 4,
"stability": {
"performed": true,
"mode": "auto",
"n_subsamples": 3,
"subsample_fraction": 0.7,
"skipped_reason": null,
"note": "top_lever_confidence = fraction of subsample re-rankings in which top_lever stayed #1."
},
"unranked_likely_drivers": [],
"n_candidates": 8,
"n_ranked": 8,
"explanation": "Ranked 8 candidate interventions on 'power_bill'",
"confidence": 0.999302647365917,
"elapsed_seconds": 7.222316063998733,
"fit_report": null,
"diagnostics": {
"data_report": {
"n_rows_original": 799,
"n_cols_original": 10,
"rows_used": 799,
"cols_used": 10,
"dropped_columns": [],
"columns_used": [
"heater_power",
"room_temp",
"fan_speed",
"power_bill",
"random_noise",
"avg_of_3",
"heater_power_^2",
"room_temp_sin",
"fan_speed_sqrt",
"power_bill_linear"
],
"sampling_interval": null,
"sampling_interval_seconds": null
},
"switches": {
"TCPFN_HYBRID_PRUNE": "0",
"TCPFN_EXOGENEITY_SCREEN": "0",
"TCPFN_HYBRID_K": "3",
"TCPFN_HYBRID_ALPHA": "0.01",
"TCPFN_EXOGENEITY_LOOKBACK": "5",
"TCPFN_CI_REFINE": "1",
"TCPFN_SHAPE_EDGES": "1",
"TCPFN_DISC_N_LAGS": "3",
"TCPFN_DISC_LONG_LAG_WINDOW": "1",
"TCPFN_COVARIATE_SELECTION": "by_index",
"TCPFN_XS_HONESTY_GATES": "1"
},
"warnings": [
{
"code": "derived_mixes",
"severity": "warning",
"message": "derived-mix candidate(s) carry no information of their own \u2014 their effect restates their components': avg_of_3 (reconstructible as a linear mix of fan_speed, heater_power, room_temp (R2>=0.999; a minimal derivable-from set, not necessarily the formula)). They stay ranked for transparency; prefer the component candidates as levers.",
"field": null
},
{
"code": "equivalence_classes",
"severity": "warning",
"message": "functionally-tied candidates share one underlying signal and SPLIT its effect between them in this ranking \u2014 compare each class against the other candidates, not its members against each other (class_1: {fan_speed, fan_speed_sqrt}; class_2: {heater_power, heater_power_^2}; class_3: {room_temp, room_temp_sin}). Which member is the physically-measured one cannot be determined from observation; only an intervention can settle it.",
"field": null
},
{
"code": "general",
"severity": "warning",
"message": "necessary-condition firewall: ranked below the identifiable candidates because they cannot be a direct cause of power_bill: random_noise (independent); room_temp (confounded); room_temp_sin (independent). ate_mean is reported unchanged \u2014 only the ranking and the identifiable flag carry the constraint.",
"field": null
},
{
"code": "top_lever_cross_check_disagrees",
"severity": "warning",
"message": "A model-free ordering of the same levers (standardised lagged least squares, no model) puts fan_speed first, not heater_power_^2. The two rulers disagree on the #1 lever \u2014 treat the order as unresolved and settle it with an experiment; do not act on heater_power_^2 as the strongest lever.",
"field": null
},
{
"code": "top_lever_unstable",
"severity": "warning",
"message": "the #1 lever changed across data subsamples: heater_power_^2 held #1 in 0 of 3 re-rankings (others: fan_speed_sqrt, fan_speed, heater_power). The order between these levers is data-window-sensitive.",
"field": null
},
{
"code": "general",
"severity": "warning",
"message": "1 candidate(s) could not be estimated and are missing from the ranking: ['power_bill_linear']. Each lacked enough natural experiments (sharp changes); check_data shows per-variable sharp_change_count.",
"field": null
}
],
"errors": [
{
"code": "target_leakage",
"severity": "error",
"message": "candidate 'power_bill_linear' rejected before ranking: it is a functional restatement of the target 'power_bill' (linear R2=0.921 vs target@lag0). Reporting the target's own copy as a cause would be circular. If this column is a genuine physical measurement, verify it and rerun without it named as itself \u2014 this check fires only on near-deterministic dependence, far above any plausible causal correlation.",
"field": null
}
],
"empty_result_reason": null,
"suggested_next_calls": []
}
}
Over streamable-HTTP the response arrives wrapped in the
JSON-RPC envelope {"jsonrpc":"2.0","id":1,"result":{…}}.
check_data
Data-suitability report — the first move when results look wrong.
No model inference; runs in seconds even on large datasets.
Input
| Parameter | Type | Default | Description | |
|---|---|---|---|---|
data_csv | string | optional | Inline CSV text. Rows are timesteps, columns are variables. Header row required. Use this when the data lives on the client side (Claude Desktop chat upload, ChatGPT file) and the MCP server is remote. Subject to TCPFN_MCP_MAX_INLINE_CSV_MB (default 50 MB) and the deployment's HTTP request body limit. | |
| ExampleSay the utility_bill readings are in a spreadsheet on your laptop and the TCPFN server runs in the cloud. Paste the CSV text — the heater_power, room_temp, fan_speed, power_bill and random_noise columns with their header row — straight into the call, and nothing has to be uploaded or hosted first. | ||||
data_path | string | optional | Local filesystem path OR remote URL pointing at a CSV / Parquet file. Rows are timesteps, columns are variables. Supported URL schemes: https:// (any signed URL), gs:// (GCS via container service account), s3:// (S3 via container IAM role), azure:// (Blob Storage via DefaultAzureCredential). Local paths still work for stdio servers and mounted-volume deployments. | |
| ExampleSay the same utility_bill readings already sit in a bucket your server can reach. Point data_path at them instead of pasting the rows, and every later call re-reads the file rather than sending the data again. | ||||
variable_names | array<string> | optional | Optional subset of columns to analyze. Defaults to all numeric columns in the file, in their original order. | |
| ExampleSuppose your export carries a meter_id column and a duplicate copy of power_bill alongside the real readings. List just heater_power, room_temp, fan_speed and power_bill here and the rest sit the analysis out — so a copy of the bill is never discovered as a 'cause' of the bill. | ||||
model_version | enum(v2.1, v2.2, v3) | optional | TCPFN weight version. v2.1 is best for general discovery / RCA; v2.2 for 12+ hour industrial lags. When omitted, the deployment default (TCPFN_MODEL_VERSION env, else v2.1) is used. | |
| ExampleNormally leave this out and let the deployment choose. Pin it when you are running the utility_bill analysis repeatedly and want every run answered by the same weights, so a change to the deployment cannot move your results mid-comparison. | ||||
auto_fit | boolean | optional | If True, automatically apply safe reductions (drop constant/NaN columns, uniformly downsample rows) when the dataset would exceed device memory budget. Will not drop informative columns — refuses if column subsetting would be needed. Defaults to TCPFN_MCP_AUTO_FIT env var (off by default). | |
| ExampleSuppose your utility_bill export covers years of readings and is too large for the machine. Turn this on and the call thins the rows and drops columns that never move — a flat sensor, say — instead of refusing outright, and fit_report tells you exactly what it did. | ||||
target | string | optional | Optional: the variable you plan to pass as discover's target. Adds a target-specific suitability verdict. | |
| ExampleName the column you intend to explain — power_bill — and the report answers for that column specifically, including whether the bill itself has enough movement in it to be worth analysing. | ||||
Provide exactly one data source: data_csv (inline CSV text) or data_path (local path or https:// / gs:// / s3:// / azure:// URL). Both are marked optional individually because the constraint is between them — omitting both is a 422. Over multipart/form-data, the uploaded file IS the data source, so neither field is sent.
Returns (CheckDataOutput)
| Field | Type | Description |
|---|---|---|
n_samples | integer | Usable rows found. |
| ExampleHow many usable utility_bill readings were found. This is what the suitability verdicts are judged against, and it can be well below your file's line count. | ||
n_variables | integer | Usable numeric variables found. |
| ExampleHow many usable measurement columns were found; labels and timestamps do not count. | ||
sampling_interval | string | null | Median time between rows, detected from a timestamp column (e.g. '60s', '1.0h'). Lags shorter than this are invisible; if the process reacts faster, export finer-grained data. None when no timestamp column was found. |
| ExampleHow far apart your readings are. It sets a floor on what can be seen: an effect that plays out within a day is invisible in monthly billing data, however good the model is. | ||
variables | array<VariableCheck> | Per-variable suitability checks. |
| ExampleThe column-by-column report — the quickest way to find the one bad sensor in a wide file, such as a thermometer that never moved all year. | ||
suitable_for_full_graph_discovery | boolean | True if the dataset meets the statistical floor (~10×n_vars samples, min 500) and memory budget for full-graph discovery. |
| ExampleWhether there is enough data to map every relationship among the columns — the heater's effect on the room as well as on the bill. | ||
suitable_for_target_scoped_discovery | boolean | Same check for discover with target= (much cheaper). |
| ExampleThe same question for the cheaper run that only looks at causes of power_bill. This is often yes when the full map is no, which is a way forward rather than a dead end. | ||
suitable_for_effect_estimation | boolean | True if single-pair effect estimation has enough samples. |
| ExampleWhether there is enough to size a single lever's effect on the bill. A no here tells you not to bother calling estimate_effect yet. | ||
collinear_pairs | array<string> | Variable pairs with |correlation| > 0.99. Near-duplicate columns make direction resolution unreliable — drop one of each pair via variable_names. |
| ExampleColumns that are effectively the same reading twice — the bill in pounds and the bill in euros, say. They confuse which way an arrow points, so drop one of each pair. | ||
derived_columns | array<DerivedColumn> | Columns that are transforms or restatements of other columns rather than independent measurements — the same judgment the analysis tools apply, run here as a report so it is visible BEFORE an analysis. Empty means no verdict fired, which is not the same as proof every column is measured: the coverage caps that applied are listed in `warnings`. |
| ExampleColumns that are not measurements but arithmetic on other columns — a bill-per-hour you computed from the bill, or a rolling average of the heater. They look like strong causes because they are partly made of the thing you are explaining, so the analysis tools set them aside; this is where you see which ones, before you spend a run on them. | ||
derived_groups | array<DerivedGroup> | Families of columns that share information (connected components over derived_columns' `related` links). Keep one measured column per family — same-family columns split ranking credit and compete as candidate causes. A flagged column whose sources could not be isolated joins no family. |
| ExampleThe same verdicts folded into families: columns that carry one signal between them — a total, the average built from it, the rate built from both. Keep one column per family, preferably the one a sensor actually measured; keeping several makes them compete and splits the credit in the rankings. Which member is the original cannot be read off the data, so the choice of which to keep is yours. | ||
nonmonotone_pairs | array<NonMonotonePair> | Candidate→target pairs with REAL but NON-MONOTONE dependence — invisible to the linear/rank statistics discover and estimate_effect use (their signed effects assume a monotone dose-response), so without this flag such a pair returns an empty/'independent' answer. Populated only when `target` is given. Each row carries a reshaping RECOMMENDATION; nothing is transformed automatically. |
| ExamplePairs where the fan drives the bill through a sweet spot or threshold rather than a straight line — invisible to every straight-line test, so without this flag the answer just comes back empty. Each row says which reshaped column to add so the effect becomes estimable. | ||
shared_clocks | array<SharedClock> | Cycles or a trend that several columns SHARE. Two columns on the same clock look connected whether or not they are — the single most common way an analysis invents a relationship. Discovery and the single-pair tools apply the shared_clock necessary-condition check to every pair; this lists the pairs at risk up front, before any model runs. Empty = no shared rhythm detected among the usable columns. |
| ExampleThe rhythms several utility_bill columns share — fan_speed and power_bill both following a 24-step daily cycle, say. Two columns on the same clock look linked whether or not one drives the other; this names the pairs at risk BEFORE any model runs, and tells you discover will test each such pair with the clock removed. | ||
prior_coverage | array<PriorCoverageFlag> | Detected data patterns the TCPFN training prior does not cover (oscillatory/chaotic dynamics, long-period seasonality, near-linear systems, discrete variables). Empty = no known-uncovered pattern detected — NOT a guarantee the data is in-prior. Each flag cites its benchmark evidence; treat results on flagged data as unvalidated. |
| ExamplePatterns in your data that the model was not trained for. When this is filled in it is an honest warning that your plant does not look like anything the model has seen, and the most important thing on the page. | ||
warnings | array<string> | Dataset-level warnings (short series, heavy missingness, ...). |
| ExampleProblems with the dataset as a whole — too few readings, too many gaps. Worth reading before you spend time on discovery rather than after a puzzling result. | ||
recommendations | array<string> | Concrete next steps, in priority order. |
| ExampleWhat to fix first. This is the whole point of the tool: it costs almost nothing, runs no model, and tells you whether the expensive calls on your utility_bill data are worth making. | ||
diagnostics | Diagnostics | null | Data report for consistency with the other tools. |
| ExampleThe same account of what was read that the other tools carry, so the two agree. | ||
regime | string | Detected data regime. 'temporal' — time-ordered rows carrying autocorrelation, so the temporal discover path (natural-experiment lags, stationarity) applies. 'cross_sectional' — i.i.d. rows with ~0 lag-1 autocorrelation, where the temporal path would fabricate lags; use cross-sectional discovery / effect estimation instead. Classified by mean absolute lag-1 autocorrelation — the SAME signal discover(mode='auto') routes on, surfaced here so the regime is visible before a graph is read. |
| ExampleWhether your readings behave like a time series at all. Checking here first means you are not surprised later when a file of monthly totals produces a very different kind of answer. | ||
regime_lag1_autocorr | number | Mean absolute lag-1 autocorrelation across usable variables — the statistic behind `regime` (>= 0.2 ⇒ temporal). |
| ExampleThe measurement behind that verdict — how much each reading resembles the one before it. Near zero means the rows carry no memory: sorted by meter, aggregated, or genuinely unrelated to each other. | ||
Types marked | null may be absent or null in a response — check before dereferencing.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "check_data",
"arguments": {
"data_path": "https://<your-tcpfn-host>/api/sample.csv",
"target": "power_bill"
}
}
}
{
"n_samples": 799,
"n_variables": 5,
"sampling_interval": null,
"variables": [
{
"name": "heater_power",
"ok": true,
"issues": [],
"std": 16.987435625763514,
"missing_fraction": 0.0,
"autocorr_lag1": 0.6073879119622362,
"sharp_change_count": 158,
"stationary": true
},
{
"name": "room_temp",
"ok": true,
"issues": [],
"std": 7.285835864859985,
"missing_fraction": 0.0,
"autocorr_lag1": 0.5869305966629326,
"sharp_change_count": 148,
"stationary": true
},
{
"name": "fan_speed",
"ok": true,
"issues": [],
"std": 22.88786267021232,
"missing_fraction": 0.0,
"autocorr_lag1": 0.6289269808214812,
"sharp_change_count": 153,
"stationary": true
},
{
"name": "power_bill",
"ok": true,
"issues": [],
"std": 5.408623357627738,
"missing_fraction": 0.0,
"autocorr_lag1": 0.6191331841862169,
"sharp_change_count": 159,
"stationary": true
},
{
"name": "random_noise",
"ok": true,
"issues": [],
"std": 28.825055177869064,
"missing_fraction": 0.0,
"autocorr_lag1": 0.011028893081771378,
"sharp_change_count": 111,
"stationary": true
}
],
"suitable_for_full_graph_discovery": true,
"suitable_for_target_scoped_discovery": true,
"suitable_for_effect_estimation": true,
"collinear_pairs": [],
"derived_columns": [],
"derived_groups": [],
"nonmonotone_pairs": [],
"shared_clocks": [],
"prior_coverage": [],
"warnings": [],
"recommendations": [],
"diagnostics": {
"data_report": {
"n_rows_original": 799,
"n_cols_original": 5,
"rows_used": 799,
"cols_used": 5,
"dropped_columns": [],
"columns_used": [
"heater_power",
"room_temp",
"fan_speed",
"power_bill",
"random_noise"
],
"sampling_interval": null,
"sampling_interval_seconds": null
},
"switches": {
"TCPFN_HYBRID_PRUNE": "0",
"TCPFN_EXOGENEITY_SCREEN": "0",
"TCPFN_HYBRID_K": "3",
"TCPFN_HYBRID_ALPHA": "0.01",
"TCPFN_EXOGENEITY_LOOKBACK": "5",
"TCPFN_CI_REFINE": "1",
"TCPFN_SHAPE_EDGES": "1",
"TCPFN_DISC_N_LAGS": "3",
"TCPFN_DISC_LONG_LAG_WINDOW": "1",
"TCPFN_COVARIATE_SELECTION": "by_index",
"TCPFN_XS_HONESTY_GATES": "1"
},
"warnings": [],
"errors": [],
"empty_result_reason": null,
"suggested_next_calls": [
"call discover \u2014 no blockers found"
]
},
"regime": "temporal",
"regime_lag1_autocorr": 0.49046232089868563
}
Over streamable-HTTP the response arrives wrapped in the
JSON-RPC envelope {"jsonrpc":"2.0","id":1,"result":{…}}.
explain
Root-cause analysis — explain a specific event on a target variable.
Worked example — a plant's power bill spikes at 2pm. explain with target="power_bill" and event_time="...14:00" ranks which upstream variables (heater_power, fan_speed, ...) drove that specific spike, with causal chains and counterfactuals ("if fan_speed had stayed normal, the spike would have been ~40% smaller"). Use discover for the general graph; use explain to diagnose one incident.
Input
| Parameter | Type | Default | Description | |
|---|---|---|---|---|
data_csv | string | optional | Inline CSV text. Rows are timesteps, columns are variables. Header row required. Use this when the data lives on the client side (Claude Desktop chat upload, ChatGPT file) and the MCP server is remote. Subject to TCPFN_MCP_MAX_INLINE_CSV_MB (default 50 MB) and the deployment's HTTP request body limit. | |
| ExampleSay the utility_bill readings are in a spreadsheet on your laptop and the TCPFN server runs in the cloud. Paste the CSV text — the heater_power, room_temp, fan_speed, power_bill and random_noise columns with their header row — straight into the call, and nothing has to be uploaded or hosted first. | ||||
data_path | string | optional | Local filesystem path OR remote URL pointing at a CSV / Parquet file. Rows are timesteps, columns are variables. Supported URL schemes: https:// (any signed URL), gs:// (GCS via container service account), s3:// (S3 via container IAM role), azure:// (Blob Storage via DefaultAzureCredential). Local paths still work for stdio servers and mounted-volume deployments. | |
| ExampleSay the same utility_bill readings already sit in a bucket your server can reach. Point data_path at them instead of pasting the rows, and every later call re-reads the file rather than sending the data again. | ||||
variable_names | array<string> | optional | Optional subset of columns to analyze. Defaults to all numeric columns in the file, in their original order. | |
| ExampleSuppose your export carries a meter_id column and a duplicate copy of power_bill alongside the real readings. List just heater_power, room_temp, fan_speed and power_bill here and the rest sit the analysis out — so a copy of the bill is never discovered as a 'cause' of the bill. | ||||
model_version | enum(v2.1, v2.2, v3) | optional | TCPFN weight version. v2.1 is best for general discovery / RCA; v2.2 for 12+ hour industrial lags. When omitted, the deployment default (TCPFN_MODEL_VERSION env, else v2.1) is used. | |
| ExampleNormally leave this out and let the deployment choose. Pin it when you are running the utility_bill analysis repeatedly and want every run answered by the same weights, so a change to the deployment cannot move your results mid-comparison. | ||||
auto_fit | boolean | optional | If True, automatically apply safe reductions (drop constant/NaN columns, uniformly downsample rows) when the dataset would exceed device memory budget. Will not drop informative columns — refuses if column subsetting would be needed. Defaults to TCPFN_MCP_AUTO_FIT env var (off by default). | |
| ExampleSuppose your utility_bill export covers years of readings and is too large for the machine. Turn this on and the call thins the rows and drops columns that never move — a flat sensor, say — instead of refusing outright, and fit_report tells you exactly what it did. | ||||
context_vars | array<string> | optional | Declared CONTEXT variables (shift flags, calendar features such as is_peak_hour). They stay in the analysis as conditioning context but are never reported as root causes - an operating condition is not an actionable cause, and binary context columns otherwise dominate anomaly scoring (a state flip is a >=2-sigma move on the column's own scale). | |
| ExampleColumns that describe operating conditions rather than causes - a peak-hours flag, a weekend marker. Declare them here and they stay in the analysis as context but are never named as the culprit: 'it was rush hour' is a circumstance, not something an operator can fix, and without this a binary shift flag can out-score every real driver. | ||||
target | string | required | Variable the event occurred on (the outcome to explain). | |
| ExampleThe thing that went wrong: power_bill, on the month it spiked. This tool answers why that particular spike happened, which is a narrower question than what drives the bill in general. | ||||
event_index | integer | optional | Row index (0-based) where the event occurred, into the dataset as analysed. Provide this OR event_time, not both. Rows are not downsampled for this tool, so the index matches your input rows. | |
| ExampleWhich reading the spike is on, when you can point at it directly — the row your alerting picked out. Use this or the timestamp, not both. | ||||
event_time | string | optional | Timestamp of the event (any pandas-parseable form, e.g. '2026-01-03 14:00'). Resolved to the nearest row using a timestamp column in the data. Provide this OR event_index. Requires a parseable timestamp column (auto-detected, or name it via timestamp_column). | |
| ExampleThe same thing expressed as a date, for when your incident came from a ticket rather than a row number. It is matched to the nearest reading. | ||||
lookback_hours | number | optional | 4.0 | How many hours before the event to analyse. Converted to rows using the detected sampling interval; needs at least ~10 rows of history before the event to run. |
| ExampleHow much history before the spike to consider. It has to be long enough to contain the cause — if the heater was left on since the previous evening, a window of an hour will never see it. | ||||
timestamp_column | string | optional | Force a specific column as the time axis for event_time resolution instead of auto-detecting. Ignored when event_index is used. | |
| ExampleName the time column yourself when the file has more than one — the date the reading was taken and the date it was loaded — and you want the readings, not the loads. | ||||
threshold | number | optional | 0.1 | Edge-strength threshold for the internal causal-graph discovery the root-cause trace walks. Lower = denser graph (more candidate chains, slower); default 0.1 matches discover. |
| ExampleHow strict to be while building the graph this tool traces back through. If the answer comes back empty even though you know the heater was involved, loosen it: with too sparse a graph there is no path back from the bill to the heater to walk. | ||||
Provide exactly one data source: data_csv (inline CSV text) or data_path (local path or https:// / gs:// / s3:// / azure:// URL). Both are marked optional individually because the constraint is between them — omitting both is a 422. Over multipart/form-data, the uploaded file IS the data source, so neither field is sent.
Returns (ExplainOutput)
| Field | Type | Description |
|---|---|---|
target_variable | string | The variable whose event was explained (echoed). |
| ExampleThe thing that was explained — the bill — echoed back. | ||
event_time_idx | integer | Resolved row index of the event. |
| ExampleWhich reading was actually treated as the incident. Worth checking when you supplied a date: this confirms it landed on the reading you meant, and being one out changes the whole answer. | ||
root_causes | array<RootCauseItem> | Root causes ranked by combined_score (strongest first). |
| ExampleThe ranked answer to why the bill spiked, strongest first — the fan and the heater ahead of everything that merely happened to be unusual that day. | ||
causal_chains | array<CausalChainItem> | Multi-hop paths from upstream causes into the event. |
| ExampleThe routes taken when the culprit is not directly connected to the bill. This is what turns a number into a story you can tell: the heater ran hard, which warmed the room, which pushed the bill up. | ||
counterfactuals | array<CounterfactualItem> | Per-variable 'what if this had stayed normal' severity reductions. |
| ExampleFor each column, how much smaller the spike would have been if it had behaved normally. The most directly useful part of the answer: it ranks what to fix by how much of the incident it would have prevented. | ||
n_samples | integer | Rows analysed. |
| ExampleHow many readings from the window before the spike were used. | ||
model_version | string | TCPFN weight version used for the internal discovery. |
| ExampleWhich weights ran the analysis behind this explanation. | ||
explanation | string | Plain-language summary of the top root causes. |
| ExampleThe incident in prose — usually the paragraph that goes into the write-up. | ||
elapsed_seconds | number | Server-side compute time, in seconds. |
| ExampleHow long the server took. This is the heaviest of the tools because it maps the whole graph before tracing anything, so allow for it if you call it from an alert. | ||
fit_report | FitReport | null | Present only when auto_fit reduced the data; describes what was changed. |
| ExampleFilled in when the utility_bill data was thinned before the analysis ran. | ||
diagnostics | Diagnostics | null | Self-troubleshooting block: data report, warnings, empty-result reason. |
| ExampleWhere an empty answer is explained — most often too little history before the spike, or a graph too sparse at the strictness you asked for. | ||
Types marked | null may be absent or null in a response — check before dereferencing.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "explain",
"arguments": {
"data_path": "https://<your-tcpfn-host>/api/sample.csv",
"target": "power_bill",
"event_index": 486
}
}
}
{
"target_variable": "power_bill",
"event_time_idx": 486,
"root_causes": [
{
"variable": "fan_speed",
"lag": 1,
"combined_score": 0.7963677677793733,
"direction": "stable",
"anomaly_score": 0.34940270344306923,
"esd_score": 0.5,
"conditional_anomaly_score": 0.5483561098434602,
"noise_attribution": 1.0,
"icc_score": 0.9480862412494672,
"counterfactual_score": 1.0,
"shapley_value": 0.7135131192053822
},
{
"variable": "heater_power",
"lag": 1,
"combined_score": 0.3576979942974683,
"direction": "stable",
"anomaly_score": 0.3806779638167884,
"esd_score": 1.0,
"conditional_anomaly_score": 1.0,
"noise_attribution": 0.05475636460635232,
"icc_score": 0.05191375590412192,
"counterfactual_score": 0.16238026998062136,
"shapley_value": 0.2864868767162723
}
],
"causal_chains": [
{
"path": [
{
"variable": "fan_speed",
"lag": 1
},
{
"variable": "power_bill",
"lag": 0
}
],
"total_lag": 1,
"strength": 1.0
},
{
"path": [
{
"variable": "heater_power",
"lag": 1
},
{
"variable": "power_bill",
"lag": 0
}
],
"total_lag": 1,
"strength": 0.42686962530432304
},
{
"path": [
{
"variable": "room_temp",
"lag": 0
},
{
"variable": "heater_power",
"lag": 1
},
{
"variable": "power_bill",
"lag": 0
}
],
"total_lag": 1,
"strength": 0.20081349404221574
}
],
"counterfactuals": [
{
"variable": "fan_speed",
"severity_reduction": 1.0
},
{
"variable": "heater_power",
"severity_reduction": 0.16238026998062136
}
],
"n_samples": 799,
"model_version": "v2.1",
"explanation": "Top root cause of 'power_bill' at row 486: fan_speed (lag 1, stable, score 0.796). 2 candidate cause(s), 3 causal chain(s).",
"elapsed_seconds": 1.601,
"fit_report": null,
"diagnostics": {
"data_report": {
"n_rows_original": 799,
"n_cols_original": 5,
"rows_used": 799,
"cols_used": 5,
"dropped_columns": [],
"columns_used": [
"heater_power",
"room_temp",
"fan_speed",
"power_bill",
"random_noise"
],
"sampling_interval": null,
"sampling_interval_seconds": null
},
"switches": {
"TCPFN_HYBRID_PRUNE": "0",
"TCPFN_EXOGENEITY_SCREEN": "0",
"TCPFN_HYBRID_K": "3",
"TCPFN_HYBRID_ALPHA": "0.01",
"TCPFN_EXOGENEITY_LOOKBACK": "5",
"TCPFN_CI_REFINE": "1",
"TCPFN_SHAPE_EDGES": "1",
"TCPFN_DISC_N_LAGS": "3",
"TCPFN_DISC_LONG_LAG_WINDOW": "1",
"TCPFN_COVARIATE_SELECTION": "by_index",
"TCPFN_XS_HONESTY_GATES": "1"
},
"warnings": [
{
"code": "indirect_route_refused",
"severity": "warning",
"message": "1 indirect candidate(s) were not traced as root causes of 'power_bill': each reaches it only through another variable, and 0 of them are independent of 'power_bill' on the pair checks while the rest enter through a leg the gate refused or labelled without evidence \u2014 room_temp. An indirect cause must pass the same checks estimate_effect and rank_interventions apply; see causal_chains for routes.",
"field": null
}
],
"errors": [],
"empty_result_reason": null,
"suggested_next_calls": []
}
}
Over streamable-HTTP the response arrives wrapped in the
JSON-RPC envelope {"jsonrpc":"2.0","id":1,"result":{…}}.
causal_analysis
Find every event on a target, explain each, and return the report.
Worked example — a warehouse asks "what has been driving downtime this month?". Neither discover ("what causes what, in general") nor explain ("why did THIS event happen") answers it alone, because you first have to know which downtime episodes are worth explaining. causal_analysis with target="system_downtime" detects the episodes, explains each, and returns them with their causes, timelines and risk factors already attached.
Input
| Parameter | Type | Default | Description | |
|---|---|---|---|---|
data_csv | string | optional | Inline CSV text. Rows are timesteps, columns are variables. Header row required. Use this when the data lives on the client side (Claude Desktop chat upload, ChatGPT file) and the MCP server is remote. Subject to TCPFN_MCP_MAX_INLINE_CSV_MB (default 50 MB) and the deployment's HTTP request body limit. | |
| ExampleSay the utility_bill readings are in a spreadsheet on your laptop and the TCPFN server runs in the cloud. Paste the CSV text — the heater_power, room_temp, fan_speed, power_bill and random_noise columns with their header row — straight into the call, and nothing has to be uploaded or hosted first. | ||||
data_path | string | optional | Local filesystem path OR remote URL pointing at a CSV / Parquet file. Rows are timesteps, columns are variables. Supported URL schemes: https:// (any signed URL), gs:// (GCS via container service account), s3:// (S3 via container IAM role), azure:// (Blob Storage via DefaultAzureCredential). Local paths still work for stdio servers and mounted-volume deployments. | |
| ExampleSay the same utility_bill readings already sit in a bucket your server can reach. Point data_path at them instead of pasting the rows, and every later call re-reads the file rather than sending the data again. | ||||
variable_names | array<string> | optional | Optional subset of columns to analyze. Defaults to all numeric columns in the file, in their original order. | |
| ExampleSuppose your export carries a meter_id column and a duplicate copy of power_bill alongside the real readings. List just heater_power, room_temp, fan_speed and power_bill here and the rest sit the analysis out — so a copy of the bill is never discovered as a 'cause' of the bill. | ||||
model_version | enum(v2.1, v2.2, v3) | optional | TCPFN weight version. v2.1 is best for general discovery / RCA; v2.2 for 12+ hour industrial lags. When omitted, the deployment default (TCPFN_MODEL_VERSION env, else v2.1) is used. | |
| ExampleNormally leave this out and let the deployment choose. Pin it when you are running the utility_bill analysis repeatedly and want every run answered by the same weights, so a change to the deployment cannot move your results mid-comparison. | ||||
auto_fit | boolean | optional | If True, automatically apply safe reductions (drop constant/NaN columns, uniformly downsample rows) when the dataset would exceed device memory budget. Will not drop informative columns — refuses if column subsetting would be needed. Defaults to TCPFN_MCP_AUTO_FIT env var (off by default). | |
| ExampleSuppose your utility_bill export covers years of readings and is too large for the machine. Turn this on and the call thins the rows and drops columns that never move — a flat sensor, say — instead of refusing outright, and fit_report tells you exactly what it did. | ||||
context_vars | array<string> | optional | Declared CONTEXT variables (shift flags, calendar features such as is_peak_hour). They stay in the analysis as conditioning context but are never reported as root causes - an operating condition is not an actionable cause, and binary context columns otherwise dominate anomaly scoring (a state flip is a >=2-sigma move on the column's own scale). | |
| ExampleColumns that describe operating conditions rather than causes - a peak-hours flag, a weekend marker. Declare them here and they stay in the analysis as context but are never named as the culprit: 'it was rush hour' is a circumstance, not something an operator can fix, and without this a binary shift flag can out-score every real driver. | ||||
candidate_causes | array<string> | optional | Declared ACTIONABLE-LEVER columns (same contract as rank_interventions' `candidates`). Optional; changes one thing: when the model's causal score proposes NO surviving driver for the target, the analysis re-examines these declared candidates directly with its statistical necessary-condition checks, and any that pass (with replication evidence) are reported as drivers, disclosed via a `stat_admitted` warning. Without this list the fallback stays off — statistics alone cannot tell an actionable cause from a fellow output that co-moves with the target, so the lever declaration is what makes the re-check safe. | |
| ExampleThe knobs an operator can actually turn — heater_power, fan_speed — same idea as rank_interventions' candidates. Declaring them changes one thing: when the model proposes no provable driver at all, the analysis re-checks these declared levers directly with its statistical tests, and one that passes with replicated evidence is reported (flagged stat_admitted) instead of silently lost. Without the declaration that re-check stays off — from data alone, a fellow symptom that moves with the target is indistinguishable from a lever. | ||||
target | string | required | Variable to analyse — the KPI whose events you want explained. | |
| ExampleThe column whose bad moments you want explained — power_bill. Unlike explain, you do not have to know when anything went wrong: the spikes are found for you, and each one is explained. | ||||
event_time | string | optional | Analyse ONE specific event instead of auto-detecting them all. Any pandas-parseable timestamp, or a row index as a string. Omit to detect every significant event on the target. | |
| ExampleLeave this out to analyse every event on the bill. Set it when you already have one incident in mind — the month the bill spiked — and want that one analysed rather than the whole history. | ||||
threshold | number | optional | 0.1 | Edge-strength threshold for the internal causal graph the root-cause trace walks. Lower = denser graph = more candidate paths from a driver to the target. |
| ExampleHow strict to be while building the causal graph the explanations are traced through. If the bill's spikes come back with no causes attached even though you know the heater was involved, loosen it: too sparse a graph leaves no path from the heater to the bill to walk. | ||||
lookback_hours | number | optional | 4.0 | How much history before each event to attribute over. Must be long enough to contain the cause. |
| ExampleHow much history before each spike to attribute over. It has to reach back far enough to contain the cause — if the heater was left running since the previous evening, a window of one hour will never see it. | ||||
timestamp_column | string | optional | Force which column is the time axis, instead of auto-detecting. Use when the file carries more than one date column. | |
| ExampleName the time column yourself when the file has more than one — the reading date and the upload date — and you mean the readings. | ||||
objective_name | string | optional | Optional label for what the target represents (e.g. 'unplanned downtime'), echoed into the report for display. | |
| ExampleA label for what the target means in your world — 'energy cost', say, rather than the raw column name power_bill. Carried into the report for display; it changes nothing about the analysis. | ||||
Provide exactly one data source: data_csv (inline CSV text) or data_path (local path or https:// / gs:// / s3:// / azure:// URL). Both are marked optional individually because the constraint is between them — omitting both is a 422. Over multipart/form-data, the uploaded file IS the data source, so neither field is sent.
Returns (CausalAnalysisOutput)
| Field | Type | Description |
|---|---|---|
schema_version | integer | Report shape version; additions are backwards-safe. |
| ExampleWhich version of the report shape you received. Check it if you store reports and later read them back — new fields get added over time, and this says which vintage you are holding. | ||
engine | DashboardEngine | Which model produced this result, which weights, and against which target — the provenance to record beside any result you act on. |
| ExampleWhich model produced this and how to read its numbers: the checkpoint, the target, and notes on what each derived value really is. Read the notes before quoting a number to anyone. | ||
kpis | DashboardKpis | Headline numbers: n_events, the increase/decrease split, durations, health score, and the time since / until an event. |
| ExampleThe headline summary — how many spikes the bill had, how long they lasted, how long since the last one, and roughly when to expect the next. This is what a status header is built from. | ||
events | array<DashboardEvent> | One entry per detected event — its window, duration, top cause and risk-factor count. Chronological. |
| ExampleOne entry per spike found in the bill, in time order, each with its window, how long it lasted, and the cause that led it. This is the list you could not get before: explain needs you to already know which moment to ask about, and this is where those moments come from. | ||
causal_effects | array<DashboardCausalEffect> | Per-driver effect on the target, with the lag at which it acts. |
| ExampleAcross the whole history rather than one spike: how much the heater and the fan each move the bill, and after how long. The general picture behind the individual incidents. | ||
event_risk_factors | object | Ranked contributors per event, keyed by event id. |
| ExampleFor each spike, the contributors ranked — so a chart can show why THAT one happened rather than repeating the overall story. Keyed by event, so you look up the spike the user clicked. | ||
sensor_timelines | object | Per-event chart data: the target's value series across the window plus per-variable z-scores and precursor markers. |
| ExampleThe chart data behind each spike: the bill's own line across the window plus every other reading, scored so the unusual ones stand out. This is what a per-event deep-dive view draws. | ||
early_warning | array<EarlyWarningItem> | Precursor signals that led the detected events. |
| ExampleWhat moved BEFORE the bill did — the fan creeping up in the hours before a spike. The part you would build an alert on, since it is the signal that arrives while there is still time to act. | ||
sensitivity | array<SensitivityItem> | What-if magnitudes per driver. |
| ExampleHow much each driver would have to move to matter — the answer to 'if we held the fan steady, how much of the bill would we get back'. | ||
causal_network | CausalNetwork | `sensor_network` (nodes + edges, sections as integer indices) and `causal_chains` (multi-hop paths into the target). |
| ExampleThe graph as a picture: which readings drive which, plus the multi-hop routes into the bill (heater warms the room, the room drives the bill). Feeds the network diagram. | ||
driver_evidence | object | Supporting evidence per driver, for the deep-dive panels. |
| ExampleThe supporting detail behind each driver, for when someone asks 'why do you say the fan did this?' and a ranked list is not enough on its own. | ||
merged_columns | array<MergedColumn> | Functional twins merged before the joint model ran — one node per signal, both names kept. Empty when no two columns carry the same signal. Nothing here is silent: every merge names the dropped column, the representative it was analysed under, the evidence, and the rule that picked the representative. |
| ExampleColumns that were the same signal under another name — the derived panel's fan_speed_sqrt is fan_speed square-rooted — and were analysed ONCE under the representative name so the graph does not blame the same fan twice. Each entry says which name was dropped, which it lives under, the evidence, and how the representative was picked. If the dropped name is your physical sensor, read its result under the kept one. | ||
data_start_time | string | null | First timestamp in the dataset, when one is parseable. |
| ExampleThe first reading's timestamp, so a chart can label its axis with real dates rather than row numbers. Null when the file carries no parseable time column. | ||
data_end_time | string | null | Last timestamp in the dataset, when one is parseable. |
| ExampleThe last reading's timestamp — with the start, it tells you what period this whole report covers. | ||
elapsed_seconds | number | Server-side compute time, in seconds. |
| ExampleHow long the server took. Expect it to be the slowest of the tools: it maps the whole graph once and then explains every spike it found, so a history with many spikes costs more than one with a couple. | ||
fit_report | FitReport | null | Present only when auto_fit reduced the data; describes what was changed. |
| ExampleFilled in when the readings were thinned to fit the machine before analysis. Worth reading before quoting the result — the spikes were found on less data than you uploaded. | ||
diagnostics | Diagnostics | null | Self-troubleshooting block: data result, warnings, empty-result reason. |
| ExampleWhere to look when the report comes back thin. In particular it explains an empty event list — the bill never moved far enough from its usual level to count, which is a different situation from the analysis having failed. | ||
Types marked | null may be absent or null in a response — check before dereferencing.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "causal_analysis",
"arguments": {
"data_path": "https://<your-tcpfn-host>/api/sample.csv",
"target": "power_bill"
}
}
}
{
"schema_version": 1,
"engine": {
"model": "TCPFN",
"model_version": "v2.1",
"target": "power_bill",
"objective": null
},
"kpis": {
"n_events": 9,
"n_events_increase": 4,
"n_events_decrease": 5,
"target_is_binary": false,
"total_downtime_min": 15.0,
"avg_duration_min": 1.667,
"median_duration_min": 1.0,
"max_duration_min": 4.0,
"n_causal_vars": 2,
"n_total_vars": 5,
"n_sig_links": 4,
"n_total_links": 16,
"n_sections": 1,
"global_tau_max": 1,
"global_tau_max_hours": null,
"sampling_interval_seconds": null,
"top2_cause_names": [
"fan_speed",
"heater_power"
],
"top2_cause_counts": [
1,
1
],
"top2_cause_pct": 100.0,
"top_cause_scores": [
null,
null
],
"health_score": null,
"minutes_since_last_event": 53.0,
"next_event_eta_hours": null
},
"events": [
{
"event_id": 1,
"start_idx": 158,
"end_idx": 158,
"duration_steps": 1,
"duration_min": null,
"start_time": null,
"explained": true,
"top_cause": null,
"tag": null,
"top_contribution": null,
"n_risk_factors": 0,
"anomaly_direction": "increase",
"process_section": null,
"model": "tcpfn-rca-anomaly+counterfactual+shapley"
},
{
"event_id": 2,
"start_idx": 245,
"end_idx": 245,
"duration_steps": 1,
"duration_min": null,
"start_time": null,
"explained": true,
"top_cause": "fan_speed",
"tag": "fan_speed",
"top_contribution": null,
"n_risk_factors": 1,
"anomaly_direction": "decrease",
"process_section": "unknown",
"model": "tcpfn-rca-anomaly+counterfactual+shapley"
},
{
"event_id": 3,
"start_idx": 296,
"end_idx": 299,
"duration_steps": 4,
"duration_min": null,
"start_time": null,
"explained": true,
"top_cause": null,
"tag": null,
"top_contribution": null,
"n_risk_factors": 0,
"anomaly_direction": "increase",
"process_section": null,
"model": "tcpfn-rca-anomaly+counterfactual+shapley"
},
{
"event_id": 4,
"start_idx": 485,
"end_idx": 486,
"duration_steps": 2,
"duration_min": null,
"start_time": null,
"explained": true,
"top_cause": "fan_speed",
"tag": "fan_speed",
"top_contribution": 0.7963677677793733,
"n_risk_factors": 3,
"anomaly_direction": "decrease",
"process_section": "unknown",
"model": "tcpfn-rca-anomaly+counterfactual+shapley"
},
{
"event_id": 5,
"start_idx": 595,
"end_idx": 596,
"duration_steps": 2,
"duration_min": null,
"start_time": null,
"explained": true,
"top_cause": "fan_speed",
"tag": "fan_speed",
"top_contribution": null,
"n_risk_factors": 1,
"anomaly_direction": "increase",
"process_section": "unknown",
"model": "tcpfn-rca-anomaly+counterfactual+shapley"
},
{
"event_id": 6,
"start_idx": 639,
"end_idx": 639,
"duration_steps": 1,
"duration_min": null,
"start_time": null,
"explained": true,
"top_cause": "fan_speed",
"tag": "fan_speed",
"top_contribution": null,
"n_risk_factors": 1,
"anomaly_direction": "increase",
"process_section": "unknown",
"model": "tcpfn-rca-anomaly+counterfactual+shapley"
},
{
"event_id": 7,
"start_idx": 656,
"end_idx": 656,
"duration_steps": 1,
"duration_min": null,
"start_time": null,
"explained": true,
"top_cause": null,
"tag": null,
"top_contribution": null,
"n_risk_factors": 0,
"anomaly_direction": "decrease",
"process_section": null,
"model": "tcpfn-rca-anomaly+counterfactual+shapley"
},
{
"event_id": 8,
"start_idx": 675,
"end_idx": 675,
"duration_steps": 1,
"duration_min": null,
"start_time": null,
"explained": true,
"top_cause": null,
"tag": null,
"top_contribution": null,
"n_risk_factors": 0,
"anomaly_direction": "decrease",
"process_section": null,
"model": "tcpfn-rca-anomaly+counterfactual+shapley"
},
{
"event_id": 9,
"start_idx": 744,
"end_idx": 745,
"duration_steps": 2,
"duration_min": null,
"start_time": null,
"explained": true,
"top_cause": null,
"tag": null,
"top_contribution": null,
"n_risk_factors": 0,
"anomaly_direction": "decrease",
"process_section": null,
"model": "tcpfn-rca-anomaly+counterfactual+shapley"
}
],
"causal_effects": [
{
"cause": "fan_speed",
"tag": "fan_speed",
"also_recorded_as": [],
"description": "",
"process_section": "unknown",
"section_nice": "Unknown",
"tcpfn_strength": 1.0,
"total_mci": 1.0,
"max_mci": 1.0,
"lag_at_max": 1,
"lag_hours": null,
"n_sig_lags": 1,
"lag_window": [
25,
12
],
"sign": "+",
"sign_source": "estimate_effect_ate",
"is_direct_edge": true,
"rca_combined_score": null,
"rca_anomaly_score": null,
"rca_counterfactual_score": null,
"direction": null
},
{
"cause": "heater_power",
"tag": "heater_power",
"also_recorded_as": [],
"description": "",
"process_section": "unknown",
"section_nice": "Unknown",
"tcpfn_strength": 0.4269,
"total_mci": 0.4269,
"max_mci": 0.4269,
"lag_at_max": 1,
"lag_hours": null,
"n_sig_lags": 1,
"lag_window": [
25,
12
],
"sign": "+",
"sign_source": "estimate_effect_ate; sign corrected by decisive lead-lag association (overrides the model contrast; same evidence bar as discovery)",
"is_direct_edge": true,
"rca_combined_score": null,
"rca_anomaly_score": null,
"rca_counterfactual_score": null,
"direction": null
}
],
"event_risk_factors": {
"1": [],
"2": [
{
"tag": "fan_speed",
"variable": "fan_speed",
"description": "",
"process_section": "unknown",
"section_nice": "Unknown",
"contribution": null,
"lag": 1,
"lag_hours": null,
"direction": "decrease",
"onset_row": 244,
"link_confirmed": true,
"evidence": "timing+repetition",
"rca_combined_score": null,
"rca_anomaly_score": null,
"rca_counterfactual_score": null
}
],
"3": [],
"4": [
{
"tag": "fan_speed",
"variable": "fan_speed",
"description": "",
"process_section": "unknown",
"section_nice": "Unknown",
"contribution": 0.7963677677793733,
"lag": 1,
"lag_hours": null,
"direction": "decrease",
"onset_row": 484,
"link_confirmed": true,
"evidence": "onset",
"rca_combined_score": 0.7963677677793733,
"rca_anomaly_score": 0.3494027034430693,
"rca_counterfactual_score": 1.0
},
{
"tag": "heater_power",
"variable": "heater_power",
"description": "",
"process_section": "unknown",
"section_nice": "Unknown",
"contribution": 0.3576979942974683,
"lag": 1,
"lag_hours": null,
"direction": "increase",
"onset_row": 473,
"link_confirmed": true,
"evidence": "onset",
"rca_combined_score": 0.3576979942974683,
"rca_anomaly_score": 0.38067796381678864,
"rca_counterfactual_score": 0.16238026998062138
},
{
"tag": "room_temp",
"variable": "room_temp",
"description": "",
"process_section": "unknown",
"section_nice": "Unknown",
"contribution": null,
"lag": null,
"lag_hours": null,
"direction": "increase",
"onset_row": 474,
"link_confirmed": false,
"evidence": "onset",
"rca_combined_score": null,
"rca_anomaly_score": null,
"rca_counterfactual_score": null
}
],
"5": [
{
"tag": "fan_speed",
"variable": "fan_speed",
"description": "",
"process_section": "unknown",
"section_nice": "Unknown",
"contribution": null,
"lag": 1,
"lag_hours": null,
"direction": "increase",
"onset_row": 594,
"link_confirmed": true,
"evidence": "timing+repetition",
"rca_combined_score": null,
"rca_anomaly_score": null,
"rca_counterfactual_score": null
}
],
"6": [
{
"tag": "fan_speed",
"variable": "fan_speed",
"description": "",
"process_section": "unknown",
"section_nice": "Unknown",
"contribution": null,
"lag": 1,
"lag_hours": null,
"direction": "increase",
"onset_row": 638,
"link_confirmed": true,
"evidence": "timing+repetition",
"rca_combined_score": null,
"rca_anomaly_score": null,
"rca_counterfactual_score": null
}
],
"7": [],
"8": [],
"9": []
},
"sensor_timelines": {
"1": {
"time_rel": [
-96.0,
-95.0,
-94.0,
-93.0,
-92.0,
-91.0
],
"event": [
26.56,
20.63,
24.11,
25.24,
24.08,
21.79
],
"variables": {
"power_bill": {
"raw_values": [
26.56,
20.63,
24.11,
25.24,
24.08,
21.79
],
"z_scores": [
0.4457,
-0.5273,
0.0437,
0.2291,
0.0387,
-0.337
],
"is_risk": false,
"sample_idx": 50,
"has_precursor": true,
"normal_median": 23.77,
"value_at_peak": 10.41,
"z_at_peak": -2.2042,
"description": ""
}
},
"idx_lo": 62,
"idx_hi": 170
},
"2": {
"time_rel": [
-96.0,
-95.0,
-94.0,
-93.0,
-92.0,
-91.0
],
"event": [
29.38,
22.43,
21.05,
21.07,
27.51,
24.37
],
"variables": {
"fan_speed": {
"raw_values": [
77.75,
73.7,
68.65,
93.95,
91.16,
80.62
],
"z_scores": [
0.1093,
-0.0706,
-0.2949,
0.829,
0.705,
0.2368
],
"is_risk": true,
"sample_idx": 95,
"has_precursor": true,
"normal_median": 76.98,
"value_at_peak": 19.65,
"z_at_peak": -2.4716,
"description": ""
},
"power_bill": {
"raw_values": [
29.38,
22.43,
21.05,
21.07,
27.51,
24.37
],
"z_scores": [
1.3727,
0.0935,
-0.1606,
-0.1569,
1.0285,
0.4505
],
"is_risk": false,
"sample_idx": 9,
"has_precursor": true,
"normal_median": 21.79,
"value_at_peak": 37.15,
"z_at_peak": 2.8028,
"description": ""
}
},
"idx_lo": 149,
"idx_hi": 257
},
"3": {
"time_rel": [
-96.0,
-95.0,
-94.0,
-93.0,
-92.0,
-91.0
],
"event": [
18.46,
19.41,
22.21,
24.05,
24.79,
22.88
],
"variables": {
"power_bill": {
"raw_values": [
18.46,
19.41,
22.21,
24.05,
24.79,
22.88
],
"z_scores": [
-0.7528,
-0.5788,
-0.0662,
0.2707,
0.4062,
0.0565
],
"is_risk": false,
"sample_idx": 45,
"has_precursor": true,
"normal_median": 22.76,
"value_at_peak": 7.91,
"z_at_peak": -2.6843,
"description": ""
}
},
"idx_lo": 200,
"idx_hi": 311
},
"4": {
"time_rel": [
-96.0,
-95.0,
-94.0,
-93.0,
-92.0,
-91.0
],
"event": [
29.85,
22.82,
23.01,
24.47,
24.42,
31.64
],
"variables": {
"fan_speed": {
"raw_values": [
84.8,
78.67,
78.39,
88.04,
109.21,
118.78
],
"z_scores": [
0.3195,
0.0528,
0.0407,
0.4604,
1.3813,
1.7976
],
"is_risk": true,
"sample_idx": 95,
"has_precursor": true,
"normal_median": 78.58,
"value_at_peak": 0.0,
"z_at_peak": -3.3693,
"description": ""
},
"heater_power": {
"raw_values": [
22.93,
44.9,
51.77,
34.12,
61.89,
48.99
],
"z_scores": [
-1.4026,
-0.1566,
0.233,
-0.768,
0.8069,
0.0753
],
"is_risk": true,
"sample_idx": 84,
"has_precursor": true,
"normal_median": 47.91,
"value_at_peak": 97.1,
"z_at_peak": 2.8038,
"description": ""
},
"room_temp": {
"raw_values": [
22.25,
24.09,
33.3,
35.9,
29.32,
42.62
],
"z_scores": [
-1.7055,
-1.4605,
-0.2342,
0.112,
-0.7641,
1.0068
],
"is_risk": true,
"sample_idx": 85,
"has_precursor": true,
"normal_median": 35.06,
"value_at_peak": 56.94,
"z_at_peak": 2.9135,
"description": ""
},
"power_bill": {
"raw_values": [
29.85,
22.82,
23.01,
24.47,
24.42,
31.64
],
"z_scores": [
1.2893,
0.0208,
0.0551,
0.3185,
0.3095,
1.6123
],
"is_risk": false,
"sample_idx": 27,
"has_precursor": false,
"normal_median": 22.58,
"value_at_peak": 13.38,
"z_at_peak": -1.6826,
"description": ""
}
},
"idx_lo": 389,
"idx_hi": 498
},
"5": {
"time_rel": [
-96.0,
-95.0,
-94.0,
-93.0,
-92.0,
-91.0
],
"event": [
27.56,
23.93,
20.19,
15.11,
21.03,
13.01
],
"variables": {
"fan_speed": {
"raw_values": [
76.11,
65.79,
34.63,
74.45,
43.76,
56.87
],
"z_scores": [
0.0343,
-0.41,
-1.7512,
-0.0372,
-1.3582,
-0.7939
],
"is_risk": true,
"sample_idx": 95,
"has_precursor": true,
"normal_median": 76.98,
"value_at_peak": 124.95,
"z_at_peak": 2.1366,
"description": ""
},
"power_bill": {
"raw_values": [
27.56,
23.93,
20.19,
15.11,
21.03,
13.01
],
"z_scores": [
0.9477,
0.2814,
-0.4051,
-1.3375,
-0.2509,
-1.7229
],
"is_risk": false,
"sample_idx": 58,
"has_precursor": true,
"normal_median": 22.28,
"value_at_peak": 8.9,
"z_at_peak": -2.4773,
"description": ""
}
},
"idx_lo": 499,
"idx_hi": 608
},
"6": {
"time_rel": [
-96.0,
-95.0,
-94.0,
-93.0,
-92.0,
-91.0
],
"event": [
14.16,
20.49,
17.64,
19.57,
13.65,
16.19
],
"variables": {
"fan_speed": {
"raw_values": [
67.94,
52.58,
55.98,
30.2,
51.86,
65.96
],
"z_scores": [
-0.2943,
-0.9627,
-0.8148,
-1.9366,
-0.9941,
-0.3805
],
"is_risk": true,
"sample_idx": 95,
"has_precursor": true,
"normal_median": 76.11,
"value_at_peak": 133.88,
"z_at_peak": 2.5751,
"description": ""
},
"power_bill": {
"raw_values": [
14.16,
20.49,
17.64,
19.57,
13.65,
16.19
],
"z_scores": [
-1.5158,
-0.3404,
-0.8696,
-0.5113,
-1.6105,
-1.1389
],
"is_risk": false,
"sample_idx": 52,
"has_precursor": true,
"normal_median": 22.09,
"value_at_peak": 37.05,
"z_at_peak": 2.7345,
"description": ""
}
},
"idx_lo": 543,
"idx_hi": 651
},
"7": {
"time_rel": [
-96.0,
-95.0,
-94.0,
-93.0,
-92.0,
-91.0
],
"event": [
21.42,
21.24,
17.44,
20.7,
24.46,
20.3
],
"variables": {
"power_bill": {
"raw_values": [
21.42,
21.24,
17.44,
20.7,
24.46,
20.3
],
"z_scores": [
-0.1345,
-0.1676,
-0.8655,
-0.2668,
0.4238,
-0.3402
],
"is_risk": false,
"sample_idx": 35,
"has_precursor": true,
"normal_median": 21.785,
"value_at_peak": 37.05,
"z_at_peak": 2.7363,
"description": ""
}
},
"idx_lo": 560,
"idx_hi": 668
},
"8": {
"time_rel": [
-96.0,
-95.0,
-94.0,
-93.0,
-92.0,
-91.0
],
"event": [
18.55,
21.01,
20.62,
18.71,
19.56,
23.15
],
"variables": {
"power_bill": {
"raw_values": [
18.55,
21.01,
20.62,
18.71,
19.56,
23.15
],
"z_scores": [
-0.6499,
-0.1956,
-0.2677,
-0.6204,
-0.4634,
0.1996
],
"is_risk": false,
"sample_idx": 16,
"has_precursor": true,
"normal_median": 21.58,
"value_at_peak": 37.05,
"z_at_peak": 2.7665,
"description": ""
}
},
"idx_lo": 579,
"idx_hi": 687
},
"9": {
"time_rel": [
-96.0,
-95.0,
-94.0,
-93.0,
-92.0,
-91.0
],
"event": [
24.46,
26.23,
25.88,
13.78,
18.45,
18.04
],
"variables": {
"power_bill": {
"raw_values": [
24.46,
26.23,
25.88,
13.78,
18.45,
18.04
],
"z_scores": [
0.4424,
0.77,
0.7053,
-1.5342,
-0.6699,
-0.7457
],
"is_risk": false,
"sample_idx": 27,
"has_precursor": true,
"normal_median": 21.435,
"value_at_peak": 8.17,
"z_at_peak": -2.5724,
"description": ""
}
},
"idx_lo": 648,
"idx_hi": 757
}
},
"early_warning": [
{
"cause": "fan_speed",
"tag": "fan_speed",
"description": "",
"total_mci": 1.0,
"median_lag_hours": null,
"early_warning_score": 0.5,
"process_section": "unknown",
"tcpfn_strength": 1.0
},
{
"cause": "heater_power",
"tag": "heater_power",
"description": "",
"total_mci": 0.4269,
"median_lag_hours": null,
"early_warning_score": 0.2135,
"process_section": "unknown",
"tcpfn_strength": 0.4269
}
],
"sensitivity": [
{
"variable": "fan_speed",
"tag": "fan_speed",
"description": "",
"increase_effect": 7.2677,
"decrease_effect": -7.2677,
"net_effect": 0.0,
"priority": 7.2677,
"process_section": "unknown",
"basis": "tcpfn_estimate_effect_ate"
},
{
"variable": "heater_power",
"tag": "heater_power",
"description": "",
"increase_effect": 7.4376,
"decrease_effect": -7.4376,
"net_effect": 0.0,
"priority": 7.4376,
"process_section": "unknown",
"basis": "tcpfn_estimate_effect_ate"
}
],
"causal_network": {
"sensor_network": {
"nodes": [
{
"id": "fan_speed",
"section": "unknown",
"out_deg": 1,
"in_deg": 0,
"mci_target": 1.0,
"desc": ""
},
{
"id": "heater_power",
"section": "unknown",
"out_deg": 2,
"in_deg": 1,
"mci_target": 0.4269,
"desc": ""
},
{
"id": "power_bill",
"section": "unknown",
"out_deg": 0,
"in_deg": 2,
"mci_target": 0.0,
"desc": ""
},
{
"id": "room_temp",
"section": "unknown",
"out_deg": 1,
"in_deg": 1,
"mci_target": 0.0,
"desc": ""
}
],
"edges": [
{
"s": "fan_speed",
"t": "power_bill",
"m": 1.0,
"a": 1.0,
"l": 1,
"x": 0,
"ss": 0,
"ts": 0,
"tcpfn_strength": 1.0,
"lag_hours": null,
"sign": "+",
"sign_source": "estimate_effect_ate"
},
{
"s": "room_temp",
"t": "heater_power",
"m": -0.4704,
"a": 0.4704,
"l": 0,
"x": 0,
"ss": 0,
"ts": 0,
"tcpfn_strength": 0.47043284913760774,
"lag_hours": null,
"sign": "-",
"sign_source": "estimate_effect_ate"
},
{
"s": "heater_power",
"t": "power_bill",
"m": 0.4269,
"a": 0.4269,
"l": 1,
"x": 0,
"ss": 0,
"ts": 0,
"tcpfn_strength": 0.42686962530432304,
"lag_hours": null,
"sign": "+",
"sign_source": "estimate_effect_ate; sign corrected by decisive lead-lag association (overrides the model contrast; same evidence bar as discovery)"
},
{
"s": "heater_power",
"t": "room_temp",
"m": 0.3053,
"a": 0.3053,
"l": 1,
"x": 0,
"ss": 0,
"ts": 0,
"tcpfn_strength": 0.3053061733762152,
"lag_hours": null,
"sign": "+",
"sign_source": "estimate_effect_ate"
}
],
"sec_list": [
"unknown"
]
},
"causal_chains": {
"chains": [],
"derived_from_event_id": 1,
"by_event": {
"1": [],
"2": [
{
"path": [
{
"var": "fan_speed",
"lag": 1,
"lag_hours": null
},
{
"var": "power_bill",
"lag": 0,
"lag_hours": null
}
],
"path_str": "fan_speed \u2192 power_bill",
"total_lag": 1,
"total_lag_hours": null,
"strength": 1.0,
"evidence": "timing+repetition",
"link_confirmed": true
}
],
"3": [],
"4": [
{
"path": [
{
"var": "fan_speed",
"lag": 1,
"lag_hours": null
},
{
"var": "power_bill",
"lag": 0,
"lag_hours": null
}
],
"path_str": "fan_speed \u2192 power_bill",
"total_lag": 1,
"total_lag_hours": null,
"strength": 1.0,
"evidence": "onset",
"link_confirmed": true
},
{
"path": [
{
"var": "heater_power",
"lag": 1,
"lag_hours": null
},
{
"var": "power_bill",
"lag": 0,
"lag_hours": null
}
],
"path_str": "heater_power \u2192 power_bill",
"total_lag": 1,
"total_lag_hours": null,
"strength": 0.42686962530432304,
"evidence": "onset",
"link_confirmed": true
},
{
"path": [
{
"var": "room_temp",
"lag": 12,
"lag_hours": null
},
{
"var": "power_bill",
"lag": 0,
"lag_hours": null
}
],
"path_str": "room_temp \u2192 power_bill",
"total_lag": 12,
"total_lag_hours": null,
"strength": null,
"evidence": "onset",
"link_confirmed": false
}
],
"5": [
{
"path": [
{
"var": "fan_speed",
"lag": 1,
"lag_hours": null
},
{
"var": "power_bill",
"lag": 0,
"lag_hours": null
}
],
"path_str": "fan_speed \u2192 power_bill",
"total_lag": 1,
"total_lag_hours": null,
"strength": 1.0,
"evidence": "timing+repetition",
"link_confirmed": true
}
],
"6": [
{
"path": [
{
"var": "fan_speed",
"lag": 1,
"lag_hours": null
},
{
"var": "power_bill",
"lag": 0,
"lag_hours": null
}
],
"path_str": "fan_speed \u2192 power_bill",
"total_lag": 1,
"total_lag_hours": null,
"strength": 1.0,
"evidence": "timing+repetition",
"link_confirmed": true
}
],
"7": [],
"8": [],
"9": []
}
}
},
"driver_evidence": {
"Unknown": [
{
"rank": 1,
"tag": "fan_speed",
"description": "",
"process_section": "unknown",
"total_mci": 1.0,
"lag_at_max": 1,
"lag_hours": null,
"rca_combined_score": null
},
{
"rank": 2,
"tag": "heater_power",
"description": "",
"process_section": "unknown",
"total_mci": 0.4269,
"lag_at_max": 1,
"lag_hours": null,
"rca_combined_score": null
}
]
},
"merged_columns": [],
"data_start_time": null,
"data_end_time": null,
"elapsed_seconds": 15.793,
"fit_report": null,
"diagnostics": {
"data_report": {
"n_rows_original": 799,
"n_cols_original": 5,
"rows_used": 799,
"cols_used": 5,
"dropped_columns": [],
"columns_used": [
"heater_power",
"room_temp",
"fan_speed",
"power_bill",
"random_noise"
],
"sampling_interval": null,
"sampling_interval_seconds": null
},
"switches": {
"TCPFN_HYBRID_PRUNE": "0",
"TCPFN_EXOGENEITY_SCREEN": "0",
"TCPFN_HYBRID_K": "3",
"TCPFN_HYBRID_ALPHA": "0.01",
"TCPFN_EXOGENEITY_LOOKBACK": "5",
"TCPFN_CI_REFINE": "1",
"TCPFN_SHAPE_EDGES": "1",
"TCPFN_DISC_N_LAGS": "3",
"TCPFN_DISC_LONG_LAG_WINDOW": "1",
"TCPFN_COVARIATE_SELECTION": "by_index",
"TCPFN_XS_HONESTY_GATES": "1"
},
"warnings": [],
"errors": [],
"empty_result_reason": null,
"suggested_next_calls": []
}
}
Long numeric series in this example are shortened to 6 points for readability — the real response returns the full traces. Everything else is verbatim.
Over streamable-HTTP the response arrives wrapped in the
JSON-RPC envelope {"jsonrpc":"2.0","id":1,"result":{…}}.
troubleshoot
Why does this result look wrong? — answered from the result itself.
Input
| Parameter | Type | Default | Description | |
|---|---|---|---|---|
result | object | string | required | The tool response to explain. Either the response object itself, a saved {request, response} capture, or a path to a JSON file containing one. This is the result you already got back — it is not re-computed. | |
| ExampleThe result you are unhappy with — the response you already got back, pasted in whole. Nothing is re-run: every answer is read out of what that response already carries, which is why this is cheap enough to use on any disagreement. A file path works too, for results too large to paste. | ||||
question | string | required | What looks wrong. One of: 'no_edge' (you expect one variable to drive another and it is not reported), 'reversed' (it says A drives B and you know it is the other way round), 'effect_size' (the direction is right but the number is wrong), 'missing_var' (a variable appears nowhere), 'wrong_lag' (the delay does not match the plant), 'dense' (everything appears to cause everything). | |
| ExampleWhat looks wrong, in your words: the fan is missing from the result ('missing_var'), you expected fan_speed to drive the bill and it is not there ('no_edge'), it says the bill drives the fan ('reversed'), the number is too big ('effect_size'), the delay is wrong ('wrong_lag'), or everything appears to cause everything ('dense'). | ||||
cause | string | optional | The variable you expect to be the cause. Required for every question except 'dense'; for 'missing_var' it is simply the variable you are looking for. | |
| ExampleThe variable you are asking about — the fan, in the examples here. For 'missing_var' it is simply the tag you cannot find. Only the 'dense' question works without one. | ||||
effect | string | optional | The variable it should affect. Optional — defaults to the result's target when it has one. | |
| ExampleWhat it should be affecting — the bill. Leave it out and the result's own target is used, which is usually what you meant. | ||||
expected_sign | string | optional | '+' or '-' — the direction you expected, if that is the disagreement. | |
| Example'+' or '-', when your disagreement is about DIRECTION: you know turning the fan up raises the bill and the result says otherwise. Given this, the answer engages your expectation rather than restating the model's. | ||||
Returns (TroubleshootOutput)
| Field | Type | Description |
|---|---|---|
question | string | The question that was asked. |
| ExampleThe question that was asked, echoed back. | ||
tool | string | Which tool produced the result being explained. |
| ExampleWhich tool produced the result being explained. Worth checking: a strength from discover and an effect from estimate_effect are different kinds of number, and confusing them is itself a common reason a result looks wrong. | ||
status | string | The specific finding, e.g. 'rejected' (found and refused by a named check), 'too_quiet' (the variable barely moves), 'never_analysed' (dropped before the model ran), 'not_a_size' (that number is a ranking score, not a magnitude). This is the discriminating field: several very different situations all look like 'the model is wrong' to a customer. |
| ExampleWHICH of the look-alike problems this is — the field to branch on. 'never_analysed' (the column was dropped before the model ran), 'too_quiet' (the fan barely moves, so there is nothing to learn from), 'rejected' (found and refused, with a recorded reason), 'not_a_size' (that number is a ranking score, not a change in the bill's units), 'below_cutoff' (scored, under your threshold). All of these look like 'the model is wrong' from the outside, and each has a different fix. | ||
headline | string | The answer in one line. |
| ExampleThe answer in one line — what is actually true about the fan. | ||
detail | string | The numbers and evidence behind it. |
| ExampleThe numbers behind it: how many sharp changes the fan has against the panel's typical, which check rejected it, what the edge counts were at each cut-off. | ||
meaning | string | What it means for the customer's decision. |
| ExampleWhat it means for your decision — in particular whether this is a limit of the DATA (get a period where the fan varies) or a judgement by the model (read the reason and decide whether its assumption holds on your plant). Those call for opposite actions. | ||
next_steps | array<TroubleshootStep> | What to do — at most one call. |
| ExampleWhat to do. Either the thing that settles it outright, or the one call to make and bring back. | ||
settle | string | null | What would settle the disagreement — usually an observation on the plant, not another model run. None when the answer is already terminal. |
| ExampleWhat would end the disagreement, usually an observation rather than another run: raise the fan and watch the bill. When you and the model disagree, this is the experiment that decides it — and the result is the most useful thing you can send back. | ||
evidence_windows | array<EvidenceWindow> | null | The dated events behind the edge, when the result carries them. Checkable against the customer's own logs — the one form of evidence that does not require trusting the model. |
| ExampleThe dated occasions behind the edge, when the result carries them. This is the part you can check without trusting the model at all: look up the times in your own logs and see whether the fan really moved and the bill really followed. | ||
evidence_agreement | string | null | e.g. '6 of 7 events agree'. |
| ExampleHow many of those occasions went the way the edge claims, as in '6 of 7 events agree'. Not all will, and that is normal — the edge is a tendency, not a rule every moment obeys. | ||
Types marked | null may be absent or null in a response — check before dereferencing.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "troubleshoot",
"arguments": {
"question": "no_edge",
"cause": "room_temp",
"effect": "power_bill",
"result": {
"variables": [
"heater_power",
"room_temp",
"fan_speed",
"power_bill"
],
"n_samples": 799,
"edges": [
{
"cause": "fan_speed",
"effect": "power_bill",
"strength": 1.85,
"lag": 1,
"effect_sign": 1
}
],
"edges_at_thresholds": {
"0.05": 4,
"0.1": 4,
"0.2": 3
}
}
}
}
}
{
"question": "no_edge",
"tool": "discover",
"status": "below_threshold",
"headline": "Nothing reported for room_temp at the threshold used",
"detail": "Looser thresholds do return more edges (0.05 \u2192 4, 0.1 \u2192 4, 0.2 \u2192 3).",
"meaning": "The pair was scored; it did not clear the cut-off you ran with.",
"next_steps": [
{
"text": "Re-run at a lower threshold and see whether this pair appears:",
"call": "discover(threshold=0.05)"
}
],
"settle": "Whether the pair appears once the cut-off is lowered.",
"evidence_windows": null,
"evidence_agreement": null
}
Over streamable-HTTP the response arrives wrapped in the
JSON-RPC envelope {"jsonrpc":"2.0","id":1,"result":{…}}.
StabilityInput
Robustness check: re-run discovery on temporal subsamples.
| Field | Type | Description |
|---|---|---|
n_subsamples | integer | Number of subsample discovery runs (each on a contiguous window). |
| ExampleHow many stretches of the utility_bill history to re-check the graph on. A few is enough to tell an edge that always appears from one that comes and goes; each extra stretch is another full pass over the data. | ||
subsample_fraction | number | Fraction of rows each subsample window covers. |
| ExampleHow much history each stretch covers — a season of readings rather than the whole record. Shorter stretches are a tougher test of the fan_speed link, but make them too short and no stretch holds enough readings to find anything. | ||
CausalEdge
| Field | Type | Description |
|---|---|---|
cause | string | Cause variable name. |
| ExampleThe upstream end — fan_speed in the fan-to-bill link. This is the thing you would actually change. | ||
effect | string | Effect variable name. |
| ExampleThe downstream end — power_bill. When you asked only about the bill, every edge ends here; on a full graph you also see the heater's effect on room_temp, and you join the two to see the route from heater to bill through the room. | ||
strength | number | Edge strength; higher = stronger causal evidence. The SCALE depends on the run: full-graph discovery (no `target`) normalises against the strongest edge, so strengths land in [0, 1]; target-scoped discovery reports the RAW effect magnitude, which routinely exceeds 1. Compare strengths only within one response — a target-scoped 0.9 and a full-graph 0.9 do not mean the same thing, and a fixed threshold does not transfer between them. |
| ExampleBoth heater_power and fan_speed drive power_bill; strength is what tells you which of them the data backs more firmly, so you know which to look at first. Compare strengths inside one response only — across two runs they are not on the same footing. | ||
lag | integer | Time lag (in samples) from cause to effect; 0 = contemporaneous. |
| ExampleHow long the bill takes to respond after the fan changes, counted in rows. Multiply by your reading interval to get real time — and note that if you only record the bill monthly, an effect that lands within a day cannot be seen at all. | ||
lag_window | array<integer> | null | [T_pre, T_post] — the history/response window (in samples) of the discovery pass that produced this edge's strength. Discovery scores every pair on TCPFN_DISC_N_LAGS windows of different scale and keeps the best; this is the one that won. None when the response was not produced by a scored pass. |
| ExampleHow much history and how long an after-window the model was given when it found this edge, in rows. Discovery tries a short, a medium and a long window for every pair and keeps the best: a fan-to-bill link that only shows up in the long window is a slow effect, and a bill recorded on a coarser grid than that window cannot show it at all. | ||
effect_sign | integer | +1, -1, or 0 (sign unknown). LIMITATION: a signed effect assumes a MONOTONE dose-response; a mechanism that reverses direction (an operating optimum — too little starves, too much jams) cannot be represented by any sign and typically fails the independence check entirely. check_data's `nonmonotone_pairs` detects that case and recommends the reshaped driver that makes it estimable. |
| ExampleWhether turning the fan up pushes power_bill up or pulls it down. This is what turns an edge into an instruction: without it you know the fan matters, but not which way to move it. | ||
p_null | number | null | Model-estimated probability the relationship is spurious (no real causal link). High p_null with non-trivial strength = treat the edge with suspicion. None when the model path did not produce judgment components. |
| ExampleThe chance the link is a mirage. Where random_noise ever coincides with power_bill by luck, this is the number that keeps it from being treated as a driver. CALIBRATION CAUTION (measured 2026-08-31, real registered-truth data): this field's raw value is poorly calibrated — near-zero readings occurred mostly on pairs that were in fact null — and its ranking power is close to chance. Until it is recalibrated, prefer the edge's veto, support and evidence fields, which are computed from the data rather than the judgment head. | ||
p_ident | number | null | Model-estimated identifiability of the relationship in [0, 1]. Low p_ident = the effect may be confounded by other variables — the edge may be real but its strength untrustworthy. None when unavailable. |
| ExampleWhether the fan's effect on the bill can be told apart from everything else going on. A low value does not mean the link is false — it means something you did not record may be moving both, so the size of the effect cannot be trusted even though the link can. CALIBRATION CAUTION (measured 2026-08-31): the raw value did not separate a known non-identifiable case from identifiable ones; the necessary-condition vetoes (the identifiable flag and veto field) are the data-side check to rely on. | ||
raw_magnitude | number | null | Raw |ATE| effect magnitude before the p_null / p_ident discounts. Distinguishes 'weak effect' (low raw_magnitude) from 'strong but suspect effect' (high raw_magnitude, heavily discounted). None when unavailable. |
| ExampleThe size of the effect before the doubts above are applied. Comparing it with strength tells you whether an edge ranks low because the effect is small or because the evidence is thin — two different problems with two different fixes. | ||
support | number | null | Present when the stability check ran: fraction of subsample discovery runs in which this edge re-appeared above the threshold. A real relationship appears in every slice (1.0); anything less is data-window-sensitive. |
| ExamplePresent when you asked for the robustness check: how much of the utility_bill history agrees this edge exists. A fan-to-bill link found in every stretch is something to act on; one found in half of them depends on which months you looked at. | ||
veto | string | null | Necessary-condition check this edge FAILED, if any: 'independent' (no association at any lag), 'reversed' (the effect leads the cause), 'confounded' (association vanishes given a named common cause), 'shared_clock' (association vanishes once a shared cycle/trend is removed), 'underpowered_for_marginal_test' (the association sits too close to the independence floor for the common-cause checks to decide either way — demoted identically, but no confounding claim is made). 'schedule_indicator' (an endpoint is a fixed schedule — the clock itself; refused outright). None = passed. A vetoed edge is kept for transparency at its gated strength; do not act on it. |
| ExampleThe check this edge failed, if any. fan_speed and power_bill on the same daily rhythm come back with veto 'shared_clock' — the link is the clock, not the fan; a decoy that never moves with the bill comes back 'independent'. Empty means the edge passed every necessary-condition check — the first thing to look at before acting. | ||
veto_detail | string | null | Evidence for the veto (association numbers, the confounder or rhythm named). |
| ExampleThe numbers behind the veto in one line: for the fan-to-bill rhythm case, the association before and after the 24-step cycle is removed and how the strength was gated. Read it to see WHY, not just that. | ||
evidence | number | null | Per-edge evidence score in [0, 1]: the model's judgment (p_ident × (1 − p_null), or strength when unavailable) × the necessary-condition gate factor (1 when no veto) × stability support (1 when the check did not run). The graph `confidence` is the mean of this over kept edges. |
| ExampleOne number per edge that already has every discount folded in: the model's judgment, times the gate if a check failed, times how often the edge reappeared across stretches of the utility_bill history. A fan-to-bill edge at 0.9 has earned it; one at 0.2 is there for transparency. The graph confidence is the average of these. | ||
orientation_confidence | string | null | Cross-sectional edges only: how decisively the arrow's direction beat the reverse direction. 'moderate' = clear winner; 'low' = the two directions scored nearly equal — the ASSOCIATION is real but the arrow is close to a coin flip (no time order to orient it). None on the temporal path, where direction comes from temporal precedence instead. |
| ExampleOnly when the rows had no time order. Without knowing which reading came first, the arrow between heater_power and room_temp has to be inferred from shape alone, and this says how clearly the chosen direction beat the opposite one. | ||
evidence_windows | array<EvidenceWindow> | null | The dated events this edge was scored on, oldest first — the moments where the cause moved sharply, with what the effect did next. Check them against your own logs: this is the edge's evidence in a form a plant record can confirm or contradict. Events that disagree with the reported direction are included. None when the response did not come from a scored temporal pass. |
| ExampleThe individual occasions this edge is built on — the times the fan actually changed, and what the bill did afterwards. Every other number here summarises those moments; this is the moments themselves, so you can take a date to your own records and see whether it happened. It is the one field you can check without trusting the model at all. | ||
evidence_agreement | string | null | Shorthand for the windows above, e.g. '6 of 7 events agree'. None when the edge has no determined direction, so there is nothing for an event to agree with. |
| ExampleHow many of those occasions went the way the edge claims, as in '6 of 7 events agree'. Not all of them will, and that is normal: the edge says the fan TENDS to push the bill up, not that it did so every single time. A count well short of all of them is worth opening up — the occasions that went the other way are listed, and they are usually the interesting ones. | ||
lag_caveat | string | null | Set when the cause or the effect changes only every few rows (a held or forward-filled column, e.g. a slow sensor joined to a fast one). Its lagged correlation is flat across the hold, so a lag shorter than the hold cannot be resolved; the lag shown is the model's and the caveat names the column and the hold. |
| ExampleSet when one side of the edge only changes every few rows — say the fan speed is logged once a minute and the bill every ten seconds, so the fan column repeats each value ten times. A delay shorter than that repeat cannot be told from the data, and this says so rather than reporting a lag of 0 as if it were measured. | ||
direction_undecided | boolean | null | True when this pair was scored in BOTH directions at lag 0 and is listed once, the stronger arrow shown. Same-row association carries no time order, so the direction is not decided by this data — treat it as a link, not an arrow. reverse_strength carries the other arrow's strength. |
| ExampleTrue when the fan and the bill moved in the same row and the model could not say which led. Both arrows scored, the stronger is shown once, and the field tells you the arrow was not decided — act on the link, not on its direction. | ||
reverse_lag | integer | null | When the reverse arrow is also in the result, its lag (0 with direction_undecided). Two arrows at DIFFERENT lags are a control loop — a controller and the process it regulates are both real causes — not a contradiction. |
| ExampleWhen the bill also appears as a cause of the fan, the delay of that reverse arrow. A thermostat is the honest case: the room drives the heater one step later and the heater drives the room three steps later — both real, and each entry names the other. | ||
reverse_strength | number | null | Strength of the reverse arrow that was folded into this entry (direction_undecided); None otherwise. |
| ExampleThe strength the weaker arrow had before the two were folded into one undecided entry. Present only with direction_undecided. | ||
block_support | number | null | Fraction of contiguous, NON-overlapping thirds of the file in which this pair's lagged association is present (model-free). 1.0 = held throughout; less = the relationship stops, starts or changes partway through, and block_note says where. The stability field's windows overlap the middle of the file and cannot see that. |
| ExampleSplit the file into three stretches that do not overlap and ask in how many the fan still moves the bill. 1.0 means all three; 0.67 means it stopped or started partway through the year — which the overlapping stability windows would have missed. | ||
block_note | string | null | Plain reading of block_support: which stretches of the file the relationship is present in, and whether its sign changes between them. |
| ExampleThe words behind block_support: 'present in 2 of 3 contiguous blocks; absent in rows 4000-6000', or that the sign flipped between stretches, which is what a retuned controller looks like. | ||
closed_loop | boolean | null | True when this arrow is the regulated PROCESS of a closed control loop: the effect also leads the cause (the controller, reported as the reverse arrow), the raw association carries the controller's sign, and the cause still moves the effect with the effect's own recent past held fixed. lag and effect_sign come from that history-adjusted fit, not from the raw peak. |
| ExampleTrue when this arrow is the regulated process of a control loop: a thermostat heats harder when the room ran cold, so room_temp leads heater_power (the controller, reported as the reverse arrow) and the raw heater/room association reads backwards. With the room's own recent past held fixed, heating still warms the room; that fit is where this edge's lag and effect_sign come from. | ||
closed_loop_note | string | null | Plain reading of closed_loop: what was measured and how to act on it. |
| ExampleThe words behind closed_loop: which way the controller pulls, the history-adjusted per-unit effect and its lag, and that the raw association carries the controller's sign — act on this arrow with the adjusted sign and lag, not the raw correlation. | ||
mediation_note | string | null | Set on a target-scoped run when this edge's association with the target is fully carried by another reported cause on this data. The edge is kept, because a target-scoped run cannot confirm the path from the cause to that mediator; a run without a target decides and removes truly indirect edges. |
| ExampleOn a run asked about one target, this says the fan's link to the bill is fully carried by room_temp on this data — the fan may only reach the bill through the room. The edge is kept, because asking about one target cannot confirm that the fan drives the room; a run without a target settles it and removes the edge if it is indirect. | ||
EvidenceWindow
One dated event behind an edge — checkable against a plant log.
| Field | Type | Description |
|---|---|---|
at | string | null | Timestamp of the moment the cause moved, taken from the source file's time column. None when the file has no parseable time column — the row indices below still locate the event. |
| ExampleWhen the fan changed, taken from the time column of the file you uploaded. This is the value you look up in your own logs. If your file has no time column the field is empty and the row number is used instead — the occasion is still real, it just has to be found by position. | ||
at_index | integer | Row index in the analysed data where the cause moved. |
| ExampleThe same moment as a row number in the data that was analysed. Useful when your file has no time column, or when you want to pull the surrounding rows out of the file yourself. | ||
window_rows | array<integer> | [first_row, last_row] of the whole window: the history before the move and the response period after it. |
| ExampleThe first and last row of the stretch that was examined: some history before the fan moved, then the period afterwards in which the bill was allowed to respond. Slice your file to these rows to see exactly what the model saw. | ||
cause_change | number | How far the cause moved — its mean over the response period minus its mean over the history before it, in the cause's own units. |
| ExampleHow far the fan moved on this occasion — its average over the period after the change, minus its average over the history before it, in the fan's own units. The sign tells you which way: a fan being turned DOWN is just as informative as one being turned up. | ||
effect_change | number | The same difference for the effect, in the effect's units. |
| ExampleWhat the bill did over that same stretch, in the bill's units. Read it together with the fan's move: both rising, or both falling, is the same relationship seen twice. | ||
agrees | boolean | null | Whether this single event moved the effect in the reported direction. None when the edge has no determined direction. Disagreeing events ARE reported: an edge is a claim about a tendency, not about every moment, and the dissenting dates are the first thing worth looking up. |
| ExampleWhether this one occasion backs the direction the edge reports. It compares the RELATIONSHIP — fan up with bill up, and fan down with bill down, both support a positive edge. Empty when the edge has no established direction, because then there is nothing for an occasion to agree with. | ||
FitReport
Summary of automatic data reduction applied before model invocation.
| Field | Type | Description |
|---|---|---|
original_shape | array | (n_samples, n_vars) before reduction. |
| ExampleThe utility_bill file as you sent it — readings by columns. Read it next to final_shape to see how much was set aside. | ||
final_shape | array | (n_samples, n_vars) actually used. |
| ExampleWhat was actually analysed. If the reading count dropped sharply, the effective gap between rows grew, and short delays between the fan and the bill may no longer be visible. | ||
estimated_working_set_mb_before | number | Estimated model working-set memory before reduction (MB). |
| ExampleHow much memory the utility_bill run was expected to need before anything was trimmed. Quote it with the budget below when asking for a bigger machine. | ||
estimated_working_set_mb_after | number | Estimated model working-set memory after reduction (MB). |
| ExampleThe same estimate after trimming. If it only just fits, a slightly larger export will be trimmed harder — worth knowing before you widen the date range. | ||
device_budget_mb | number | Device memory budget the reduction targeted (MB). |
| ExampleThe memory ceiling the trimming aimed at. Raising it is the alternative to letting the utility_bill data be thinned. | ||
applied | array<FitStep> | The reduction steps applied, in order. |
| ExampleExactly what was done, in order. Dropping a sensor that never moved costs you nothing; thinning the readings changes which delays remain detectable, so it is worth reading before you quote the result. | ||
warning | string | Warning |
| ExampleSet when the trimming probably cost you something real. Treat it as a reason to re-run the utility_bill analysis on a bigger machine before acting on what came back. | ||
FitStep
One auto-fit reduction step applied to the input data.
| Field | Type | Description |
|---|---|---|
step | string | Reduction kind: drop_constant_cols, contiguous_tail_rows, ... |
| ExampleWhich kind of trimming this was. Removing a flat sensor is harmless; thinning the readings is the step that trades away time detail between the fan and the bill. | ||
reason | string | Why this step is safe / what it preserves. |
| ExampleWhy this step was safe to take — the sentence to reach for when someone asks what was dropped from the utility_bill data and why the answer still stands. | ||
dropped_columns | array<string> | null | Columns dropped by this step, if any. |
| ExampleWhich columns this step removed. If room_temp is listed, the thermometer read the same value throughout — and a reading that never changes cannot explain a bill that does. | ||
kept_every_nth_row | integer | null | Legacy field from the retired every-Nth-row decimation step (decimation rescales lags and is no longer used); always absent on current releases. Row reduction is now a contiguous tail — see rows_after. |
| ExampleHow aggressively the readings were thinned — keeping every fourth row, say. Your effective gap between readings grows by that factor, and effects faster than the new gap disappear. | ||
rows_after | integer | null | Row count after this step. |
| ExampleHow many utility_bill readings were left after this step, as the trimming stacks up. | ||
cols_after | integer | null | Column count after this step. |
| ExampleHow many columns were left after this step, as the trimming stacks up. | ||
Diagnostics
Self-troubleshooting block attached to every tool output.
| Field | Type | Description |
|---|---|---|
data_report | DataReport | What the model analysed vs. what was uploaded. |
| ExampleWhat was analysed against what you uploaded. The first place to look when room_temp appears nowhere in the utility_bill results. | ||
switches | object | The configuration switches that change what the tools report, as set when this response was produced (defaults filled in). TCPFN_HYBRID_PRUNE=1 applies a conditional-independence prune that removes sibling edges under a shared parent (measured: clique false-positive rate 0.775 -> 0.010); with it OFF a dense graph is often that clique. TCPFN_EXOGENEITY_SCREEN is experimental and should be 0. Read this FIRST when a result surprises you: two responses with different switches are not comparable. |
| ExampleWhich configuration produced this answer. Say two colleagues run the same utility_bill panel and get graphs of different density — the usual explanation is that one had the hybrid prune on and the other did not, and without this field neither could tell. Read it before comparing any two results: TCPFN_HYBRID_PRUNE=1 removes edges between variables that merely share a parent, so a graph produced with it off is a different question answered, not a worse answer to the same one. | ||
warnings | array<Note> | Structured advisory notes — each {code, severity, message, field}. Gate on `code` for programmatic checks; read `message` for prose. Data/result-quality concerns (short series, unsigned edges, unreliable magnitude, ...). Empty = no known concerns. Plain strings are coerced to {code:'general'} for back-compat. |
| ExampleThe caveats attached to this result, each with a fixed code your own software can react to. Show them next to the numbers — they are what turns a bare effect on power_bill into a properly qualified one. | ||
errors | array<Note> | Structured BLOCKING notes — the result is present but should NOT be acted on (e.g. an effect estimate whose pair fails a necessary condition). Same shape as `warnings`, severity 'error'. Empty = nothing blocking. |
| ExampleStronger than a warning: the answer is present but should not be acted on. If anything appears here, do not take the fan_speed recommendation to the plant. | ||
empty_result_reason | string | null | Set when the tool returned no edges / no actions / no rankings: explains why (e.g. which threshold filtered everything out and what the strongest candidate scored). |
| ExampleExplains an empty answer, and distinguishes two opposite situations: the utility_bill graph came back empty because your threshold filtered everything out, or because there was genuinely nothing left to report — which can be good news. | ||
suggested_next_calls | array<string> | Concrete next tool calls to diagnose or improve the result, e.g. "retry discover with threshold=0.2" or "call check_data to test data suitability". |
| ExampleThe follow-up calls to run, written out ready to use. If a dashboard wants a 'what should I do next' button for the utility_bill result, this is what it should offer. | ||
DataReport
What the model actually analysed, versus what was uploaded.
| Field | Type | Description |
|---|---|---|
n_rows_original | integer | Rows in the uploaded file. |
| ExampleHow many utility_bill readings were in the file you sent, before anything was set aside. | ||
n_cols_original | integer | Columns in the uploaded file (including non-numeric). |
| ExampleHow many columns arrived, counting the ones that are not measurements. A timestamp column shows up here but not in the analysed count, which explains a difference of one. | ||
rows_used | integer | Rows the model actually analysed (after any downsampling). |
| ExampleHow many readings reached the model. Well below what you uploaded means much of the utility_bill history was thinned or unusable, and the result rests on less than you think. | ||
cols_used | integer | Columns the model actually analysed. |
| ExampleHow many of the columns were analysed. When fan_speed never appears in any edge, a shortfall here is the most common explanation. | ||
dropped_columns | array<DroppedColumn> | Columns excluded before analysis, each with the reason. |
| ExampleEvery column left out, with the reason — so nothing disappears quietly. A thermometer stuck at one value and a text label column are both dropped, for different reasons and with different fixes. | ||
columns_used | array<string> | The variable names the model analysed, in order. |
| ExampleThe exact list the model saw. Worth checking when a rename upstream means the column you think is power_bill is not the one being analysed. | ||
sampling_interval | string | null | Median time between rows, detected from a timestamp column when one is present (e.g. '60s', '1.0h'). Cause→effect lags shorter than one sampling interval are invisible at this resolution. None when no parseable timestamp column exists. |
| ExampleHow far apart your utility_bill readings are. This is what turns a delay of two rows into a real answer like two hours — without a usable time column, delays stay in rows. | ||
sampling_interval_seconds | number | null | Machine-readable form of sampling_interval, in seconds. |
| ExampleThe same spacing as a plain number, for doing that arithmetic in your own code rather than reading it off the page. | ||
DroppedColumn
One input column that was excluded before the model ran.
| Field | Type | Description |
|---|---|---|
name | string | Column name as it appeared in the input file. |
| ExampleThe column that was left out of the utility_bill analysis, named as it appeared in your file. | ||
reason | enum(non_numeric, constant_or_all_nan, non_finite, not_requested, analyzer_preprocessing, target_leakage, derived_mix, merged_twin) | Why the column was excluded: non_numeric (timestamps, labels), constant_or_all_nan (zero variance — no causal signal), non_finite (contains NaN/inf, which would corrupt discovery), not_requested (not in variable_names). causal_analysis only: target_leakage (a functional restatement of the target — reporting it as a cause would be circular), derived_mix (a linear mix of other columns — no information of its own), merged_twin (the same signal as another column; analysed under that column's name — see `detail` and merged_columns), and analyzer_preprocessing for anything else the analyzer's own pipeline removed (e.g. the min-variance filter). |
| ExampleWhy it went — a text label rather than a measurement, a thermometer that never moved, gaps in the readings, or simply a column you did not ask for. In causal_analysis also: a copy of the bill itself, a blend of other columns, or a twin analysed under its partner's name. Each one points at a different fix. | ||
detail | string | null | Free-text evidence behind the reason, when there is any: the leakage test that fired, the mix verdict, or for merged_twin the representative the column was analysed under and the twin evidence. |
| ExampleThe evidence behind that reason when there is any — which leakage test fired, or 'same signal as fan_speed (|spearman|=1.000); analysed under fan_speed' for a merged twin. Null for the plain structural reasons. | ||
Note
One structured diagnostic entry — a warning or an error.
| Field | Type | Description |
|---|---|---|
code | string | Stable machine key for programmatic gating, e.g. 'magnitude_unreliable', 'confounded', 'independent', 'reversed', 'shared_clock' (association carried by a shared cycle/trend), 'underpowered_for_marginal_test' (association too close to the independence floor to decide; no confounding claim made), 'schedule_indicator' (the cause or effect is a fixed schedule — a deterministic function of the clock; a conditioning variable, never a cause), 'sign_corrected', 'no_signal', 'low_consistency', 'target_leakage' (error: the variable restates the target — the result must not be acted on), 'ranking_indistinguishable', 'equivalence_classes', 'derived_mixes', 'functional_twin', 'derived_mix'. 'general' when the note is untyped prose. |
| ExampleA fixed key your software can branch on, so an alert about the power_bill estimate can be handled automatically without reading the sentence that accompanies it. | ||
severity | enum(info, warning, error) | 'error' = result present but must NOT be acted on; 'warning' = advisory; 'info' = context. |
| ExampleHow hard to stop. Some notes merely add context to the utility_bill result; others mean the number must not be acted on, and a dashboard can colour them differently on this alone. | ||
message | string | Human/LLM-readable explanation. |
| ExampleThe same point in plain language, written to be shown to whoever is reading the result. | ||
field | string | null | Output field this note qualifies (e.g. 'ate_mean'); None = general. |
| ExampleWhich number the note is about, so a caveat about the effect on power_bill is displayed beside that effect rather than at the top of the page. | ||
StabilityReport
Result (or disclosed omission) of the subsample stability check.
| Field | Type | Description |
|---|---|---|
performed | boolean | False when the check did not run this call — then `skipped_reason` says why (auto mode over the compute budget, or explicitly disabled) and no edge carries `support`. |
| ExampleWhether the re-check actually ran. It runs by default, but on a very large panel the server may skip it to stay within budget — then this is false, and no edge in the answer has been seen again on another stretch of the utility_bill history. | ||
mode | enum(requested, auto, disabled) | 'requested' = caller passed a StabilityInput; 'auto' = the default policy decided; 'disabled' = caller passed stability=false. |
| ExampleHow the re-check was decided: you asked for it, the default policy ran it, or you switched it off for a quick look. | ||
n_subsamples | integer | Subsample discovery runs performed (planned, if skipped). |
| ExampleHow many stretches of utility_bill history were re-checked. You need this to read an edge's reappearance rate — the same proportion means more when it comes from more stretches. | ||
subsample_fraction | number | Row fraction each window covered. |
| ExampleHow much history each stretch covered, which sets how demanding the check on the fan_speed edge really was. | ||
skipped_reason | string | null | Present when performed=False: the plain reason and how to force the check. |
| ExampleWhen the re-check did not run: the plain reason (how long the main pass took, what the re-checks would have cost) and the exact setting that forces it. | ||
note | string | Note |
| ExampleAny caveat about how the check ran — for instance that the utility_bill record was short enough that the stretches had to overlap. | ||
EffectScale
Per-unit scale of the effect — MEASURED from the data, not derived from the model's do-contrast. A 32-case probe with planted coefficients (scripts/probe_per_unit_horizon.py) showed no rescaling of the model ATE tracks the true coefficient; the direct lagged slope does (median 1.00x of planted truth, 23/24 cases within 2x, exact lag recovery). The model keeps identification and direction; this field carries the number an engineer acts on. The superseded ate/(dose × decay) conversion is kept as ``model_ate_conversion`` for comparison.
| Field | Type | Description |
|---|---|---|
per_unit_effect | number | MEASURED structural coefficient dY/dX: expected change in the effect (its units) per 1-unit sustained increase of the cause (its units) — the slope of the effect on the cause at the best-fitting lag, measured directly from the data. Validated on planted coefficients across beta 0.1-0.5, persistence 0.3-0.95: median 1.00x of truth, 23/24 within 2x; the one known failure regime is a WEAK effect on an ultra-persistent cause (persistence >= ~0.9), where it can OVERestimate (measured up to ~3x at beta=0.1, phi=0.95) — the magnitude warning names the regime per pair. Causal reading requires the pair to pass the necessary-condition checks; on a vetoed pair this is descriptive only. |
| ExampleThe plain statement: turn fan_speed up by one and power_bill moves by this much. This is the number to put in front of an engineer, rather than the headline effect — and it is measured straight from your data at the lag that fits best, not scaled out of the model's internal contrast. | ||
per_unit_source | string | Provenance of per_unit_effect. 'adjusted_lagged_slope': conditional slope adjusted for the effect's own recent history — robust to a controller regulating the cause (closed-loop control sign-inverts the marginal slope by design); shipped when its fit is decisive. 'measured_lagged_slope': the marginal data-side slope (fallback when the adjusted fit is unavailable). |
| ExampleSays how the per-unit number was produced — 'adjusted_lagged_slope' means the slope was measured with the effect's own recent history accounted for, which keeps a thermostat-style controller from inverting it; 'measured_lagged_slope' is the plain two-column slope, used when the adjusted fit is not decisive. | ||
per_unit_lag | integer | null | The lag (in sampling steps) at which the slope was measured over the adaptive window. |
| ExampleAt what delay the fan's effect shows up in the bill — the lag the slope above was measured at. | ||
marginal_slope | number | null | The UNADJUSTED pairwise slope, kept alongside for comparison. When its sign disagrees with per_unit_effect the cause is likely under closed-loop control (see closed_loop_detected) — the marginal number is then the controller's artifact, not the causal effect. |
| ExampleThe naive two-column slope, kept for comparison. On a regulated process it can point the WRONG WAY — a heater that genuinely warms the room reads negative because the thermostat only runs it when the room is cold. When this and per_unit_effect disagree in sign, believe per_unit_effect. | ||
marginal_lag | integer | null | The lag of the marginal (unadjusted) slope. |
| ExampleThe delay at which the naive slope was measured. Under a controller it often locks onto the controller's instant reaction (lag 0) instead of the true physical delay. | ||
closed_loop_detected | boolean | null | True when the adjusted and marginal slopes disagree in sign — the signature of a controller regulating the cause (it raises the cause exactly when the effect runs low, inverting the observed association). Treat the marginal association and any naive reading of the raw data with caution on this pair. |
| ExampleTrue means the two slopes disagree in sign — the fingerprint of a controller regulating the cause (it raises the heater exactly when the room runs cold). A scatter plot of the raw pair will look backwards on such a process; that is the controller talking, not the physics. | ||
model_ate_conversion | number | null | The superseded ate_mean/(dose_slope * horizon_decay) conversion, kept for cross-checking the measured slope. Measured to UNDERestimate systematically (median ~0.3-0.5x of planted truth, sign flips in weak/persistent regimes) — do not act on it; when it and per_unit_effect diverge wildly, treat the pair's magnitude with extra caution. |
| ExampleThe older way of getting the per-unit number, kept so the two can be compared. When they tell very different stories, trust neither blindly and look at the pair. | ||
sign_agreement | boolean | null | Whether the measured slope and the sign-resolved model contrast agree on direction. False = the two rulers disagree — inspect before acting on either sign. |
| ExampleDo the data slope and the model agree the fan RAISES the bill? False means one of them says down while the other says up — look at the pair before acting on either. | ||
dose_slope | number | null | Cause units represented by the encoded treatment's 0→1 swing in this panel (regression of the cause's intervention-point level on the encoded t; ~4x the cause's σ at the analysis windows). Input to model_ate_conversion; None when the dose could not be measured. |
| ExampleHow large a change in the fan the model's internal 'push' represents in your data. You rarely read it directly — it is what converts the headline number into the per-unit one above. | ||
cause_persistence | number | null | Lag-1 autocorrelation of the cause — how long an impulse to the cause persists before mean-reverting. |
| ExampleHow long a change to the fan tends to stay put before drifting back. A setting that holds by itself is a different proposition from one someone must keep re-applying for the saving to last. | ||
horizon_decay | number | null | Mean of persistence^(h-1) over the forecast horizons: the fraction of the initial dose still present under an AR(1) assumption. Input to model_ate_conversion only. |
| ExampleHow much of that change is still in place by the end of the window being measured. It explains why the headline effect on the bill is smaller than a simple multiplication would suggest. | ||
EventStudy
During-vs-local-baseline evidence for a sparse, event-shaped cause.
| Field | Type | Description |
|---|---|---|
n_events | integer | Events detected at the threshold. |
| ExampleHow many times the gate actually opened past the threshold in this data — the raw event count before quality filtering. | ||
n_used | integer | Events with a clean local baseline (not contaminated by a neighbouring event) — the sample behind every number here. |
| ExampleEvents that had a clean quiet stretch just before them to compare against. Every number in this block rests on these. | ||
threshold | number | Cause level defining an event (default: median of above-resting excursions). |
| ExampleWhat counts as 'the gate is open': cause values at or above this. Picked from the data (the typical excursion), not tuned. | ||
duty | number | Fraction of samples above the cause's resting level; the shape gate refuses above 0.35 (smooth/cyclic causes). |
| ExampleHow much of the time the cause is away from its resting level. Small = a true event cause; above 0.35 the block refuses because a smooth or cyclic signal is not events. | ||
baseline_len | integer | Length (steps) of the local pre-event baseline window. |
| ExampleHow many steps of 'just before' each event is compared with. | ||
event_len_median | integer | Median event length in steps. |
| ExampleTypical event duration in steps. | ||
mean_delta | number | Mean over events of (effect during event − effect in local baseline), in the effect's own units. THE event-study answer. |
| ExampleThe sentence for the plant manager: while the gate was open, missions ran this much lower (negative) or higher than just before it opened — measured on the real events, in the effect's own units. | ||
median_delta | number | Median per-event delta (robust twin). |
| ExampleSame story ignoring extreme events — quote this when one monster event dominates the mean. | ||
z | number | mean_delta against a placebo null (same number+durations of events at deterministic random non-event positions). |z| >= 2 is the bar `direction` uses. |
| ExampleHow far the measured change sits outside what identically shaped FAKE events at random times produce. Past ±2 the change belongs to the events, not to chance. | ||
n_negative | integer | Events where the effect fell vs baseline. |
| ExampleIn how many individual events the effect fell. 60 of 71 falling is a story no average can fake. | ||
n_positive | integer | Events where the effect rose vs baseline. |
| ExampleIn how many individual events the effect rose. | ||
direction | string | 'decrease' / 'increase' when |z| >= 2, else 'unclear'. Direction of the CHANGE AROUND EVENTS — see the class docstring for the reverse-causation caveat. |
| Example'decrease' = the effect drops around these events (backed by |z| >= 2). 'unclear' = the events do not move the effect beyond chance. Remember it reads the change AROUND events — if breakdowns trigger the gate rather than the reverse, timing alone cannot say which way the arrow points. | ||
gap_cv | number | null | Coefficient of variation of inter-event spacing; None with too few events to judge. |
| ExampleHow irregular the event spacing is. Near zero means the events follow a timetable. | ||
schedule_caution | boolean | True when events are near-regularly spaced (gap_cv < 0.3): the change could belong to the schedule the events follow, not the cause itself. |
| ExampleTrue when events run on a near-fixed timetable — whatever changes around them could belong to the timetable (shift start, nightly job), not the events. | ||
pre_trend_delta | number | Mean drift of the effect in the window BEFORE the baseline vs the baseline itself — nonzero means the effect was already moving before events started. |
| ExampleWas the effect already drifting before events began? Nonzero here means part of the 'change' predates the event. | ||
pre_trend_caution | boolean | True when the pre-trend exceeds half the measured delta — treat the delta as contaminated by an ongoing trend. |
| ExampleTrue when that pre-drift is big enough (over half the measured change) to contaminate the answer. | ||
aligned_pre_mean | array<number> | Mean effect over the baseline_len steps before each used event (aligned at event start) — the left half of the replay chart. |
| ExampleThe left half of the replay picture: the effect's average path in the steps before an event starts. | ||
aligned_during_mean | array<number> | Mean effect over the first event_len_median steps of each sufficiently long event — the right half of the replay chart. |
| ExampleThe right half of the replay picture: the effect's average path once the event is on. Chart the two halves and the drop is visible to anyone. | ||
aligned_post_mean | array<number> | Mean effect over up to 2x baseline_len steps AFTER each used event ends (bins inside another event masked; trailing bins kept only while >= 5 events contribute) — the recovery tail of the replay chart. Empty on blocks computed before this field existed. |
| ExampleThe recovery tail of the replay picture: the effect's average path after the event ends. Coming back to the pre-event level is itself evidence the event was the cause. | ||
horizon_deltas | array<number> | MEASURED per-horizon impact: for step h (1-indexed) from event onset, mean over observable events of (effect at that step − the event's own clean local baseline), centred on the placebo null. The measured counterpart of the model's per-period impact table for event-shaped causes; may stop early when fewer than 5 events remain observable, and is EMPTY (never fabricated) when the clean-slot space cannot support a per-horizon null. |
| ExampleThe measured impact table: at 1 step after the event started, 2 steps after, and so on, how far the effect sat from its own just-before level, averaged over the real events. For event causes quote THIS table, not the model's per-period one — it compares each event with its own quiet moments instead of busy periods with quiet periods. | ||
horizon_lo | array<number> | Per-horizon 95% lower bound (placebo-null spread), aligned with horizon_deltas. |
| ExampleFor each step of that table, the low end of where the true impact plausibly lies (95%). A range that stays below zero means the drop at that step is beyond chance. | ||
horizon_hi | array<number> | Per-horizon 95% upper bound (placebo-null spread), aligned with horizon_deltas. |
| ExampleFor each step of that table, the high end of where the true impact plausibly lies (95%). A range straddling zero means that step proves nothing on its own. | ||
horizon_n | array<integer> | Events contributing at each horizon (events end, panels end, and bins inside another event are masked), aligned with horizon_deltas. |
| ExampleHow many real events could still be observed at each step (events end, data ends, and steps inside a following event are excluded). | ||
DisambiguationAction
One proposed action — measure X, intervene on Y, etc.
| Field | Type | Description |
|---|---|---|
action_type | string | Kind of action: 'measure', 'intervene', etc. |
| ExampleWhether the suggestion is to start recording something — the thermostat setting next to room_temp — or to run a deliberate test, such as holding the fan steady for a week. Watching is usually cheap; testing is usually conclusive. | ||
target_variable | string | Variable to measure or intervene on. |
| ExampleWhat to measure or act on. Note it is often not one of your existing columns at all — the point is that it is missing from the utility_bill file. | ||
expected_gain | number | Expected reduction in causal uncertainty if performed. |
| ExampleHow much this would clear up, so you can choose between suggestions when you cannot instrument everything at once. | ||
rationale | string | null | Why this action helps, in plain language. |
| ExampleWhy this particular measurement unblocks the question — the sentence to put in the request for a new sensor. | ||
CandidateRanking
| Field | Type | Description |
|---|---|---|
cause | string | Candidate intervention variable. |
| ExampleThe lever this row is about — the fan, or the heater. | ||
ate_mean | number | Average treatment effect on the target (cross-fitted over all natural experiments) — a do-contrast response in the target's units, NOT a per-unit coefficient. Cross-fitting stabilises the RANKING (the order is the reliable signal); the absolute scale is over-attributed and not a literal effect size. Still read distinguishable_from_next / the ranking_indistinguishable diagnostic — adjacent candidates whose bands overlap should not be treated as confidently ordered. |
| ExampleIts effect on power_bill, and what the ordering is based on. Compare these rows with each other; do not carry a number across to a different run or a different target. | ||
ate_std | number | Cross-unit spread of the effect. |
| ExampleHow much that effect varied between occasions. A wide spread means the lever works on average but not every time — worth knowing before promising a saving. | ||
consistency | number | Effect HOMOGENEITY (1 - coefficient of variation, clamped to [0,1]): 1.0 = uniform effect across units. 0.0 is COMMON and is not distrust — it means effects vary across units more than their mean, which is true of most real heterogeneous causes. For 'can I trust the direction', read sign_certainty. |
| ExampleHow alike the effect was across occasions for this lever. As elsewhere it is a description, not the thing that decides whether the row is actionable — 0.0 here is routine for a genuine cause whose effect varies by conditions. For trust, read sign_certainty. | ||
sign_certainty | number | Probability that the SIGN of the mean effect is right, from the t-statistic of the mean (erf(|t|/sqrt(2)), in [0,1]). This is the number to read as confidence in the candidate: ~1.0 = the direction is statistically solid even when per-unit effects are heterogeneous (consistency 0.0). |
| ExampleThe number to read as confidence in this row: how sure the tool is that the effect's DIRECTION is right. A lever can be near 1.0 here while consistency reads 0.0 — the fan reliably raises the bill even though how much varies with the weather. Low values mean even up-versus-down is unsettled. | ||
identifiable | boolean | False when the candidate fails a necessary condition for causation (independent of / not preceding / confounded with / sharing only a cycle or trend with / a functional restatement of the target). ate_mean is still reported for transparency, but such a candidate cannot be a reliable lever and is ranked below the identifiable ones. |
| ExampleWhether this lever passed the trust checks. Rejected candidates are still listed rather than hidden, so you can see that random_noise was considered and dismissed — but they sit below every genuine lever no matter how big their number. | ||
reliability_note | string | null | Why the candidate was flagged not identifiable (present only when identifiable is False). |
| ExampleOn a rejected row, which check it failed. That is the actionable part: 'something else explains both' points at measuring that something, while 'these never move together' points at dropping the idea. | ||
distinguishable_from_next | boolean | null | Whether this candidate's effect is statistically distinguishable from the next-ranked one: False means their ±1σ effect bands overlap, so the order between them is driven by noise, not signal — do not treat the higher rank as a confidently bigger lever. None for the last row and for non-identifiable candidates. |
| ExampleWhether this lever is really ahead of the one below it or merely listed first. When the fan and the heater are too close to separate, this says so — and you should not present the order as settled. | ||
equivalence_class | string | null | Set when this candidate is functionally tied to other candidates (a transform, copy, or restatement of the same signal). All members share one class key in equivalence_classes. Members of one class SPLIT the underlying effect between them — compare the CLASS against other candidates, not its members against each other; only an intervention can distinguish within a class. |
| ExampleSet when this row is one face of a signal that appears under several names. Rows sharing a class label split one effect between them — compare the class to the other candidates, not its members to each other. | ||
LeverCrossCheck
Model-free ordering of the ranked levers, as a second ruler.
| Field | Type | Description |
|---|---|---|
agrees | boolean | True when the data-only ordering puts top_lever (or its equivalence class) first. |
| ExampleTrue when plain lagged regression on the data names the same #1 as the model (or a copy of it). False is the important case: two methods disagree about the strongest lever. | ||
data_top | string | null | The lever the data-only ordering puts first. |
| ExampleThe lever the data-only ordering puts first. | ||
data_ordering | array<LeverScore> | Data-only ordering, strongest first: standardised effect of a one-spread change in the lever, in target spreads, at its best lag. |
| ExampleThe data-only ordering itself, strongest first, so you can see whether the disagreement is a close call or a reversal. | ||
note | string | How to read the agreement or disagreement. |
| ExampleThe agreement or disagreement in words. | ||
LeverScore
| Field | Type | Description |
|---|---|---|
cause | string | Cause |
| ExampleThe lever this row scores. | ||
score | number | Absolute standardised effect (target spreads per lever spread), partialled against the other levers. |
| ExampleHow much power_bill moves, in its own spreads, for a one-spread change in this lever — with the other levers held fixed. Units cancel, so fan_speed and heater_power are comparable on it. | ||
lag | integer | Lag (steps) at which the lever's association with the target peaks. |
| ExampleHow many steps after a change in the lever the bill responds most — the delay the model-free ordering measured at. | ||
UnrankedDriver
A candidate that could NOT be ranked by effect size (too rare for the natural-experiment estimator) but whose raw event timing shows it likely drives the target. Surfaced so a rare-event root cause (a fault counter, an out-of-service span) is never silently hidden behind a ranking of dense proxy variables. Evidence is simple event counting — deliberately NOT a model score, so it is robust and direction-correct on rare events.
| Field | Type | Description |
|---|---|---|
cause | string | Candidate variable, too rare to rank by effect size. |
| ExampleA lever that fires too rarely to be sized, but whose timing still implicates it — a defrost cycle that runs a handful of times a year, say, rather than the fan that runs constantly. | ||
n_events | integer | Times this variable departed from its baseline value. |
| ExampleHow many times it actually happened. A small count is precisely why it could not be ranked alongside the fan, and why the evidence here is about timing instead. | ||
hit_rate | number | Fraction of those events where the target went elevated (>2 sigma) at or within 6 samples — the direct evidence this drives the target. |
| ExampleHow often power_bill jumped shortly after one of those occasions. This is the evidence: either the bill follows the events or it does not. | ||
chance_rate | number | Baseline probability of such an elevation at a random moment. |
| ExampleHow often the bill jumps anyway, at any random moment. Without this, a rate above looks impressive when it may be ordinary. | ||
lift | number | hit_rate / chance_rate; >= 2 required to appear here. |
| ExampleHow much more often the bill followed this lever's events than it would by chance. That ratio is what earns a place on this list. | ||
note | string | Plain-language summary of the evidence. |
| ExampleThe evidence in words — the sentence to use when explaining why a lever with no measured effect size still deserves attention. | ||
VariableCheck
Suitability report for one input variable.
| Field | Type | Description |
|---|---|---|
name | string | Variable (column) name. |
| ExampleWhich column this row is about — heater_power, room_temp, and so on. | ||
ok | boolean | True if no issues were found for this variable. |
| ExampleThe quick verdict for that column. Scan these first in a wide file: one bad sensor explains a strange graph faster than anything about the model does. | ||
issues | array<string> | Human-readable issues found (constant, slow-varying, non-stationary, ...). |
| ExampleWhat is wrong, in words — the thermometer never moved, the bill only drifts, the readings trend upwards all year. Each points at a different remedy. | ||
std | number | Standard deviation of the variable. |
| ExampleHow much the column moved at all. Almost nothing means a sensor that sat still, and something that never changed cannot be shown to change anything else. | ||
missing_fraction | number | Fraction of missing (NaN) values. |
| ExampleHow much of the column is missing. Heavy gaps quietly shrink the usable history even when the line count looks healthy. | ||
autocorr_lag1 | number | Lag-1 autocorrelation. Values very close to 1.0 mean the signal is slow-varying; discovery relies on sharp changes (natural experiments) and may score such variables near 0. |
| ExampleHow much each reading looks like the one before. Very high means a slow drift with no distinct events to learn from; very low means no pattern in time at all, which is what random_noise looks like. | ||
sharp_change_count | integer | Number of sharp level changes detected. 0 means the natural-experiment finder will have nothing to work with for this variable as a cause. |
| ExampleHow many times this column clearly changed level — the moments when someone turned the fan up. These are what effect estimation learns from, and a column with none is the direct explanation for 'not enough data'. | ||
stationary | boolean | null | Augmented Dickey-Fuller verdict at p<0.05. False = trending / non-stationary, which inflates spurious edges. None if the test could not run. |
| ExampleWhether the column holds a steady level or drifts. A drifting heater reading and a drifting bill will look related simply because both rise over the year, whether or not one causes the other. | ||
DerivedColumn
One column that is a transform or restatement of other columns.
| Field | Type | Description |
|---|---|---|
name | string | Column name as it appeared in the input file. |
| ExampleThe column the verdict is about, named as it appeared in your file. | ||
kind | enum(target_restatement, mix, equivalence_class) | target_restatement: reconstructs the target itself — reporting it as a cause would be circular, and the analysis tools reject it before discovery. mix: a transform/combination of other columns (a*b, a/b, f(a)); it carries no information its sources do not. equivalence_class: mutually derivable with the columns in `related` — only one of the group is independent information, and which one is the measurement cannot be decided from the data alone. |
| ExampleWhich kind of not-a-measurement it is. A target_restatement is the bill wearing a hat — reporting it as a cause of the bill would be circular. A mix is arithmetic on other columns, like heater times fan. An equivalence_class means two or more columns carry one column's worth of information and the data cannot say which of them is the real reading. | ||
reason | string | The measured evidence for the verdict (fit quality, lag, tier). |
| ExampleThe evidence behind the verdict, so you can argue with it — which test fired, how well the column was reconstructed, and at what lag. | ||
related | array<string> | The other columns implicated: the rest of the equivalence class, or — for a mix — A minimal set of columns it is derivable from at the same evidence bar that produced the flag (not necessarily the formula: minimal sets are not unique under collinearity). Empty when the verdict is against the target alone, or when the sources could not be isolated — e.g. calendar parts of a timestamp that is not in the file are not computable from each other. |
| ExampleThe other columns caught up in it: the rest of the group, or a minimal set of columns the mix can be rebuilt from — what tells you which family to thin out. It is a derivable-from set, not the formula. Empty for a target-only verdict, or when no small source set exists — columns that merely share an origin outside the file (the hour and weekday of one absent timestamp) cannot be linked from the data alone. | ||
DerivedGroup
One family of columns that share information.
| Field | Type | Description |
|---|---|---|
columns | array<string> | The family, sorted; every member appears in derived_columns or is a source of one that does. |
| ExampleOne family: a total, the average built from it, the index built from both — columns that carry one signal between them. Keep one, preferably the one a sensor measured; the data cannot say which member that is. | ||
reason | string | Why these columns form one family (the evidence tier). |
| ExampleHow much of the family is independent measurement and how much is reconstruction, so you know how many columns the family is really worth. The per-column evidence sits in derived_columns. | ||
NonMonotonePair
A candidate→target pair whose dependence is REAL but NON-MONOTONE — the measured blind spot of every linear/rank statistic the tools use. A cause with an operating optimum (deviate either way, the effect drops) reads as "independent" to the gate and cannot be represented by a signed monotone effect; this row turns that silently empty answer into an explained one. Detection rule (nonmonotone module): shape statistic eta² over lags and bin counts, circular-shift null, >= 10% of the outcome's variance, confirmed independently in each half of the data — measured zero false alarms across 1,700+ null pairs.
| Field | Type | Description |
|---|---|---|
cause | string | Candidate whose SHAPE carries the dependence. |
| ExampleThe column whose SHAPE drives the target — say fan_speed has a sweet spot: the bill rises when it is set too low or too high. | ||
target | string | Outcome column the shape explains. |
| ExampleThe outcome the shape explains — power_bill in that example. | ||
eta2 | number | Fraction of the target's variance explained by the cause's binned means at the best lag — shape-agnostic dependence strength. |
| ExampleHow much of the bill's movement the fan's setting explains once you stop assuming a straight line — 0.85 means the shape carries almost everything a straight-line test saw nothing of. | ||
linear_assoc | number | The gate's max |Pearson|,|Spearman| over lags for the same pair — how invisible the dependence is to monotone statistics. |
| ExampleWhat the straight-line/rank tests see for the same pair — near zero, which is exactly why discover came back empty. | ||
corr_threshold | number | The pair's independence cut; linear_assoc sits below it (that is what makes this the blind spot). |
| ExampleThe independence bar the pair fails to clear on the straight-line reading; being under it is what made the result silent. | ||
lag | integer | Lag (samples) at which the shape is strongest. |
| ExampleHow many samples after a fan change the shaped response shows up in the bill. | ||
recommendation | string | GUIDANCE, never an automatic transformation: the derived column to add (e.g. distance from the operating optimum) so the monotone tools can estimate the effect. |
| ExampleThe concrete next step: plot bill against fan, find the sweet spot c, add a column like abs(fan - c), and re-run with that as the candidate. Guidance only — your data is never transformed behind your back. | ||
PriorCoverageFlag
One detected data pattern the training prior does not cover.
| Field | Type | Description |
|---|---|---|
pattern | enum(oscillatory_dynamics, long_period_seasonality, near_linear_system, discrete_variables) | Which known-uncovered pattern was detected. |
| ExampleWhich unfamiliar shape was spotted — a strong repeating cycle, or readings that swing chaotically. These are the behaviours the model was not trained on. | ||
affected_variables | array<string> | Variables exhibiting the pattern. |
| ExampleWhich of your columns show it, so you can judge whether they are central to your question or beside the point. | ||
evidence | string | What was measured in THIS dataset (numbers included). |
| ExampleWhat was actually measured in your own data, so the warning can be checked rather than taken on faith. | ||
benchmark_note | string | Why this pattern is outside validated competence — cites the benchmark result or training-prior fact. |
| ExampleWhy this shape sits outside what has been validated. Use it when someone asks how we know the model struggles here. | ||
recommendation | string | What to do about it. |
| ExampleWhat to do about it — usually reshape the column first, or treat any result involving it as unproven. | ||
RootCauseItem
One ranked root cause with per-method attribution scores.
| Field | Type | Description |
|---|---|---|
variable | string | Candidate cause variable name. |
| ExampleThe column being blamed — the fan, the heater, the room. | ||
lag | integer | Lag (in samples) from this cause to the event. |
| ExampleHow long before the spike this column moved. Check it against what you know of the plant: something that supposedly acted moments before a slow-warming room deserves suspicion. | ||
combined_score | number | Weighted combination of all attribution methods; the ranking key. NOT a probability: may exceed 1.0 when the descendant-anomaly boost fires (an upstream cause whose downstream symptoms are also anomalous gets lifted above them — clamping would collapse boosted candidates into ties and change RCA rankings, so the overflow is intentional). |
| ExampleWhat the ranking is based on, pulling together all the evidence below. Read the order rather than the number — it says the fan dominates this incident, not that it accounts for a precise share of it. | ||
direction | string | 'increase' or 'decrease' — how this variable moved vs. its normal range before the event. |
| ExampleWhich way the column had moved before the spike — the fan running high, the heater running low, or nothing out of the ordinary. This is what makes the finding actionable. | ||
anomaly_score | number | AERCA anomaly z-score (0-1). |
| ExampleHow unusual this column looked on its own. Something can be very unusual that day and still have had nothing to do with the bill. | ||
esd_score | number | ESD outlier score (0-1). |
| ExampleA second, independent opinion on how unusual it was, so the verdict does not rest on a single test. Agreement between methods is what makes a culprit convincing. | ||
conditional_anomaly_score | number | Parent-conditional anomaly (0-1). |
| ExampleHow unusual it looked once its own drivers are taken into account. A room that is hot only because the heater was on is passing the problem along, not causing it. | ||
noise_attribution | number | GCM noise contribution (0-1). |
| ExampleHow much of the spike traces to something happening in this column itself rather than upstream of it. A high value is what being the origin looks like. | ||
icc_score | number | Intrinsic causal contribution (0-1). |
| ExampleWhat this column added beyond what it merely passed on — the measure that separates where the trouble started from the links in the chain. | ||
counterfactual_score | number | Counterfactual severity reduction (0-1). |
| ExampleHow much of the spike would have gone away had this column stayed normal. The most decision-shaped part: it estimates what fixing this would actually have bought you. | ||
shapley_value | number | Causal Shapley value (contributions sum to ~1). |
| ExampleThis column's share of the blame, with the shares adding up. Use it when the answer is not one culprit but several contributors you need to weigh against each other. | ||
CausalChainItem
A multi-hop causal path leading into the event.
| Field | Type | Description |
|---|---|---|
path | array<CausalChainStep> | Ordered hops from the upstream cause to the target. |
| ExampleThe route the trouble took, in order — heater to room to bill. Read it left to right as the sequence of what happened. | ||
total_lag | integer | Sum of lags along the path (samples). |
| ExampleHow long the whole route took. Sanity-check it against the plant: a chain that supposedly completed faster than a room can warm up is a sign of a link that is not real. | ||
strength | number | Product of the hop strengths along the path. Because every hop multiplies by a value <= 1, longer chains always score lower — compare chains of the same length, not a long chain against a one-hop shortcut. |
| ExampleThe product of the step strengths — heater-to-room times room-to-bill. Longer routes tend to score lower because each extra step multiplies in another factor: compare routes of similar length, and never dismiss the full route just because a one-hop shortcut carries a bigger number. | ||
CausalChainStep
| Field | Type | Description |
|---|---|---|
variable | string | Variable at this hop. |
| ExampleThe column at this point in the route — the room, on the way from heater to bill. | ||
lag | integer | Lag (in samples) at this hop. |
| ExampleThe delay contributed by this step alone, so you can see which part of the route accounts for most of the wait. | ||
CounterfactualItem
| Field | Type | Description |
|---|---|---|
variable | string | Variable held at its normal value in the counterfactual. |
| ExampleThe column being held at its normal value in the what-if — supposing the fan had run as usual that week. | ||
severity_reduction | number | Estimated fraction (0-1) by which event severity would drop if this variable had stayed normal. |
| ExampleHow much smaller the spike would have been in that case. It is what separates the fix that would have prevented the incident from the one that would only have softened it. | ||
DashboardEngine
Provenance for everything in the result.
| Field | Type | Description |
|---|---|---|
model | string | Engine that produced this result. |
model_version | string | null | Weight version. |
target | string | The variable analysed. |
objective | string | null | Display label for what the target represents. |
DashboardKpis
Headline numbers for the result header.
| Field | Type | Description |
|---|---|---|
n_events | integer | null | How many significant events were detected on the target. |
n_events_increase | integer | null | Of those, how many were above-baseline excursions. |
n_events_decrease | integer | null | Of those, how many were below-baseline excursions. |
target_is_binary | boolean | null | True when the target is a 0/1 flag rather than a continuous reading. |
total_downtime_min | number | null | Total minutes spent inside an event. |
avg_duration_min | number | null | Mean event duration, in minutes. |
median_duration_min | number | null | Median event duration, in minutes. |
max_duration_min | number | null | Longest single event, in minutes. |
n_causal_vars | integer | null | Variables found to drive the target. |
n_total_vars | integer | null | Variables analysed in total. |
n_sig_links | integer | null | Edges that survived the strength threshold. |
n_total_links | integer | null | Edges considered before thresholding. |
n_sections | integer | null | Distinct process sections represented among the variables. |
global_tau_max | integer | null | Largest lag considered, in samples. |
global_tau_max_hours | number | null | The same maximum lag expressed in hours. |
sampling_interval_seconds | number | null | Detected seconds between readings. |
top2_cause_names | array<string> | The two most frequently implicated drivers. |
top2_cause_counts | array<integer> | How often each of those two led an event. |
top2_cause_pct | number | null | Share of events those two account for. |
top_cause_scores | array<number> | Attribution scores of the leading drivers. |
health_score | number | null | Percentage of the period spent outside any event. |
minutes_since_last_event | number | null | Minutes from the last event to the end of the data. |
next_event_eta_hours | number | null | Hours to the next event, from the mean recurrence interval. Null with fewer than two events. |
DashboardEvent
One detected event on the target.
| Field | Type | Description |
|---|---|---|
event_id | integer | Identifier used to look this event up in the per-event blocks. |
start_idx | integer | Row where the event began. |
end_idx | integer | Row where the event ended. |
duration_steps | integer | null | Length in rows. |
duration_min | number | null | Length in minutes, via the detected sampling interval. |
start_time | string | null | Timestamp of the start, when the data carries one. |
explained | boolean | null | Whether a root-cause analysis was run for this event. |
top_cause | string | null | Highest-attribution driver for this event. |
tag | string | null | Display label for that driver. |
top_contribution | number | null | That driver's attribution score. |
n_risk_factors | integer | How many contributors were identified. |
anomaly_direction | string | null | 'increase', 'decrease', or 'onset' for a binary flag. |
process_section | string | null | Section the leading driver belongs to. |
model | string | null | Engine that produced the explanation. |
DashboardCausalEffect
One driver's effect on the target, across the whole history.
| Field | Type | Description |
|---|---|---|
cause | string | The driving variable. |
tag | string | null | Display label. |
also_recorded_as | array<string> | Other column names carrying this same signal (functional twins merged before the joint model ran). Both names refer to this one cause; see merged_columns for the evidence. |
description | string | null | Human description from the field config. |
process_section | string | null | Section the driver belongs to. |
section_nice | string | null | Display form of the section name. |
tcpfn_strength | number | null | TCPFN edge strength — the unambiguous magnitude. |
total_mci | number | null | Inherited key: carries the SIGNED TCPFN strength, not a partial-correlation statistic. |
max_mci | number | null | Inherited key: equals total_mci, since TCPFN returns one best lag per edge. |
lag_at_max | integer | null | Lag in samples at which the effect is strongest. |
lag_hours | number | null | That lag in the dataset's real cadence. |
n_sig_lags | integer | Lags found significant — 1 for a direct edge, 0 when the driver reaches the target only via a chain. |
lag_window | array<integer> | null | [T_pre, T_post] of the discovery window that produced the direct edge — which of the scored lag configurations found this driver. None for chain-only drivers. |
sign | string | null | Whether raising the driver raises or lowers the target. |
sign_source | string | null | Where the sign came from (discovery, or the effect estimate). |
is_direct_edge | boolean | False when the driver acts only through intermediates. |
rca_combined_score | number | null | Attribution score from the root-cause pass. |
rca_anomaly_score | number | null | How unusual this driver looked around the events. |
rca_counterfactual_score | number | null | How much of the event severity it accounts for. |
direction | string | null | Direction the driver moved before the events. |
EventRiskFactor
One contributor to one event.
| Field | Type | Description |
|---|---|---|
tag | string | null | Display label. |
variable | string | The contributing variable. |
description | string | null | Human description from the field config. |
process_section | string | null | Section it belongs to. |
section_nice | string | null | Display form of the section name. |
contribution | number | null | Share of this event attributed to it. |
lag | integer | null | How many samples before the event it moved. |
lag_hours | number | null | That lag in real time. |
direction | string | null | Which way it moved. |
onset_row | integer | null | Row where this variable's deviation started before the event (its verified onset). Null when history was too short to judge. |
link_confirmed | boolean | null | True: on the causal map AND deviated before this event. False: deviated before this event but has no map edge into the target — a real mover whose causal link is unconfirmed. Rows without a deviation in this event's window are not listed at all. |
evidence | string | null | What earned this row its place. "onset": the standard proof — a sustained multi-sigma deviation before the event. "timing+repetition": the weak-signal tier — the move stayed under the onset bar, but it landed exactly at this variable's wired lag before the event AND the same variable repeats that timing across several of the file's events, which chance cannot do. Treat these as strong leads rather than onset-proven causes. |
rca_combined_score | number | null | Overall attribution score. |
rca_anomaly_score | number | null | How unusual it looked. |
rca_counterfactual_score | number | null | Severity reduction had it stayed normal. |
SensorTimeline
Chart data for one event.
| Field | Type | Description |
|---|---|---|
time_rel | array<number> | Hours relative to the event, 0 at the event itself. |
event | array<number> | The TARGET's own values across the window — a series, not a moment. Distinct from the top-level `events`. |
variables | object | Per-variable traces, keyed by variable name. |
idx_lo | integer | null | First row of the window. |
idx_hi | integer | null | Last row of the window. |
TimelineVariable
One variable's trace across an event window.
| Field | Type | Description |
|---|---|---|
raw_values | array<number> | The variable's readings across the window. |
z_scores | array<number> | Those readings as deviations from the variable's own normal. |
is_risk | boolean | Whether this variable was implicated in this event — drives the line colour. |
sample_idx | integer | Index of the pre-event peak deviation; the marker position. |
has_precursor | boolean | False when no real pre-event excursion exists, so the marker sits on noise and should be suppressed. |
normal_median | number | null | The variable's typical level outside events. |
value_at_peak | number | null | Reading at the marked index. |
z_at_peak | number | null | Deviation at the marked index. |
description | string | null | Human description from the field config. |
EarlyWarningItem
A driver that moves BEFORE the target does.
| Field | Type | Description |
|---|---|---|
cause | string | The leading variable. |
tag | string | null | Display label. |
description | string | null | Human description. |
total_mci | number | null | Inherited key: signed TCPFN strength. |
median_lag_hours | number | null | Typical warning time before the target reacts. |
early_warning_score | number | null | How useful it is as an alert — strength weighted by lead time. |
process_section | string | null | Section it belongs to. |
tcpfn_strength | number | null | The unambiguous magnitude. |
SensitivityItem
How much moving one variable would move the target.
| Field | Type | Description |
|---|---|---|
variable | string | The lever. |
tag | string | null | Display label. |
description | string | null | Human description. |
increase_effect | number | null | Effect of raising it. |
decrease_effect | number | null | Effect of lowering it. |
net_effect | number | null | Combined effect. |
priority | number | null | Ranking key for which lever to consider first. |
process_section | string | null | Section it belongs to. |
basis | string | null | What the number was derived from. |
CausalNetwork
The graph, in the two shapes the diagram needs.
| Field | Type | Description |
|---|---|---|
sensor_network | SensorNetwork | Nodes and edges. |
causal_chains | CausalChains | Multi-hop routes. |
SensorNetwork
Nodes and edges for the network diagram.
| Field | Type | Description |
|---|---|---|
nodes | array<NetworkNode> | One per variable. |
edges | array<NetworkEdge> | One per discovered link. |
sec_list | array<string> | Section names; edges reference these by index. |
NetworkNode
A variable, as a node in the graph diagram.
| Field | Type | Description |
|---|---|---|
id | string | Variable name. |
section | string | null | Process section, used for grouping. |
out_deg | integer | How many variables it drives. |
in_deg | integer | How many drive it. |
mci_target | number | null | Inherited key: signed TCPFN strength into the target. |
desc | string | null | Human description. |
NetworkEdge
An edge in the graph diagram. Keys are the dashboard's abbreviations.
| Field | Type | Description |
|---|---|---|
s | string | Source variable. |
t | string | Target variable. |
m | number | null | Inherited key: the SIGNED TCPFN strength. |
a | number | null | Inherited key: the same magnitude, unsigned. |
l | integer | null | Lag in samples. |
x | integer | null | Inherited key: significant-lag count. |
ss | integer | null | Source section, as an index into sec_list. |
ts | integer | null | Target section, as an index into sec_list. |
tcpfn_strength | number | null | The unambiguous magnitude, beside the inherited keys. |
lag_hours | number | null | Lag in real time. |
sign | string | null | Direction of the effect. |
sign_source | string | null | Where the sign came from. |
CausalChains
| Field | Type | Description |
|---|---|---|
chains | array<NetworkChain> | Multi-hop routes into the target for the primary event (`derived_from_event_id` says which one), onset-verified: every member of a listed route actually left its pre-event baseline, in causal order, before that event. |
derived_from_event_id | integer | null | The entry in `events` these headline chains were derived from (the routes are traced around one event's window, not from the whole dataset). Null when no event could be matched. |
by_event | object | Each explained event's OWN routes, keyed by event_id like `event_risk_factors`. Onset-verified routes appear when their members actually deviated, in causal order, in that event's rows; additionally, every link-confirmed suspect in `event_risk_factors` contributes at least its one-hop route (see each route's `evidence`), so the chains never contradict the suspect list. An empty list means nobody was named. |
NetworkChain
A route from an upstream driver into the target.
| Field | Type | Description |
|---|---|---|
path | array<ChainStep> | The hops, in order. |
path_str | string | null | The route as readable text. |
total_lag | integer | Total delay along the route, in samples. |
total_lag_hours | number | null | Total delay in real time. |
strength | number | null | Product of the hop strengths along the route (each hop <= 1, so longer routes always score lower than short ones — do not rank a multi-hop route against a one-hop shortcut on this number). |
evidence | string | null | How this route earned its place under the event. None or "onset" = every member passed the fever check in the event's own rows (onset-verified). "timing+repetition" = the route is the one-hop edge of a suspect named by the weak-signal tier — shown so the chart agrees with the suspect list, but its evidence is lag-exact timing repeated across events, not a fever onset; consumers should render it as a weaker route. |
link_confirmed | boolean | null | None or True: the route runs along edges of the causal map. False: a mover route — the suspect left its baseline before the event (so it is listed in `event_risk_factors`) but the causal map has no edge from it into the target; the hop is placed by the suspect's own onset timing and has no strength. Draw it as an unconfirmed mover, never as verified propagation. |
ChainStep
One hop of a multi-hop path.
| Field | Type | Description |
|---|---|---|
var | string | Variable at this hop. |
lag | integer | null | Lag contributed by this hop, in samples. |
lag_hours | number | null | That hop's lag in real time. |
DriverEvidenceItem
A driver's supporting evidence, grouped by section.
| Field | Type | Description |
|---|---|---|
rank | integer | null | Position within its section. |
tag | string | null | Display label. |
description | string | null | Human description. |
process_section | string | null | Section it belongs to. |
total_mci | number | null | Inherited key: signed TCPFN strength. |
lag_at_max | integer | null | Lag at peak effect, in samples. |
lag_hours | number | null | That lag in real time. |
rca_combined_score | number | null | Attribution score. |
MergedColumn
A column merged into a representative before the joint model ran.
| Field | Type | Description |
|---|---|---|
dropped | string | The column that was merged away — its result is reported under `kept`. |
ExampleThe name that was merged away — fan_speed_sqrt on the derived panel. Its result did not vanish; it lives under kept. | ||
kept | string | The representative name the shared signal was analysed under. |
| ExampleThe name the shared signal was analysed under — fan_speed. Every driver row, event cause and network node uses this name. | ||
evidence | string | Why the two carry one signal (identical values, |pearson|/|spearman|, or a one-way functional map). |
| ExampleWhy the two are one signal: identical values, |spearman|=1.000 for a square/sqrt, or 'X is a functional map of Y' for a one-way transform like sin(). | ||
rule | string | How the representative was chosen: operator-preferred, source of a one-way map, or first in column order (mutually derivable — the data cannot say which is the physical sensor). |
| ExampleHow the representative was picked, in words. A one-way map keeps its source (the sine cannot be undone). Mutually derivable twins keep the first in column order — and the rule says so, because the data genuinely cannot tell which is the physical sensor; the joint model sees the same information either way. | ||
TroubleshootStep
One thing to do next. At most one call — a list of possibilities is not an answer.
| Field | Type | Description |
|---|---|---|
text | string | What to do, in plain words. |
| ExampleOne thing to do next, in plain words. There is at most one of these that names a call — a list of possibilities is not an answer, and handing someone five things to try is the support thread this is meant to replace. | ||
call | string | null | The single call that resolves this, when one does. |
| ExampleThe single call that would settle it, ready to run — e.g. estimate_effect on the pair you are arguing about. Empty when the answer is already final, or when what settles it is something on the plant rather than another analysis. | ||
Worked scenarios — summary
Every dataset below was generated by us, so the true causal structure is known exactly, and TCPFN is never shown it. Each scenario changes exactly one thing against the baseline and is grouped by what it tests — click a scenario for its ground truth, diagram and graded results.
| # | Scenario | What it tests | Result |
|---|---|---|---|
| Baseline and variants — The sample data and small variations of it — timestamps, injected faults, a spike, a long delay. | |||
| 1 | Single lag (sample) | Plain data, one shared delay — the easy case. | handled |
| 2 | Base (mixed lags) | Three real effects, each at a different delay (1/2/3). | handled |
| 3 | Daily timestamps | Same data plus a date column — must change nothing. | handled |
| 4 | Planted anomalies | Five injected faults — the structure must not change. | handled |
| 5 | Heater spike | One extreme heater spike — must not break detection. | handled |
| 6 | Long lag (7 steps) | Effect arrives 7 steps late — right lag, or mislabeled? | handled |
| Levers switched off — A lever that never moves cannot be a cause; the tool must drop it, not invent it. | |||
| 7 | Fan off | A lever that never moves must be dropped honestly. | handled |
| 8 | Heater off | Both heater effects must vanish; only the fan remains. | handled |
| 9 | All levers off (pure noise) | Pure noise — the only correct answer is an empty graph. | handled |
| Effect sign and chains — A negative effect must come out negative; a variable in the middle of a chain must be promoted. | |||
| 10 | Negative effect (fan saves energy) | The fan LOWERS the bill — the minus sign must be recovered. | handled — with flagged extras |
| 11 | Chain (heater→room→bill) | Room temp becomes a real cause — must be promoted, not refused. | handled |
| Direction of cause — Correlation is symmetric; only time order tells cause from effect — including when both directions are real. | |||
| 12 | Reverse causation | The BILL drives the FAN — direction, not correlation, is the test. | handled — with flagged extras |
| 13 | Feedback loop (thermostat) | A thermostat loop — BOTH directions are true at once. | handled — with flagged extras |
| Shared rhythms and drift — Two series on the same clock or the same drift look linked whether or not one drives the other. | |||
| 14 | Shared daily cycle | Fan and bill follow the SAME daily rhythm — the ice-cream-and-drowning trap. | handled — with flagged extras |
| 15 | Shared cycle, offset | Same rhythm, the bill's copy shifted 3 steps — looks exactly like a lag. | handled — with flagged extras |
| 16 | Three shared cycles | Daily + weekly + fortnightly rhythms shared, none causal. | handled — with flagged extras |
| 17 | Shared trend | Both drift upward — no rhythm, no link, just wear and inflation. | handled — with flagged extras |
| Weak signals — One real effect at decreasing strength among realistic noise — find it, and nothing else. | |||
| 18 | Weak-signal ladder: strong | One strong real effect among realistic noise — nothing else may appear. | handled |
| 19 | Weak-signal ladder: quarter | Same, at a quarter of the strength. | handled |
| 20 | Weak-signal ladder: absent | Same, with the effect removed — an empty graph is the only right answer. | handled |
| Closed-loop control — A controller pushes the cause AGAINST the effect, so the raw correlation has the WRONG SIGN — the estimator must recover the true positive effect. | |||
| 21 | Closed-loop control (thermostat) | A thermostat fights the heater — raw correlation has the WRONG sign. | handled — with flagged extras |
| Non-monotone (shape) dependence — A cause with an operating optimum: throughput peaks at a mid setting. Invisible to every monotone statistic — a dedicated shape detector must find it. | |||
| 22 | Non-monotone (U-shape) | Throughput peaks at a MID setting — invisible to every monotone statistic. | handled |
| Curved (nonlinear) confounding — The confounder acts through a CURVE, not a line. The linear deconfounder alone would miss it. | |||
| 23 | Curved confounding (observed) | The confounder acts through a CURVE — a linear deconfounder alone misses it. | handled |
| Derived and bookkeeping columns — Averages, counts and indexes computed FROM real measurements: the provenance layer must map them as transforms, not causes. | |||
| 24 | Derived and bookkeeping columns | Averages, counts and indexes computed from real measurements must not become causes. | handled — with flagged extras |
| Data with nothing to find — Every column alive with realistic sensor memory, and no relationship anywhere: the right answer is an empty graph. | |||
| 25 | Realistic empty file (1) | Nothing connected, every column alive with realistic memory — seed 1 of 3. | handled — with flagged extras |
| 26 | Realistic empty file (2) | Nothing connected — seed 2 of 3. | handled |
| 27 | Realistic empty file (3) | Nothing connected — seed 3 of 3. | handled |
| 28 | Shuffled rows | Row order destroyed — no lagged causal claim can stand. | known limit |
| Known limits — What no method can recover from the columns present — stated, not hidden. | |||
| 29 | Hidden confounder (documented limit) | An UNSEEN outside temperature drives both heater and bill — unfixable, stated honestly. | known limit |
Single lag (sample)
The original sample dataset (also used by the Quickstart). Simple case: all effects arrive about one step later. The starting point every other example varies from.
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
heater | room temp | 1 | +0.4 |
heater | power bill | 1 | +0.11 |
fan | power bill | 1 | +0.22 |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| fan → power bill | 1 | + | ✓ correct — matches the planted edge |
| heater → room temp | 1 | + | ✓ correct — matches the planted edge |
| heater → power bill | 1 | + | ✓ correct — matches the planted edge |
All tool results (target: power bill — click a tool to expand; every row graded against the ground truth)
discover3 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| fan → power bill | 1 | + | ✓ correct — matches the planted edge |
| heater → room temp | 1 | + | ✓ correct — matches the planted edge |
| heater → power bill | 1 | + | ✓ correct — matches the planted edge |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the power bill change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → power bill | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | +0.089 | yes | truth +0.11 — ✓ trusted, right direction |
room temp | +0.076 | no | ✓ correctly NOT trusted — no real effect exists |
fan | +0.213 | yes | truth +0.22 — ✓ trusted, right direction |
noise | -0.012 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable2 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → power bill | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m… |
room temp | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (confounded) — the association between roo… |
fan | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'fan_speed' passes independence, temporal precedence, and deconfounding against every other meas… |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'power_bill' fails (independent) — random_noise is ~margin… |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionsstrongest trusted lever: fan — matches the truth
The action question: if you could only turn one knob to move the power bill, which one? Ground truth: the biggest planted coefficient is fan (+0.22). Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on power bill | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | fan | +3.86 | yes | real lever (truth +0.22) |
| 2 | heater | +1.91 | yes | real lever (truth +0.11) |
| 3 | noise | +3.44 | no | not a real lever — check the Trusted column |
| 4 | room temp | -2.40 | no | not a real lever — check the Trusted column |
check_datadata usable — 799 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
799 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 16.99 | 0.61 | 158 | moves and has time structure — analyzable |
room temp | yes | 7.29 | 0.59 | 148 | moves and has time structure — analyzable |
fan | yes | 22.89 | 0.63 | 153 | moves and has time structure — analyzable |
power bill | yes | 5.41 | 0.62 | 159 | moves and has time structure — analyzable |
noise | yes | 28.83 | 0.01 | 111 | ✓ no time pattern — pure noise, as designed |
explaintop root cause of the event: fan
The incident question: this one power bill spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the power bill) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the power bill event at row 486.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
fan | 1 | 0.80 | stable | ✓ a real driver of the bill |
heater | 1 | 0.36 | stable | ✓ a real driver of the bill |
causal_analysisevery planted cause reported
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
| Reported driver | Lag | Verdict vs ground truth |
|---|---|---|
fan | — | ✓ correct — a planted cause of the target |
heater | — | ✓ correct — a planted cause of the target |
Base (mixed lags)
The harder realistic case: three real effects, each with a DIFFERENT delay (1, 2 and 3 steps). A method that assumes one shared lag fails here.
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
heater | power bill | 1 | +0.08 |
heater | room temp | 2 | +0.40 |
fan | power bill | 3 | +0.16 |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| fan → power bill | 3 | + | ✓ correct — matches the planted edge |
| heater → power bill | 1 | + | ✓ correct — matches the planted edge |
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
All tool results (target: power bill — click a tool to expand; every row graded against the ground truth)
discover3 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| fan → power bill | 3 | + | ✓ correct — matches the planted edge |
| heater → power bill | 1 | + | ✓ correct — matches the planted edge |
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the power bill change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → power bill | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | +0.074 | yes | truth +0.08 — ✓ trusted, right direction |
room temp | -0.138 | no | ✓ correctly NOT trusted — no real effect exists |
fan | +0.167 | yes | truth +0.16 — ✓ trusted, right direction |
noise | -0.011 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable2 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → power bill | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m… |
room temp | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (reversed) — power_bill LEADS room_temp in… |
fan | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'fan_speed' passes independence, temporal precedence, and deconfounding against every other meas… |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'power_bill' fails (independent) — random_noise is ~margin… |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionsstrongest trusted lever: fan — matches the truth
The action question: if you could only turn one knob to move the power bill, which one? Ground truth: the biggest planted coefficient is fan (+0.16). Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on power bill | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | fan | +4.36 | yes | real lever (truth +0.16) |
| 2 | heater | +0.76 | yes | real lever (truth +0.08) |
| 3 | noise | -2.30 | no | not a real lever — check the Trusted column |
| 4 | room temp | -0.33 | no | not a real lever — check the Trusted column |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 16.72 | 0.84 | 158 | moves and has time structure — analyzable |
room temp | yes | 7.02 | 0.74 | 161 | moves and has time structure — analyzable |
fan | yes | 21.40 | 0.76 | 141 | moves and has time structure — analyzable |
power bill | yes | 4.01 | 0.73 | 162 | moves and has time structure — analyzable |
noise | yes | 29.18 | -0.02 | 130 | ✓ no time pattern — pure noise, as designed |
explaintop root cause of the event: fan
The incident question: this one power bill spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the power bill) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the power bill event at row 612.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
fan | 3 | 0.80 | stable | ✓ a real driver of the bill |
heater | 1 | 0.30 | stable | ✓ a real driver of the bill |
causal_analysisevery planted cause reported
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
| Reported driver | Lag | Verdict vs ground truth |
|---|---|---|
heater | — | ✓ correct — a planted cause of the target |
fan | — | ✓ correct — a planted cause of the target |
Daily timestamps
Exactly the baseline numbers plus a daily timestamp column. Timestamps must not change the answer — lags now simply mean days.
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
heater | power bill | 1 | +0.08 |
heater | room temp | 2 | +0.40 |
fan | power bill | 3 | +0.16 |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| fan → power bill | 3 | + | ✓ correct — matches the planted edge |
| heater → power bill | 1 | + | ✓ correct — matches the planted edge |
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
All tool results (target: power bill — click a tool to expand; every row graded against the ground truth)
discover3 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| fan → power bill | 3 | + | ✓ correct — matches the planted edge |
| heater → power bill | 1 | + | ✓ correct — matches the planted edge |
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the power bill change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → power bill | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | +0.074 | yes | truth +0.08 — ✓ trusted, right direction |
room temp | -0.138 | no | ✓ correctly NOT trusted — no real effect exists |
fan | +0.167 | yes | truth +0.16 — ✓ trusted, right direction |
noise | -0.011 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable2 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → power bill | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m… |
room temp | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (reversed) — power_bill LEADS room_temp in… |
fan | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'fan_speed' passes independence, temporal precedence, and deconfounding against every other meas… |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'power_bill' fails (independent) — random_noise is ~margin… |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionsstrongest trusted lever: fan — matches the truth
The action question: if you could only turn one knob to move the power bill, which one? Ground truth: the biggest planted coefficient is fan (+0.16). Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on power bill | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | fan | +4.36 | yes | real lever (truth +0.16) |
| 2 | heater | +0.76 | yes | real lever (truth +0.08) |
| 3 | noise | -2.30 | no | not a real lever — check the Trusted column |
| 4 | room temp | -0.33 | no | not a real lever — check the Trusted column |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 16.72 | 0.84 | 158 | moves and has time structure — analyzable |
room temp | yes | 7.02 | 0.74 | 161 | moves and has time structure — analyzable |
fan | yes | 21.40 | 0.76 | 141 | moves and has time structure — analyzable |
power bill | yes | 4.01 | 0.73 | 162 | moves and has time structure — analyzable |
noise | yes | 29.18 | -0.02 | 130 | ✓ no time pattern — pure noise, as designed |
explaintop root cause of the event: None
The incident question: this one power bill spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the power bill) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the power bill event at row 612.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
| (no root causes reported) | — | — | — | — |
causal_analysisevery planted cause reported
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
| Reported driver | Lag | Verdict vs ground truth |
|---|---|---|
fan | — | ✓ correct — a planted cause of the target |
heater | — | ✓ correct — a planted cause of the target |
Planted anomalies
Baseline + five injected events. Anomalies make the trap variables co-move with the bill — the causal structure must NOT change.
- A1 — heater surge: pinned near maximum for 11 days (rows 150–160)
- A2 — heater dropout: switched to 0 for 9 days (rows 450–458)
- A3 — fan surge: pinned near maximum for 11 days (rows 350–360)
- A4 — noise meter stuck: constant 99.5 for 11 days (rows 650–660) — must NOT create an edge
- A5 — room-temperature sensor fault: reads +15° too high for 9 days (rows 550–558), NOT caused by the heater — the bill must NOT react
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
heater | power bill | 1 | +0.08 |
heater | room temp | 2 | +0.40 |
fan | power bill | 3 | +0.16 |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| heater → power bill | 1 | + | ✓ correct — matches the planted edge |
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
| fan → power bill | 3 | + | ✓ correct — matches the planted edge |
All tool results (target: power bill — click a tool to expand; every row graded against the ground truth)
discover3 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| heater → power bill | 1 | + | ✓ correct — matches the planted edge |
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
| fan → power bill | 3 | + | ✓ correct — matches the planted edge |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the power bill change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → power bill | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | +0.057 | yes | truth +0.08 — ✓ trusted, right direction |
room temp | -0.090 | no | ✓ correctly NOT trusted — no real effect exists |
fan | +0.161 | yes | truth +0.16 — ✓ trusted, right direction |
noise | +0.013 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable2 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → power bill | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m… |
room temp | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (confounded) — the association between roo… |
fan | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'fan_speed' passes independence, temporal precedence, and deconfounding against every other meas… |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'power_bill' fails (independent) — random_noise is ~margin… |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionsstrongest trusted lever: fan — matches the truth
The action question: if you could only turn one knob to move the power bill, which one? Ground truth: the biggest planted coefficient is fan (+0.16). Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on power bill | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | fan | +1.80 | yes | real lever (truth +0.16) |
| 2 | heater | +0.18 | yes | real lever (truth +0.08) |
| 3 | room temp | +4.26 | no | not a real lever — check the Trusted column |
| 4 | noise | -0.34 | no | not a real lever — check the Trusted column |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 18.00 | 0.86 | 152 | moves and has time structure — analyzable |
room temp | yes | 7.87 | 0.79 | 126 | moves and has time structure — analyzable |
fan | yes | 22.72 | 0.77 | 122 | moves and has time structure — analyzable |
power bill | yes | 4.08 | 0.74 | 149 | moves and has time structure — analyzable |
noise | yes | 29.49 | 0.02 | 144 | ✓ no time pattern — pure noise, as designed |
explaintop root cause of the event: None
The incident question: this one power bill spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the power bill) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the power bill event at row 191.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
| (no root causes reported) | — | — | — | — |
causal_analysisevery planted cause reported
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
| Reported driver | Lag | Verdict vs ground truth |
|---|---|---|
heater | — | ✓ correct — a planted cause of the target |
fan | — | ✓ correct — a planted cause of the target |
Heater spike
Baseline + one sharp heater spike (4 rows pinned at maximum). A single clean shock must not break edge or lag detection.
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
heater | power bill | 1 | +0.08 |
heater | room temp | 2 | +0.40 |
fan | power bill | 3 | +0.16 |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| fan → power bill | 3 | + | ✓ correct — matches the planted edge |
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
| heater → power bill | 1 | + | ✓ correct — matches the planted edge |
All tool results (target: power bill — click a tool to expand; every row graded against the ground truth)
discover3 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| fan → power bill | 3 | + | ✓ correct — matches the planted edge |
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
| heater → power bill | 1 | + | ✓ correct — matches the planted edge |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the power bill change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → power bill | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | +0.075 | yes | truth +0.08 — ✓ trusted, right direction |
room temp | -0.123 | no | ✓ correctly NOT trusted — no real effect exists |
fan | +0.168 | yes | truth +0.16 — ✓ trusted, right direction |
noise | -0.011 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable2 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → power bill | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m… |
room temp | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (reversed) — power_bill LEADS room_temp in… |
fan | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'fan_speed' passes independence, temporal precedence, and deconfounding against every other meas… |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'power_bill' fails (independent) — random_noise is ~margin… |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionsstrongest trusted lever: fan — matches the truth
The action question: if you could only turn one knob to move the power bill, which one? Ground truth: the biggest planted coefficient is fan (+0.16). Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on power bill | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | fan | +4.36 | yes | real lever (truth +0.16) |
| 2 | heater | +0.82 | yes | real lever (truth +0.08) |
| 3 | noise | -1.73 | no | not a real lever — check the Trusted column |
| 4 | room temp | -0.49 | no | not a real lever — check the Trusted column |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 17.12 | 0.84 | 166 | moves and has time structure — analyzable |
room temp | yes | 7.17 | 0.75 | 164 | moves and has time structure — analyzable |
fan | yes | 21.40 | 0.76 | 141 | moves and has time structure — analyzable |
power bill | yes | 4.03 | 0.73 | 162 | moves and has time structure — analyzable |
noise | yes | 29.18 | -0.02 | 130 | ✓ no time pattern — pure noise, as designed |
explaintop root cause of the event: None
The incident question: this one power bill spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the power bill) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the power bill event at row 612.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
| (no root causes reported) | — | — | — | — |
causal_analysisevery planted cause reported
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
| Reported driver | Lag | Verdict vs ground truth |
|---|---|---|
fan | — | ✓ correct — a planted cause of the target |
heater | — | ✓ correct — a planted cause of the target |
Long lag (7 steps)
The fan's effect reaches the bill after SEVEN steps — far beyond a naive short search window. Tests whether a delayed effect is found with the right lag or silently mislabeled.
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
heater | power bill | 1 | +0.08 |
heater | room temp | 2 | +0.40 |
fan | power bill | 7 | +0.16 |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| fan → power bill | 7 | + | ✓ correct — matches the planted edge |
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
| heater → power bill | 1 | + | ✓ correct — matches the planted edge |
All tool results (target: power bill — click a tool to expand; every row graded against the ground truth)
discover3 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| fan → power bill | 7 | + | ✓ correct — matches the planted edge |
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
| heater → power bill | 1 | + | ✓ correct — matches the planted edge |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the power bill change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → power bill | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | +0.057 | yes | truth +0.08 — ✓ trusted, right direction |
room temp | -0.119 | no | ✓ correctly NOT trusted — no real effect exists |
fan | +0.157 | yes | truth +0.16 — ✓ trusted, right direction |
noise | -0.013 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable2 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → power bill | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m… |
room temp | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (confounded) — the association between roo… |
fan | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'fan_speed' passes independence, temporal precedence, and deconfounding against every other meas… |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'power_bill' fails (independent) — random_noise is ~margin… |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionsstrongest trusted lever: fan — matches the truth
The action question: if you could only turn one knob to move the power bill, which one? Ground truth: the biggest planted coefficient is fan (+0.16). Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on power bill | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | fan | +1.47 | yes | real lever (truth +0.16) |
| 2 | heater | +0.89 | yes | real lever (truth +0.08) |
| 3 | noise | -4.30 | no | not a real lever — check the Trusted column |
| 4 | room temp | -0.31 | no | not a real lever — check the Trusted column |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 16.72 | 0.84 | 158 | moves and has time structure — analyzable |
room temp | yes | 7.02 | 0.74 | 161 | moves and has time structure — analyzable |
fan | yes | 21.40 | 0.76 | 141 | moves and has time structure — analyzable |
power bill | yes | 3.77 | 0.72 | 163 | moves and has time structure — analyzable |
noise | yes | 29.18 | -0.02 | 130 | ✓ no time pattern — pure noise, as designed |
explaintop root cause of the event: fan
The incident question: this one power bill spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the power bill) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the power bill event at row 658.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
fan | 7 | 0.96 | stable | ✓ a real driver of the bill |
causal_analysisevery planted cause reported
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
| Reported driver | Lag | Verdict vs ground truth |
|---|---|---|
heater | — | ✓ correct — a planted cause of the target |
fan | — | ✓ correct — a planted cause of the target |
Fan off
The fan is held at 0 the whole series. A lever that never moves cannot be identified — its edge must disappear, and the heater edges must keep their positive sign.
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
heater | power bill | 1 | +0.08 |
heater | room temp | 2 | +0.40 |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
| heater → power bill | 1 | + | ✓ correct — matches the planted edge |
All tool results (target: power bill — click a tool to expand; every row graded against the ground truth)
discover2 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
| heater → power bill | 1 | + | ✓ correct — matches the planted edge |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the power bill change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → power bill | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | +0.081 | yes | truth +0.08 — ✓ trusted, right direction |
room temp | +0.103 | no | ✓ correctly NOT trusted — no real effect exists |
fan | refused (no variation) | — | ✓ correct — this lever is switched off |
noise | +0.005 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable1 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → power bill | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m… |
room temp | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (reversed) — power_bill LEADS room_temp in… |
fan | — | ✓ correctly refused — lever is off | |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'power_bill' fails (independent) — random_noise is ~margin… |
what_should_i_measure5 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
| # | Proposed action |
|---|---|
| 1 | {'action_type': 'measure', 'target_variable': 'mediator_room_temp_random_noise', 'expected_gain': 0.5006057381629944, 'rationale': None} |
| 2 | {'action_type': 'measure', 'target_variable': 'mediator_heater_power_room_temp', 'expected_gain': 0.4221646785736084, 'rationale': None} |
| 3 | {'action_type': 'measure', 'target_variable': 'mediator_heater_power_power_bill', 'expected_gain': 0.4075261831283569, 'rationale': None} |
| 4 | {'action_type': 'measure', 'target_variable': 'mediator_power_bill_heater_power', 'expected_gain': 0.37819494009017945, 'rationale': None} |
| 5 | {'action_type': 'measure', 'target_variable': 'mediator_heater_power_random_noise', 'expected_gain': 0.3550814151763916, 'rationale': None} |
rank_interventionsstrongest trusted lever: heater — matches the truth
The action question: if you could only turn one knob to move the power bill, which one? Ground truth: the biggest planted coefficient is heater (+0.08). Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on power bill | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | heater | +0.79 | yes | real lever (truth +0.08) |
| 2 | noise | -0.60 | no | not a real lever — check the Trusted column |
| 3 | room temp | +0.37 | no | not a real lever — check the Trusted column |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 16.28 | 0.84 | 166 | moves and has time structure — analyzable |
room temp | yes | 6.75 | 0.75 | 164 | moves and has time structure — analyzable |
fan | no | 0.00 | 0.00 | 0 | ✓ correctly shows the lever never moved |
power bill | yes | 1.69 | 0.49 | 166 | moves and has time structure — analyzable |
noise | yes | 28.99 | 0.01 | 97 | ✓ no time pattern — pure noise, as designed |
explaintop root cause of the event: None
The incident question: this one power bill spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the power bill) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the power bill event at row 506.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
| (no root causes reported) | — | — | — | — |
causal_analysisevery planted cause reported
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
| Reported driver | Lag | Verdict vs ground truth |
|---|---|---|
heater | — | ✓ correct — a planted cause of the target |
Heater off
The heater is held at 0. Both heater edges must vanish and the room temperature becomes flat noise — only the fan edge remains.
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
fan | power bill | 3 | +0.16 |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| fan → power bill | 3 | + | ✓ correct — matches the planted edge |
All tool results (target: power bill — click a tool to expand; every row graded against the ground truth)
discover1 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| fan → power bill | 3 | + | ✓ correct — matches the planted edge |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the power bill change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → power bill | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | refused (no variation) | — | ✓ correct — this lever is switched off |
room temp | +0.161 | no | ✓ correctly NOT trusted — no real effect exists |
fan | +0.159 | yes | truth +0.16 — ✓ trusted, right direction |
noise | +0.014 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable1 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → power bill | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | — | ✓ correctly refused — lever is off | |
room temp | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (independent) — room_temp is ~marginally i… |
fan | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'fan_speed' passes independence, temporal precedence, and deconfounding against every other meas… |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'power_bill' fails (independent) — random_noise is ~margin… |
what_should_i_measure5 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
| # | Proposed action |
|---|---|
| 1 | {'action_type': 'measure', 'target_variable': 'mediator_fan_speed_random_noise', 'expected_gain': 0.5017503261566162, 'rationale': None} |
| 2 | {'action_type': 'observe_more', 'target_variable': 'random_noise', 'expected_gain': 0.39372313270966214, 'rationale': None} |
| 3 | {'action_type': 'measure', 'target_variable': 'mediator_fan_speed_power_bill', 'expected_gain': 0.37650175094604493, 'rationale': None} |
| 4 | {'action_type': 'measure', 'target_variable': 'mediator_fan_speed_room_temp', 'expected_gain': 0.35800899267196656, 'rationale': None} |
| 5 | {'action_type': 'measure', 'target_variable': 'mediator_room_temp_fan_speed', 'expected_gain': 0.33511440753936766, 'rationale': None} |
rank_interventionsstrongest trusted lever: fan — matches the truth
The action question: if you could only turn one knob to move the power bill, which one? Ground truth: the biggest planted coefficient is fan (+0.16). Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on power bill | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | fan | +0.16 | yes | real lever (truth +0.16) |
| 2 | noise | -1.47 | no | not a real lever — check the Trusted column |
| 3 | room temp | +0.56 | no | not a real lever — check the Trusted column |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | no | 0.00 | 0.00 | 0 | ✓ correctly shows the lever never moved |
room temp | yes | 2.21 | -0.01 | 62 | moves and has time structure — analyzable |
fan | yes | 22.72 | 0.80 | 173 | moves and has time structure — analyzable |
power bill | yes | 3.77 | 0.73 | 166 | moves and has time structure — analyzable |
noise | yes | 28.99 | 0.01 | 97 | ✓ no time pattern — pure noise, as designed |
explaintop root cause of the event: None
The incident question: this one power bill spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the power bill) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the power bill event at row 415.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
| (no root causes reported) | — | — | — | — |
causal_analysisevery planted cause reported
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
| Reported driver | Lag | Verdict vs ground truth |
|---|---|---|
fan | — | ✓ correct — a planted cause of the target |
All levers off (pure noise)
Both levers at 0; the bill and room temperature are pure noise around their baselines. The honesty test: the correct answer is an EMPTY graph — any reported edge is invented.
Ground truth — the structure that generated the data
No causal edges exist — every column is noise or a constant.
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| (no edges reported) | — | — | ✓ correct — pure noise refused, empty graph |
All tool results (target: power bill — click a tool to expand; every row graded against the ground truth)
discover0 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| (no edges reported) | — | — | ✓ correct — pure noise refused, empty graph |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the power bill change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → power bill | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | refused (no variation) | — | ✓ correct — this lever is switched off |
room temp | -0.043 | no | ✓ correctly NOT trusted — no real effect exists |
fan | refused (no variation) | — | ✓ correct — this lever is switched off |
noise | -0.003 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable0 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → power bill | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | — | ✓ correctly refused — lever is off | |
room temp | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (independent) — room_temp is ~marginally i… |
fan | — | ✓ correctly refused — lever is off | |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'power_bill' fails (independent) — random_noise is ~margin… |
what_should_i_measure1 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
| # | Proposed action |
|---|---|
| 1 | {'action_type': 'measure', 'target_variable': 'mediator_random_noise_room_temp', 'expected_gain': 0.3536327362060547, 'rationale': None} |
rank_interventionsno real lever exists in this scenario
The action question: if you could only turn one knob to move the power bill, which one? Ground truth: the biggest planted coefficient — none, nothing drives the power bill here. Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on power bill | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | noise | -0.24 | no | not a real lever — check the Trusted column |
| 2 | room temp | +0.17 | no | not a real lever — check the Trusted column |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | no | 0.00 | 0.00 | 0 | ✓ correctly shows the lever never moved |
room temp | yes | 2.24 | 0.07 | 160 | moves and has time structure — analyzable |
fan | no | 0.00 | 0.00 | 0 | ✓ correctly shows the lever never moved |
power bill | yes | 1.11 | -0.01 | 68 | moves and has time structure — analyzable |
noise | yes | 29.03 | 0.03 | 112 | ✓ no time pattern — pure noise, as designed |
explaintop root cause of the event: None
The incident question: this one power bill spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the power bill) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the power bill event at row 705.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
| (no root causes reported) | — | — | — | — |
causal_analysisempty — correct
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
No driver reported — the analysis names no provable cause of the target (the correct answer here).
Negative effect (fan saves energy)
First planted MINUS sign: the fan REDUCES the bill (an economizer). Tests that a negative effect is reported as negative, not just that positives come out positive.
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
heater | power bill | 1 | +0.08 |
heater | room temp | 2 | +0.40 |
fan | power bill | 3 | −0.16 |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| fan → power bill | 3 | − | ✓ correct — matches the planted edge |
| heater → power bill | 0 | + | ⚠ right edge, wrong lag (truth lag 1) |
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
All tool results (target: power bill — click a tool to expand; every row graded against the ground truth)
discover3 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| fan → power bill | 3 | − | ✓ correct — matches the planted edge |
| heater → power bill | 0 | + | ⚠ right edge, wrong lag (truth lag 1) |
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the power bill change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → power bill | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | +0.041 | no | truth +0.08 — ⚠ flagged untrustworthy |
room temp | +0.101 | no | ✓ correctly NOT trusted — no real effect exists |
fan | -0.146 | yes | truth -0.16 — ✓ trusted, right direction |
noise | -0.012 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable1 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → power bill | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | no | ⚠ real cause flagged not identifiable | Not identifiable: a necessary condition for a causal effect of 'heater_power' on 'power_bill' fails (chance_level) — the association betwee… |
room temp | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (reversed) — power_bill LEADS room_temp in… |
fan | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'fan_speed' passes independence, temporal precedence, and deconfounding against every other meas… |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'power_bill' fails (independent) — random_noise is ~margin… |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionsstrongest trusted lever: fan — matches the truth
The action question: if you could only turn one knob to move the power bill, which one? Ground truth: the biggest planted coefficient is fan (-0.16). Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on power bill | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | fan | -4.80 | yes | real lever (truth -0.16) |
| 2 | room temp | -0.63 | no | not a real lever — check the Trusted column |
| 3 | noise | -0.63 | no | not a real lever — check the Trusted column |
| 4 | heater | -0.53 | no | real lever (truth +0.08) |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 16.72 | 0.84 | 158 | moves and has time structure — analyzable |
room temp | yes | 7.02 | 0.74 | 161 | moves and has time structure — analyzable |
fan | yes | 21.40 | 0.76 | 141 | moves and has time structure — analyzable |
power bill | yes | 3.68 | 0.69 | 166 | moves and has time structure — analyzable |
noise | yes | 29.18 | -0.02 | 130 | ✓ no time pattern — pure noise, as designed |
explaintop root cause of the event: fan
The incident question: this one power bill spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the power bill) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the power bill event at row 535.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
fan | 3 | 0.86 | stable | ✓ a real driver of the bill |
causal_analysis1 of 2 planted causes reported
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
| Reported driver | Lag | Verdict vs ground truth |
|---|---|---|
fan | — | ✓ correct — a planted cause of the target |
Chain (heater→room→bill)
The heater no longer hits the bill directly: it heats the room, and the ROOM drives the bill. room_temp switches from trap to genuine middleman — the model must promote it.
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
heater | room temp | 2 | +0.40 |
room temp | power bill | 1 | +0.30 |
fan | power bill | 3 | +0.16 |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| room temp → power bill | 1 | + | ✓ correct — matches the planted edge |
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
| fan → power bill | 3 | + | ✓ correct — matches the planted edge |
heater→bill at lag 3 is the TRUE indirect (total) effect through the room (0.40 × 0.30 ≈ 0.12) — acceptable, not an error.
All tool results (target: power bill — click a tool to expand; every row graded against the ground truth)
discover3 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| room temp → power bill | 1 | + | ✓ correct — matches the planted edge |
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
| fan → power bill | 3 | + | ✓ correct — matches the planted edge |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the power bill change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → power bill | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | +0.134 | yes | ⚠ trusted, but the true effect is 0 |
room temp | +0.276 | yes | truth +0.30 — ✓ trusted, right direction |
fan | +0.170 | yes | truth +0.16 — ✓ trusted, right direction |
noise | +0.013 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable3 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → power bill | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | yes | ✓ acceptable either way (indirect effect) | Identifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m… |
room temp | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'room_temp' passes independence, temporal precedence, and deconfounding against every other meas… |
fan | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'fan_speed' passes independence, temporal precedence, and deconfounding against every other meas… |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'power_bill' fails (independent) — random_noise is ~margin… |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionstop trusted lever: fan (truth: room temp)
The action question: if you could only turn one knob to move the power bill, which one? Ground truth: the biggest planted coefficient is room temp (+0.30). Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on power bill | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | fan | +4.70 | yes | real lever (truth +0.16) |
| 2 | room temp | +1.15 | yes | real lever (truth +0.30) |
| 3 | heater | +0.87 | yes | not a real lever — check the Trusted column |
| 4 | noise | -4.57 | no | not a real lever — check the Trusted column |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 16.72 | 0.84 | 158 | moves and has time structure — analyzable |
room temp | yes | 7.02 | 0.74 | 161 | moves and has time structure — analyzable |
fan | yes | 21.40 | 0.76 | 141 | moves and has time structure — analyzable |
power bill | yes | 4.41 | 0.72 | 148 | moves and has time structure — analyzable |
noise | yes | 29.18 | -0.02 | 130 | ✓ no time pattern — pure noise, as designed |
explaintop root cause of the event: room temp
The incident question: this one power bill spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the power bill) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the power bill event at row 612.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
room temp | 1 | 0.69 | stable | ✓ a real driver of the bill |
fan | 3 | 0.60 | stable | ✓ a real driver of the bill |
heater | 3 | 0.17 | stable | — |
causal_analysisevery planted cause reported
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
| Reported driver | Lag | Verdict vs ground truth |
|---|---|---|
room temp | — | ✓ correct — a planted cause of the target |
fan | — | ✓ correct — a planted cause of the target |
Reverse causation
The bill is the exogenous driver and the fan follows it two steps later — the opposite of every other utility scenario. Correlation looks identical either way; only time order tells them apart. A fan → bill edge is the failure this file exists for.
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
power bill | fan | 2 | +2.5 |
heater | room temp | 2 | +0.40 |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
| power bill → fan | 2 | + | ✓ correct — matches the planted edge |
| noise → heater | 0 | + | ⚠ not planted — vetoed by the tool (chance_level), evidence 0.20; reported for transparency, marked do-not-act |
All tool results (target: fan — click a tool to expand; every row graded against the ground truth)
discover3 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
| power bill → fan | 2 | + | ✓ correct — matches the planted edge |
| noise → heater | 0 | + | ⚠ not planted — vetoed by the tool (chance_level), evidence 0.20; reported for transparency, marked do-not-act |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the fan change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → fan | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | -0.112 | no | ✓ correctly NOT trusted — no real effect exists |
room temp | +0.223 | no | ✓ correctly NOT trusted — no real effect exists |
power bill | +2.570 | yes | truth +2.50 — ✓ trusted, right direction |
noise | +0.034 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable1 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → fan | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'heater_power' on 'fan_speed' fails (independent) — heater_power is ~margina… |
room temp | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'room_temp' on 'fan_speed' fails (independent) — room_temp is ~marginally in… |
power bill | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'power_bill' passes independence, temporal precedence, and deconfounding against every other mea… |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'fan_speed' fails (independent) — random_noise is ~margina… |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionsstrongest trusted lever: power bill — matches the truth
The action question: if you could only turn one knob to move the fan, which one? Ground truth: the biggest planted coefficient is power bill (+2.50). Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on fan | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | power bill | +12.23 | yes | real lever (truth +2.50) |
| 2 | heater | -6.28 | no | not a real lever — check the Trusted column |
| 3 | noise | -3.15 | no | not a real lever — check the Trusted column |
| 4 | room temp | -1.51 | no | not a real lever — check the Trusted column |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 11.19 | 0.59 | 176 | moves and has time structure — analyzable |
room temp | yes | 5.05 | 0.48 | 167 | moves and has time structure — analyzable |
fan | yes | 10.47 | 0.54 | 152 | moves and has time structure — analyzable |
power bill | yes | 3.76 | 0.62 | 166 | moves and has time structure — analyzable |
noise | yes | 28.53 | 0.05 | 159 | ✓ no time pattern — pure noise, as designed |
explaintop root cause of the event: power bill
The incident question: this one fan spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the fan) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the fan event at row 142.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
power bill | 2 | 0.96 | stable | ✓ a real driver of the bill |
causal_analysisevery planted cause reported
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
| Reported driver | Lag | Verdict vs ground truth |
|---|---|---|
power bill | — | ✓ correct — a planted cause of the target |
Feedback loop (thermostat)
The heater warms the room (lag 2, +) AND the room turns the heater down (lag 1, −). Both are real. A one-way graph can hold only one arrow per pair; the honest behaviour is to report each direction from its own side and say so, not to drop half the loop silently.
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
heater | room temp | 2 | +0.10 |
room temp | heater | 1 | −3.0 |
fan | power bill | 3 | +0.16 |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
| fan → power bill | 3 | + | ✓ correct — matches the planted edge |
| noise → fan | 0 | − | ⚠ not planted — vetoed by the tool (chance_level), evidence 0.21; reported for transparency, marked do-not-act |
| room temp → heater | 1 | − | ✓ correct — matches the planted edge |
All tool results (target: room temp — click a tool to expand; every row graded against the ground truth)
discover4 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
| fan → power bill | 3 | + | ✓ correct — matches the planted edge |
| noise → fan | 0 | − | ⚠ not planted — vetoed by the tool (chance_level), evidence 0.21; reported for transparency, marked do-not-act |
| room temp → heater | 1 | − | ✓ correct — matches the planted edge |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the room temp change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → room temp | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | +0.108 | yes | truth +0.10 — ✓ trusted, right direction |
fan | +0.008 | no | ✓ correctly NOT trusted — no real effect exists |
power bill | -0.035 | no | ✓ correctly NOT trusted — no real effect exists |
noise | -0.004 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable1 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → room temp | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m… |
fan | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'fan_speed' on 'room_temp' fails (independent) — fan_speed is ~marginally in… |
power bill | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'power_bill' on 'room_temp' fails (independent) — power_bill is ~marginally … |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'room_temp' fails (independent) — random_noise is ~margina… |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionsstrongest trusted lever: heater — matches the truth
The action question: if you could only turn one knob to move the room temp, which one? Ground truth: the biggest planted coefficient is heater (+0.10). Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on room temp | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | heater | +0.10 | yes | real lever (truth +0.10) |
| 2 | power bill | -0.72 | no | not a real lever — check the Trusted column |
| 3 | fan | -0.43 | no | not a real lever — check the Trusted column |
| 4 | noise | -0.10 | no | not a real lever — check the Trusted column |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 7.45 | 0.39 | 106 | moves and has time structure — analyzable |
room temp | yes | 1.19 | 0.42 | 119 | moves and has time structure — analyzable |
fan | yes | 17.40 | 0.59 | 177 | moves and has time structure — analyzable |
power bill | yes | 3.04 | 0.51 | 164 | moves and has time structure — analyzable |
noise | yes | 28.58 | 0.05 | 158 | ✓ no time pattern — pure noise, as designed |
explaintop root cause of the event: heater
The incident question: this one room temp spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the room temp) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the room temp event at row 476.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
heater | 2 | 0.91 | stable | ✓ a real driver of the bill |
causal_analysisevery planted cause reported
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
| Reported driver | Lag | Verdict vs ground truth |
|---|---|---|
heater | — | ✓ correct — a planted cause of the target |
Three shared cycles
fan_speed and power_bill share 24-, 168- and 336-step rhythms (the bill's copy offset by 5). Removing only the daily cycle is not enough; each significant cycle is found and modelled with the harmonics the record can support.
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
heater | room temp | 2 | +0.40 |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
| noise → heater | 0 | − | ⚠ not planted — vetoed by the tool (chance_level), evidence 0.20; reported for transparency, marked do-not-act |
All tool results (target: power bill — click a tool to expand; every row graded against the ground truth)
discover2 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
| noise → heater | 0 | − | ⚠ not planted — vetoed by the tool (chance_level), evidence 0.20; reported for transparency, marked do-not-act |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the power bill change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → power bill | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | +0.041 | no | ✓ correctly NOT trusted — no real effect exists |
room temp | +0.121 | no | ✓ correctly NOT trusted — no real effect exists |
fan | +0.165 | no | ✓ correctly NOT trusted — no real effect exists |
noise | -0.018 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable0 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → power bill | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'heater_power' on 'power_bill' fails (independent) — heater_power is ~margin… |
room temp | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (independent) — room_temp is ~marginally i… |
fan | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'fan_speed' on 'power_bill' fails (shared_clock) — the association between f… |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'power_bill' fails (confounded) — the association between … |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionsno real lever exists in this scenario
The action question: if you could only turn one knob to move the power bill, which one? Ground truth: the biggest planted coefficient — none, nothing drives the power bill here. Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on power bill | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | room temp | -2.83 | no | not a real lever — check the Trusted column |
| 2 | fan | +0.85 | no | not a real lever — check the Trusted column |
| 3 | heater | -0.85 | no | not a real lever — check the Trusted column |
| 4 | noise | +0.45 | no | not a real lever — check the Trusted column |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 11.20 | 0.59 | 177 | moves and has time structure — analyzable |
room temp | yes | 5.04 | 0.48 | 168 | moves and has time structure — analyzable |
fan | no | 19.74 | 0.93 | 149 | moves and has time structure — analyzable |
power bill | no | 3.97 | 0.92 | 139 | moves and has time structure — analyzable |
noise | yes | 28.54 | 0.05 | 159 | ✓ no time pattern — pure noise, as designed |
Shared clocks — columns on one rhythm or drift (pairs among them look linked whether or not one drives the other):
| Component | Columns | Verdict vs ground truth |
|---|---|---|
| shared 24-step cycle | power bill, fan | ✓ names the planted trap pair before any model runs |
| shared trend | power bill, fan | ✓ names the planted trap pair before any model runs |
explaintop root cause of the event: None
The incident question: this one power bill spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the power bill) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the power bill event at row 397.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
| (no root causes reported) | — | — | — | — |
causal_analysisempty — correct
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
No driver reported — the analysis names no provable cause of the target (the correct answer here).
Weak-signal ladder: strong
One real relationship (heater warms the room, lag 2, coefficient 0.40) and everything else independent noise with sensor memory. The test: find the one edge and nothing else.
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
heater | room temp | 2 | +0.40 |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
All tool results (target: room temp — click a tool to expand; every row graded against the ground truth)
discover1 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the room temp change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → room temp | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | +0.397 | yes | truth +0.40 — ✓ trusted, right direction |
fan | -0.044 | no | ✓ correctly NOT trusted — no real effect exists |
power bill | +0.122 | no | ✓ correctly NOT trusted — no real effect exists |
noise | +0.025 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable1 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → room temp | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m… |
fan | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'fan_speed' on 'room_temp' fails (independent) — fan_speed is ~marginally in… |
power bill | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'power_bill' on 'room_temp' fails (independent) — power_bill is ~marginally … |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'room_temp' fails (independent) — random_noise is ~margina… |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionsstrongest trusted lever: heater — matches the truth
The action question: if you could only turn one knob to move the room temp, which one? Ground truth: the biggest planted coefficient is heater (+0.40). Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on room temp | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | heater | +1.22 | yes | real lever (truth +0.40) |
| 2 | power bill | -3.71 | no | not a real lever — check the Trusted column |
| 3 | fan | +0.88 | no | not a real lever — check the Trusted column |
| 4 | noise | -0.55 | no | not a real lever — check the Trusted column |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 11.28 | 0.62 | 166 | moves and has time structure — analyzable |
room temp | yes | 5.42 | 0.63 | 153 | moves and has time structure — analyzable |
fan | yes | 17.42 | 0.59 | 177 | moves and has time structure — analyzable |
power bill | yes | 3.74 | 0.60 | 176 | moves and has time structure — analyzable |
noise | yes | 25.00 | 0.62 | 172 | ⚠ unexpected time pattern in the noise column |
Shared clocks — columns on one rhythm or drift (pairs among them look linked whether or not one drives the other):
| Component | Columns | Verdict vs ground truth | |
|---|---|---|---|
| (none) | — | ✗ the planted shared rhythm was not named | bad |
explaintop root cause of the event: heater
The incident question: this one room temp spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the room temp) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the room temp event at row 507.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
heater | 2 | 0.86 | stable | ✓ a real driver of the bill |
causal_analysisevery planted cause reported
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
| Reported driver | Lag | Verdict vs ground truth |
|---|---|---|
heater | — | ✓ correct — a planted cause of the target |
Weak-signal ladder: quarter
The same single relationship at coefficient 0.10 — four times weaker. Still found, still alone.
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
heater | room temp | 2 | +0.10 |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
All tool results (target: room temp — click a tool to expand; every row graded against the ground truth)
discover1 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| heater → room temp | 2 | + | ✓ correct — matches the planted edge |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the room temp change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → room temp | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | +0.102 | yes | truth +0.10 — ✓ trusted, right direction |
fan | -0.024 | no | ✓ correctly NOT trusted — no real effect exists |
power bill | -0.106 | no | ✓ correctly NOT trusted — no real effect exists |
noise | +0.015 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable1 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → room temp | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m… |
fan | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'fan_speed' on 'room_temp' fails (independent) — fan_speed is ~marginally in… |
power bill | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'power_bill' on 'room_temp' fails (independent) — power_bill is ~marginally … |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'room_temp' fails (independent) — random_noise is ~margina… |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionsstrongest trusted lever: heater — matches the truth
The action question: if you could only turn one knob to move the room temp, which one? Ground truth: the biggest planted coefficient is heater (+0.10). Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on room temp | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | heater | +0.11 | yes | real lever (truth +0.10) |
| 2 | power bill | -1.28 | no | not a real lever — check the Trusted column |
| 3 | fan | +0.42 | no | not a real lever — check the Trusted column |
| 4 | noise | +0.06 | no | not a real lever — check the Trusted column |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 11.28 | 0.62 | 166 | moves and has time structure — analyzable |
room temp | yes | 3.19 | 0.66 | 166 | moves and has time structure — analyzable |
fan | yes | 17.42 | 0.59 | 177 | moves and has time structure — analyzable |
power bill | yes | 3.74 | 0.60 | 176 | moves and has time structure — analyzable |
noise | yes | 25.00 | 0.62 | 172 | ⚠ unexpected time pattern in the noise column |
Shared clocks — columns on one rhythm or drift (pairs among them look linked whether or not one drives the other):
| Component | Columns | Verdict vs ground truth | |
|---|---|---|---|
| (none) | — | ✗ the planted shared rhythm was not named | bad |
explaintop root cause of the event: heater
The incident question: this one room temp spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the room temp) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the room temp event at row 723.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
heater | 2 | 0.86 | stable | ✓ a real driver of the bill |
causal_analysisevery planted cause reported
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
| Reported driver | Lag | Verdict vs ground truth |
|---|---|---|
heater | — | ✓ correct — a planted cause of the target |
Weak-signal ladder: absent
The bottom rung: the heater → room effect is switched off entirely. Same noise, same memory, no relationship anywhere.
Ground truth — the structure that generated the data
No causal edges exist — every column is noise or a constant.
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| (no edges reported) | — | — | ✓ correct — pure noise refused, empty graph |
All tool results (target: room temp — click a tool to expand; every row graded against the ground truth)
discover0 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| (no edges reported) | — | — | ✓ correct — pure noise refused, empty graph |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the room temp change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → room temp | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | -0.024 | no | ✓ correctly NOT trusted — no real effect exists |
fan | -0.017 | no | ✓ correctly NOT trusted — no real effect exists |
power bill | -0.106 | no | ✓ correctly NOT trusted — no real effect exists |
noise | +0.013 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable0 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → room temp | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'heater_power' on 'room_temp' fails (independent) — heater_power is ~margina… |
fan | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'fan_speed' on 'room_temp' fails (independent) — fan_speed is ~marginally in… |
power bill | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'power_bill' on 'room_temp' fails (independent) — power_bill is ~marginally … |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'room_temp' fails (independent) — random_noise is ~margina… |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionsno real lever exists in this scenario
The action question: if you could only turn one knob to move the room temp, which one? Ground truth: the biggest planted coefficient — none, nothing drives the room temp here. Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on room temp | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | power bill | -0.78 | no | not a real lever — check the Trusted column |
| 2 | heater | -0.56 | no | not a real lever — check the Trusted column |
| 3 | noise | +0.29 | no | not a real lever — check the Trusted column |
| 4 | fan | -0.28 | no | not a real lever — check the Trusted column |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 11.28 | 0.62 | 166 | moves and has time structure — analyzable |
room temp | yes | 2.97 | 0.66 | 165 | moves and has time structure — analyzable |
fan | yes | 17.42 | 0.59 | 177 | moves and has time structure — analyzable |
power bill | yes | 3.74 | 0.60 | 176 | moves and has time structure — analyzable |
noise | yes | 25.00 | 0.62 | 172 | ⚠ unexpected time pattern in the noise column |
explaintop root cause of the event: None
The incident question: this one room temp spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the room temp) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the room temp event at row 419.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
| (no root causes reported) | — | — | — | — |
causal_analysisempty — correct
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
No driver reported — the analysis names no provable cause of the target (the correct answer here).
Closed-loop control (thermostat)
The heater genuinely warms the room (+0.55 per unit, one step later) — but a thermostat raises the heater exactly when the room ran cold, so the raw correlation between them comes out NEGATIVE. Acting on that sign would tell an operator to turn the heater DOWN to warm the room. The estimator conditions on the room's own history (the controller's input), recovering the true positive effect, and flags the loop via closed_loop_detected. From the external closed-loop finding (2026-09); regulated processes — pumps, valves, temperatures — are the rule in industry, not the exception.
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
heater | room temp | 1 | +0.55 (true effect) |
room temp | heater | 1 | -0.30 (controller) |
outside temp | room temp | 0 | -1.0 (disturbance) |
fan | duct pressure | 2 | control edge |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
Both directions of the heater-room pair are REAL (the physics forward, the controller backward) — a correct answer reports the loop, not just one arrow.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| outside_temp → room temp | 0 | − | ✓ correct — matches the planted edge |
| fan → duct_pressure | 2 | + | ✓ correct — matches the planted edge |
| room temp → heater | 1 | − | ✓ correct — matches the planted edge |
| heater → room temp | 0 | + | ⚠ right edge, wrong lag (truth lag 1) |
All tool results (target: room temp — click a tool to expand; every row graded against the ground truth)
discover4 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| outside_temp → room temp | 0 | − | ✓ correct — matches the planted edge |
| fan → duct_pressure | 2 | + | ✓ correct — matches the planted edge |
| room temp → heater | 1 | − | ✓ correct — matches the planted edge |
| heater → room temp | 0 | + | ⚠ right edge, wrong lag (truth lag 1) |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the room temp change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → room temp | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | +0.570 | yes | truth +0.55 — ✓ trusted, right direction |
outside_temp | -0.751 | yes | truth -1.00 — ✓ trusted, right direction |
fan | -0.049 | no | ✓ correctly NOT trusted — no real effect exists |
duct_pressure | -0.098 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable2 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → room temp | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m… |
outside_temp | yes | ✓ real cause — correctly identifiable | Identifiable given the observed variables: 'outside_temp' passes independence, temporal precedence, and deconfounding against every other m… |
fan | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'fan_speed' on 'room_temp' fails (independent) — fan_speed is ~marginally in… |
duct_pressure | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'duct_pressure' on 'room_temp' fails (independent) — duct_pressure is ~margi… |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionsstrongest trusted lever: outside_temp — matches the truth
The action question: if you could only turn one knob to move the room temp, which one? Ground truth: the biggest planted coefficient is outside_temp (-1.00). Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on room temp | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | outside_temp | -3.28 | yes | real lever (truth -1.00) |
| 2 | heater | +0.43 | yes | real lever (truth +0.55) |
| 3 | duct_pressure | -0.03 | no | not a real lever — check the Trusted column |
| 4 | fan | +0.02 | no | not a real lever — check the Trusted column |
check_datadata usable — 3000 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
3000 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 1.68 | 0.11 | 591 | moves and has time structure — analyzable |
room temp | yes | 2.58 | 0.56 | 611 | moves and has time structure — analyzable |
outside_temp | yes | 2.51 | 0.71 | 614 | moves and has time structure — analyzable |
fan | yes | 2.44 | 0.59 | 598 | moves and has time structure — analyzable |
duct_pressure | yes | 1.08 | 0.27 | 617 | moves and has time structure — analyzable |
explaintop root cause of the event: outside_temp
The incident question: this one room temp spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the room temp) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the room temp event at row 2086.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
outside_temp | 0 | 0.87 | stable | ✓ a real driver of the bill |
causal_analysisevery planted cause reported
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
| Reported driver | Lag | Verdict vs ground truth |
|---|---|---|
outside temp | — | ✓ correct — a planted cause of the target |
heater | — | ✓ correct — a planted cause of the target |
Non-monotone (U-shape)
Five planted cause-effect pairs whose shapes range from a symmetric optimum (pure U: correlation ~0 although the cause fully determines throughput) to plain monotone. The U-shaped pair is the trap: every monotone statistic reads it as independence, so a dedicated shape detector (validated at zero false alarms on 1,477 real pairs) admits it as a labelled shape_only edge — reported as a driver, but never given a single per-unit slope, because no single direction exists.
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
speed_sym | tput_sym | 1 | U-shape (optimum mid-range) |
speed_asym | tput_asym | 1 | asymmetric curve |
feed_mono | tput_mono | 2 | monotone curve |
load_sat | tput_sat | 1 | saturating |
temp_thr | tput_thr | 1 | threshold |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
The dashed purple edge is the U-shape: real dependence with NO single monotone sign (shown as sign '?').
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| feed_mono → tput_mono | 2 | + | ✓ correct — matches the planted edge |
| load_sat → tput_sat | 1 | + | ✓ correct — matches the planted edge |
| speed_asym → tput_asym | 1 | + | ✓ correct — matches the planted edge |
| temp_thr → tput_thr | 1 | + | ✓ correct — matches the planted edge |
| speed_sym → tput_sym | 1 | ? | ✓ correct — matches the planted edge |
All tool results (target: tput_sym — click a tool to expand; every row graded against the ground truth)
discover5 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| feed_mono → tput_mono | 2 | + | ✓ correct — matches the planted edge |
| load_sat → tput_sat | 1 | + | ✓ correct — matches the planted edge |
| speed_asym → tput_asym | 1 | + | ✓ correct — matches the planted edge |
| temp_thr → tput_thr | 1 | + | ✓ correct — matches the planted edge |
| speed_sym → tput_sym | 1 | ? | ✓ correct — matches the planted edge |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
causal_analysisevery planted cause reported
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
| Reported driver | Lag | Verdict vs ground truth |
|---|---|---|
speed_sym | — | ✓ correct — a planted cause of the target |
Curved confounding (observed)
Outside temperature drives BOTH the heater and the bill, but through curved (nonlinear) relationships. Heater and bill therefore co-move with no causal link between them — the trap is reporting heater as a cause of the bill. The confounder is a column in the file, and the shape-aware detection maps both curved edges; the correct graph has exactly three edges and NO heater-to-bill arrow.
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
outside temp | heater | 1 | curved |
outside temp | power bill | 2 | curved |
fan | room temp | 2 | + linear |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| fan → room temp | 2 | + | ✓ correct — matches the planted edge |
| outside_temp → heater | 1 | ? | ✓ correct — matches the planted edge |
| outside_temp → power bill | 2 | ? | ✓ correct — matches the planted edge |
The dashed purple edges are curved (shape) relationships — detected by the shape scanner; no single slope exists.
All tool results (target: power bill — click a tool to expand; every row graded against the ground truth)
discover3 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| fan → room temp | 2 | + | ✓ correct — matches the planted edge |
| outside_temp → heater | 1 | ? | ✓ correct — matches the planted edge |
| outside_temp → power bill | 2 | ? | ✓ correct — matches the planted edge |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
causal_analysisevery planted cause reported
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
| Reported driver | Lag | Verdict vs ground truth |
|---|---|---|
outside temp | — | ✓ correct — a planted cause of the target |
Derived and bookkeeping columns
Two real levers (pump runtime, batch total lines) drive throughput — surrounded by columns COMPUTED from them: averages, counts, load indexes, a line index, plus calendar columns. Every derived column correlates almost perfectly with its source. The provenance layer maps each as a transform of a measurement (functional map, near-perfect fit) so the drivers surface reports the true levers; the derivation web is drawn by discover for transparency and labelled equivalent, not causal.
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
pump_runtime | batch_total_lines | 1 | + |
batch_total_lines | throughput | 1 | + |
pump_runtime | throughput | 2 | + (direct) |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| batch_count → batch_avg_lines | 0 | − | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| pump_avg_cycle → pump_cycles | 0 | − | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| batch_avg_lines → batch_count | 0 | − | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| pump_cycles → pump_avg_cycle | 0 | − | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| pump_runtime → pump_load_index | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| batch_line_index → batch_total_lines | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| batch_total_lines → batch_line_index | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| pump_load_index → pump_runtime | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| batch_total_lines → batch_avg_lines | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| batch_total_lines → throughput | 1 | + | ✓ correct — matches the planted edge |
| pump_avg_cycle → pump_runtime | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| pump_runtime → batch_line_index | 1 | + | ✓ acceptable — true indirect (total) effect |
| pump_load_index → pump_avg_cycle | 2 | − | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| pump_runtime → throughput | 2 | + | ✓ correct — matches the planted edge |
| batch_line_index → batch_total_tasks | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| pump_runtime → batch_total_lines | 1 | + | ✓ correct — matches the planted edge |
| pump_cycles → pump_load_index | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| pump_load_index → pump_cycles | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| pump_runtime → batch_avg_lines | 1 | + | ✓ acceptable — true indirect (total) effect |
| batch_total_tasks → batch_line_index | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
Edges among one family (pump or batch) are the derivation web: transforms of one measurement, mapped and labelled equivalent by provenance.
All tool results (target: throughput — click a tool to expand; every row graded against the ground truth)
discover20 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| batch_count → batch_avg_lines | 0 | − | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| pump_avg_cycle → pump_cycles | 0 | − | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| batch_avg_lines → batch_count | 0 | − | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| pump_cycles → pump_avg_cycle | 0 | − | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| pump_runtime → pump_load_index | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| batch_line_index → batch_total_lines | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| batch_total_lines → batch_line_index | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| pump_load_index → pump_runtime | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| batch_total_lines → batch_avg_lines | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| batch_total_lines → throughput | 1 | + | ✓ correct — matches the planted edge |
| pump_avg_cycle → pump_runtime | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| pump_runtime → batch_line_index | 1 | + | ✓ acceptable — true indirect (total) effect |
| pump_load_index → pump_avg_cycle | 2 | − | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| pump_runtime → throughput | 2 | + | ✓ correct — matches the planted edge |
| batch_line_index → batch_total_tasks | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| pump_runtime → batch_total_lines | 1 | + | ✓ correct — matches the planted edge |
| pump_cycles → pump_load_index | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| pump_load_index → pump_cycles | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
| pump_runtime → batch_avg_lines | 1 | + | ✓ acceptable — true indirect (total) effect |
| batch_total_tasks → batch_line_index | 0 | + | derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the throughput change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → throughput | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
pump_runtime | +0.774 | yes | ⚠ trusted, but the true effect is 0 |
pump_cycles | +0.054 | no | ✓ correctly NOT trusted — no real effect exists |
pump_avg_cycle | +2.794 | no | ✓ correctly NOT trusted — no real effect exists |
pump_load_index | +1.063 | no | ✓ correctly NOT trusted — no real effect exists |
batch_total_lines | +0.609 | yes | ⚠ trusted, but the true effect is 0 |
batch_total_tasks | +0.101 | no | ✓ correctly NOT trusted — no real effect exists |
batch_count | +0.093 | no | ✓ correctly NOT trusted — no real effect exists |
batch_avg_lines | +0.818 | no | ✓ correctly NOT trusted — no real effect exists |
batch_line_index | +0.818 | no | ✓ correctly NOT trusted — no real effect exists |
op_hour | — | no | ✓ correctly NOT trusted — no real effect exists |
op_dow | +0.057 | no | ✓ correctly NOT trusted — no real effect exists |
op_month | refused (no variation) | — | — |
is_identifiable2 of 12 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → throughput | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
pump_runtime | yes | ✗ decoy marked identifiable | Identifiable given the observed variables: 'pump_runtime' passes independence, temporal precedence, and deconfounding against every other m… |
pump_cycles | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'pump_cycles' on 'throughput' fails (independent) — pump_cycles is ~marginal… |
pump_avg_cycle | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'pump_avg_cycle' on 'throughput' fails (confounded) — the association betwee… |
pump_load_index | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'pump_load_index' on 'throughput' fails (confounded) — the association betwe… |
batch_total_lines | yes | ✗ decoy marked identifiable | Identifiable given the observed variables: 'batch_total_lines' passes independence, temporal precedence, and deconfounding against every ot… |
batch_total_tasks | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'batch_total_tasks' on 'throughput' fails (confounded) — the association bet… |
batch_count | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'batch_count' on 'throughput' fails (independent) — batch_count is ~marginal… |
batch_avg_lines | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'batch_avg_lines' on 'throughput' fails (confounded) — the association betwe… |
batch_line_index | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'batch_line_index' on 'throughput' fails (confounded) — the association betw… |
op_hour | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'op_hour' on 'throughput' fails (schedule_indicator) — op_hour is a fixed sc… |
op_dow | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'op_dow' on 'throughput' fails (schedule_indicator) — op_dow is a fixed sche… |
op_month | — | ✓ correctly refused — not a real cause |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionsno real lever exists in this scenario
The action question: if you could only turn one knob to move the throughput, which one? Ground truth: the biggest planted coefficient — none, nothing drives the throughput here. Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on throughput | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | batch_total_lines | +0.23 | yes | not a real lever — check the Trusted column |
| 2 | pump_runtime | +0.13 | yes | not a real lever — check the Trusted column |
| 3 | batch_total_tasks | -1.62 | no | not a real lever — check the Trusted column |
| 4 | batch_count | -1.16 | no | not a real lever — check the Trusted column |
| 5 | pump_cycles | -1.11 | no | not a real lever — check the Trusted column |
| 6 | pump_load_index | -0.39 | no | not a real lever — check the Trusted column |
| 7 | op_dow | -0.38 | no | not a real lever — check the Trusted column |
| 8 | pump_avg_cycle | +0.21 | no | not a real lever — check the Trusted column |
| 9 | batch_avg_lines | +0.10 | no | not a real lever — check the Trusted column |
| 10 | batch_line_index | +0.05 | no | not a real lever — check the Trusted column |
check_datadata usable — 4000 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
4000 rows · 13 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
pump_runtime | yes | 2.46 | 0.58 | 777 | moves and has time structure — analyzable |
pump_cycles | yes | 1.74 | 0.50 | 826 | moves and has time structure — analyzable |
pump_avg_cycle | yes | 0.33 | 0.52 | 787 | moves and has time structure — analyzable |
pump_load_index | yes | 1.62 | 0.57 | 826 | moves and has time structure — analyzable |
batch_total_lines | yes | 4.02 | 0.50 | 768 | moves and has time structure — analyzable |
batch_total_tasks | yes | 2.32 | 0.49 | 772 | moves and has time structure — analyzable |
batch_count | yes | 1.07 | 0.40 | 788 | moves and has time structure — analyzable |
batch_avg_lines | yes | 1.08 | 0.41 | 759 | moves and has time structure — analyzable |
batch_line_index | yes | 2.88 | 0.49 | 794 | moves and has time structure — analyzable |
op_hour | yes | 6.90 | 0.96 | 324 | moves and has time structure — analyzable |
op_dow | yes | 1.99 | 1.00 | 321 | moves and has time structure — analyzable |
op_month | no | 0.00 | 0.00 | 0 | moves and has time structure — analyzable |
throughput | yes | 2.99 | 0.51 | 799 | moves and has time structure — analyzable |
explaintop root cause of the event: None
The incident question: this one throughput spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the throughput) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the throughput event at row 0.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
| (no root causes reported) | — | — | — | — |
causal_analysisevery planted cause reported
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
| Reported driver | Lag | Verdict vs ground truth |
|---|---|---|
batch_total_lines | — | ✓ correct — a planted cause of the target |
pump_runtime | — | ✓ correct — a planted cause of the target |
Realistic empty file (1)
The honest version of 'all levers off': every column moves, every column remembers yesterday, and nothing causes anything. Three seeds ship so the suite sees a RATE, not one pass/fail. On this seed a chance association reaches the model; the re-check on other stretches of the data is what exposes it (support 0 of 3, evidence 0.00).
Ground truth — the structure that generated the data
No causal edges exist — every column is noise or a constant.
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| fan → power bill | 0 | + | ⚠ not planted — vetoed by the tool (chance_level), evidence 0.00; reported for transparency, marked do-not-act |
All tool results (target: power bill — click a tool to expand; every row graded against the ground truth)
discover1 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| fan → power bill | 0 | + | ⚠ not planted — vetoed by the tool (chance_level), evidence 0.00; reported for transparency, marked do-not-act |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the power bill change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → power bill | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | -0.032 | no | ✓ correctly NOT trusted — no real effect exists |
room temp | +0.130 | no | ✓ correctly NOT trusted — no real effect exists |
fan | +0.033 | no | ✓ correctly NOT trusted — no real effect exists |
noise | -0.009 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable0 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → power bill | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'heater_power' on 'power_bill' fails (independent) — heater_power is ~margin… |
room temp | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (independent) — room_temp is ~marginally i… |
fan | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'fan_speed' on 'power_bill' fails (chance_level) — the association between f… |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'power_bill' fails (independent) — random_noise is ~margin… |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionsno real lever exists in this scenario
The action question: if you could only turn one knob to move the power bill, which one? Ground truth: the biggest planted coefficient — none, nothing drives the power bill here. Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on power bill | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | noise | -1.36 | no | not a real lever — check the Trusted column |
| 2 | heater | -1.35 | no | not a real lever — check the Trusted column |
| 3 | room temp | -0.79 | no | not a real lever — check the Trusted column |
| 4 | fan | -0.33 | no | not a real lever — check the Trusted column |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 11.72 | 0.62 | 156 | moves and has time structure — analyzable |
room temp | yes | 2.77 | 0.61 | 154 | moves and has time structure — analyzable |
fan | yes | 18.10 | 0.62 | 171 | moves and has time structure — analyzable |
power bill | yes | 3.74 | 0.60 | 169 | moves and has time structure — analyzable |
noise | yes | 27.34 | 0.65 | 166 | ⚠ unexpected time pattern in the noise column |
explaintop root cause of the event: None
The incident question: this one power bill spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the power bill) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the power bill event at row 426.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
| (no root causes reported) | — | — | — | — |
causal_analysisempty — correct
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
No driver reported — the analysis names no provable cause of the target (the correct answer here).
Realistic empty file (2)
Second seed of the realistic empty file. The only correct answer is an empty graph with confidence 0.
Ground truth — the structure that generated the data
No causal edges exist — every column is noise or a constant.
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| (no edges reported) | — | — | ✓ correct — pure noise refused, empty graph |
All tool results (target: power bill — click a tool to expand; every row graded against the ground truth)
discover0 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| (no edges reported) | — | — | ✓ correct — pure noise refused, empty graph |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the power bill change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → power bill | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | -0.033 | no | ✓ correctly NOT trusted — no real effect exists |
room temp | -0.205 | no | ✓ correctly NOT trusted — no real effect exists |
fan | -0.025 | no | ✓ correctly NOT trusted — no real effect exists |
noise | +0.014 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable0 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → power bill | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'heater_power' on 'power_bill' fails (independent) — heater_power is ~margin… |
room temp | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (independent) — room_temp is ~marginally i… |
fan | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'fan_speed' on 'power_bill' fails (independent) — fan_speed is ~marginally i… |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'power_bill' fails (independent) — random_noise is ~margin… |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionsno real lever exists in this scenario
The action question: if you could only turn one knob to move the power bill, which one? Ground truth: the biggest planted coefficient — none, nothing drives the power bill here. Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on power bill | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | room temp | -3.54 | no | not a real lever — check the Trusted column |
| 2 | noise | -2.79 | no | not a real lever — check the Trusted column |
| 3 | heater | -1.45 | no | not a real lever — check the Trusted column |
| 4 | fan | -0.67 | no | not a real lever — check the Trusted column |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 11.99 | 0.65 | 153 | moves and has time structure — analyzable |
room temp | yes | 2.71 | 0.59 | 131 | moves and has time structure — analyzable |
fan | yes | 17.36 | 0.57 | 141 | moves and has time structure — analyzable |
power bill | yes | 3.82 | 0.61 | 166 | moves and has time structure — analyzable |
noise | yes | 26.71 | 0.65 | 165 | ⚠ unexpected time pattern in the noise column |
explaintop root cause of the event: None
The incident question: this one power bill spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the power bill) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the power bill event at row 64.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
| (no root causes reported) | — | — | — | — |
causal_analysisempty — correct
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
No driver reported — the analysis names no provable cause of the target (the correct answer here).
Ground truth — the structure that generated the data
No causal edges exist — every column is noise or a constant.
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| (no edges reported) | — | — | ✓ correct — pure noise refused, empty graph |
All tool results (target: power bill — click a tool to expand; every row graded against the ground truth)
discover0 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| (no edges reported) | — | — | ✓ correct — pure noise refused, empty graph |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the power bill change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → power bill | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | +0.038 | no | ✓ correctly NOT trusted — no real effect exists |
room temp | +0.137 | no | ✓ correctly NOT trusted — no real effect exists |
fan | +0.028 | no | ✓ correctly NOT trusted — no real effect exists |
noise | -0.017 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable0 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → power bill | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'heater_power' on 'power_bill' fails (independent) — heater_power is ~margin… |
room temp | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (independent) — room_temp is ~marginally i… |
fan | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'fan_speed' on 'power_bill' fails (independent) — fan_speed is ~marginally i… |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'power_bill' fails (independent) — random_noise is ~margin… |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionsno real lever exists in this scenario
The action question: if you could only turn one knob to move the power bill, which one? Ground truth: the biggest planted coefficient — none, nothing drives the power bill here. Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on power bill | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | fan | +1.21 | no | not a real lever — check the Trusted column |
| 2 | heater | -1.17 | no | not a real lever — check the Trusted column |
| 3 | room temp | +1.05 | no | not a real lever — check the Trusted column |
| 4 | noise | -0.38 | no | not a real lever — check the Trusted column |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 11.78 | 0.66 | 155 | moves and has time structure — analyzable |
room temp | yes | 2.66 | 0.58 | 171 | moves and has time structure — analyzable |
fan | yes | 16.65 | 0.57 | 156 | moves and has time structure — analyzable |
power bill | yes | 3.64 | 0.58 | 157 | moves and has time structure — analyzable |
noise | yes | 25.11 | 0.58 | 159 | ⚠ unexpected time pattern in the noise column |
explaintop root cause of the event: None
The incident question: this one power bill spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the power bill) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the power bill event at row 238.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
| (no root causes reported) | — | — | — | — |
causal_analysisempty — correct
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
No driver reported — the analysis names no provable cause of the target (the correct answer here).
Shuffled rows
The baseline data with its rows randomly permuted: the relationships between columns survive, but time order — the only evidence for CAUSAL direction — is gone. The honest answer reports related pairs with orientation flagged untrustworthy, and the graded truth is ZERO recoverable causes.
Ground truth — the structure that generated the data
| Cause | Effect | Lag | Coefficient |
|---|---|---|---|
heater | power bill | 1 | +0.08 (destroyed) |
heater | room temp | 2 | +0.40 (destroyed) |
fan | power bill | 3 | +0.16 (destroyed) |
TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| heater → room temp | 0 | + | ⚠ right edge, wrong lag (truth lag 2) |
| power bill → fan | 0 | + | related pair only — the arrow is NOT trustworthy (labeled orientation_confidence = moderate) |
| room temp → power bill | 0 | + | related pair only — the arrow is NOT trustworthy (labeled orientation_confidence = low) |
| power bill → heater | 0 | + | related pair only — the arrow is NOT trustworthy (labeled orientation_confidence = low) |
| heater → power bill | 1 | + | ✗ expected but NOT found — missing edge |
| fan → power bill | 3 | + | ✗ expected but NOT found — missing edge |
All tool results (target: power bill — click a tool to expand; every row graded against the ground truth)
discover4 edge(s) found in the full graph
The headline question: which variables cause which? With no target given, discover maps the full causal graph — every edge between every pair of variables, with its delay (lag) and direction (sign). Every row is compared against the structure we planted in the generator.
| Edge TCPFN reported | Lag | Sign | Verdict vs ground truth |
|---|---|---|---|
| heater → room temp | 0 | + | ⚠ right edge, wrong lag (truth lag 2) |
| power bill → fan | 0 | + | related pair only — the arrow is NOT trustworthy (labeled orientation_confidence = moderate) |
| room temp → power bill | 0 | + | related pair only — the arrow is NOT trustworthy (labeled orientation_confidence = low) |
| power bill → heater | 0 | + | related pair only — the arrow is NOT trustworthy (labeled orientation_confidence = low) |
| heater → power bill | 1 | + | ✗ expected but NOT found — missing edge |
| fan → power bill | 3 | + | ✗ expected but NOT found — missing edge |
estimate_effectper-unit effect of every candidate, vs the planted coefficients
The dose question: raise the cause by 1 unit — how much does the power bill change? The number in 'truth' is the exact coefficient written into the generator. 'Trusted' is TCPFN's own honesty flag: for decoy variables the CORRECT answer is to refuse the number.
| Cause → power bill | Per-unit effect | Trusted | Verdict vs ground truth |
|---|---|---|---|
heater | +0.089 | no | truth +0.08 — ⚠ flagged untrustworthy |
room temp | +0.208 | no | ✓ correctly NOT trusted — no real effect exists |
fan | +0.071 | no | truth +0.16 — ⚠ flagged untrustworthy |
noise | +0.011 | no | ✓ correctly NOT trusted — no real effect exists |
is_identifiable0 of 4 candidates identifiable
The trust question: can this cause→effect claim be supported by this data at all? A real cause should come back YES; a decoy (correlated but not causal) should come back NO. Refusing a decoy is just as important as confirming a real cause.
| Cause → power bill | Identifiable | Verdict vs ground truth | TCPFN's own reason (short) |
|---|---|---|---|
heater | no | ⚠ real cause flagged not identifiable | Not identifiable: a necessary condition for a causal effect of 'heater_power' on 'power_bill' fails (not_temporal) — the rows do not behave… |
room temp | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (not_temporal) — the rows do not behave li… |
fan | no | ⚠ real cause flagged not identifiable | Not identifiable: a necessary condition for a causal effect of 'fan_speed' on 'power_bill' fails (not_temporal) — the rows do not behave li… |
noise | no | ✓ correctly refused — not a real cause | Not identifiable: a necessary condition for a causal effect of 'random_noise' on 'power_bill' fails (independent) — random_noise is ~margin… |
what_should_i_measure0 action(s) proposed
The advisory question: what extra measurement would most improve causal certainty? There is no planted truth to grade here — on these small, fully-measured panels the correct behaviour is usually to propose nothing.
No actions proposed — the causal structure is already resolvable from the existing columns.
rank_interventionstop trusted lever: None (truth: fan)
The action question: if you could only turn one knob to move the power bill, which one? Ground truth: the biggest planted coefficient is fan (+0.16). Untrusted rows are shown for transparency but should not be acted on.
| Rank | Intervention | Expected effect on power bill | Trusted | Verdict vs ground truth |
|---|---|---|---|---|
| 1 | fan | -1.14 | no | real lever (truth +0.16) |
| 2 | heater | -1.03 | no | real lever (truth +0.08) |
| 3 | noise | +0.74 | no | not a real lever — check the Trusted column |
| 4 | room temp | +0.68 | no | not a real lever — check the Trusted column |
check_datadata usable — 800 rows
The health check that runs before any analysis — no AI model involved.
- Std — how much the variable actually moved; a knob nobody turned teaches nothing.
- Autocorr — whether today remembers yesterday, i.e. the data has a real sense of time.
- Sharp changes — sudden jumps that act like natural experiments; the raw material for causal evidence.
800 rows · 5 variables · suitable for: full-graph discovery: yes · target-scoped: yes · effect estimation: yes
| Variable | Usable | Std (movement) | Autocorr (time pattern) | Sharp changes | Verdict vs ground truth |
|---|---|---|---|---|---|
heater | yes | 16.72 | 0.01 | 155 | moves and has time structure — analyzable |
room temp | yes | 7.02 | -0.01 | 85 | moves and has time structure — analyzable |
fan | yes | 21.40 | 0.00 | 114 | moves and has time structure — analyzable |
power bill | yes | 4.01 | -0.01 | 84 | moves and has time structure — analyzable |
noise | yes | 29.18 | -0.06 | 107 | ✓ no time pattern — pure noise, as designed |
explaintop root cause of the event: None
The incident question: this one power bill spike — what caused it? Unlike discover (the overall structure), explain looks at a single event. Ground truth: only the real drivers (the planted causes of the power bill) can be behind a genuine spike — a decoy in this list would be an error.
Root-cause analysis of the power bill event at row 540.
| Root cause | Lag | Score | Direction | Verdict vs ground truth |
|---|---|---|---|---|
| (no root causes reported) | — | — | — | — |
causal_analysis0 driver(s)
The operator question: what actually drives the target? This is the full analysis surface — every candidate passes the necessary-condition checks, borderline candidates must show replicated evidence, and anything withheld is disclosed by name rather than silently dropped.
No driver reported — the analysis names no provable cause of the target.