pycausalsim 0.6.0__py3-none-any.whl
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.
- pycausalsim/__init__.py +40 -0
- pycausalsim/adapters/__init__.py +425 -0
- pycausalsim/agents/__init__.py +605 -0
- pycausalsim/attribution.py +576 -0
- pycausalsim/discovery/__init__.py +662 -0
- pycausalsim/experiment.py +676 -0
- pycausalsim/futures/__init__.py +81 -0
- pycausalsim/futures/backtest.py +159 -0
- pycausalsim/futures/grading.py +157 -0
- pycausalsim/futures/loop.py +148 -0
- pycausalsim/futures/marketing.py +634 -0
- pycausalsim/futures/mechanism.py +118 -0
- pycausalsim/futures/results.py +245 -0
- pycausalsim/futures/scenario.py +173 -0
- pycausalsim/futures/structural.py +126 -0
- pycausalsim/futures/temporal.py +448 -0
- pycausalsim/models/__init__.py +540 -0
- pycausalsim/results.py +331 -0
- pycausalsim/simulator.py +884 -0
- pycausalsim/uplift/__init__.py +635 -0
- pycausalsim/utils/__init__.py +649 -0
- pycausalsim/validation/__init__.py +674 -0
- pycausalsim/visualization/__init__.py +621 -0
- pycausalsim-0.6.0.dist-info/METADATA +484 -0
- pycausalsim-0.6.0.dist-info/RECORD +29 -0
- pycausalsim-0.6.0.dist-info/WHEEL +5 -0
- pycausalsim-0.6.0.dist-info/licenses/LICENSE +201 -0
- pycausalsim-0.6.0.dist-info/licenses/NOTICE +28 -0
- pycausalsim-0.6.0.dist-info/top_level.txt +1 -0
pycausalsim/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Copyright 2025 - 2026 Brian Curry / Vector1 Research
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""
|
|
15
|
+
PyCausalSim: Causal Discovery and Inference Through Simulation
|
|
16
|
+
|
|
17
|
+
A Python framework for discovering and validating causal relationships
|
|
18
|
+
using counterfactual simulation, designed for digital metrics optimization.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
__version__ = "0.6.0"
|
|
22
|
+
__author__ = "Brian Curry"
|
|
23
|
+
|
|
24
|
+
from .simulator import CausalSimulator
|
|
25
|
+
from .attribution import MarketingAttribution
|
|
26
|
+
from .experiment import ExperimentAnalysis
|
|
27
|
+
from .models import StructuralCausalModel
|
|
28
|
+
from .results import CausalEffect, SimulationResult, SensitivityResult
|
|
29
|
+
from . import futures
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"CausalSimulator",
|
|
33
|
+
"MarketingAttribution",
|
|
34
|
+
"ExperimentAnalysis",
|
|
35
|
+
"StructuralCausalModel",
|
|
36
|
+
"CausalEffect",
|
|
37
|
+
"SimulationResult",
|
|
38
|
+
"SensitivityResult",
|
|
39
|
+
"futures",
|
|
40
|
+
]
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# Copyright 2025 - 2026 Brian Curry / Vector1 Research
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""
|
|
15
|
+
Adapters for interoperability with other causal inference libraries.
|
|
16
|
+
|
|
17
|
+
Integrates with:
|
|
18
|
+
- DoWhy (Microsoft)
|
|
19
|
+
- EconML (Microsoft)
|
|
20
|
+
- CausalML (Uber)
|
|
21
|
+
- Papilon (Complex systems)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from typing import Dict, List, Optional, Any
|
|
25
|
+
import pandas as pd
|
|
26
|
+
import numpy as np
|
|
27
|
+
import warnings
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def to_dowhy(simulator):
|
|
31
|
+
"""
|
|
32
|
+
Convert PyCausalSim simulator to DoWhy CausalModel.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
simulator : CausalSimulator
|
|
37
|
+
PyCausalSim simulator instance
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
dowhy.CausalModel
|
|
42
|
+
DoWhy causal model
|
|
43
|
+
|
|
44
|
+
Examples
|
|
45
|
+
--------
|
|
46
|
+
>>> from pycausalsim.adapters import to_dowhy
|
|
47
|
+
>>> dowhy_model = to_dowhy(simulator)
|
|
48
|
+
>>> effect = dowhy_model.identify_effect()
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
import dowhy
|
|
52
|
+
from dowhy import CausalModel
|
|
53
|
+
except ImportError:
|
|
54
|
+
raise ImportError("DoWhy not installed. Run: pip install dowhy")
|
|
55
|
+
|
|
56
|
+
# Build GML graph string from our graph
|
|
57
|
+
gml = _graph_to_gml(simulator._graph)
|
|
58
|
+
|
|
59
|
+
# Determine treatment and outcome
|
|
60
|
+
treatment = simulator.treatment_vars[0] if simulator.treatment_vars else simulator._feature_vars[0]
|
|
61
|
+
outcome = simulator.target
|
|
62
|
+
|
|
63
|
+
model = CausalModel(
|
|
64
|
+
data=simulator.data,
|
|
65
|
+
treatment=treatment,
|
|
66
|
+
outcome=outcome,
|
|
67
|
+
graph=gml
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return model
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def from_dowhy(dowhy_model, data: Optional[pd.DataFrame] = None):
|
|
74
|
+
"""
|
|
75
|
+
Create PyCausalSim simulator from DoWhy CausalModel.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
dowhy_model : dowhy.CausalModel
|
|
80
|
+
DoWhy model
|
|
81
|
+
data : pd.DataFrame, optional
|
|
82
|
+
Data to use (defaults to DoWhy model's data)
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
CausalSimulator
|
|
87
|
+
PyCausalSim simulator
|
|
88
|
+
"""
|
|
89
|
+
from ..simulator import CausalSimulator
|
|
90
|
+
|
|
91
|
+
data = data if data is not None else dowhy_model._data
|
|
92
|
+
target = dowhy_model._outcome[0] if isinstance(dowhy_model._outcome, list) else dowhy_model._outcome
|
|
93
|
+
treatment = dowhy_model._treatment[0] if isinstance(dowhy_model._treatment, list) else dowhy_model._treatment
|
|
94
|
+
|
|
95
|
+
simulator = CausalSimulator(
|
|
96
|
+
data=data,
|
|
97
|
+
target=target,
|
|
98
|
+
treatment_vars=[treatment]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Extract graph from DoWhy if available
|
|
102
|
+
if hasattr(dowhy_model, '_graph'):
|
|
103
|
+
graph = _gml_to_graph(dowhy_model._graph)
|
|
104
|
+
simulator.set_graph(graph)
|
|
105
|
+
|
|
106
|
+
return simulator
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def to_econml(simulator):
|
|
110
|
+
"""
|
|
111
|
+
Convert to EconML-compatible format.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
simulator : CausalSimulator
|
|
116
|
+
PyCausalSim simulator
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
dict
|
|
121
|
+
Dictionary with Y, T, X, W arrays for EconML
|
|
122
|
+
|
|
123
|
+
Examples
|
|
124
|
+
--------
|
|
125
|
+
>>> from pycausalsim.adapters import to_econml
|
|
126
|
+
>>> arrays = to_econml(simulator)
|
|
127
|
+
>>> from econml.dml import LinearDML
|
|
128
|
+
>>> est = LinearDML()
|
|
129
|
+
>>> est.fit(arrays['Y'], arrays['T'], X=arrays['X'], W=arrays['W'])
|
|
130
|
+
"""
|
|
131
|
+
treatment = simulator.treatment_vars[0] if simulator.treatment_vars else simulator._feature_vars[0]
|
|
132
|
+
|
|
133
|
+
Y = simulator.data[simulator.target].values
|
|
134
|
+
T = simulator.data[treatment].values
|
|
135
|
+
|
|
136
|
+
# Features for heterogeneity
|
|
137
|
+
feature_cols = [c for c in simulator._feature_vars if c != treatment]
|
|
138
|
+
X = simulator.data[feature_cols].values if feature_cols else None
|
|
139
|
+
|
|
140
|
+
# Confounders
|
|
141
|
+
W = simulator.data[simulator.confounders].values if simulator.confounders else None
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
'Y': Y,
|
|
145
|
+
'T': T,
|
|
146
|
+
'X': X,
|
|
147
|
+
'W': W,
|
|
148
|
+
'feature_names': feature_cols,
|
|
149
|
+
'confounder_names': simulator.confounders
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def from_econml(econml_model, data: pd.DataFrame, target: str, treatment: str):
|
|
154
|
+
"""
|
|
155
|
+
Create simulator from fitted EconML model.
|
|
156
|
+
|
|
157
|
+
Parameters
|
|
158
|
+
----------
|
|
159
|
+
econml_model : econml estimator
|
|
160
|
+
Fitted EconML model
|
|
161
|
+
data : pd.DataFrame
|
|
162
|
+
Data used for fitting
|
|
163
|
+
target : str
|
|
164
|
+
Outcome variable name
|
|
165
|
+
treatment : str
|
|
166
|
+
Treatment variable name
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
CausalSimulator
|
|
171
|
+
"""
|
|
172
|
+
from ..simulator import CausalSimulator
|
|
173
|
+
|
|
174
|
+
simulator = CausalSimulator(
|
|
175
|
+
data=data,
|
|
176
|
+
target=target,
|
|
177
|
+
treatment_vars=[treatment]
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Store EconML model for CATE predictions
|
|
181
|
+
simulator._econml_model = econml_model
|
|
182
|
+
|
|
183
|
+
return simulator
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def to_causalml(simulator):
|
|
187
|
+
"""
|
|
188
|
+
Convert to CausalML-compatible format.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
simulator : CausalSimulator
|
|
193
|
+
PyCausalSim simulator
|
|
194
|
+
|
|
195
|
+
Returns
|
|
196
|
+
-------
|
|
197
|
+
dict
|
|
198
|
+
Dictionary with arrays for CausalML uplift models
|
|
199
|
+
"""
|
|
200
|
+
treatment = simulator.treatment_vars[0] if simulator.treatment_vars else simulator._feature_vars[0]
|
|
201
|
+
|
|
202
|
+
feature_cols = [c for c in simulator._feature_vars if c != treatment]
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
'X': simulator.data[feature_cols],
|
|
206
|
+
'treatment': simulator.data[treatment],
|
|
207
|
+
'y': simulator.data[simulator.target],
|
|
208
|
+
'feature_names': feature_cols
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def from_papilon(papilon_results, data: Optional[pd.DataFrame] = None):
|
|
213
|
+
"""
|
|
214
|
+
Create simulator from Papilon complex system results.
|
|
215
|
+
|
|
216
|
+
Parameters
|
|
217
|
+
----------
|
|
218
|
+
papilon_results : dict or PapilonResult
|
|
219
|
+
Results from Papilon simulation
|
|
220
|
+
data : pd.DataFrame, optional
|
|
221
|
+
Observational data
|
|
222
|
+
|
|
223
|
+
Returns
|
|
224
|
+
-------
|
|
225
|
+
CausalSimulator
|
|
226
|
+
"""
|
|
227
|
+
from ..simulator import CausalSimulator
|
|
228
|
+
|
|
229
|
+
# Extract data from Papilon results
|
|
230
|
+
if hasattr(papilon_results, 'to_dataframe'):
|
|
231
|
+
data = papilon_results.to_dataframe()
|
|
232
|
+
elif isinstance(papilon_results, dict) and 'data' in papilon_results:
|
|
233
|
+
data = papilon_results['data']
|
|
234
|
+
|
|
235
|
+
if data is None:
|
|
236
|
+
raise ValueError("No data found in Papilon results")
|
|
237
|
+
|
|
238
|
+
# Detect target (usually last column or specified)
|
|
239
|
+
if hasattr(papilon_results, 'target'):
|
|
240
|
+
target = papilon_results.target
|
|
241
|
+
else:
|
|
242
|
+
target = data.columns[-1]
|
|
243
|
+
|
|
244
|
+
simulator = CausalSimulator(
|
|
245
|
+
data=data,
|
|
246
|
+
target=target
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Extract causal structure if available
|
|
250
|
+
if hasattr(papilon_results, 'causal_graph'):
|
|
251
|
+
simulator.set_graph(papilon_results.causal_graph)
|
|
252
|
+
|
|
253
|
+
return simulator
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _graph_to_gml(graph: Dict[str, List[str]]) -> str:
|
|
257
|
+
"""Convert adjacency list to GML format."""
|
|
258
|
+
lines = ["graph [", " directed 1"]
|
|
259
|
+
|
|
260
|
+
# Collect all nodes
|
|
261
|
+
nodes = set(graph.keys())
|
|
262
|
+
for parents in graph.values():
|
|
263
|
+
nodes.update(parents)
|
|
264
|
+
|
|
265
|
+
# Add nodes
|
|
266
|
+
for i, node in enumerate(sorted(nodes)):
|
|
267
|
+
lines.append(f' node [ id {i} label "{node}" ]')
|
|
268
|
+
|
|
269
|
+
# Create node index mapping
|
|
270
|
+
node_idx = {node: i for i, node in enumerate(sorted(nodes))}
|
|
271
|
+
|
|
272
|
+
# Add edges
|
|
273
|
+
for child, parents in graph.items():
|
|
274
|
+
for parent in parents:
|
|
275
|
+
lines.append(f" edge [ source {node_idx[parent]} target {node_idx[child]} ]")
|
|
276
|
+
|
|
277
|
+
lines.append("]")
|
|
278
|
+
return "\n".join(lines)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _gml_to_graph(gml_string: str) -> Dict[str, List[str]]:
|
|
282
|
+
"""Convert GML format to adjacency list."""
|
|
283
|
+
graph = {}
|
|
284
|
+
|
|
285
|
+
# Simple GML parser
|
|
286
|
+
import re
|
|
287
|
+
|
|
288
|
+
# Extract nodes
|
|
289
|
+
node_pattern = r'node\s*\[\s*id\s+(\d+)\s+label\s+"([^"]+)"\s*\]'
|
|
290
|
+
nodes = {}
|
|
291
|
+
for match in re.finditer(node_pattern, gml_string, re.IGNORECASE):
|
|
292
|
+
node_id, label = match.groups()
|
|
293
|
+
nodes[int(node_id)] = label
|
|
294
|
+
graph[label] = []
|
|
295
|
+
|
|
296
|
+
# Extract edges
|
|
297
|
+
edge_pattern = r'edge\s*\[\s*source\s+(\d+)\s+target\s+(\d+)\s*\]'
|
|
298
|
+
for match in re.finditer(edge_pattern, gml_string, re.IGNORECASE):
|
|
299
|
+
source, target = match.groups()
|
|
300
|
+
source_node = nodes.get(int(source))
|
|
301
|
+
target_node = nodes.get(int(target))
|
|
302
|
+
if source_node and target_node:
|
|
303
|
+
if target_node not in graph:
|
|
304
|
+
graph[target_node] = []
|
|
305
|
+
graph[target_node].append(source_node)
|
|
306
|
+
|
|
307
|
+
return graph
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class DoWhyAdapter:
|
|
311
|
+
"""
|
|
312
|
+
Adapter class for DoWhy integration.
|
|
313
|
+
|
|
314
|
+
Provides bidirectional conversion between PyCausalSim and DoWhy.
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
def __init__(self, simulator=None, dowhy_model=None):
|
|
318
|
+
self.simulator = simulator
|
|
319
|
+
self.dowhy_model = dowhy_model
|
|
320
|
+
|
|
321
|
+
def to_dowhy(self):
|
|
322
|
+
"""Convert simulator to DoWhy model."""
|
|
323
|
+
if self.simulator is None:
|
|
324
|
+
raise ValueError("No simulator set")
|
|
325
|
+
return to_dowhy(self.simulator)
|
|
326
|
+
|
|
327
|
+
def from_dowhy(self):
|
|
328
|
+
"""Convert DoWhy model to simulator."""
|
|
329
|
+
if self.dowhy_model is None:
|
|
330
|
+
raise ValueError("No DoWhy model set")
|
|
331
|
+
return from_dowhy(self.dowhy_model)
|
|
332
|
+
|
|
333
|
+
def identify_effect(self):
|
|
334
|
+
"""Use DoWhy for effect identification."""
|
|
335
|
+
model = self.to_dowhy()
|
|
336
|
+
return model.identify_effect()
|
|
337
|
+
|
|
338
|
+
def estimate_effect(self, method_name: str = "backdoor.linear_regression"):
|
|
339
|
+
"""Use DoWhy for effect estimation."""
|
|
340
|
+
model = self.to_dowhy()
|
|
341
|
+
identified = model.identify_effect()
|
|
342
|
+
return model.estimate_effect(identified, method_name=method_name)
|
|
343
|
+
|
|
344
|
+
def refute_estimate(self, estimate, method_name: str = "random_common_cause"):
|
|
345
|
+
"""Use DoWhy refutation methods."""
|
|
346
|
+
model = self.to_dowhy()
|
|
347
|
+
return model.refute_estimate(
|
|
348
|
+
model.identify_effect(),
|
|
349
|
+
estimate,
|
|
350
|
+
method_name=method_name
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class EconMLAdapter:
|
|
355
|
+
"""
|
|
356
|
+
Adapter class for EconML integration.
|
|
357
|
+
|
|
358
|
+
Provides access to EconML's heterogeneous treatment effect estimators.
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
def __init__(self, simulator=None):
|
|
362
|
+
self.simulator = simulator
|
|
363
|
+
self._fitted_model = None
|
|
364
|
+
|
|
365
|
+
def fit_dml(self, **kwargs):
|
|
366
|
+
"""Fit Double Machine Learning estimator."""
|
|
367
|
+
try:
|
|
368
|
+
from econml.dml import LinearDML
|
|
369
|
+
except ImportError:
|
|
370
|
+
raise ImportError("EconML not installed. Run: pip install econml")
|
|
371
|
+
|
|
372
|
+
arrays = to_econml(self.simulator)
|
|
373
|
+
|
|
374
|
+
model = LinearDML(**kwargs)
|
|
375
|
+
model.fit(
|
|
376
|
+
arrays['Y'],
|
|
377
|
+
arrays['T'],
|
|
378
|
+
X=arrays['X'],
|
|
379
|
+
W=arrays['W']
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
self._fitted_model = model
|
|
383
|
+
return model
|
|
384
|
+
|
|
385
|
+
def fit_causal_forest(self, **kwargs):
|
|
386
|
+
"""Fit Causal Forest estimator."""
|
|
387
|
+
try:
|
|
388
|
+
from econml.dml import CausalForestDML
|
|
389
|
+
except ImportError:
|
|
390
|
+
raise ImportError("EconML not installed. Run: pip install econml")
|
|
391
|
+
|
|
392
|
+
arrays = to_econml(self.simulator)
|
|
393
|
+
|
|
394
|
+
model = CausalForestDML(**kwargs)
|
|
395
|
+
model.fit(
|
|
396
|
+
arrays['Y'],
|
|
397
|
+
arrays['T'],
|
|
398
|
+
X=arrays['X'],
|
|
399
|
+
W=arrays['W']
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
self._fitted_model = model
|
|
403
|
+
return model
|
|
404
|
+
|
|
405
|
+
def effect(self, X=None):
|
|
406
|
+
"""Get treatment effects."""
|
|
407
|
+
if self._fitted_model is None:
|
|
408
|
+
raise RuntimeError("Must fit model first")
|
|
409
|
+
|
|
410
|
+
if X is None:
|
|
411
|
+
arrays = to_econml(self.simulator)
|
|
412
|
+
X = arrays['X']
|
|
413
|
+
|
|
414
|
+
return self._fitted_model.effect(X)
|
|
415
|
+
|
|
416
|
+
def effect_interval(self, X=None, alpha: float = 0.05):
|
|
417
|
+
"""Get treatment effect confidence intervals."""
|
|
418
|
+
if self._fitted_model is None:
|
|
419
|
+
raise RuntimeError("Must fit model first")
|
|
420
|
+
|
|
421
|
+
if X is None:
|
|
422
|
+
arrays = to_econml(self.simulator)
|
|
423
|
+
X = arrays['X']
|
|
424
|
+
|
|
425
|
+
return self._fitted_model.effect_interval(X, alpha=alpha)
|