rootcause-sdk 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rootcause_sdk-1.0.0/.gitignore +10 -0
- rootcause_sdk-1.0.0/.gitlab-ci.yml +65 -0
- rootcause_sdk-1.0.0/PKG-INFO +148 -0
- rootcause_sdk-1.0.0/README.md +116 -0
- rootcause_sdk-1.0.0/examples/.ipynb_checkpoints/temporal-panel-checkpoint.ipynb +296 -0
- rootcause_sdk-1.0.0/examples/.ipynb_checkpoints/temporal-panel-executed-checkpoint.ipynb +946 -0
- rootcause_sdk-1.0.0/examples/quickstart.ipynb +1267 -0
- rootcause_sdk-1.0.0/examples/temporal-panel.ipynb +296 -0
- rootcause_sdk-1.0.0/pyproject.toml +45 -0
- rootcause_sdk-1.0.0/rootcause/__init__.py +175 -0
- rootcause_sdk-1.0.0/rootcause/_http.py +361 -0
- rootcause_sdk-1.0.0/rootcause/direct.py +178 -0
- rootcause_sdk-1.0.0/rootcause/errors.py +58 -0
- rootcause_sdk-1.0.0/rootcause/graph.py +119 -0
- rootcause_sdk-1.0.0/rootcause/interventions.py +149 -0
- rootcause_sdk-1.0.0/rootcause/jupyter.py +237 -0
- rootcause_sdk-1.0.0/rootcause/ontology.py +188 -0
- rootcause_sdk-1.0.0/rootcause/results.py +266 -0
- rootcause_sdk-1.0.0/rootcause/twin.py +428 -0
- rootcause_sdk-1.0.0/rootcause/workspace.py +338 -0
- rootcause_sdk-1.0.0/tests/conftest.py +61 -0
- rootcause_sdk-1.0.0/tests/test_direct.py +44 -0
- rootcause_sdk-1.0.0/tests/test_interventions.py +61 -0
- rootcause_sdk-1.0.0/tests/test_jupyter.py +54 -0
- rootcause_sdk-1.0.0/tests/test_new_verbs.py +232 -0
- rootcause_sdk-1.0.0/tests/test_transport.py +58 -0
- rootcause_sdk-1.0.0/tests/test_twin.py +152 -0
- rootcause_sdk-1.0.0/tests/test_twin_reuse.py +78 -0
- rootcause_sdk-1.0.0/tests/test_workspace_and_ontology.py +112 -0
- rootcause_sdk-1.0.0/uv.lock +1076 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
stages:
|
|
2
|
+
- generate
|
|
3
|
+
- build
|
|
4
|
+
- publish
|
|
5
|
+
|
|
6
|
+
variables:
|
|
7
|
+
PYTHON_IMAGE: python:3.12-slim
|
|
8
|
+
|
|
9
|
+
generate:
|
|
10
|
+
stage: generate
|
|
11
|
+
image: $PYTHON_IMAGE
|
|
12
|
+
script:
|
|
13
|
+
- pip install openapi-python-client
|
|
14
|
+
- openapi-python-client generate --path $OPENAPI_SPEC_URL --output-path /tmp/gen --overwrite
|
|
15
|
+
- cp -r /tmp/gen/root_cause_public_api_client/* generated/
|
|
16
|
+
artifacts:
|
|
17
|
+
paths:
|
|
18
|
+
- generated/
|
|
19
|
+
rules:
|
|
20
|
+
- if: $CI_PIPELINE_SOURCE == "trigger"
|
|
21
|
+
- if: $CI_PIPELINE_SOURCE == "web"
|
|
22
|
+
|
|
23
|
+
build:
|
|
24
|
+
stage: build
|
|
25
|
+
image: $PYTHON_IMAGE
|
|
26
|
+
script:
|
|
27
|
+
# On a tag, the tag is the single source of truth for the version.
|
|
28
|
+
- |
|
|
29
|
+
if [ -n "$CI_COMMIT_TAG" ]; then
|
|
30
|
+
echo "Pinning version to tag $CI_COMMIT_TAG"
|
|
31
|
+
sed -i -E "s/^version = \".*\"/version = \"$CI_COMMIT_TAG\"/" pyproject.toml
|
|
32
|
+
grep -E '^version = ' pyproject.toml
|
|
33
|
+
fi
|
|
34
|
+
- pip install build twine
|
|
35
|
+
- python -m build
|
|
36
|
+
- twine check --strict dist/*
|
|
37
|
+
- ls -l dist/
|
|
38
|
+
artifacts:
|
|
39
|
+
paths:
|
|
40
|
+
- dist/
|
|
41
|
+
expire_in: 30 days
|
|
42
|
+
rules:
|
|
43
|
+
- if: $CI_COMMIT_TAG =~ /^[0-9]+\.[0-9]+\.[0-9]+([ab]|rc)?[0-9]*$/
|
|
44
|
+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
45
|
+
|
|
46
|
+
publish:
|
|
47
|
+
stage: publish
|
|
48
|
+
image: $PYTHON_IMAGE
|
|
49
|
+
needs:
|
|
50
|
+
- job: build
|
|
51
|
+
artifacts: true
|
|
52
|
+
environment:
|
|
53
|
+
name: pypi
|
|
54
|
+
url: https://pypi.org/project/rootcause-sdk/
|
|
55
|
+
variables:
|
|
56
|
+
TWINE_USERNAME: __token__
|
|
57
|
+
TWINE_PASSWORD: $PYPI_API_TOKEN
|
|
58
|
+
TWINE_REPOSITORY_URL: https://upload.pypi.org/legacy/
|
|
59
|
+
script:
|
|
60
|
+
- pip install twine
|
|
61
|
+
- twine check --strict dist/*
|
|
62
|
+
- twine upload --non-interactive --disable-progress-bar dist/*
|
|
63
|
+
rules:
|
|
64
|
+
- if: $CI_COMMIT_TAG =~ /^[0-9]+\.[0-9]+\.[0-9]+([ab]|rc)?[0-9]*$/
|
|
65
|
+
when: manual
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: rootcause-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for the RootCause causal AI platform
|
|
5
|
+
Project-URL: Homepage, https://rootcause.ai
|
|
6
|
+
Project-URL: Repository, https://gitlab.com/perceptura/sdks/rootcause-python
|
|
7
|
+
Project-URL: Documentation, https://docs.rootcause.ai
|
|
8
|
+
Author-email: RootCause <engineering@rootcause.ai>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: api-client,causal-inference,digital-twin,jupyter,rootcause,sdk
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Requires-Dist: pandas>=2.0.0
|
|
23
|
+
Requires-Dist: pyarrow>=14.0.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
27
|
+
Provides-Extra: graph
|
|
28
|
+
Requires-Dist: networkx>=3.0; extra == 'graph'
|
|
29
|
+
Provides-Extra: jupyter
|
|
30
|
+
Requires-Dist: anywidget>=0.9; extra == 'jupyter'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# rootcause-sdk
|
|
34
|
+
|
|
35
|
+
[RootCause](https://rootcause.ai) is a causal AI platform: bring your data, discover causal structure, train digital twins, and ask what-if questions. This is the official Python SDK. Full docs: [docs.rootcause.ai](https://docs.rootcause.ai).
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install rootcause-sdk
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import rootcause as rc
|
|
47
|
+
import pandas as pd
|
|
48
|
+
|
|
49
|
+
rc.login() # ROOTCAUSE_API_KEY, or browser login
|
|
50
|
+
|
|
51
|
+
df = pd.read_csv("lalonde.csv")
|
|
52
|
+
|
|
53
|
+
graph = rc.discover(df, target="re78") # causal discovery on a DataFrame
|
|
54
|
+
graph.pin("treat", "re78") # domain knowledge
|
|
55
|
+
twin = graph.train()
|
|
56
|
+
|
|
57
|
+
ate = twin.intervene({"treat": rc.set(1)}, where={"re75": ("<", 5000)})
|
|
58
|
+
ate.summary
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Nothing above mentions a workspace: direct mode keeps platform ceremony out of sight and reuses uploads by content hash.
|
|
62
|
+
|
|
63
|
+
## Platform mode
|
|
64
|
+
|
|
65
|
+
The same classes work against everything your team builds in the RootCause UI:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
ws = rc.workspace("Calix Forecasting")
|
|
69
|
+
|
|
70
|
+
ws.sources["shipments"].to_frame() # tab-completes live names
|
|
71
|
+
ws.upload(df, name="shipments-v2")
|
|
72
|
+
|
|
73
|
+
twin = ws.twin("C8 Temporal") # trained by a colleague — just there
|
|
74
|
+
fc = twin.forecast(horizon=24)
|
|
75
|
+
fc.to_frame() # tidy long format, straight to pandas
|
|
76
|
+
|
|
77
|
+
twin.ask("what happens to bookings if we cut trade shows entirely?")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## The power-user primitive
|
|
81
|
+
|
|
82
|
+
Every simulation family is a wrapper over conditional sampling. The SDK exposes it raw:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
draws = twin.sample(n=10_000, do={"price": rc.pct(+10)}, where={"region": "FL"}, seed=42)
|
|
86
|
+
draws.to_frame() # one row per joint posterior draw
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Interventions: `rc.set(value)`, `rc.pct(+15)`, `rc.add(-5)`, `rc.prob("yes", 0.8)`, `rc.adjust_prob("yes", +10)`, `rc.members(include=[...], size=4)`. Bare values mean `rc.set`. Conditions: `{"region": "EMEA"}` or `{"re75": ("<", 5000)}`.
|
|
90
|
+
|
|
91
|
+
## Ontology queries
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
onto = ws.ontology
|
|
95
|
+
onto.concepts
|
|
96
|
+
|
|
97
|
+
result = onto.query(
|
|
98
|
+
select=["customer", "revenue"],
|
|
99
|
+
where=[("region", "==", "US")],
|
|
100
|
+
group_by=["customer"],
|
|
101
|
+
order_by="-revenue",
|
|
102
|
+
aggregate={"revenue": "sum"},
|
|
103
|
+
)
|
|
104
|
+
result.to_frame()
|
|
105
|
+
|
|
106
|
+
onto.ask("average revenue per customer in Florida last quarter")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Portable twins
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
twin.save("c8.rctwin") # export zip with trained model params
|
|
113
|
+
twin2 = rc.load_twin("c8.rctwin") # later, anywhere, same auth
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Authentication
|
|
117
|
+
|
|
118
|
+
`rc.login()` resolves credentials in order: explicit `api_key="pk_…"` → `ROOTCAUSE_API_KEY` / `ROOTCAUSE_BASE_URL` env vars → cached OAuth token in `~/.rootcause/` → interactive browser login (PKCE; remote kernels get a paste-the-code fallback). Create API keys under **Organisation → API** on your platform.
|
|
119
|
+
|
|
120
|
+
## Releasing
|
|
121
|
+
|
|
122
|
+
Publishing to PyPI is driven entirely by git tags. The tag is the version — `pyproject.toml`
|
|
123
|
+
is patched in CI at build time, so don't bother bumping it by hand.
|
|
124
|
+
|
|
125
|
+
1. Push a tag matching `MAJOR.MINOR.PATCH` (pre-releases like `1.2.0rc1` also work):
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
git tag 0.2.0 && git push origin 0.2.0
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
2. The `build` job builds the sdist + wheel and runs `twine check --strict`.
|
|
132
|
+
3. Trigger the `publish` job (manual) to release to PyPI.
|
|
133
|
+
|
|
134
|
+
### Required CI variable
|
|
135
|
+
|
|
136
|
+
Set as a **masked** and **protected** project-level variable in GitLab
|
|
137
|
+
(Settings → CI/CD → Variables):
|
|
138
|
+
|
|
139
|
+
| Variable | Purpose |
|
|
140
|
+
| ---------------- | --------------------------------------------------- |
|
|
141
|
+
| `PYPI_API_TOKEN` | PyPI API token (`pypi-...`), scoped to this project |
|
|
142
|
+
|
|
143
|
+
It is passed to twine as the password with username `__token__`; nothing is
|
|
144
|
+
hardcoded in the pipeline.
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# rootcause-sdk
|
|
2
|
+
|
|
3
|
+
[RootCause](https://rootcause.ai) is a causal AI platform: bring your data, discover causal structure, train digital twins, and ask what-if questions. This is the official Python SDK. Full docs: [docs.rootcause.ai](https://docs.rootcause.ai).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install rootcause-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import rootcause as rc
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
rc.login() # ROOTCAUSE_API_KEY, or browser login
|
|
18
|
+
|
|
19
|
+
df = pd.read_csv("lalonde.csv")
|
|
20
|
+
|
|
21
|
+
graph = rc.discover(df, target="re78") # causal discovery on a DataFrame
|
|
22
|
+
graph.pin("treat", "re78") # domain knowledge
|
|
23
|
+
twin = graph.train()
|
|
24
|
+
|
|
25
|
+
ate = twin.intervene({"treat": rc.set(1)}, where={"re75": ("<", 5000)})
|
|
26
|
+
ate.summary
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Nothing above mentions a workspace: direct mode keeps platform ceremony out of sight and reuses uploads by content hash.
|
|
30
|
+
|
|
31
|
+
## Platform mode
|
|
32
|
+
|
|
33
|
+
The same classes work against everything your team builds in the RootCause UI:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
ws = rc.workspace("Calix Forecasting")
|
|
37
|
+
|
|
38
|
+
ws.sources["shipments"].to_frame() # tab-completes live names
|
|
39
|
+
ws.upload(df, name="shipments-v2")
|
|
40
|
+
|
|
41
|
+
twin = ws.twin("C8 Temporal") # trained by a colleague — just there
|
|
42
|
+
fc = twin.forecast(horizon=24)
|
|
43
|
+
fc.to_frame() # tidy long format, straight to pandas
|
|
44
|
+
|
|
45
|
+
twin.ask("what happens to bookings if we cut trade shows entirely?")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## The power-user primitive
|
|
49
|
+
|
|
50
|
+
Every simulation family is a wrapper over conditional sampling. The SDK exposes it raw:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
draws = twin.sample(n=10_000, do={"price": rc.pct(+10)}, where={"region": "FL"}, seed=42)
|
|
54
|
+
draws.to_frame() # one row per joint posterior draw
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Interventions: `rc.set(value)`, `rc.pct(+15)`, `rc.add(-5)`, `rc.prob("yes", 0.8)`, `rc.adjust_prob("yes", +10)`, `rc.members(include=[...], size=4)`. Bare values mean `rc.set`. Conditions: `{"region": "EMEA"}` or `{"re75": ("<", 5000)}`.
|
|
58
|
+
|
|
59
|
+
## Ontology queries
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
onto = ws.ontology
|
|
63
|
+
onto.concepts
|
|
64
|
+
|
|
65
|
+
result = onto.query(
|
|
66
|
+
select=["customer", "revenue"],
|
|
67
|
+
where=[("region", "==", "US")],
|
|
68
|
+
group_by=["customer"],
|
|
69
|
+
order_by="-revenue",
|
|
70
|
+
aggregate={"revenue": "sum"},
|
|
71
|
+
)
|
|
72
|
+
result.to_frame()
|
|
73
|
+
|
|
74
|
+
onto.ask("average revenue per customer in Florida last quarter")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Portable twins
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
twin.save("c8.rctwin") # export zip with trained model params
|
|
81
|
+
twin2 = rc.load_twin("c8.rctwin") # later, anywhere, same auth
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Authentication
|
|
85
|
+
|
|
86
|
+
`rc.login()` resolves credentials in order: explicit `api_key="pk_…"` → `ROOTCAUSE_API_KEY` / `ROOTCAUSE_BASE_URL` env vars → cached OAuth token in `~/.rootcause/` → interactive browser login (PKCE; remote kernels get a paste-the-code fallback). Create API keys under **Organisation → API** on your platform.
|
|
87
|
+
|
|
88
|
+
## Releasing
|
|
89
|
+
|
|
90
|
+
Publishing to PyPI is driven entirely by git tags. The tag is the version — `pyproject.toml`
|
|
91
|
+
is patched in CI at build time, so don't bother bumping it by hand.
|
|
92
|
+
|
|
93
|
+
1. Push a tag matching `MAJOR.MINOR.PATCH` (pre-releases like `1.2.0rc1` also work):
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
git tag 0.2.0 && git push origin 0.2.0
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
2. The `build` job builds the sdist + wheel and runs `twine check --strict`.
|
|
100
|
+
3. Trigger the `publish` job (manual) to release to PyPI.
|
|
101
|
+
|
|
102
|
+
### Required CI variable
|
|
103
|
+
|
|
104
|
+
Set as a **masked** and **protected** project-level variable in GitLab
|
|
105
|
+
(Settings → CI/CD → Variables):
|
|
106
|
+
|
|
107
|
+
| Variable | Purpose |
|
|
108
|
+
| ---------------- | --------------------------------------------------- |
|
|
109
|
+
| `PYPI_API_TOKEN` | PyPI API token (`pypi-...`), scoped to this project |
|
|
110
|
+
|
|
111
|
+
It is passed to twine as the password with username `__token__`; nothing is
|
|
112
|
+
hardcoded in the pipeline.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "markdown",
|
|
5
|
+
"metadata": {},
|
|
6
|
+
"source": [
|
|
7
|
+
"# Temporal and panel twins\n",
|
|
8
|
+
"\n",
|
|
9
|
+
"Time series and multi-environment (panel) data get their own twin kinds with their own machinery:\n",
|
|
10
|
+
"lagged dependencies, latent influence detection, per-environment models, forecasts with\n",
|
|
11
|
+
"attribution, and interventions that are scheduled in time. This notebook runs all of it live.\n",
|
|
12
|
+
"\n",
|
|
13
|
+
"| Kind | Kwargs | What it models |\n",
|
|
14
|
+
"| --- | --- | --- |\n",
|
|
15
|
+
"| `temporal` | `time=` | one time series with lagged causal structure |\n",
|
|
16
|
+
"| `multi-environment-static` | `entity=` | the same system observed across environments |\n",
|
|
17
|
+
"| `multi-environment-temporal` | `time=` + `entity=` | a panel: many environments, each a time series |"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"cell_type": "code",
|
|
22
|
+
"execution_count": null,
|
|
23
|
+
"metadata": {},
|
|
24
|
+
"outputs": [],
|
|
25
|
+
"source": [
|
|
26
|
+
"import os\n",
|
|
27
|
+
"import numpy as np\n",
|
|
28
|
+
"import pandas as pd\n",
|
|
29
|
+
"\n",
|
|
30
|
+
"import rootcause as rc\n",
|
|
31
|
+
"\n",
|
|
32
|
+
"rc.login(base_url=os.environ.get(\"ROOTCAUSE_BASE_URL\", \"https://platform.rootcause.ai\"))"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"cell_type": "markdown",
|
|
37
|
+
"metadata": {},
|
|
38
|
+
"source": [
|
|
39
|
+
"## A panel: three stores, thirty months\n",
|
|
40
|
+
"\n",
|
|
41
|
+
"Ground truth: price suppresses demand, demand carries momentum (a lag), seasonality moves both,\n",
|
|
42
|
+
"and each store runs at its own scale. Long format: one row per store per month."
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"cell_type": "code",
|
|
47
|
+
"execution_count": null,
|
|
48
|
+
"metadata": {},
|
|
49
|
+
"outputs": [],
|
|
50
|
+
"source": [
|
|
51
|
+
"rng = np.random.default_rng(11)\n",
|
|
52
|
+
"months = pd.date_range(\"2024-01-01\", periods=30, freq=\"MS\")\n",
|
|
53
|
+
"stores = {\"london\": 1.0, \"paris\": 0.8, \"berlin\": 1.25}\n",
|
|
54
|
+
"rows = []\n",
|
|
55
|
+
"for store, scale in stores.items():\n",
|
|
56
|
+
" demand_prev = 100.0 * scale\n",
|
|
57
|
+
" for i, month in enumerate(months):\n",
|
|
58
|
+
" season = 12 * np.sin(2 * np.pi * (i % 12) / 12)\n",
|
|
59
|
+
" price = 20 + 2 * np.sin(2 * np.pi * (i % 12) / 12 + 1) + rng.normal(0, 0.5)\n",
|
|
60
|
+
" demand = 0.55 * demand_prev + 60 * scale - 2.4 * price + season + rng.normal(0, 4)\n",
|
|
61
|
+
" revenue = price * demand * 0.1 + rng.normal(0, 3)\n",
|
|
62
|
+
" rows.append({\"month\": month.strftime(\"%Y-%m-%d\"), \"store\": store,\n",
|
|
63
|
+
" \"price\": round(price, 2), \"demand\": round(demand, 1), \"revenue\": round(revenue, 1)})\n",
|
|
64
|
+
" demand_prev = demand\n",
|
|
65
|
+
"panel = pd.DataFrame(rows)\n",
|
|
66
|
+
"panel.head()"
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"cell_type": "markdown",
|
|
71
|
+
"metadata": {},
|
|
72
|
+
"source": [
|
|
73
|
+
"## Discovery finds more than edges\n",
|
|
74
|
+
"\n",
|
|
75
|
+
"`time=` and `entity=` make this a panel-temporal twin. Note the graph: alongside the causal\n",
|
|
76
|
+
"edges, discovery surfaced a **latent influence**, a hidden common cause it detected in the data\n",
|
|
77
|
+
"but could not name, and it attributes the store-level differences to the environment itself."
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"cell_type": "code",
|
|
82
|
+
"execution_count": null,
|
|
83
|
+
"metadata": {},
|
|
84
|
+
"outputs": [],
|
|
85
|
+
"source": [
|
|
86
|
+
"graph = rc.discover(panel, time=\"month\", entity=\"store\", force=True)\n",
|
|
87
|
+
"graph.edges"
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"cell_type": "code",
|
|
92
|
+
"execution_count": null,
|
|
93
|
+
"metadata": {},
|
|
94
|
+
"outputs": [],
|
|
95
|
+
"source": [
|
|
96
|
+
"twin = graph.train()\n",
|
|
97
|
+
"twin"
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"cell_type": "markdown",
|
|
102
|
+
"metadata": {},
|
|
103
|
+
"source": [
|
|
104
|
+
"## Per-environment sampling\n",
|
|
105
|
+
"\n",
|
|
106
|
+
"Panel twins hold one model per environment. Sampling narrows with `environments=` and the\n",
|
|
107
|
+
"returned frame carries an `environment` column; seeds derive stable per-environment children,\n",
|
|
108
|
+
"so comparisons are deterministic."
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"cell_type": "code",
|
|
113
|
+
"execution_count": null,
|
|
114
|
+
"metadata": {},
|
|
115
|
+
"outputs": [],
|
|
116
|
+
"source": [
|
|
117
|
+
"draws = twin.sample(n=500, environments=[\"london\", \"berlin\"], seed=3)\n",
|
|
118
|
+
"draws.to_frame().groupby(\"environment\").mean(numeric_only=True).round(1)"
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"cell_type": "markdown",
|
|
123
|
+
"metadata": {},
|
|
124
|
+
"source": [
|
|
125
|
+
"## Forecasts carry their reasoning\n",
|
|
126
|
+
"\n",
|
|
127
|
+
"Each forecast step comes with bounds and an attribution: how much of the prediction is trend,\n",
|
|
128
|
+
"season, and each causal parent (with lags). `aggregate=\"sum\"` adds a combined series across\n",
|
|
129
|
+
"environments; `origin_timestamp` anchors backtests."
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"cell_type": "code",
|
|
134
|
+
"execution_count": null,
|
|
135
|
+
"metadata": {},
|
|
136
|
+
"outputs": [],
|
|
137
|
+
"source": [
|
|
138
|
+
"fc = twin.forecast(horizon=6, targets=[\"revenue\"], aggregate=\"sum\")\n",
|
|
139
|
+
"fc.to_frame()[[\"environment\", \"timestamp\", \"prediction\", \"lowerBound\", \"upperBound\"]].head(8).round(1)"
|
|
140
|
+
]
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"cell_type": "code",
|
|
144
|
+
"execution_count": null,
|
|
145
|
+
"metadata": {},
|
|
146
|
+
"outputs": [],
|
|
147
|
+
"source": [
|
|
148
|
+
"fc.to_frame().loc[0, \"attribution\"]"
|
|
149
|
+
]
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"cell_type": "markdown",
|
|
153
|
+
"metadata": {},
|
|
154
|
+
"source": [
|
|
155
|
+
"## Interventions scheduled in time\n",
|
|
156
|
+
"\n",
|
|
157
|
+
"`rc.at` wraps any intervention value with when it applies: `persistent=True` from the first\n",
|
|
158
|
+
"step onwards, `duration_steps=` for a limited window, `timestamp=` for a specific start.\n",
|
|
159
|
+
"Here: a permanent 10 percent price cut, in London only."
|
|
160
|
+
]
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"cell_type": "code",
|
|
164
|
+
"execution_count": null,
|
|
165
|
+
"metadata": {},
|
|
166
|
+
"outputs": [],
|
|
167
|
+
"source": [
|
|
168
|
+
"result = twin.intervene(\n",
|
|
169
|
+
" {\"price\": rc.at(rc.pct(-10), persistent=True)},\n",
|
|
170
|
+
" outcomes=[\"revenue\"],\n",
|
|
171
|
+
" environments=[\"london\"],\n",
|
|
172
|
+
")\n",
|
|
173
|
+
"result"
|
|
174
|
+
]
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"cell_type": "markdown",
|
|
178
|
+
"metadata": {},
|
|
179
|
+
"source": [
|
|
180
|
+
"## Monthly refresh: assimilate instead of retrain\n",
|
|
181
|
+
"\n",
|
|
182
|
+
"When next month's rows arrive, the model doesn't need rebuilding — extend the twin's\n",
|
|
183
|
+
"source and fold the new rows into the fitted model with `update()`. It finishes with a\n",
|
|
184
|
+
"status, never an error: `committed` (rows folded in), `up_to_date` (nothing new), or\n",
|
|
185
|
+
"`retrain_required` (the model can't take these rows incrementally — call `twin.retrain()`).\n",
|
|
186
|
+
"\n",
|
|
187
|
+
"Static and temporal twins assimilate out of the box. Panel twins need the v2 panel\n",
|
|
188
|
+
"engine (an opt-in in the twin builder); on anything else `update()` simply reports\n",
|
|
189
|
+
"`retrain_required`, and `twin.update_eligibility` tells you in advance. London's series\n",
|
|
190
|
+
"as its own temporal twin:"
|
|
191
|
+
]
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"cell_type": "code",
|
|
195
|
+
"metadata": {},
|
|
196
|
+
"execution_count": null,
|
|
197
|
+
"outputs": [],
|
|
198
|
+
"source": [
|
|
199
|
+
"london = panel[panel[\"store\"] == \"london\"][[\"month\", \"price\", \"demand\", \"revenue\"]].reset_index(drop=True)\n",
|
|
200
|
+
"monthly = rc.discover(london, time=\"month\", force=True).train()\n",
|
|
201
|
+
"monthly"
|
|
202
|
+
]
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"cell_type": "markdown",
|
|
206
|
+
"metadata": {},
|
|
207
|
+
"source": [
|
|
208
|
+
"Two months pass. Extend the source with the new rows and update — the transcript is\n",
|
|
209
|
+
"the whole loop:"
|
|
210
|
+
]
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"cell_type": "code",
|
|
214
|
+
"metadata": {},
|
|
215
|
+
"execution_count": null,
|
|
216
|
+
"outputs": [],
|
|
217
|
+
"source": [
|
|
218
|
+
"source = monthly.source\n",
|
|
219
|
+
"current = source.to_frame()\n",
|
|
220
|
+
"last_month = pd.to_datetime(current[\"month\"]).max()\n",
|
|
221
|
+
"demand_prev = current.sort_values(\"month\")[\"demand\"].iloc[-1]\n",
|
|
222
|
+
"\n",
|
|
223
|
+
"rows = []\n",
|
|
224
|
+
"for month in pd.date_range(last_month + pd.offsets.MonthBegin(1), periods=2, freq=\"MS\"):\n",
|
|
225
|
+
" j = (month.year - 2024) * 12 + (month.month - 1)\n",
|
|
226
|
+
" season = 12 * np.sin(2 * np.pi * (j % 12) / 12)\n",
|
|
227
|
+
" price = 20 + 2 * np.sin(2 * np.pi * (j % 12) / 12 + 1) + rng.normal(0, 0.5)\n",
|
|
228
|
+
" demand = 0.55 * demand_prev + 60 - 2.4 * price + season + rng.normal(0, 4)\n",
|
|
229
|
+
" rows.append({\"month\": month.strftime(\"%Y-%m-%d\"),\n",
|
|
230
|
+
" \"price\": round(price, 2), \"demand\": round(demand, 1),\n",
|
|
231
|
+
" \"revenue\": round(price * demand * 0.1 + rng.normal(0, 3), 1)})\n",
|
|
232
|
+
" demand_prev = demand\n",
|
|
233
|
+
"\n",
|
|
234
|
+
"source.extend(pd.DataFrame(rows))\n",
|
|
235
|
+
"result = monthly.update()\n",
|
|
236
|
+
"result"
|
|
237
|
+
]
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"cell_type": "markdown",
|
|
241
|
+
"metadata": {},
|
|
242
|
+
"source": [
|
|
243
|
+
"Running it again with nothing new in the source is how a scheduled job stays honest —\n",
|
|
244
|
+
"the second call is a cheap no-op:"
|
|
245
|
+
]
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"cell_type": "code",
|
|
249
|
+
"metadata": {},
|
|
250
|
+
"execution_count": null,
|
|
251
|
+
"outputs": [],
|
|
252
|
+
"source": [
|
|
253
|
+
"monthly.update()"
|
|
254
|
+
]
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
"cell_type": "markdown",
|
|
258
|
+
"metadata": {},
|
|
259
|
+
"source": [
|
|
260
|
+
"The refreshed model forecasts onwards from the assimilated months:"
|
|
261
|
+
]
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"cell_type": "code",
|
|
265
|
+
"metadata": {},
|
|
266
|
+
"execution_count": null,
|
|
267
|
+
"outputs": [],
|
|
268
|
+
"source": [
|
|
269
|
+
"fc = monthly.forecast(horizon=3, targets=[\"revenue\"])\n",
|
|
270
|
+
"fc.to_frame()[[\"timestamp\", \"prediction\", \"lowerBound\", \"upperBound\"]].round(1)"
|
|
271
|
+
]
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"cell_type": "markdown",
|
|
275
|
+
"metadata": {},
|
|
276
|
+
"source": [
|
|
277
|
+
"The same scheduling works on plain temporal twins, and everything else from the\n",
|
|
278
|
+
"[quickstart](quickstart.ipynb) applies unchanged: `save`, `load_twin`, `console`, and the\n",
|
|
279
|
+
"ontology all understand temporal and panel twins."
|
|
280
|
+
]
|
|
281
|
+
}
|
|
282
|
+
],
|
|
283
|
+
"metadata": {
|
|
284
|
+
"kernelspec": {
|
|
285
|
+
"display_name": "Python 3",
|
|
286
|
+
"language": "python",
|
|
287
|
+
"name": "python3"
|
|
288
|
+
},
|
|
289
|
+
"language_info": {
|
|
290
|
+
"name": "python",
|
|
291
|
+
"version": "3.12"
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
"nbformat": 4,
|
|
295
|
+
"nbformat_minor": 5
|
|
296
|
+
}
|