TCPFN v2.1.1.50.3
MCP API REST API Performance Metrics

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 tools

Quickstart — 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.

Preview. Every tool below runs end-to-end against the sample data, but this build is still calibrating effect signs and some magnitudes. Treat the values in the “What you should see” notes as the intended shape of the answer, not a guaranteed one on today’s model — and read each tool’s 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)MeaningWhat TCPFN should conclude
heater_powerroom_temp

A running heater warms the room a beat later.

A clear edge.

heater_powerpower_bill

The heater shows up on the next hour's bill — a real lever, but the smaller one.

An edge; moderate effect.

fan_speedpower_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).

shell — one-time setup
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 — check_data
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 — discover
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 — is_identifiable
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 — estimate_effect
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 — rank_interventions
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.

Next: what should you measure?
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).

MethodPathAuthPurpose
POST/mcprequired

MCP JSON-RPC endpoint — every tool call goes here.

GET/healthpublic

Liveness check → {"status":"ok","service":"tcpfn-mcp"}.

GET/docspublic

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 provideWhere it comes from

Server URL

Your deployment's public HTTPS URL (TCPFN_MCP_PUBLIC_URL) — also the OAuth issuer and audience.

Client ID + Client secret

A pair you choose (e.g. two uuidgen strings): set them as TCPFN_OAUTH_CLIENT_ID / TCPFN_OAUTH_CLIENT_SECRET on the server and enter the same pair in the connector. The server is its own OAuth provider and validates against them — nothing is issued centrally. A single fixed client; dynamic client registration is disabled.

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:

HTTP header
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:

~/.cursor/mcp.json · Claude Desktop config
{
  "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 — health
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 — REST tool call
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.

The /mcp endpoint itself is session-based (for MCP clients): initializeMcp-Session-Idtools/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.

Why call 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.

FieldTypeWhat it tells you
data_reportDataReport

What the model actually analysed vs. what was uploaded: n_rows_original/n_cols_original vs. rows_used/cols_used, the exact columns_used, and dropped_columns — each excluded column with a reason (non_numeric, constant_or_all_nan, non_finite, not_requested). If this is non-empty, your result is based on a subset of the file.

empty_result_reasonstring | 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". null means the result is legitimately non-empty.

warningsarray<string>

Data- or result-quality flags (short series, unsigned edges, unnormalised target-scoped strengths, …). Empty = no known concerns.

suggested_next_callsarray<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:

#SituationDo this
1

Any surprising result

Read diagnostics first: empty_result_reason, warnings, and data_report.dropped_columns — the result may be based on different columns than you uploaded.

2

discover returned 0 edges

Check edges_at_thresholds in the output. The default threshold is 0.1 (same as the CLI); if looser thresholds show edges, the signal exists but is weak — retry with a lower threshold rather than concluding there is no causal structure.

3

Result still looks wrong

Call check_data (cheap, no model inference) — per-variable suitability verdicts: missing values, constants, sharp-change counts, stationarity (ADF), collinear pairs, statistical-floor and memory checks, each with a remediation.

4

An expected edge is missing

Retry discover with include_debug: true and inspect debug.signal_stats (a variable with lag-1 autocorrelation near 1.0 and few sharp changes is too slow-varying to score) and debug.per_pair (each pair's raw magnitude and judgment components).

5

Before acting on an edge

Check its p_null (probability the edge is spurious; high = suspect) and p_ident (identifiability; low = possibly confounded). For decisions, re-run discover with stability: {} — each edge gets a support fraction across temporal subsamples; prefer support >= 0.67.

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

ParameterTypeDefaultDescription
data_csvstringoptional

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_pathstringoptional

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_namesarray<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_versionenum(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_fitbooleanoptional

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.
thresholdnumberoptional0.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.
targetstringoptional

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_debugbooleanoptionalFalse

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'.
stabilityStabilityInput | booleanoptional

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)

FieldTypeDescription
edgesarray<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.
variablesarray<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_samplesinteger

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_versionstring

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

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

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

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_thresholdsobject

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_secondsnumber

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_reportFitReport | 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.
diagnosticsDiagnostics | 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.
stabilityStabilityReport | 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.
debugobject | 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.

Request tools/call
{
  "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
    }
  }
}
Response DiscoverOutput
{
  "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

ParameterTypeDefaultDescription
data_csvstringoptional

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_pathstringoptional

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_namesarray<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_versionenum(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_fitbooleanoptional

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

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

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)

FieldTypeDescription
causestring

Cause variable name (echoed from input).

ExampleThe lever you asked about, echoed back so a saved result still makes sense months later.
effectstring

Effect variable name (echoed from input).

ExampleThe outcome you asked about, echoed back so a saved result still makes sense months later.
ate_meannumber | 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_stdnumber | 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.
scaleEffectScale | 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_studyEventStudy | 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.
consistencynumber

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_unitsinteger

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_stepsinteger

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

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

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

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_secondsnumber

Server-side compute time, in seconds.

ExampleHow long the server took, for sizing a batch of these across many meters.
fit_reportFitReport | 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.
diagnosticsDiagnostics | 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.

Request tools/call
{
  "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"
    }
  }
}
Response EstimateEffectOutput
{
  "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

ParameterTypeDefaultDescription
data_csvstringoptional

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_pathstringoptional

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_namesarray<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_versionenum(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_fitbooleanoptional

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

Cause variable name.

ExampleThe lever you are thinking of pulling — fan_speed, before you ask the plant to change it.
effectstringrequired

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)

FieldTypeDescription
causestring

Cause variable name (echoed from input).

ExampleThe lever you asked about, echoed back.
effectstring

Effect variable name (echoed from input).

ExampleThe outcome you asked about, echoed back.
identifiableboolean

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_notestring | 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.
consistencynumber

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

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

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_secondsnumber

Server-side compute time, in seconds.

ExampleHow long the server took.
fit_reportFitReport | 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.
diagnosticsDiagnostics | 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.

Request tools/call
{
  "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"
    }
  }
}
Response IsIdentifiableOutput
{
  "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

ParameterTypeDefaultDescription
data_csvstringoptional

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_pathstringoptional

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_namesarray<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_versionenum(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_fitbooleanoptional

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)

FieldTypeDescription
actionsarray<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_actionsinteger

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

Plain-language summary of the recommendation.

ExampleThe recommendation in a sentence, ready to forward.
confidencenumber

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_secondsnumber

Server-side compute time, in seconds.

ExampleHow long the server took.
fit_reportFitReport | 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.
diagnosticsDiagnostics | 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.

Request tools/call
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "what_should_i_measure",
    "arguments": {
      "data_path": "https://<your-tcpfn-host>/api/sample.csv"
    }
  }
}
Response WhatShouldIMeasureOutput
{
  "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

ParameterTypeDefaultDescription
data_csvstringoptional

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_pathstringoptional

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_namesarray<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_versionenum(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_fitbooleanoptional

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

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.
candidatesarray<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.
stabilityStabilityInput | booleanoptional

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)

FieldTypeDescription
targetstring

Target variable being driven (echoed from input).

ExampleThe number being driven, echoed back.
rankingsarray<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_classesobject

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_leakageobject

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_mixesobject

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_leverstring | 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_confidencenumber | 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_checkLeverCrossCheck | 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_notestring | 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_grouparray<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_topinteger

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.
stabilityStabilityReport | 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_driversarray<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_candidatesinteger

Number of candidate variables evaluated.

ExampleHow many levers you submitted.
n_rankedinteger

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

Plain-language summary of the ranking.

ExampleThe ordering in a sentence, ready for a summary slide.
confidencenumber

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_secondsnumber

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_reportFitReport | 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.
diagnosticsDiagnostics | 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.

Request tools/call
{
  "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"
      ]
    }
  }
}
Response RankInterventionsOutput
{
  "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.

Request tools/call · utility_bill_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"
      ]
    }
  }
}
Response RankInterventionsOutput
{
  "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

ParameterTypeDefaultDescription
data_csvstringoptional

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_pathstringoptional

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_namesarray<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_versionenum(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_fitbooleanoptional

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

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)

FieldTypeDescription
n_samplesinteger

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_variablesinteger

Usable numeric variables found.

ExampleHow many usable measurement columns were found; labels and timestamps do not count.
sampling_intervalstring | 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.
variablesarray<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_discoveryboolean

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_discoveryboolean

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_estimationboolean

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_pairsarray<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_columnsarray<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_groupsarray<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_pairsarray<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_clocksarray<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_coveragearray<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.
warningsarray<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.
recommendationsarray<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.
diagnosticsDiagnostics | 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.
regimestring

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_autocorrnumber

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.

Request tools/call
{
  "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"
    }
  }
}
Response CheckDataOutput
{
  "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

ParameterTypeDefaultDescription
data_csvstringoptional

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_pathstringoptional

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_namesarray<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_versionenum(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_fitbooleanoptional

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_varsarray<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.
targetstringrequired

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_indexintegeroptional

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_timestringoptional

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_hoursnumberoptional4.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_columnstringoptional

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.
thresholdnumberoptional0.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)

FieldTypeDescription
target_variablestring

The variable whose event was explained (echoed).

ExampleThe thing that was explained — the bill — echoed back.
event_time_idxinteger

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_causesarray<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_chainsarray<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.
counterfactualsarray<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_samplesinteger

Rows analysed.

ExampleHow many readings from the window before the spike were used.
model_versionstring

TCPFN weight version used for the internal discovery.

ExampleWhich weights ran the analysis behind this explanation.
explanationstring

Plain-language summary of the top root causes.

ExampleThe incident in prose — usually the paragraph that goes into the write-up.
elapsed_secondsnumber

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_reportFitReport | 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.
diagnosticsDiagnostics | 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.

Request tools/call
{
  "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
    }
  }
}
Response ExplainOutput
{
  "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

ParameterTypeDefaultDescription
data_csvstringoptional

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_pathstringoptional

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_namesarray<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_versionenum(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_fitbooleanoptional

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_varsarray<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_causesarray<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.
targetstringrequired

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_timestringoptional

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.
thresholdnumberoptional0.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_hoursnumberoptional4.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_columnstringoptional

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_namestringoptional

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)

FieldTypeDescription
schema_versioninteger

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

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

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.
eventsarray<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_effectsarray<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_factorsobject

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_timelinesobject

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_warningarray<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.
sensitivityarray<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_networkCausalNetwork

`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_evidenceobject

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_columnsarray<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_timestring | 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_timestring | 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_secondsnumber

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_reportFitReport | 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.
diagnosticsDiagnostics | 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.

Request tools/call
{
  "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"
    }
  }
}
Response CausalAnalysisOutput
{
  "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

ParameterTypeDefaultDescription
resultobject | stringrequired

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

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').
causestringoptional

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

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_signstringoptional

'+' 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)

FieldTypeDescription
questionstring

The question that was asked.

ExampleThe question that was asked, echoed back.
toolstring

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

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

The answer in one line.

ExampleThe answer in one line — what is actually true about the fan.
detailstring

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

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_stepsarray<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.
settlestring | 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_windowsarray<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_agreementstring | 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.

Request tools/call
{
  "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
        }
      }
    }
  }
}
Response TroubleshootOutput
{
  "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.

FieldTypeDescription
n_subsamplesinteger

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_fractionnumber

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

FieldTypeDescription
causestring

Cause variable name.

ExampleThe upstream end — fan_speed in the fan-to-bill link. This is the thing you would actually change.
effectstring

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

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

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_windowarray<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_signinteger

+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_nullnumber | 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_identnumber | 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_magnitudenumber | 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.
supportnumber | 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.
vetostring | 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_detailstring | 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.
evidencenumber | 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_confidencestring | 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_windowsarray<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_agreementstring | 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_caveatstring | 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_undecidedboolean | 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_laginteger | 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_strengthnumber | 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_supportnumber | 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_notestring | 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_loopboolean | 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_notestring | 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_notestring | 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.

FieldTypeDescription
atstring | 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_indexinteger

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_rowsarray<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_changenumber

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_changenumber

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

FieldTypeDescription
original_shapearray

(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_shapearray

(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_beforenumber

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_afternumber

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_mbnumber

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.
appliedarray<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.
warningstring

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.

FieldTypeDescription
stepstring

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

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_columnsarray<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_rowinteger | 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_afterinteger | null

Row count after this step.

ExampleHow many utility_bill readings were left after this step, as the trimming stacks up.
cols_afterinteger | 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.

FieldTypeDescription
data_reportDataReport

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

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.
warningsarray<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.
errorsarray<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_reasonstring | 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_callsarray<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.

FieldTypeDescription
n_rows_originalinteger

Rows in the uploaded file.

ExampleHow many utility_bill readings were in the file you sent, before anything was set aside.
n_cols_originalinteger

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_usedinteger

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_usedinteger

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_columnsarray<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_usedarray<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_intervalstring | 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_secondsnumber | 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.

FieldTypeDescription
namestring

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.
reasonenum(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.
detailstring | 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.

FieldTypeDescription
codestring

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.
severityenum(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.
messagestring

Human/LLM-readable explanation.

ExampleThe same point in plain language, written to be shown to whoever is reading the result.
fieldstring | 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.

FieldTypeDescription
performedboolean

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.
modeenum(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_subsamplesinteger

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_fractionnumber

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_reasonstring | 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.
notestring

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.

FieldTypeDescription
per_unit_effectnumber

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_sourcestring

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_laginteger | 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_slopenumber | 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_laginteger | 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_detectedboolean | 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_conversionnumber | 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_agreementboolean | 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_slopenumber | 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_persistencenumber | 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_decaynumber | 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.

FieldTypeDescription
n_eventsinteger

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_usedinteger

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

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

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_leninteger

Length (steps) of the local pre-event baseline window.

ExampleHow many steps of 'just before' each event is compared with.
event_len_medianinteger

Median event length in steps.

ExampleTypical event duration in steps.
mean_deltanumber

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_deltanumber

Median per-event delta (robust twin).

ExampleSame story ignoring extreme events — quote this when one monster event dominates the mean.
znumber

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_negativeinteger

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_positiveinteger

Events where the effect rose vs baseline.

ExampleIn how many individual events the effect rose.
directionstring

'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_cvnumber | 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_cautionboolean

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_deltanumber

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_cautionboolean

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_meanarray<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_meanarray<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_meanarray<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_deltasarray<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_loarray<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_hiarray<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_narray<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.

FieldTypeDescription
action_typestring

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_variablestring

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_gainnumber

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

FieldTypeDescription
causestring

Candidate intervention variable.

ExampleThe lever this row is about — the fan, or the heater.
ate_meannumber

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_stdnumber

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

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_certaintynumber

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

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_notestring | 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_nextboolean | 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_classstring | 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.

FieldTypeDescription
agreesboolean

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_topstring | null

The lever the data-only ordering puts first.

ExampleThe lever the data-only ordering puts first.
data_orderingarray<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.
notestring

How to read the agreement or disagreement.

ExampleThe agreement or disagreement in words.

LeverScore

FieldTypeDescription
causestring

Cause

ExampleThe lever this row scores.
scorenumber

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

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.

FieldTypeDescription
causestring

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_eventsinteger

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_ratenumber

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_ratenumber

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

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

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.

FieldTypeDescription
namestring

Variable (column) name.

ExampleWhich column this row is about — heater_power, room_temp, and so on.
okboolean

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.
issuesarray<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.
stdnumber

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_fractionnumber

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_lag1number

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_countinteger

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'.
stationaryboolean | 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.

FieldTypeDescription
namestring

Column name as it appeared in the input file.

ExampleThe column the verdict is about, named as it appeared in your file.
kindenum(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.
reasonstring

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

FieldTypeDescription
columnsarray<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.
reasonstring

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.

FieldTypeDescription
causestring

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

Outcome column the shape explains.

ExampleThe outcome the shape explains — power_bill in that example.
eta2number

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_assocnumber

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_thresholdnumber

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

Lag (samples) at which the shape is strongest.

ExampleHow many samples after a fan change the shaped response shows up in the bill.
recommendationstring

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.

SharedClock

A rhythm (or trend) shared by several columns — the pairs among them are at risk of a fabricated relationship.

FieldTypeDescription
kindenum(cycle, trend)

'cycle' with a period, or 'trend'.

ExampleWhether the shared component is a repeating cycle (a daily rhythm in fan_speed and power_bill) or a drift both columns ride over the record.
periodnumber | null

Period in samples for kind='cycle' (None for trend).

ExampleFor a cycle, how many samples one repeat takes — 24 on hourly utility_bill readings is the day. Empty for a trend.
columnsarray<string>

Columns carrying this component (strongest first).

ExampleWhich columns carry it, strongest first. Any pair among them is the pair at risk.
consequencestring

Plain statement of the risk and how the tools handle it.

ExampleThe plain statement of what this means for the analysis and how the tools handle it — including whether power_bill itself is one of the columns on the clock.

PriorCoverageFlag

One detected data pattern the training prior does not cover.

FieldTypeDescription
patternenum(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_variablesarray<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.
evidencestring

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_notestring

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

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.

FieldTypeDescription
variablestring

Candidate cause variable name.

ExampleThe column being blamed — the fan, the heater, the room.
laginteger

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_scorenumber

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

'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_scorenumber

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_scorenumber

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_scorenumber

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_attributionnumber

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_scorenumber

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_scorenumber

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_valuenumber

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.

FieldTypeDescription
patharray<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_laginteger

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

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

FieldTypeDescription
variablestring

Variable at this hop.

ExampleThe column at this point in the route — the room, on the way from heater to bill.
laginteger

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

FieldTypeDescription
variablestring

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_reductionnumber

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.

FieldTypeDescription
modelstring

Engine that produced this result.

model_versionstring | null

Weight version.

targetstring

The variable analysed.

objectivestring | null

Display label for what the target represents.

DashboardKpis

Headline numbers for the result header.

FieldTypeDescription
n_eventsinteger | null

How many significant events were detected on the target.

n_events_increaseinteger | null

Of those, how many were above-baseline excursions.

n_events_decreaseinteger | null

Of those, how many were below-baseline excursions.

target_is_binaryboolean | null

True when the target is a 0/1 flag rather than a continuous reading.

total_downtime_minnumber | null

Total minutes spent inside an event.

avg_duration_minnumber | null

Mean event duration, in minutes.

median_duration_minnumber | null

Median event duration, in minutes.

max_duration_minnumber | null

Longest single event, in minutes.

n_causal_varsinteger | null

Variables found to drive the target.

n_total_varsinteger | null

Variables analysed in total.

n_sig_linksinteger | null

Edges that survived the strength threshold.

n_total_linksinteger | null

Edges considered before thresholding.

n_sectionsinteger | null

Distinct process sections represented among the variables.

global_tau_maxinteger | null

Largest lag considered, in samples.

global_tau_max_hoursnumber | null

The same maximum lag expressed in hours.

sampling_interval_secondsnumber | null

Detected seconds between readings.

top2_cause_namesarray<string>

The two most frequently implicated drivers.

top2_cause_countsarray<integer>

How often each of those two led an event.

top2_cause_pctnumber | null

Share of events those two account for.

top_cause_scoresarray<number>

Attribution scores of the leading drivers.

health_scorenumber | null

Percentage of the period spent outside any event.

minutes_since_last_eventnumber | null

Minutes from the last event to the end of the data.

next_event_eta_hoursnumber | null

Hours to the next event, from the mean recurrence interval. Null with fewer than two events.

DashboardEvent

One detected event on the target.

FieldTypeDescription
event_idinteger

Identifier used to look this event up in the per-event blocks.

start_idxinteger

Row where the event began.

end_idxinteger

Row where the event ended.

duration_stepsinteger | null

Length in rows.

duration_minnumber | null

Length in minutes, via the detected sampling interval.

start_timestring | null

Timestamp of the start, when the data carries one.

explainedboolean | null

Whether a root-cause analysis was run for this event.

top_causestring | null

Highest-attribution driver for this event.

tagstring | null

Display label for that driver.

top_contributionnumber | null

That driver's attribution score.

n_risk_factorsinteger

How many contributors were identified.

anomaly_directionstring | null

'increase', 'decrease', or 'onset' for a binary flag.

process_sectionstring | null

Section the leading driver belongs to.

modelstring | null

Engine that produced the explanation.

DashboardCausalEffect

One driver's effect on the target, across the whole history.

FieldTypeDescription
causestring

The driving variable.

tagstring | null

Display label.

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

descriptionstring | null

Human description from the field config.

process_sectionstring | null

Section the driver belongs to.

section_nicestring | null

Display form of the section name.

tcpfn_strengthnumber | null

TCPFN edge strength — the unambiguous magnitude.

total_mcinumber | null

Inherited key: carries the SIGNED TCPFN strength, not a partial-correlation statistic.

max_mcinumber | null

Inherited key: equals total_mci, since TCPFN returns one best lag per edge.

lag_at_maxinteger | null

Lag in samples at which the effect is strongest.

lag_hoursnumber | null

That lag in the dataset's real cadence.

n_sig_lagsinteger

Lags found significant — 1 for a direct edge, 0 when the driver reaches the target only via a chain.

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

signstring | null

Whether raising the driver raises or lowers the target.

sign_sourcestring | null

Where the sign came from (discovery, or the effect estimate).

is_direct_edgeboolean

False when the driver acts only through intermediates.

rca_combined_scorenumber | null

Attribution score from the root-cause pass.

rca_anomaly_scorenumber | null

How unusual this driver looked around the events.

rca_counterfactual_scorenumber | null

How much of the event severity it accounts for.

directionstring | null

Direction the driver moved before the events.

EventRiskFactor

One contributor to one event.

FieldTypeDescription
tagstring | null

Display label.

variablestring

The contributing variable.

descriptionstring | null

Human description from the field config.

process_sectionstring | null

Section it belongs to.

section_nicestring | null

Display form of the section name.

contributionnumber | null

Share of this event attributed to it.

laginteger | null

How many samples before the event it moved.

lag_hoursnumber | null

That lag in real time.

directionstring | null

Which way it moved.

onset_rowinteger | null

Row where this variable's deviation started before the event (its verified onset). Null when history was too short to judge.

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

evidencestring | 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_scorenumber | null

Overall attribution score.

rca_anomaly_scorenumber | null

How unusual it looked.

rca_counterfactual_scorenumber | null

Severity reduction had it stayed normal.

SensorTimeline

Chart data for one event.

FieldTypeDescription
time_relarray<number>

Hours relative to the event, 0 at the event itself.

eventarray<number>

The TARGET's own values across the window — a series, not a moment. Distinct from the top-level `events`.

variablesobject

Per-variable traces, keyed by variable name.

idx_lointeger | null

First row of the window.

idx_hiinteger | null

Last row of the window.

TimelineVariable

One variable's trace across an event window.

FieldTypeDescription
raw_valuesarray<number>

The variable's readings across the window.

z_scoresarray<number>

Those readings as deviations from the variable's own normal.

is_riskboolean

Whether this variable was implicated in this event — drives the line colour.

sample_idxinteger

Index of the pre-event peak deviation; the marker position.

has_precursorboolean

False when no real pre-event excursion exists, so the marker sits on noise and should be suppressed.

normal_mediannumber | null

The variable's typical level outside events.

value_at_peaknumber | null

Reading at the marked index.

z_at_peaknumber | null

Deviation at the marked index.

descriptionstring | null

Human description from the field config.

EarlyWarningItem

A driver that moves BEFORE the target does.

FieldTypeDescription
causestring

The leading variable.

tagstring | null

Display label.

descriptionstring | null

Human description.

total_mcinumber | null

Inherited key: signed TCPFN strength.

median_lag_hoursnumber | null

Typical warning time before the target reacts.

early_warning_scorenumber | null

How useful it is as an alert — strength weighted by lead time.

process_sectionstring | null

Section it belongs to.

tcpfn_strengthnumber | null

The unambiguous magnitude.

SensitivityItem

How much moving one variable would move the target.

FieldTypeDescription
variablestring

The lever.

tagstring | null

Display label.

descriptionstring | null

Human description.

increase_effectnumber | null

Effect of raising it.

decrease_effectnumber | null

Effect of lowering it.

net_effectnumber | null

Combined effect.

prioritynumber | null

Ranking key for which lever to consider first.

process_sectionstring | null

Section it belongs to.

basisstring | null

What the number was derived from.

CausalNetwork

The graph, in the two shapes the diagram needs.

FieldTypeDescription
sensor_networkSensorNetwork

Nodes and edges.

causal_chainsCausalChains

Multi-hop routes.

SensorNetwork

Nodes and edges for the network diagram.

FieldTypeDescription
nodesarray<NetworkNode>

One per variable.

edgesarray<NetworkEdge>

One per discovered link.

sec_listarray<string>

Section names; edges reference these by index.

NetworkNode

A variable, as a node in the graph diagram.

FieldTypeDescription
idstring

Variable name.

sectionstring | null

Process section, used for grouping.

out_deginteger

How many variables it drives.

in_deginteger

How many drive it.

mci_targetnumber | null

Inherited key: signed TCPFN strength into the target.

descstring | null

Human description.

NetworkEdge

An edge in the graph diagram. Keys are the dashboard's abbreviations.

FieldTypeDescription
sstring

Source variable.

tstring

Target variable.

mnumber | null

Inherited key: the SIGNED TCPFN strength.

anumber | null

Inherited key: the same magnitude, unsigned.

linteger | null

Lag in samples.

xinteger | null

Inherited key: significant-lag count.

ssinteger | null

Source section, as an index into sec_list.

tsinteger | null

Target section, as an index into sec_list.

tcpfn_strengthnumber | null

The unambiguous magnitude, beside the inherited keys.

lag_hoursnumber | null

Lag in real time.

signstring | null

Direction of the effect.

sign_sourcestring | null

Where the sign came from.

CausalChains

FieldTypeDescription
chainsarray<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_idinteger | 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_eventobject

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.

FieldTypeDescription
patharray<ChainStep>

The hops, in order.

path_strstring | null

The route as readable text.

total_laginteger

Total delay along the route, in samples.

total_lag_hoursnumber | null

Total delay in real time.

strengthnumber | 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).

evidencestring | 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_confirmedboolean | 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.

FieldTypeDescription
varstring

Variable at this hop.

laginteger | null

Lag contributed by this hop, in samples.

lag_hoursnumber | null

That hop's lag in real time.

DriverEvidenceItem

A driver's supporting evidence, grouped by section.

FieldTypeDescription
rankinteger | null

Position within its section.

tagstring | null

Display label.

descriptionstring | null

Human description.

process_sectionstring | null

Section it belongs to.

total_mcinumber | null

Inherited key: signed TCPFN strength.

lag_at_maxinteger | null

Lag at peak effect, in samples.

lag_hoursnumber | null

That lag in real time.

rca_combined_scorenumber | null

Attribution score.

MergedColumn

A column merged into a representative before the joint model ran.

FieldTypeDescription
droppedstring

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

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

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().
rulestring

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.

FieldTypeDescription
textstring

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

#ScenarioWhat it testsResult
Baseline and variants — The sample data and small variations of it — timestamps, injected faults, a spike, a long delay.
1Single lag (sample)Plain data, one shared delay — the easy case.handled
2Base (mixed lags)Three real effects, each at a different delay (1/2/3).handled
3Daily timestampsSame data plus a date column — must change nothing.handled
4Planted anomaliesFive injected faults — the structure must not change.handled
5Heater spikeOne extreme heater spike — must not break detection.handled
6Long 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.
7Fan offA lever that never moves must be dropped honestly.handled
8Heater offBoth heater effects must vanish; only the fan remains.handled
9All 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.
10Negative effect (fan saves energy)The fan LOWERS the bill — the minus sign must be recovered.handled — with flagged extras
11Chain (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.
12Reverse causationThe BILL drives the FAN — direction, not correlation, is the test.handled — with flagged extras
13Feedback 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.
14Shared daily cycleFan and bill follow the SAME daily rhythm — the ice-cream-and-drowning trap.handled — with flagged extras
15Shared cycle, offsetSame rhythm, the bill's copy shifted 3 steps — looks exactly like a lag.handled — with flagged extras
16Three shared cyclesDaily + weekly + fortnightly rhythms shared, none causal.handled — with flagged extras
17Shared trendBoth 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.
18Weak-signal ladder: strongOne strong real effect among realistic noise — nothing else may appear.handled
19Weak-signal ladder: quarterSame, at a quarter of the strength.handled
20Weak-signal ladder: absentSame, 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.
21Closed-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.
22Non-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.
23Curved 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.
24Derived and bookkeeping columnsAverages, 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.
25Realistic empty file (1)Nothing connected, every column alive with realistic memory — seed 1 of 3.handled — with flagged extras
26Realistic empty file (2)Nothing connected — seed 2 of 3.handled
27Realistic empty file (3)Nothing connected — seed 3 of 3.handled
28Shuffled rowsRow order destroyed — no lagged causal claim can stand.known limit
Known limits — What no method can recover from the columns present — stated, not hidden.
29Hidden 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

lag 1 · +0.4lag 1 · +0.11lag 1 · +0.22heaterroom tempfanpower billnoisecauses nothing
CauseEffectLagCoefficient
heaterroom temp1+0.4
heaterpower bill1+0.11
fanpower bill1+0.22

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
fan → power bill1+✓ correct — matches the planted edge
heater → room temp1+✓ correct — matches the planted edge
heater → power bill1+✓ 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 reportedLagSignVerdict vs ground truth
fan → power bill1+✓ correct — matches the planted edge
heater → room temp1+✓ correct — matches the planted edge
heater → power bill1+✓ 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 billPer-unit effectTrustedVerdict vs ground truth
heater+0.089yestruth +0.11 — ✓ trusted, right direction
room temp+0.076no✓ correctly NOT trusted — no real effect exists
fan+0.213yestruth +0.22 — ✓ trusted, right direction
noise-0.012no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heateryes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m…
room tempno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (confounded) — the association between roo…
fanyes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'fan_speed' passes independence, temporal precedence, and deconfounding against every other meas…
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1fan+3.86yesreal lever (truth +0.22)
2heater+1.91yesreal lever (truth +0.11)
3noise+3.44nonot a real lever — check the Trusted column
4room temp-2.40nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes16.990.61158moves and has time structure — analyzable
room tempyes7.290.59148moves and has time structure — analyzable
fanyes22.890.63153moves and has time structure — analyzable
power billyes5.410.62159moves and has time structure — analyzable
noiseyes28.830.01111✓ 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 causeLagScoreDirectionVerdict vs ground truth
fan10.80stable✓ a real driver of the bill
heater10.36stable✓ 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 driverLagVerdict 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

lag 1 · +0.08lag 2 · +0.40lag 3 · +0.16heaterroom tempfanpower billnoisecauses nothing
CauseEffectLagCoefficient
heaterpower bill1+0.08
heaterroom temp2+0.40
fanpower bill3+0.16

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
fan → power bill3+✓ correct — matches the planted edge
heater → power bill1+✓ correct — matches the planted edge
heater → room temp2+✓ 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 reportedLagSignVerdict vs ground truth
fan → power bill3+✓ correct — matches the planted edge
heater → power bill1+✓ correct — matches the planted edge
heater → room temp2+✓ 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 billPer-unit effectTrustedVerdict vs ground truth
heater+0.074yestruth +0.08 — ✓ trusted, right direction
room temp-0.138no✓ correctly NOT trusted — no real effect exists
fan+0.167yestruth +0.16 — ✓ trusted, right direction
noise-0.011no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heateryes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m…
room tempno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (reversed) — power_bill LEADS room_temp in…
fanyes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'fan_speed' passes independence, temporal precedence, and deconfounding against every other meas…
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1fan+4.36yesreal lever (truth +0.16)
2heater+0.76yesreal lever (truth +0.08)
3noise-2.30nonot a real lever — check the Trusted column
4room temp-0.33nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes16.720.84158moves and has time structure — analyzable
room tempyes7.020.74161moves and has time structure — analyzable
fanyes21.400.76141moves and has time structure — analyzable
power billyes4.010.73162moves and has time structure — analyzable
noiseyes29.18-0.02130✓ 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 causeLagScoreDirectionVerdict vs ground truth
fan30.80stable✓ a real driver of the bill
heater10.30stable✓ 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 driverLagVerdict 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

lag 1 · +0.08lag 2 · +0.40lag 3 · +0.16heaterroom tempfanpower billnoisecauses nothing
CauseEffectLagCoefficient
heaterpower bill1+0.08
heaterroom temp2+0.40
fanpower bill3+0.16

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
fan → power bill3+✓ correct — matches the planted edge
heater → power bill1+✓ correct — matches the planted edge
heater → room temp2+✓ 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 reportedLagSignVerdict vs ground truth
fan → power bill3+✓ correct — matches the planted edge
heater → power bill1+✓ correct — matches the planted edge
heater → room temp2+✓ 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 billPer-unit effectTrustedVerdict vs ground truth
heater+0.074yestruth +0.08 — ✓ trusted, right direction
room temp-0.138no✓ correctly NOT trusted — no real effect exists
fan+0.167yestruth +0.16 — ✓ trusted, right direction
noise-0.011no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heateryes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m…
room tempno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (reversed) — power_bill LEADS room_temp in…
fanyes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'fan_speed' passes independence, temporal precedence, and deconfounding against every other meas…
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1fan+4.36yesreal lever (truth +0.16)
2heater+0.76yesreal lever (truth +0.08)
3noise-2.30nonot a real lever — check the Trusted column
4room temp-0.33nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes16.720.84158moves and has time structure — analyzable
room tempyes7.020.74161moves and has time structure — analyzable
fanyes21.400.76141moves and has time structure — analyzable
power billyes4.010.73162moves and has time structure — analyzable
noiseyes29.18-0.02130✓ 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 causeLagScoreDirectionVerdict 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 driverLagVerdict 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

lag 1 · +0.08lag 2 · +0.40lag 3 · +0.16heaterroom tempfanpower billnoisecauses nothing
CauseEffectLagCoefficient
heaterpower bill1+0.08
heaterroom temp2+0.40
fanpower bill3+0.16

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
heater → power bill1+✓ correct — matches the planted edge
heater → room temp2+✓ correct — matches the planted edge
fan → power bill3+✓ 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 reportedLagSignVerdict vs ground truth
heater → power bill1+✓ correct — matches the planted edge
heater → room temp2+✓ correct — matches the planted edge
fan → power bill3+✓ 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 billPer-unit effectTrustedVerdict vs ground truth
heater+0.057yestruth +0.08 — ✓ trusted, right direction
room temp-0.090no✓ correctly NOT trusted — no real effect exists
fan+0.161yestruth +0.16 — ✓ trusted, right direction
noise+0.013no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heateryes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m…
room tempno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (confounded) — the association between roo…
fanyes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'fan_speed' passes independence, temporal precedence, and deconfounding against every other meas…
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1fan+1.80yesreal lever (truth +0.16)
2heater+0.18yesreal lever (truth +0.08)
3room temp+4.26nonot a real lever — check the Trusted column
4noise-0.34nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes18.000.86152moves and has time structure — analyzable
room tempyes7.870.79126moves and has time structure — analyzable
fanyes22.720.77122moves and has time structure — analyzable
power billyes4.080.74149moves and has time structure — analyzable
noiseyes29.490.02144✓ 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 causeLagScoreDirectionVerdict 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 driverLagVerdict 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

lag 1 · +0.08lag 2 · +0.40lag 3 · +0.16heaterroom tempfanpower billnoisecauses nothing
CauseEffectLagCoefficient
heaterpower bill1+0.08
heaterroom temp2+0.40
fanpower bill3+0.16

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
fan → power bill3+✓ correct — matches the planted edge
heater → room temp2+✓ correct — matches the planted edge
heater → power bill1+✓ 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 reportedLagSignVerdict vs ground truth
fan → power bill3+✓ correct — matches the planted edge
heater → room temp2+✓ correct — matches the planted edge
heater → power bill1+✓ 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 billPer-unit effectTrustedVerdict vs ground truth
heater+0.075yestruth +0.08 — ✓ trusted, right direction
room temp-0.123no✓ correctly NOT trusted — no real effect exists
fan+0.168yestruth +0.16 — ✓ trusted, right direction
noise-0.011no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heateryes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m…
room tempno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (reversed) — power_bill LEADS room_temp in…
fanyes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'fan_speed' passes independence, temporal precedence, and deconfounding against every other meas…
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1fan+4.36yesreal lever (truth +0.16)
2heater+0.82yesreal lever (truth +0.08)
3noise-1.73nonot a real lever — check the Trusted column
4room temp-0.49nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes17.120.84166moves and has time structure — analyzable
room tempyes7.170.75164moves and has time structure — analyzable
fanyes21.400.76141moves and has time structure — analyzable
power billyes4.030.73162moves and has time structure — analyzable
noiseyes29.18-0.02130✓ 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 causeLagScoreDirectionVerdict 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 driverLagVerdict 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

lag 1 · +0.08lag 2 · +0.40lag 7 · +0.16heaterroom tempfanpower billnoisecauses nothing
CauseEffectLagCoefficient
heaterpower bill1+0.08
heaterroom temp2+0.40
fanpower bill7+0.16

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
fan → power bill7+✓ correct — matches the planted edge
heater → room temp2+✓ correct — matches the planted edge
heater → power bill1+✓ 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 reportedLagSignVerdict vs ground truth
fan → power bill7+✓ correct — matches the planted edge
heater → room temp2+✓ correct — matches the planted edge
heater → power bill1+✓ 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 billPer-unit effectTrustedVerdict vs ground truth
heater+0.057yestruth +0.08 — ✓ trusted, right direction
room temp-0.119no✓ correctly NOT trusted — no real effect exists
fan+0.157yestruth +0.16 — ✓ trusted, right direction
noise-0.013no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heateryes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m…
room tempno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (confounded) — the association between roo…
fanyes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'fan_speed' passes independence, temporal precedence, and deconfounding against every other meas…
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1fan+1.47yesreal lever (truth +0.16)
2heater+0.89yesreal lever (truth +0.08)
3noise-4.30nonot a real lever — check the Trusted column
4room temp-0.31nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes16.720.84158moves and has time structure — analyzable
room tempyes7.020.74161moves and has time structure — analyzable
fanyes21.400.76141moves and has time structure — analyzable
power billyes3.770.72163moves and has time structure — analyzable
noiseyes29.18-0.02130✓ 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 causeLagScoreDirectionVerdict vs ground truth
fan70.96stable✓ 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 driverLagVerdict 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

lag 1 · +0.08lag 2 · +0.40heaterroom tempfan= 0 (off)power billnoisecauses nothing
CauseEffectLagCoefficient
heaterpower bill1+0.08
heaterroom temp2+0.40

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
heater → room temp2+✓ correct — matches the planted edge
heater → power bill1+✓ 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 reportedLagSignVerdict vs ground truth
heater → room temp2+✓ correct — matches the planted edge
heater → power bill1+✓ 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 billPer-unit effectTrustedVerdict vs ground truth
heater+0.081yestruth +0.08 — ✓ trusted, right direction
room temp+0.103no✓ correctly NOT trusted — no real effect exists
fanrefused (no variation)✓ correct — this lever is switched off
noise+0.005no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heateryes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m…
room tempno✓ correctly refused — not a real causeNot 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
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1heater+0.79yesreal lever (truth +0.08)
2noise-0.60nonot a real lever — check the Trusted column
3room temp+0.37nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes16.280.84166moves and has time structure — analyzable
room tempyes6.750.75164moves and has time structure — analyzable
fanno0.000.000✓ correctly shows the lever never moved
power billyes1.690.49166moves and has time structure — analyzable
noiseyes28.990.0197✓ 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 causeLagScoreDirectionVerdict 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 driverLagVerdict 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

lag 3 · +0.16heater= 0 (off)room tempcauses nothingfanpower billnoisecauses nothing
CauseEffectLagCoefficient
fanpower bill3+0.16

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
fan → power bill3+✓ 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 reportedLagSignVerdict vs ground truth
fan → power bill3+✓ 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 billPer-unit effectTrustedVerdict vs ground truth
heaterrefused (no variation)✓ correct — this lever is switched off
room temp+0.161no✓ correctly NOT trusted — no real effect exists
fan+0.159yestruth +0.16 — ✓ trusted, right direction
noise+0.014no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heater✓ correctly refused — lever is off
room tempno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (independent) — room_temp is ~marginally i…
fanyes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'fan_speed' passes independence, temporal precedence, and deconfounding against every other meas…
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1fan+0.16yesreal lever (truth +0.16)
2noise-1.47nonot a real lever — check the Trusted column
3room temp+0.56nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heaterno0.000.000✓ correctly shows the lever never moved
room tempyes2.21-0.0162moves and has time structure — analyzable
fanyes22.720.80173moves and has time structure — analyzable
power billyes3.770.73166moves and has time structure — analyzable
noiseyes28.990.0197✓ 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 causeLagScoreDirectionVerdict 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 driverLagVerdict 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

heater= 0 (off)room tempcauses nothingfan= 0 (off)power billnoisecauses nothing

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 reportedLagSignVerdict 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 reportedLagSignVerdict 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 billPer-unit effectTrustedVerdict vs ground truth
heaterrefused (no variation)✓ correct — this lever is switched off
room temp-0.043no✓ correctly NOT trusted — no real effect exists
fanrefused (no variation)✓ correct — this lever is switched off
noise-0.003no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heater✓ correctly refused — lever is off
room tempno✓ correctly refused — not a real causeNot 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
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1noise-0.24nonot a real lever — check the Trusted column
2room temp+0.17nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heaterno0.000.000✓ correctly shows the lever never moved
room tempyes2.240.07160moves and has time structure — analyzable
fanno0.000.000✓ correctly shows the lever never moved
power billyes1.11-0.0168moves and has time structure — analyzable
noiseyes29.030.03112✓ 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 causeLagScoreDirectionVerdict 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

lag 1 · +0.08lag 2 · +0.40lag 3 · −0.16heaterroom tempfanpower billnoisecauses nothing
CauseEffectLagCoefficient
heaterpower bill1+0.08
heaterroom temp2+0.40
fanpower bill3−0.16

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
fan → power bill3✓ correct — matches the planted edge
heater → power bill0+⚠ right edge, wrong lag (truth lag 1)
heater → room temp2+✓ 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 reportedLagSignVerdict vs ground truth
fan → power bill3✓ correct — matches the planted edge
heater → power bill0+⚠ right edge, wrong lag (truth lag 1)
heater → room temp2+✓ 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 billPer-unit effectTrustedVerdict vs ground truth
heater+0.041notruth +0.08 — ⚠ flagged untrustworthy
room temp+0.101no✓ correctly NOT trusted — no real effect exists
fan-0.146yestruth -0.16 — ✓ trusted, right direction
noise-0.012no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heaterno⚠ real cause flagged not identifiableNot identifiable: a necessary condition for a causal effect of 'heater_power' on 'power_bill' fails (chance_level) — the association betwee…
room tempno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (reversed) — power_bill LEADS room_temp in…
fanyes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'fan_speed' passes independence, temporal precedence, and deconfounding against every other meas…
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1fan-4.80yesreal lever (truth -0.16)
2room temp-0.63nonot a real lever — check the Trusted column
3noise-0.63nonot a real lever — check the Trusted column
4heater-0.53noreal 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes16.720.84158moves and has time structure — analyzable
room tempyes7.020.74161moves and has time structure — analyzable
fanyes21.400.76141moves and has time structure — analyzable
power billyes3.680.69166moves and has time structure — analyzable
noiseyes29.18-0.02130✓ 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 causeLagScoreDirectionVerdict vs ground truth
fan30.86stable✓ 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 driverLagVerdict 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

lag 2 · +0.40lag 1 · +0.30lag 3 · +0.16indirect okheaterroom tempfanpower billnoisecauses nothing
CauseEffectLagCoefficient
heaterroom temp2+0.40
room temppower bill1+0.30
fanpower bill3+0.16

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
room temp → power bill1+✓ correct — matches the planted edge
heater → room temp2+✓ correct — matches the planted edge
fan → power bill3+✓ 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 reportedLagSignVerdict vs ground truth
room temp → power bill1+✓ correct — matches the planted edge
heater → room temp2+✓ correct — matches the planted edge
fan → power bill3+✓ 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 billPer-unit effectTrustedVerdict vs ground truth
heater+0.134yes⚠ trusted, but the true effect is 0
room temp+0.276yestruth +0.30 — ✓ trusted, right direction
fan+0.170yestruth +0.16 — ✓ trusted, right direction
noise+0.013no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heateryes✓ acceptable either way (indirect effect)Identifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m…
room tempyes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'room_temp' passes independence, temporal precedence, and deconfounding against every other meas…
fanyes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'fan_speed' passes independence, temporal precedence, and deconfounding against every other meas…
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1fan+4.70yesreal lever (truth +0.16)
2room temp+1.15yesreal lever (truth +0.30)
3heater+0.87yesnot a real lever — check the Trusted column
4noise-4.57nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes16.720.84158moves and has time structure — analyzable
room tempyes7.020.74161moves and has time structure — analyzable
fanyes21.400.76141moves and has time structure — analyzable
power billyes4.410.72148moves and has time structure — analyzable
noiseyes29.18-0.02130✓ 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 causeLagScoreDirectionVerdict vs ground truth
room temp10.69stable✓ a real driver of the bill
fan30.60stable✓ a real driver of the bill
heater30.17stable
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 driverLagVerdict 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

lag 2 · +2.5lag 2 · +0.40heaterroom tempfanpower billnoisecauses nothing
CauseEffectLagCoefficient
power billfan2+2.5
heaterroom temp2+0.40

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
heater → room temp2+✓ correct — matches the planted edge
power bill → fan2+✓ correct — matches the planted edge
noise → heater0+⚠ 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 reportedLagSignVerdict vs ground truth
heater → room temp2+✓ correct — matches the planted edge
power bill → fan2+✓ correct — matches the planted edge
noise → heater0+⚠ 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 → fanPer-unit effectTrustedVerdict vs ground truth
heater-0.112no✓ correctly NOT trusted — no real effect exists
room temp+0.223no✓ correctly NOT trusted — no real effect exists
power bill+2.570yestruth +2.50 — ✓ trusted, right direction
noise+0.034no✓ 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 → fanIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heaterno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'heater_power' on 'fan_speed' fails (independent) — heater_power is ~margina…
room tempno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'room_temp' on 'fan_speed' fails (independent) — room_temp is ~marginally in…
power billyes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'power_bill' passes independence, temporal precedence, and deconfounding against every other mea…
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on fanTrustedVerdict vs ground truth
1power bill+12.23yesreal lever (truth +2.50)
2heater-6.28nonot a real lever — check the Trusted column
3noise-3.15nonot a real lever — check the Trusted column
4room temp-1.51nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes11.190.59176moves and has time structure — analyzable
room tempyes5.050.48167moves and has time structure — analyzable
fanyes10.470.54152moves and has time structure — analyzable
power billyes3.760.62166moves and has time structure — analyzable
noiseyes28.530.05159✓ 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 causeLagScoreDirectionVerdict vs ground truth
power bill20.96stable✓ 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 driverLagVerdict 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

lag 2 · +0.10lag 1 · −3.0lag 3 · +0.16heaterroom tempfanpower billnoisecauses nothing
CauseEffectLagCoefficient
heaterroom temp2+0.10
room tempheater1−3.0
fanpower bill3+0.16

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
heater → room temp2+✓ correct — matches the planted edge
fan → power bill3+✓ correct — matches the planted edge
noise → fan0⚠ not planted — vetoed by the tool (chance_level), evidence 0.21; reported for transparency, marked do-not-act
room temp → heater1✓ 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 reportedLagSignVerdict vs ground truth
heater → room temp2+✓ correct — matches the planted edge
fan → power bill3+✓ correct — matches the planted edge
noise → fan0⚠ not planted — vetoed by the tool (chance_level), evidence 0.21; reported for transparency, marked do-not-act
room temp → heater1✓ 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 tempPer-unit effectTrustedVerdict vs ground truth
heater+0.108yestruth +0.10 — ✓ trusted, right direction
fan+0.008no✓ correctly NOT trusted — no real effect exists
power bill-0.035no✓ correctly NOT trusted — no real effect exists
noise-0.004no✓ 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 tempIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heateryes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m…
fanno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'fan_speed' on 'room_temp' fails (independent) — fan_speed is ~marginally in…
power billno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'power_bill' on 'room_temp' fails (independent) — power_bill is ~marginally …
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on room tempTrustedVerdict vs ground truth
1heater+0.10yesreal lever (truth +0.10)
2power bill-0.72nonot a real lever — check the Trusted column
3fan-0.43nonot a real lever — check the Trusted column
4noise-0.10nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes7.450.39106moves and has time structure — analyzable
room tempyes1.190.42119moves and has time structure — analyzable
fanyes17.400.59177moves and has time structure — analyzable
power billyes3.040.51164moves and has time structure — analyzable
noiseyes28.580.05158✓ 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 causeLagScoreDirectionVerdict vs ground truth
heater20.91stable✓ 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 driverLagVerdict vs ground truth
heater✓ correct — a planted cause of the target

Shared daily cycle

fan_speed and power_bill both ride one 24-step daily cycle, in phase, with no link between them. Two series on the same clock look connected whether or not they are — the classic way an analysis invents a relationship. TCPFN treats the clock as a candidate confounder: a pair whose association vanishes once the shared cycle is removed is vetoed (shared_clock), and check_data names the pair up front.

Ground truth — the structure that generated the data

lag 2 · +0.40heaterroom tempfancauses nothingpower billcauses nothingnoisecauses nothing
CauseEffectLagCoefficient
heaterroom temp2+0.40

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
heater → room temp2+✓ correct — matches the planted edge
noise → heater0+⚠ 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 reportedLagSignVerdict vs ground truth
heater → room temp2+✓ correct — matches the planted edge
noise → heater0+⚠ 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 billPer-unit effectTrustedVerdict vs ground truth
heater-0.045no✓ correctly NOT trusted — no real effect exists
room temp-0.080no✓ correctly NOT trusted — no real effect exists
fan+0.135no✓ correctly NOT trusted — no real effect exists
noise+0.007no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heaterno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'heater_power' on 'power_bill' fails (independent) — heater_power is ~margin…
room tempno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (independent) — room_temp is ~marginally i…
fanno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'fan_speed' on 'power_bill' fails (shared_clock) — the association between f…
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1fan-2.61nonot a real lever — check the Trusted column
2noise-2.19nonot a real lever — check the Trusted column
3room temp-1.87nonot a real lever — check the Trusted column
4heater-0.74nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes11.200.59177moves and has time structure — analyzable
room tempyes5.040.48168moves and has time structure — analyzable
fanyes18.780.9162moves and has time structure — analyzable
power billyes3.740.91119moves and has time structure — analyzable
noiseyes28.540.05159✓ 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):

ComponentColumnsVerdict vs ground truth
shared 24-step cyclepower 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 91.

Root causeLagScoreDirectionVerdict 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).

Shared cycle, offset

As the shared daily cycle, but the bill's rhythm lags the fan's by three steps — precisely what a real causal delay would look like. Only removing the clock separates 'follows the fan by 3' from 'follows the day, 3 steps behind'.

Ground truth — the structure that generated the data

lag 2 · +0.40heaterroom tempfancauses nothingpower billcauses nothingnoisecauses nothing
CauseEffectLagCoefficient
heaterroom temp2+0.40

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
noise → heater0+⚠ not planted — vetoed by the tool (chance_level), evidence 0.20; reported for transparency, marked do-not-act
heater → room temp2+✓ 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 reportedLagSignVerdict vs ground truth
noise → heater0+⚠ not planted — vetoed by the tool (chance_level), evidence 0.20; reported for transparency, marked do-not-act
heater → room temp2+✓ 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 billPer-unit effectTrustedVerdict vs ground truth
heater-0.042no✓ correctly NOT trusted — no real effect exists
room temp+0.094no✓ correctly NOT trusted — no real effect exists
fan-0.167no✓ correctly NOT trusted — no real effect exists
noise-0.008no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heaterno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'heater_power' on 'power_bill' fails (independent) — heater_power is ~margin…
room tempno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (independent) — room_temp is ~marginally i…
fanno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'fan_speed' on 'power_bill' fails (shared_clock) — the association between f…
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1room temp-2.74nonot a real lever — check the Trusted column
2fan-2.03nonot a real lever — check the Trusted column
3noise-0.39nonot a real lever — check the Trusted column
4heater-0.16nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes11.200.59177moves and has time structure — analyzable
room tempyes5.040.48168moves and has time structure — analyzable
fanyes18.780.9162moves and has time structure — analyzable
power billyes3.850.91123moves and has time structure — analyzable
noiseyes28.540.05159✓ 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):

ComponentColumnsVerdict vs ground truth
shared 24-step cyclepower 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 788.

Root causeLagScoreDirectionVerdict 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).

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

lag 2 · +0.40heaterroom tempfancauses nothingpower billcauses nothingnoisecauses nothing
CauseEffectLagCoefficient
heaterroom temp2+0.40

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
heater → room temp2+✓ correct — matches the planted edge
noise → heater0⚠ 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 reportedLagSignVerdict vs ground truth
heater → room temp2+✓ correct — matches the planted edge
noise → heater0⚠ 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 billPer-unit effectTrustedVerdict vs ground truth
heater+0.041no✓ correctly NOT trusted — no real effect exists
room temp+0.121no✓ correctly NOT trusted — no real effect exists
fan+0.165no✓ correctly NOT trusted — no real effect exists
noise-0.018no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heaterno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'heater_power' on 'power_bill' fails (independent) — heater_power is ~margin…
room tempno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (independent) — room_temp is ~marginally i…
fanno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'fan_speed' on 'power_bill' fails (shared_clock) — the association between f…
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1room temp-2.83nonot a real lever — check the Trusted column
2fan+0.85nonot a real lever — check the Trusted column
3heater-0.85nonot a real lever — check the Trusted column
4noise+0.45nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes11.200.59177moves and has time structure — analyzable
room tempyes5.040.48168moves and has time structure — analyzable
fanno19.740.93149moves and has time structure — analyzable
power billno3.970.92139moves and has time structure — analyzable
noiseyes28.540.05159✓ 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):

ComponentColumnsVerdict vs ground truth
shared 24-step cyclepower bill, fan✓ names the planted trap pair before any model runs
shared trendpower 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 causeLagScoreDirectionVerdict 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).

Shared trend

fan_speed and power_bill both drift steadily upward over the record with no cycle and no causal link. Needs a different remedy from cycles — the drift, not a rhythm, is what the shared_clock check removes here.

Ground truth — the structure that generated the data

lag 2 · +0.40heaterroom tempfancauses nothingpower billcauses nothingnoisecauses nothing
CauseEffectLagCoefficient
heaterroom temp2+0.40

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
noise → heater2⚠ not planted — vetoed by the tool (chance_level), evidence 0.21; reported for transparency, marked do-not-act
heater → room temp2+✓ 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 reportedLagSignVerdict vs ground truth
noise → heater2⚠ not planted — vetoed by the tool (chance_level), evidence 0.21; reported for transparency, marked do-not-act
heater → room temp2+✓ 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 billPer-unit effectTrustedVerdict vs ground truth
heater+0.025no✓ correctly NOT trusted — no real effect exists
room temp+0.064no✓ correctly NOT trusted — no real effect exists
fan+0.099no✓ correctly NOT trusted — no real effect exists
noise-0.008no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heaterno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'heater_power' on 'power_bill' fails (independent) — heater_power is ~margin…
room tempno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (independent) — room_temp is ~marginally i…
fanno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'fan_speed' on 'power_bill' fails (shared_clock) — the association between f…
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1fan+5.01nonot a real lever — check the Trusted column
2room temp+1.61nonot a real lever — check the Trusted column
3noise-0.68nonot a real lever — check the Trusted column
4heater-0.42nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes11.200.59177moves and has time structure — analyzable
room tempyes5.040.48168moves and has time structure — analyzable
fanno18.770.94144moves and has time structure — analyzable
power billno4.250.95147moves and has time structure — analyzable
noiseyes28.540.05159✓ 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):

ComponentColumnsVerdict vs ground truth
shared trendpower 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 26.

Root causeLagScoreDirectionVerdict 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

lag 2 · +0.40heaterroom tempfancauses nothingpower billcauses nothingnoisecauses nothing
CauseEffectLagCoefficient
heaterroom temp2+0.40

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
heater → room temp2+✓ 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 reportedLagSignVerdict vs ground truth
heater → room temp2+✓ 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 tempPer-unit effectTrustedVerdict vs ground truth
heater+0.397yestruth +0.40 — ✓ trusted, right direction
fan-0.044no✓ correctly NOT trusted — no real effect exists
power bill+0.122no✓ correctly NOT trusted — no real effect exists
noise+0.025no✓ 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 tempIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heateryes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m…
fanno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'fan_speed' on 'room_temp' fails (independent) — fan_speed is ~marginally in…
power billno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'power_bill' on 'room_temp' fails (independent) — power_bill is ~marginally …
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on room tempTrustedVerdict vs ground truth
1heater+1.22yesreal lever (truth +0.40)
2power bill-3.71nonot a real lever — check the Trusted column
3fan+0.88nonot a real lever — check the Trusted column
4noise-0.55nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes11.280.62166moves and has time structure — analyzable
room tempyes5.420.63153moves and has time structure — analyzable
fanyes17.420.59177moves and has time structure — analyzable
power billyes3.740.60176moves and has time structure — analyzable
noiseyes25.000.62172⚠ 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):

ComponentColumnsVerdict vs ground truth
(none)✗ the planted shared rhythm was not namedbad
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 causeLagScoreDirectionVerdict vs ground truth
heater20.86stable✓ 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 driverLagVerdict 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

lag 2 · +0.10heaterroom tempfancauses nothingpower billcauses nothingnoisecauses nothing
CauseEffectLagCoefficient
heaterroom temp2+0.10

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
heater → room temp2+✓ 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 reportedLagSignVerdict vs ground truth
heater → room temp2+✓ 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 tempPer-unit effectTrustedVerdict vs ground truth
heater+0.102yestruth +0.10 — ✓ trusted, right direction
fan-0.024no✓ correctly NOT trusted — no real effect exists
power bill-0.106no✓ correctly NOT trusted — no real effect exists
noise+0.015no✓ 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 tempIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heateryes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m…
fanno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'fan_speed' on 'room_temp' fails (independent) — fan_speed is ~marginally in…
power billno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'power_bill' on 'room_temp' fails (independent) — power_bill is ~marginally …
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on room tempTrustedVerdict vs ground truth
1heater+0.11yesreal lever (truth +0.10)
2power bill-1.28nonot a real lever — check the Trusted column
3fan+0.42nonot a real lever — check the Trusted column
4noise+0.06nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes11.280.62166moves and has time structure — analyzable
room tempyes3.190.66166moves and has time structure — analyzable
fanyes17.420.59177moves and has time structure — analyzable
power billyes3.740.60176moves and has time structure — analyzable
noiseyes25.000.62172⚠ 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):

ComponentColumnsVerdict vs ground truth
(none)✗ the planted shared rhythm was not namedbad
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 causeLagScoreDirectionVerdict vs ground truth
heater20.86stable✓ 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 driverLagVerdict 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

heatercauses nothingroom tempcauses nothingfancauses nothingpower billcauses nothingnoisecauses nothing

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 reportedLagSignVerdict 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 reportedLagSignVerdict 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 tempPer-unit effectTrustedVerdict vs ground truth
heater-0.024no✓ correctly NOT trusted — no real effect exists
fan-0.017no✓ correctly NOT trusted — no real effect exists
power bill-0.106no✓ correctly NOT trusted — no real effect exists
noise+0.013no✓ 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 tempIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heaterno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'heater_power' on 'room_temp' fails (independent) — heater_power is ~margina…
fanno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'fan_speed' on 'room_temp' fails (independent) — fan_speed is ~marginally in…
power billno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'power_bill' on 'room_temp' fails (independent) — power_bill is ~marginally …
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on room tempTrustedVerdict vs ground truth
1power bill-0.78nonot a real lever — check the Trusted column
2heater-0.56nonot a real lever — check the Trusted column
3noise+0.29nonot a real lever — check the Trusted column
4fan-0.28nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes11.280.62166moves and has time structure — analyzable
room tempyes2.970.66165moves and has time structure — analyzable
fanyes17.420.59177moves and has time structure — analyzable
power billyes3.740.60176moves and has time structure — analyzable
noiseyes25.000.62172⚠ 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 causeLagScoreDirectionVerdict 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

lag 1 · +0.55 (true effect)lag 1 · -0.30 (controller)lag 0 · -1.0 (disturbance)lag 2 · control edgeheaterroom tempoutside tempfanduct pressure
CauseEffectLagCoefficient
heaterroom temp1+0.55 (true effect)
room tempheater1-0.30 (controller)
outside temproom temp0-1.0 (disturbance)
fanduct pressure2control 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 reportedLagSignVerdict vs ground truth
outside_temp → room temp0✓ correct — matches the planted edge
fan → duct_pressure2+✓ correct — matches the planted edge
room temp → heater1✓ correct — matches the planted edge
heater → room temp0+⚠ 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 reportedLagSignVerdict vs ground truth
outside_temp → room temp0✓ correct — matches the planted edge
fan → duct_pressure2+✓ correct — matches the planted edge
room temp → heater1✓ correct — matches the planted edge
heater → room temp0+⚠ 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 tempPer-unit effectTrustedVerdict vs ground truth
heater+0.570yestruth +0.55 — ✓ trusted, right direction
outside_temp-0.751yestruth -1.00 — ✓ trusted, right direction
fan-0.049no✓ correctly NOT trusted — no real effect exists
duct_pressure-0.098no✓ 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 tempIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heateryes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m…
outside_tempyes✓ real cause — correctly identifiableIdentifiable given the observed variables: 'outside_temp' passes independence, temporal precedence, and deconfounding against every other m…
fanno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'fan_speed' on 'room_temp' fails (independent) — fan_speed is ~marginally in…
duct_pressureno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on room tempTrustedVerdict vs ground truth
1outside_temp-3.28yesreal lever (truth -1.00)
2heater+0.43yesreal lever (truth +0.55)
3duct_pressure-0.03nonot a real lever — check the Trusted column
4fan+0.02nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes1.680.11591moves and has time structure — analyzable
room tempyes2.580.56611moves and has time structure — analyzable
outside_tempyes2.510.71614moves and has time structure — analyzable
fanyes2.440.59598moves and has time structure — analyzable
duct_pressureyes1.080.27617moves 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 causeLagScoreDirectionVerdict vs ground truth
outside_temp00.87stable✓ 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 driverLagVerdict 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

lag 1 · U-shape (optimum mid-range)lag 1 · asymmetric curvelag 2 · monotone curvelag 1 · saturatinglag 1 · thresholdspeed_symtput_symspeed_asymtput_asymfeed_monotput_monoload_sattput_sattemp_thrtput_thrdecoy_acauses nothingdecoy_bcauses nothing
CauseEffectLagCoefficient
speed_symtput_sym1U-shape (optimum mid-range)
speed_asymtput_asym1asymmetric curve
feed_monotput_mono2monotone curve
load_sattput_sat1saturating
temp_thrtput_thr1threshold

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 reportedLagSignVerdict vs ground truth
feed_mono → tput_mono2+✓ correct — matches the planted edge
load_sat → tput_sat1+✓ correct — matches the planted edge
speed_asym → tput_asym1+✓ correct — matches the planted edge
temp_thr → tput_thr1+✓ correct — matches the planted edge
speed_sym → tput_sym1?✓ 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 reportedLagSignVerdict vs ground truth
feed_mono → tput_mono2+✓ correct — matches the planted edge
load_sat → tput_sat1+✓ correct — matches the planted edge
speed_asym → tput_asym1+✓ correct — matches the planted edge
temp_thr → tput_thr1+✓ correct — matches the planted edge
speed_sym → tput_sym1?✓ 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 driverLagVerdict 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

lag 1 · curvedlag 2 · curvedlag 2 · + linearoutside tempheaterpower billfanroom temp
CauseEffectLagCoefficient
outside tempheater1curved
outside temppower bill2curved
fanroom temp2+ linear

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
fan → room temp2+✓ correct — matches the planted edge
outside_temp → heater1?✓ correct — matches the planted edge
outside_temp → power bill2?✓ 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 reportedLagSignVerdict vs ground truth
fan → room temp2+✓ correct — matches the planted edge
outside_temp → heater1?✓ correct — matches the planted edge
outside_temp → power bill2?✓ 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 driverLagVerdict 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

lag 1 · +lag 1 · +lag 2 · + (direct)pump_runtimepump_cyclespump_avg_cyclepump_load_indexbatch_total_linesbatch_total_tasksbatch_countbatch_avg_linesbatch_line_indexop_hourcauses nothingop_dowcauses nothingop_monthcauses nothingthroughput
CauseEffectLagCoefficient
pump_runtimebatch_total_lines1+
batch_total_linesthroughput1+
pump_runtimethroughput2+ (direct)

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
batch_count → batch_avg_lines0derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim
pump_avg_cycle → pump_cycles0derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim
batch_avg_lines → batch_count0derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim
pump_cycles → pump_avg_cycle0derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim
pump_runtime → pump_load_index0+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_lines0+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_index0+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_runtime0+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_lines0+derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim
batch_total_lines → throughput1+✓ correct — matches the planted edge
pump_avg_cycle → pump_runtime0+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_index1+✓ acceptable — true indirect (total) effect
pump_load_index → pump_avg_cycle2derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim
pump_runtime → throughput2+✓ correct — matches the planted edge
batch_line_index → batch_total_tasks0+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_lines1+✓ correct — matches the planted edge
pump_cycles → pump_load_index0+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_cycles0+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_lines1+✓ acceptable — true indirect (total) effect
batch_total_tasks → batch_line_index0+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 reportedLagSignVerdict vs ground truth
batch_count → batch_avg_lines0derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim
pump_avg_cycle → pump_cycles0derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim
batch_avg_lines → batch_count0derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim
pump_cycles → pump_avg_cycle0derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim
pump_runtime → pump_load_index0+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_lines0+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_index0+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_runtime0+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_lines0+derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim
batch_total_lines → throughput1+✓ correct — matches the planted edge
pump_avg_cycle → pump_runtime0+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_index1+✓ acceptable — true indirect (total) effect
pump_load_index → pump_avg_cycle2derivation link — both columns are transforms of one measurement; mapped and labelled equivalent by the provenance layer, not a causal claim
pump_runtime → throughput2+✓ correct — matches the planted edge
batch_line_index → batch_total_tasks0+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_lines1+✓ correct — matches the planted edge
pump_cycles → pump_load_index0+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_cycles0+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_lines1+✓ acceptable — true indirect (total) effect
batch_total_tasks → batch_line_index0+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 → throughputPer-unit effectTrustedVerdict vs ground truth
pump_runtime+0.774yes⚠ trusted, but the true effect is 0
pump_cycles+0.054no✓ correctly NOT trusted — no real effect exists
pump_avg_cycle+2.794no✓ correctly NOT trusted — no real effect exists
pump_load_index+1.063no✓ correctly NOT trusted — no real effect exists
batch_total_lines+0.609yes⚠ trusted, but the true effect is 0
batch_total_tasks+0.101no✓ correctly NOT trusted — no real effect exists
batch_count+0.093no✓ correctly NOT trusted — no real effect exists
batch_avg_lines+0.818no✓ correctly NOT trusted — no real effect exists
batch_line_index+0.818no✓ correctly NOT trusted — no real effect exists
op_hourno✓ correctly NOT trusted — no real effect exists
op_dow+0.057no✓ correctly NOT trusted — no real effect exists
op_monthrefused (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 → throughputIdentifiableVerdict vs ground truthTCPFN's own reason (short)
pump_runtimeyes✗ decoy marked identifiableIdentifiable given the observed variables: 'pump_runtime' passes independence, temporal precedence, and deconfounding against every other m…
pump_cyclesno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'pump_cycles' on 'throughput' fails (independent) — pump_cycles is ~marginal…
pump_avg_cycleno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'pump_avg_cycle' on 'throughput' fails (confounded) — the association betwee…
pump_load_indexno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'pump_load_index' on 'throughput' fails (confounded) — the association betwe…
batch_total_linesyes✗ decoy marked identifiableIdentifiable given the observed variables: 'batch_total_lines' passes independence, temporal precedence, and deconfounding against every ot…
batch_total_tasksno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'batch_total_tasks' on 'throughput' fails (confounded) — the association bet…
batch_countno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'batch_count' on 'throughput' fails (independent) — batch_count is ~marginal…
batch_avg_linesno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'batch_avg_lines' on 'throughput' fails (confounded) — the association betwe…
batch_line_indexno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'batch_line_index' on 'throughput' fails (confounded) — the association betw…
op_hourno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'op_hour' on 'throughput' fails (schedule_indicator) — op_hour is a fixed sc…
op_downo✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on throughputTrustedVerdict vs ground truth
1batch_total_lines+0.23yesnot a real lever — check the Trusted column
2pump_runtime+0.13yesnot a real lever — check the Trusted column
3batch_total_tasks-1.62nonot a real lever — check the Trusted column
4batch_count-1.16nonot a real lever — check the Trusted column
5pump_cycles-1.11nonot a real lever — check the Trusted column
6pump_load_index-0.39nonot a real lever — check the Trusted column
7op_dow-0.38nonot a real lever — check the Trusted column
8pump_avg_cycle+0.21nonot a real lever — check the Trusted column
9batch_avg_lines+0.10nonot a real lever — check the Trusted column
10batch_line_index+0.05nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
pump_runtimeyes2.460.58777moves and has time structure — analyzable
pump_cyclesyes1.740.50826moves and has time structure — analyzable
pump_avg_cycleyes0.330.52787moves and has time structure — analyzable
pump_load_indexyes1.620.57826moves and has time structure — analyzable
batch_total_linesyes4.020.50768moves and has time structure — analyzable
batch_total_tasksyes2.320.49772moves and has time structure — analyzable
batch_countyes1.070.40788moves and has time structure — analyzable
batch_avg_linesyes1.080.41759moves and has time structure — analyzable
batch_line_indexyes2.880.49794moves and has time structure — analyzable
op_houryes6.900.96324moves and has time structure — analyzable
op_dowyes1.991.00321moves and has time structure — analyzable
op_monthno0.000.000moves and has time structure — analyzable
throughputyes2.990.51799moves 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 causeLagScoreDirectionVerdict 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 driverLagVerdict 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

heatercauses nothingroom tempcauses nothingfancauses nothingpower billcauses nothingnoisecauses nothing

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 reportedLagSignVerdict vs ground truth
fan → power bill0+⚠ 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 reportedLagSignVerdict vs ground truth
fan → power bill0+⚠ 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 billPer-unit effectTrustedVerdict vs ground truth
heater-0.032no✓ correctly NOT trusted — no real effect exists
room temp+0.130no✓ correctly NOT trusted — no real effect exists
fan+0.033no✓ correctly NOT trusted — no real effect exists
noise-0.009no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heaterno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'heater_power' on 'power_bill' fails (independent) — heater_power is ~margin…
room tempno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (independent) — room_temp is ~marginally i…
fanno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'fan_speed' on 'power_bill' fails (chance_level) — the association between f…
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1noise-1.36nonot a real lever — check the Trusted column
2heater-1.35nonot a real lever — check the Trusted column
3room temp-0.79nonot a real lever — check the Trusted column
4fan-0.33nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes11.720.62156moves and has time structure — analyzable
room tempyes2.770.61154moves and has time structure — analyzable
fanyes18.100.62171moves and has time structure — analyzable
power billyes3.740.60169moves and has time structure — analyzable
noiseyes27.340.65166⚠ 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 causeLagScoreDirectionVerdict 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

heatercauses nothingroom tempcauses nothingfancauses nothingpower billcauses nothingnoisecauses nothing

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 reportedLagSignVerdict 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 reportedLagSignVerdict 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 billPer-unit effectTrustedVerdict vs ground truth
heater-0.033no✓ correctly NOT trusted — no real effect exists
room temp-0.205no✓ correctly NOT trusted — no real effect exists
fan-0.025no✓ correctly NOT trusted — no real effect exists
noise+0.014no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heaterno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'heater_power' on 'power_bill' fails (independent) — heater_power is ~margin…
room tempno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (independent) — room_temp is ~marginally i…
fanno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'fan_speed' on 'power_bill' fails (independent) — fan_speed is ~marginally i…
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1room temp-3.54nonot a real lever — check the Trusted column
2noise-2.79nonot a real lever — check the Trusted column
3heater-1.45nonot a real lever — check the Trusted column
4fan-0.67nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes11.990.65153moves and has time structure — analyzable
room tempyes2.710.59131moves and has time structure — analyzable
fanyes17.360.57141moves and has time structure — analyzable
power billyes3.820.61166moves and has time structure — analyzable
noiseyes26.710.65165⚠ 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 causeLagScoreDirectionVerdict 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 (3)

Third seed of the realistic empty file.

Ground truth — the structure that generated the data

heatercauses nothingroom tempcauses nothingfancauses nothingpower billcauses nothingnoisecauses nothing

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 reportedLagSignVerdict 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 reportedLagSignVerdict 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 billPer-unit effectTrustedVerdict vs ground truth
heater+0.038no✓ correctly NOT trusted — no real effect exists
room temp+0.137no✓ correctly NOT trusted — no real effect exists
fan+0.028no✓ correctly NOT trusted — no real effect exists
noise-0.017no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heaterno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'heater_power' on 'power_bill' fails (independent) — heater_power is ~margin…
room tempno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (independent) — room_temp is ~marginally i…
fanno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'fan_speed' on 'power_bill' fails (independent) — fan_speed is ~marginally i…
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1fan+1.21nonot a real lever — check the Trusted column
2heater-1.17nonot a real lever — check the Trusted column
3room temp+1.05nonot a real lever — check the Trusted column
4noise-0.38nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes11.780.66155moves and has time structure — analyzable
room tempyes2.660.58171moves and has time structure — analyzable
fanyes16.650.57156moves and has time structure — analyzable
power billyes3.640.58157moves and has time structure — analyzable
noiseyes25.110.58159⚠ 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 causeLagScoreDirectionVerdict 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

lag 1 · +0.08 (destroyed)lag 2 · +0.40 (destroyed)lag 3 · +0.16 (destroyed)heaterroom tempfanpower billnoisecauses nothing
CauseEffectLagCoefficient
heaterpower bill1+0.08 (destroyed)
heaterroom temp2+0.40 (destroyed)
fanpower bill3+0.16 (destroyed)

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
heater → room temp0+⚠ right edge, wrong lag (truth lag 2)
power bill → fan0+related pair only — the arrow is NOT trustworthy (labeled orientation_confidence = moderate)
room temp → power bill0+related pair only — the arrow is NOT trustworthy (labeled orientation_confidence = low)
power bill → heater0+related pair only — the arrow is NOT trustworthy (labeled orientation_confidence = low)
heater → power bill1+✗ expected but NOT found — missing edge
fan → power bill3+✗ 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 reportedLagSignVerdict vs ground truth
heater → room temp0+⚠ right edge, wrong lag (truth lag 2)
power bill → fan0+related pair only — the arrow is NOT trustworthy (labeled orientation_confidence = moderate)
room temp → power bill0+related pair only — the arrow is NOT trustworthy (labeled orientation_confidence = low)
power bill → heater0+related pair only — the arrow is NOT trustworthy (labeled orientation_confidence = low)
heater → power bill1+✗ expected but NOT found — missing edge
fan → power bill3+✗ 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 billPer-unit effectTrustedVerdict vs ground truth
heater+0.089notruth +0.08 — ⚠ flagged untrustworthy
room temp+0.208no✓ correctly NOT trusted — no real effect exists
fan+0.071notruth +0.16 — ⚠ flagged untrustworthy
noise+0.011no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heaterno⚠ real cause flagged not identifiableNot identifiable: a necessary condition for a causal effect of 'heater_power' on 'power_bill' fails (not_temporal) — the rows do not behave…
room tempno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (not_temporal) — the rows do not behave li…
fanno⚠ real cause flagged not identifiableNot identifiable: a necessary condition for a causal effect of 'fan_speed' on 'power_bill' fails (not_temporal) — the rows do not behave li…
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1fan-1.14noreal lever (truth +0.16)
2heater-1.03noreal lever (truth +0.08)
3noise+0.74nonot a real lever — check the Trusted column
4room temp+0.68nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes16.720.01155moves and has time structure — analyzable
room tempyes7.02-0.0185moves and has time structure — analyzable
fanyes21.400.00114moves and has time structure — analyzable
power billyes4.01-0.0184moves and has time structure — analyzable
noiseyes29.18-0.06107✓ 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 causeLagScoreDirectionVerdict 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.

Hidden confounder (documented limit)

An outside temperature that is NOT in the file drives both the heater and the bill; there is no heater → bill link. No method can recover an unobserved common cause from these columns — this scenario is kept to state that limit, not to pass. The expected outcome is a heater → bill edge that the data cannot refute; what the tool owes the customer is the caveat, not false certainty.

Ground truth — the structure that generated the data

lag 2 · +0.40heaterroom tempfanpower billnoisecauses nothing
CauseEffectLagCoefficient
heaterroom temp2+0.40

TCPFN result vs ground truth (full-graph discover, from the shipped golden snapshot)

Edge TCPFN reportedLagSignVerdict vs ground truth
heater → room temp2+✓ correct — matches the planted edge
heater → power bill0+⚠ documented limit — an unobserved common cause the data cannot reveal; reported because nothing in the file refutes it
power bill → heater0+⚠ documented limit — an unobserved common cause the data cannot reveal; reported because nothing in the file refutes it

heater ↔ bill: both follow the unobserved outside temperature; the columns present cannot rule this edge out — the documented limit this scenario exists to state.

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 reportedLagSignVerdict vs ground truth
heater → room temp2+✓ correct — matches the planted edge
heater → power bill0+⚠ documented limit — an unobserved common cause the data cannot reveal; reported because nothing in the file refutes it
power bill → heater0+⚠ documented limit — an unobserved common cause the data cannot reveal; reported because nothing in the file refutes it
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 billPer-unit effectTrustedVerdict vs ground truth
heater+0.241yes⚠ trusted, but the true effect is 0
room temp+0.437no✓ correctly NOT trusted — no real effect exists
fan-0.030no✓ correctly NOT trusted — no real effect exists
noise+0.015no✓ 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 billIdentifiableVerdict vs ground truthTCPFN's own reason (short)
heateryes✗ decoy marked identifiableIdentifiable given the observed variables: 'heater_power' passes independence, temporal precedence, and deconfounding against every other m…
room tempno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'room_temp' on 'power_bill' fails (reversed) — power_bill LEADS room_temp in…
fanno✓ correctly refused — not a real causeNot identifiable: a necessary condition for a causal effect of 'fan_speed' on 'power_bill' fails (independent) — fan_speed is ~marginally i…
noiseno✓ correctly refused — not a real causeNot 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.

RankInterventionExpected effect on power billTrustedVerdict vs ground truth
1heater+6.70yesnot a real lever — check the Trusted column
2fan-0.86nonot a real lever — check the Trusted column
3noise-0.79nonot a real lever — check the Trusted column
4room temp-0.54nonot 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

VariableUsableStd (movement)Autocorr (time pattern)Sharp changesVerdict vs ground truth
heateryes10.740.63177moves and has time structure — analyzable
room tempyes4.850.52178moves and has time structure — analyzable
fanyes17.490.62172moves and has time structure — analyzable
power billyes4.580.70158moves and has time structure — analyzable
noiseyes28.50-0.02113✓ no time pattern — pure noise, as designed
explaintop root cause of the event: heater

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

Root causeLagScoreDirectionVerdict vs ground truth
heater00.96stable
causal_analysis1 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.

Reported driverLagVerdict vs ground truth
heater✗ not a planted cause