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