├── VERSION ├── tests ├── __init__.py ├── test_sgm.py └── test_cgm.py ├── .gitignore ├── causalgraphicalmodels ├── __init__.py ├── examples.py ├── csm.py └── cgm.py ├── LICENSE ├── setup.py ├── README.md └── notebooks └── cgm-examples.ipynb /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.4 -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | __pycache__/ 4 | .cache/ 5 | .idea/ 6 | .ipynb_checkpoints/ 7 | .pytest_cache/ 8 | dist/ 9 | build/ 10 | *.egg-info/ 11 | 12 | *~ 13 | -------------------------------------------------------------------------------- /causalgraphicalmodels/__init__.py: -------------------------------------------------------------------------------- 1 | from causalgraphicalmodels.cgm import CausalGraphicalModel 2 | from causalgraphicalmodels.csm import StructuralCausalModel 3 | import causalgraphicalmodels.examples as examples 4 | import causalgraphicalmodels.csm as csm 5 | -------------------------------------------------------------------------------- /tests/test_sgm.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | from causalgraphicalmodels.examples import chain_csm 4 | 5 | 6 | class TestSGM(unittest.TestCase): 7 | def test_cgm(self): 8 | chain_cgm = chain_csm.cgm 9 | self.assertTrue(chain_cgm.is_d_separated("a", "c", {"b"})) 10 | self.assertFalse(chain_cgm.is_d_separated("a", "c", set())) 11 | 12 | def test_sample(self): 13 | sample = chain_csm.sample(n_samples=1) 14 | 15 | self.assertEqual(sample.c.squeeze(), 6) 16 | 17 | def test_do(self): 18 | sample = ( 19 | chain_csm 20 | .do("a") 21 | .sample(n_samples=1, set_values={"a": np.array([5])}) 22 | ) 23 | 24 | self.assertEqual(sample.c.squeeze(), 14) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Iain Barr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup.py for CausalGraphicalModels""" 2 | 3 | from setuptools import setup, find_packages 4 | from codecs import open 5 | from os import path 6 | 7 | here = path.abspath(path.dirname(__file__)) 8 | 9 | with open(path.join(here, "README.md"), encoding="utf-8") as f: 10 | long_description = f.read() 11 | 12 | with open(path.join(here, 'VERSION')) as version_file: 13 | version = version_file.read().strip() 14 | 15 | setup( 16 | name="causalgraphicalmodels", 17 | version=version, 18 | description="Causality Graphical Models in Python", 19 | long_description_content_type="text/markdown", 20 | long_description=long_description, 21 | url="https://github.com/ijmbarr/causalgraphicalmodels", 22 | author="Iain Barr", 23 | author_email="iain@degeneratestate.org", 24 | license="MIT", 25 | classifiers=[ 26 | "Development Status :: 2 - Pre-Alpha", 27 | "Intended Audience :: Science/Research", 28 | "Topic :: Scientific/Engineering", 29 | "License :: OSI Approved :: MIT License", 30 | "Programming Language :: Python :: 3.5", 31 | ], 32 | keywords="causal inference causal graphical models causality", 33 | packages=find_packages(exclude=["notebook", "test"]), 34 | install_requires=["graphviz", "networkx", "numpy", "pandas"] 35 | ) 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CausalGraphicalModels 2 | 3 | ## Introduction 4 | 5 | `causalgraphicalmodels` is a python module for describing and manipulating [Causal Graphical Models](https://en.wikipedia.org/wiki/Causal_graph) and [Structural Causal Models](https://en.wikipedia.org/wiki/Structural_equation_modeling). Behind the scenes it is a light wrapper around the python graph library [networkx](https://networkx.github.io/), together with some CGM specific tools. 6 | 7 | It is currently in a very early stage of development. All feedback is welcome. 8 | 9 | 10 | ## Example 11 | 12 | For a quick overview of `CausalGraphicalModel`, see [this example notebook](https://github.com/ijmbarr/causalgraphicalmodels/blob/master/notebooks/cgm-examples.ipynb). 13 | 14 | ## Install 15 | 16 | ``` 17 | pip install causalgraphicalmodels 18 | ``` 19 | 20 | 21 | ## Resources 22 | My understanding of Causality comes mainly from the reading of the follow work: 23 | - Causality, Pearl, 2009, 2nd Editing. (An overview available [here](http://ftp.cs.ucla.edu/pub/stat_ser/r350.pdf)) 24 | - A fantastic blog post, [If correlation doesn’t imply causation, then what does?](http://www.michaelnielsen.org/ddi/if-correlation-doesnt-imply-causation-then-what-does/) from Michael Nielsen 25 | - [These lecture notes](http://www.math.ku.dk/~peters/jonas_files/scriptChapter1-4.pdf) from Jonas Peters 26 | - The draft of [Elements of Causal Inference](http://www.math.ku.dk/~peters/jonas_files/bookDRAFT5-online-2017-02-27.pdf) 27 | - http://mlss.tuebingen.mpg.de/2017/speaker_slides/Causality.pdf 28 | 29 | ## Related Packages 30 | - [Causality](https://github.com/akelleh/causality) 31 | - [CausalInference](https://github.com/laurencium/causalinference) 32 | - [DoWhy](https://github.com/Microsoft/dowhy) 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/test_cgm.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from causalgraphicalmodels.examples import sprinkler, simple_confounded, \ 3 | simple_confounded_hidden_confounder, front_door_example 4 | 5 | 6 | class TestCGM(unittest.TestCase): 7 | def test_is_d_separated(self): 8 | s = sprinkler 9 | 10 | self.assertTrue(s.is_d_separated("season", "slippery", {"wet"})) 11 | self.assertTrue(s.is_d_separated("season", "slippery", 12 | {"rain", "sprinkler"})) 13 | self.assertTrue(s.is_d_separated("season", "slippery", 14 | {"rain", "season", "wet"})) 15 | 16 | self.assertFalse(s.is_d_separated("rain", "sprinkler")) 17 | self.assertTrue(s.is_d_separated("rain", "sprinkler", {"season"})) 18 | self.assertFalse(s.is_d_separated("rain", "sprinkler", {"wet"})) 19 | 20 | def test_get_all_backdoor_adjustment_sets(self): 21 | s = simple_confounded.get_all_backdoor_adjustment_sets("x", "y") 22 | expected_results = frozenset([frozenset(["z"])]) 23 | self.assertEqual(s, expected_results) 24 | 25 | def test_get_all_backdoor_adjustment_sets_hidden(self): 26 | self.assertFalse( 27 | simple_confounded_hidden_confounder 28 | .get_all_backdoor_adjustment_sets("x", "y")) 29 | 30 | def test_is_valid_adjustment_set(self): 31 | self.assertTrue( 32 | simple_confounded.is_valid_backdoor_adjustment_set("x", "y", {"z"})) 33 | self.assertFalse( 34 | simple_confounded.is_valid_backdoor_adjustment_set("x", "y", set())) 35 | self.assertFalse( 36 | simple_confounded_hidden_confounder 37 | .is_valid_backdoor_adjustment_set("x", "y", set())) 38 | 39 | def test_get_all_independence_relationships(self): 40 | self.assertFalse( 41 | simple_confounded_hidden_confounder 42 | .get_all_independence_relationships()) 43 | 44 | def test_is_valid_frontdoor_adjustment_set(self): 45 | self.assertTrue( 46 | front_door_example.is_valid_frontdoor_adjustment_set("x", "y", "z")) 47 | 48 | self.assertFalse( 49 | simple_confounded.is_valid_frontdoor_adjustment_set("x", "y", "z")) 50 | 51 | -------------------------------------------------------------------------------- /causalgraphicalmodels/examples.py: -------------------------------------------------------------------------------- 1 | """ 2 | A set of example causal systems 3 | """ 4 | 5 | import numpy as np 6 | 7 | from causalgraphicalmodels.cgm import CausalGraphicalModel 8 | from causalgraphicalmodels.csm import StructuralCausalModel, \ 9 | linear_model, logistic_model 10 | 11 | 12 | chain = CausalGraphicalModel( 13 | nodes=["x1", "x2", "x3"], 14 | edges=[("x1", "x2"), ("x2", "x3")] 15 | ) 16 | 17 | collider = CausalGraphicalModel( 18 | nodes=["x1", "x2", "x3"], 19 | edges=[("x1", "x2"), ("x3", "x2")] 20 | ) 21 | 22 | fork = CausalGraphicalModel( 23 | nodes=["x1", "x2", "x3"], 24 | edges=[("x2", "x1"), ("x2", "x3")] 25 | ) 26 | 27 | example_path_one = CausalGraphicalModel( 28 | nodes=["x1", "x2", "x3", "x4", "x5"], 29 | edges=[("x1", "x2"), ("x2", "x3"), ("x3", "x4"), ("x4", "x5")] 30 | ) 31 | 32 | sprinkler = CausalGraphicalModel( 33 | nodes=["season", "rain", "sprinkler", "wet", "slippery"], 34 | edges=[("season", "rain"), ("season", "sprinkler"), ("rain", "wet"), 35 | ("sprinkler", "wet"), ("wet", "slippery")] 36 | ) 37 | 38 | simple_confounded = CausalGraphicalModel( 39 | nodes=["x", "y", "z"], 40 | edges=[("z", "x"), ("z", "y"), ("x", "y")] 41 | ) 42 | 43 | simple_confounded_potential_outcomes = CausalGraphicalModel( 44 | nodes=["x", "y_0", "y_1", "y", "z"], 45 | edges=[("z", "x"), ("z", "y_0"), ("z", "y_1"), 46 | ("y_0", "y"), ("y_1", "y"), ("x", "y")] 47 | ) 48 | 49 | 50 | simple_confounded_hidden_confounder = CausalGraphicalModel( 51 | nodes=["x", "y"], 52 | edges=[("x", "y")], 53 | latent_edges=[("x", "y")] 54 | 55 | ) 56 | 57 | front_door_example = CausalGraphicalModel( 58 | nodes=["x", "y", "z"], 59 | edges=[("x", "z"), ("z", "y")], 60 | latent_edges=[("x", "y")] 61 | ) 62 | 63 | 64 | chain_csm = StructuralCausalModel({ 65 | "a": lambda n_samples: np.ones(n_samples), 66 | "b": lambda a, n_samples: a + 2, 67 | "c": lambda b, n_samples: b * 2 68 | }) 69 | 70 | big_csm = StructuralCausalModel({ 71 | "a": lambda n_samples: np.random.normal(size=n_samples), 72 | "b": lambda n_samples: np.random.normal(size=n_samples), 73 | "x": logistic_model(["a", "b"], [-1, 1]), 74 | "c": linear_model(["x"], [1]), 75 | "y": linear_model(["c", "e"], [3,-1]), 76 | "d": linear_model(["b"], [-1]), 77 | "e": linear_model(["d"], [2]), 78 | "f": linear_model(["y"], [0.7]), 79 | "h": linear_model(["y", "a"], [1.3, 2.1]) 80 | }) 81 | 82 | -------------------------------------------------------------------------------- /causalgraphicalmodels/csm.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import numpy as np 3 | import pandas as pd 4 | import networkx as nx 5 | 6 | from causalgraphicalmodels.cgm import CausalGraphicalModel 7 | 8 | 9 | class StructuralCausalModel: 10 | def __init__(self, assignment): 11 | """ 12 | Creates StructuralCausalModel from assignment of the form 13 | { variable: Function(parents) } 14 | """ 15 | 16 | self.assignment = assignment.copy() 17 | nodes = list(assignment.keys()) 18 | set_nodes = [] 19 | edges = [] 20 | 21 | for node, model in assignment.items(): 22 | if model is None: 23 | set_nodes.append(node) 24 | 25 | elif isinstance(model, CausalAssignmentModel): 26 | edges.extend([ 27 | (parent, node) 28 | for parent in model.parents 29 | ]) 30 | 31 | elif callable(model): 32 | sig = inspect.signature(model) 33 | parents = [ 34 | parent 35 | for parent in sig.parameters.keys() 36 | if parent != "n_samples" 37 | ] 38 | self.assignment[node] = CausalAssignmentModel(model, parents) 39 | edges.extend([(p, node) for p in parents]) 40 | 41 | else: 42 | raise ValueError("Model must be either callable or None. " 43 | "Instead got {} for node {}." 44 | .format(model, node)) 45 | 46 | self.cgm = CausalGraphicalModel( 47 | nodes=nodes, edges=edges, set_nodes=set_nodes) 48 | 49 | def __repr__(self): 50 | variables = ", ".join(map(str, sorted(self.cgm.dag.nodes()))) 51 | return ("{classname}({vars})" 52 | .format(classname=self.__class__.__name__, 53 | vars=variables)) 54 | 55 | def sample(self, n_samples=100, set_values=None): 56 | """ 57 | Sample from CSM 58 | 59 | Arguments 60 | --------- 61 | n_samples: int 62 | the number of samples to return 63 | 64 | set_values: dict[variable:str, set_value:np.array] 65 | the values of the interventional variable 66 | 67 | Returns 68 | ------- 69 | samples: pd.DataFrame 70 | """ 71 | samples = {} 72 | 73 | if set_values is None: 74 | set_values = dict() 75 | 76 | for node in nx.topological_sort(self.cgm.dag): 77 | c_model = self.assignment[node] 78 | 79 | if c_model is None: 80 | assert len(set_values[node]) == n_samples 81 | samples[node] = np.array(set_values[node]) 82 | else: 83 | parent_samples = { 84 | parent: samples[parent] 85 | for parent in c_model.parents 86 | } 87 | parent_samples["n_samples"] = n_samples 88 | samples[node] = c_model(**parent_samples) 89 | 90 | return pd.DataFrame(samples) 91 | 92 | def do(self, node): 93 | """ 94 | Returns a StructualCausalModel after an intervention on node 95 | """ 96 | new_assignment = self.assignment.copy() 97 | new_assignment[node] = None 98 | return StructuralCausalModel(new_assignment) 99 | 100 | 101 | class CausalAssignmentModel: 102 | """ 103 | Basically just a hack to allow me to provide information about the 104 | arguments of a dynamically generated function. 105 | """ 106 | def __init__(self, model, parents): 107 | self.model = model 108 | self.parents = parents 109 | 110 | def __call__(self, *args, **kwargs): 111 | assert len(args) == 0 112 | return self.model(**kwargs) 113 | 114 | def __repr__(self): 115 | return "CausalAssignmentModel({})".format(",".join(self.parents)) 116 | 117 | 118 | # Some Helper functions for defining models 119 | 120 | def _sigma(x): 121 | return 1 / (1 + np.exp(-x)) 122 | 123 | 124 | def linear_model(parents, weights, offset=0, noise_scale=1): 125 | """ 126 | Create CausalAssignmentModel for node y of the form 127 | \sum_{i} x_{i}w_{i} + a + \epsilon 128 | 129 | Arguments 130 | --------- 131 | parents: list 132 | variable names of parents 133 | 134 | weights: list 135 | weigths of each variable in the sum 136 | 137 | offset: float 138 | offset for sum 139 | 140 | noise_scale: float 141 | scale of the normal noise 142 | 143 | Returns 144 | ------- 145 | model: CausalAssignmentModel 146 | """ 147 | assert len(parents) == len(weights) 148 | assert len(parents) > 0 149 | def model(**kwargs): 150 | n_samples = kwargs["n_samples"] 151 | a = np.array([kwargs[p] * w for p, w in zip(parents, weights)], dtype=np.float) 152 | a = np.sum(a, axis=0) 153 | a += np.random.normal(loc=offset, scale=noise_scale, size=n_samples) 154 | return a 155 | 156 | return CausalAssignmentModel(model, parents) 157 | 158 | 159 | def logistic_model(parents, weights, offset=0): 160 | """ 161 | Create CausalAssignmentModel for node y of the form 162 | z = \sum_{i} x_{i}w_{i} + a 163 | y ~ Binomial(\simga(z)) 164 | 165 | Arguments 166 | --------- 167 | parents: list 168 | variable names of parents 169 | 170 | weights: list 171 | weigths of each variable in the sum 172 | 173 | offset: float 174 | offset for sum 175 | 176 | Returns 177 | ------- 178 | model: CausalAssignmentModel 179 | """ 180 | assert len(parents) == len(weights) 181 | assert len(parents) > 0 182 | def model(**kwargs): 183 | a = np.array([kwargs[p] * w for p, w in zip(parents, weights)]) 184 | a = np.sum(a, axis=0) + offset 185 | a = _sigma(a) 186 | a = np.random.binomial(n=1, p=a) 187 | return a 188 | 189 | return CausalAssignmentModel(model, parents) 190 | 191 | 192 | def discrete_model(parents, lookup_table): 193 | """ 194 | Create CausalAssignmentModel based on a lookup table. 195 | 196 | Lookup_table maps inputs values to weigths of the output values 197 | The actual output values are sampled from a discrete distribution 198 | of integers with probability proportional to the weights. 199 | 200 | Lookup_table for the form: 201 | 202 | Dict[Tuple(input_vales): (output_weights)] 203 | 204 | Arguments 205 | --------- 206 | parents: list 207 | variable names of parents 208 | 209 | lookup_table: dict 210 | lookup table 211 | 212 | Returns 213 | ------- 214 | model: CausalAssignmentModel 215 | """ 216 | assert len(parents) > 0 217 | 218 | # create input/output mapping 219 | inputs, weights = zip(*lookup_table.items()) 220 | 221 | output_length = len(weights[0]) 222 | assert all(len(w) == output_length for w in weights) 223 | outputs = np.arange(output_length) 224 | 225 | ps = [np.array(w) / sum(w) for w in weights] 226 | 227 | def model(**kwargs): 228 | n_samples = kwargs["n_samples"] 229 | a = np.vstack([kwargs[p] for p in parents]).T 230 | 231 | b = np.zeros(n_samples) * np.nan 232 | for m, p in zip(inputs, ps): 233 | b = np.where( 234 | (a == m).all(axis=1), 235 | np.random.choice(outputs, size=n_samples, p=p), b) 236 | 237 | if np.isnan(b).any(): 238 | raise ValueError("It looks like an input was provided which doesn't have a lookup.") 239 | 240 | return b 241 | 242 | return CausalAssignmentModel(model, parents) 243 | 244 | -------------------------------------------------------------------------------- /causalgraphicalmodels/cgm.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | import graphviz 3 | from itertools import combinations, chain 4 | from collections import Iterable 5 | 6 | 7 | class CausalGraphicalModel: 8 | """ 9 | Causal Graphical Models 10 | """ 11 | 12 | def __init__(self, nodes, edges, latent_edges=None, set_nodes=None): 13 | """ 14 | Create CausalGraphicalModel 15 | 16 | Arguments 17 | --------- 18 | nodes: list[node:str] 19 | 20 | edges: list[tuple[node:str, node:str]] 21 | 22 | latent_edges: list[tuple[node:str, node:str]] or None 23 | 24 | set_nodes: list[node:str] or None 25 | """ 26 | if set_nodes is None: 27 | self.set_nodes = frozenset() 28 | else: 29 | self.set_nodes = frozenset(set_nodes) 30 | 31 | if latent_edges is None: 32 | self.latent_edges = frozenset() 33 | else: 34 | self.latent_edges = frozenset(latent_edges) 35 | 36 | self.dag = nx.DiGraph() 37 | self.dag.add_nodes_from(nodes) 38 | self.dag.add_edges_from(edges) 39 | 40 | # Add latent connections to the graph 41 | self.observed_variables = frozenset(nodes) 42 | self.unobserved_variable_edges = dict() 43 | unobserved_variables = [] 44 | unobserved_variable_counter = 0 45 | for n1, n2 in self.latent_edges: 46 | new_node = "Unobserved_{}".format(unobserved_variable_counter) 47 | unobserved_variable_counter += 1 48 | self.dag.add_node(new_node) 49 | self.dag.add_edge(new_node, n1) 50 | self.dag.add_edge(new_node, n2) 51 | unobserved_variables.append(new_node) 52 | self.unobserved_variable_edges[new_node] = (n1, n2) 53 | self.unobserved_variables = frozenset(unobserved_variables) 54 | 55 | assert nx.is_directed_acyclic_graph(self.dag) 56 | 57 | for set_node in self.set_nodes: 58 | # set nodes cannot have parents 59 | assert not nx.ancestors(self.dag, set_node) 60 | 61 | self.graph = self.dag.to_undirected() 62 | 63 | def __repr__(self): 64 | variables = ", ".join(map(str, sorted(self.observed_variables))) 65 | return ("{classname}({vars})" 66 | .format(classname=self.__class__.__name__, 67 | vars=variables)) 68 | 69 | def draw(self): 70 | """ 71 | dot file representation of the CGM. 72 | """ 73 | dot = graphviz.Digraph() 74 | 75 | for node in self.observed_variables: 76 | if node in self.set_nodes: 77 | dot.node(node, node, {"shape": "ellipse", "peripheries": "2"}) 78 | else: 79 | dot.node(node, node, {"shape": "ellipse"}) 80 | 81 | for a, b in self.dag.edges(): 82 | if a in self.observed_variables and b in self.observed_variables: 83 | dot.edge(a, b) 84 | 85 | for n, (a, b) in self.unobserved_variable_edges.items(): 86 | dot.node(n, _attributes={"shape": "point"}) 87 | dot.edge(n, a, _attributes={"style": "dashed"}) 88 | dot.edge(n, b, _attributes={"style": "dashed"}) 89 | 90 | return dot 91 | 92 | def get_distribution(self): 93 | """ 94 | Returns a string representing the factorized distribution implied by 95 | the CGM. 96 | """ 97 | products = [] 98 | for node in nx.topological_sort(self.dag): 99 | if node in self.set_nodes: 100 | continue 101 | 102 | parents = list(self.dag.predecessors(node)) 103 | if not parents: 104 | p = "P({})".format(node) 105 | else: 106 | parents = [ 107 | "do({})".format(n) if n in self.set_nodes else str(n) 108 | for n in parents 109 | ] 110 | p = "P({}|{})".format(node, ",".join(parents)) 111 | products.append(p) 112 | return "".join(products) 113 | 114 | def do(self, node): 115 | """ 116 | Apply intervention on node to CGM 117 | """ 118 | assert node in self.observed_variables 119 | set_nodes = self.set_nodes | frozenset([node]) 120 | nodes = self.observed_variables 121 | edges = [ 122 | (a, b) 123 | for a, b in self.dag.edges() 124 | if b != node 125 | and a in self.observed_variables 126 | and b in self.observed_variables 127 | ] 128 | latent_edges = [ 129 | (a, b) 130 | for a, b in self.latent_edges 131 | if a not in set_nodes 132 | and b not in set_nodes 133 | ] 134 | return CausalGraphicalModel( 135 | nodes=nodes, edges=edges, 136 | latent_edges=latent_edges, set_nodes=set_nodes) 137 | 138 | def _check_d_separation(self, path, zs=None): 139 | """ 140 | Check if a path is d-separated by set of variables zs. 141 | """ 142 | zs = _variable_or_iterable_to_set(zs) 143 | 144 | if len(path) < 3: 145 | return False 146 | 147 | for a, b, c in zip(path[:-2], path[1:-1], path[2:]): 148 | structure = self._classify_three_structure(a, b, c) 149 | 150 | if structure in ("chain", "fork") and b in zs: 151 | return True 152 | 153 | if structure == "collider": 154 | descendants = (nx.descendants(self.dag, b) | {b}) 155 | if not descendants & set(zs): 156 | return True 157 | 158 | return False 159 | 160 | def _classify_three_structure(self, a, b, c): 161 | """ 162 | Classify three structure as a chain, fork or collider. 163 | """ 164 | if self.dag.has_edge(a, b) and self.dag.has_edge(b, c): 165 | return "chain" 166 | 167 | if self.dag.has_edge(c, b) and self.dag.has_edge(b, a): 168 | return "chain" 169 | 170 | if self.dag.has_edge(a, b) and self.dag.has_edge(c, b): 171 | return "collider" 172 | 173 | if self.dag.has_edge(b, a) and self.dag.has_edge(b, c): 174 | return "fork" 175 | 176 | raise ValueError("Unsure how to classify ({},{},{})".format(a, b, c)) 177 | 178 | def is_d_separated(self, x, y, zs=None): 179 | """ 180 | Is x d-separated from y, conditioned on zs? 181 | """ 182 | zs = _variable_or_iterable_to_set(zs) 183 | assert x in self.observed_variables 184 | assert y in self.observed_variables 185 | assert all([z in self.observed_variables for z in zs]) 186 | 187 | paths = nx.all_simple_paths(self.graph, x, y) 188 | return all(self._check_d_separation(path, zs) for path in paths) 189 | 190 | def get_all_independence_relationships(self): 191 | """ 192 | Returns a list of all pairwise conditional independence relationships 193 | implied by the graph structure. 194 | """ 195 | conditional_independences = [] 196 | for x, y in combinations(self.observed_variables, 2): 197 | remaining_variables = set(self.observed_variables) - {x, y} 198 | for cardinality in range(len(remaining_variables) + 1): 199 | for z in combinations(remaining_variables, cardinality): 200 | if self.is_d_separated(x, y, frozenset(z)): 201 | conditional_independences.append((x, y, set(z))) 202 | 203 | return conditional_independences 204 | 205 | def get_all_backdoor_paths(self, x, y): 206 | """ 207 | Get all backdoor paths between x and y 208 | """ 209 | return [ 210 | path 211 | for path in nx.all_simple_paths(self.graph, x, y) 212 | if len(path) > 2 213 | and path[1] in self.dag.predecessors(x) 214 | ] 215 | 216 | def is_valid_backdoor_adjustment_set(self, x, y, z): 217 | """ 218 | Test whether z is a valid backdoor adjustment set for 219 | estimating the causal impact of x on y via the backdoor 220 | adjustment formula: 221 | 222 | P(y|do(x)) = \sum_{z}P(y|x,z)P(z) 223 | 224 | Arguments 225 | --------- 226 | x: str 227 | Intervention Variable 228 | 229 | y: str 230 | Target Variable 231 | 232 | z: str or set[str] 233 | Adjustment variables 234 | 235 | Returns 236 | ------- 237 | is_valid_adjustment_set: bool 238 | """ 239 | z = _variable_or_iterable_to_set(z) 240 | 241 | assert x in self.observed_variables 242 | assert y in self.observed_variables 243 | assert x not in z 244 | assert y not in z 245 | 246 | if any([zz in nx.descendants(self.dag, x) for zz in z]): 247 | return False 248 | 249 | unblocked_backdoor_paths = [ 250 | path 251 | for path in self.get_all_backdoor_paths(x, y) 252 | if not self._check_d_separation(path, z) 253 | ] 254 | 255 | if unblocked_backdoor_paths: 256 | return False 257 | 258 | return True 259 | 260 | def get_all_backdoor_adjustment_sets(self, x, y): 261 | """ 262 | Get all sets of variables which are valid adjustment sets for 263 | estimating the causal impact of x on y via the back door 264 | adjustment formula: 265 | 266 | P(y|do(x)) = \sum_{z}P(y|x,z)P(z) 267 | 268 | Note that the empty set can be a valid adjustment set for some CGMs, 269 | in this case frozenset(frozenset(), ...) is returned. This is different 270 | from the case where there are no valid adjustment sets where the 271 | empty set is returned. 272 | 273 | Arguments 274 | --------- 275 | x: str 276 | Intervention Variable 277 | y: str 278 | Target Variable 279 | 280 | Returns 281 | ------- 282 | condition set: frozenset[frozenset[variables]] 283 | """ 284 | assert x in self.observed_variables 285 | assert y in self.observed_variables 286 | 287 | possible_adjustment_variables = ( 288 | set(self.observed_variables) 289 | - {x} - {y} 290 | - set(nx.descendants(self.dag, x)) 291 | ) 292 | 293 | valid_adjustment_sets = frozenset([ 294 | frozenset(s) 295 | for s in _powerset(possible_adjustment_variables) 296 | if self.is_valid_backdoor_adjustment_set(x, y, s) 297 | ]) 298 | 299 | return valid_adjustment_sets 300 | 301 | def is_valid_frontdoor_adjustment_set(self, x, y, z): 302 | """ 303 | Test whether z is a valid frontdoor adjustment set for 304 | estimating the causal impact of x on y via the frontdoor 305 | adjustment formula: 306 | 307 | P(y|do(x)) = \sum_{z}P(z|x)\sum_{x'}P(y|x',z)P(x') 308 | 309 | Arguments 310 | --------- 311 | x: str 312 | Intervention Variable 313 | 314 | y: str 315 | Target Variable 316 | 317 | z: set 318 | Adjustment variables 319 | 320 | Returns 321 | ------- 322 | is_valid_adjustment_set: bool 323 | """ 324 | z = _variable_or_iterable_to_set(z) 325 | 326 | # 1. does z block all directed paths from x to y? 327 | unblocked_directed_paths = [ 328 | path for path in 329 | nx.all_simple_paths(self.dag, x, y) 330 | if not any(zz in path for zz in z) 331 | ] 332 | 333 | if unblocked_directed_paths: 334 | return False 335 | 336 | # 2. no unblocked backdoor paths between x and z 337 | unblocked_backdoor_paths_x_z = [ 338 | path 339 | for zz in z 340 | for path in self.get_all_backdoor_paths(x, zz) 341 | if not self._check_d_separation(path, z - {zz}) 342 | ] 343 | 344 | if unblocked_backdoor_paths_x_z: 345 | return False 346 | 347 | # 3. x is a valid backdoor adjustment set for z 348 | if not all(self.is_valid_backdoor_adjustment_set(zz, y, x) for zz in z): 349 | return False 350 | 351 | return True 352 | 353 | def get_all_frontdoor_adjustment_sets(self, x, y): 354 | """ 355 | Get all sets of variables which are valid frontdoor adjustment sets for 356 | estimating the causal impact of x on y via the frontdoor adjustment 357 | formula: 358 | 359 | P(y|do(x)) = \sum_{z}P(z|x)\sum_{x'}P(y|x',z)P(x') 360 | 361 | Note that the empty set can be a valid adjustment set for some CGMs, 362 | in this case frozenset(frozenset(), ...) is returned. This is different 363 | from the case where there are no valid adjustment sets where the 364 | empty set is returned. 365 | 366 | Arguments 367 | --------- 368 | x: str 369 | Intervention Variable 370 | y: str 371 | Target Variable 372 | 373 | Returns 374 | ------- 375 | condition set: frozenset[frozenset[variables]] 376 | """ 377 | assert x in self.observed_variables 378 | assert y in self.observed_variables 379 | 380 | possible_adjustment_variables = ( 381 | set(self.observed_variables) 382 | - {x} - {y} 383 | ) 384 | 385 | valid_adjustment_sets = frozenset( 386 | [ 387 | frozenset(s) 388 | for s in _powerset(possible_adjustment_variables) 389 | if self.is_valid_frontdoor_adjustment_set(x, y, s) 390 | ]) 391 | 392 | return valid_adjustment_sets 393 | 394 | 395 | def _variable_or_iterable_to_set(x): 396 | """ 397 | Convert variable or iterable x to a frozenset. 398 | 399 | If x is None, returns the empty set. 400 | 401 | Arguments 402 | --------- 403 | x: None, str or Iterable[str] 404 | 405 | Returns 406 | ------- 407 | x: frozenset[str] 408 | 409 | """ 410 | if x is None: 411 | return frozenset([]) 412 | 413 | if isinstance(x, str): 414 | return frozenset([x]) 415 | 416 | if not isinstance(x, Iterable) or not all(isinstance(xx, str) for xx in x): 417 | raise ValueError( 418 | "{} is expected to be either a string or an iterable of strings" 419 | .format(x)) 420 | 421 | return frozenset(x) 422 | 423 | 424 | def _powerset(iterable): 425 | """ 426 | https://docs.python.org/3/library/itertools.html#recipes 427 | powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3) 428 | """ 429 | s = list(iterable) 430 | return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1)) 431 | -------------------------------------------------------------------------------- /notebooks/cgm-examples.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# An Introduction to `CausalGraphicalModels`\n", 8 | "\n", 9 | "`CausalGraphicalModel` is a python module for describing and manipulating [Causal Graphical Models](https://en.wikipedia.org/wiki/Causal_graph) and [Structural Causal Models](https://en.wikipedia.org/wiki/Structural_equation_modeling). Behind the curtain, it is a light wrapper around the python graph library [networkx](https://networkx.github.io/).\n", 10 | "\n", 11 | "This notebook is designed to give a quick overview of the functionality of this package." 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "# CausalGraphicalModels" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 1, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "from causalgraphicalmodels import CausalGraphicalModel" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": 2, 33 | "metadata": {}, 34 | "outputs": [ 35 | { 36 | "data": { 37 | "image/svg+xml": [ 38 | "\n", 39 | "\n", 41 | "\n", 43 | "\n", 44 | "\n", 46 | "\n", 47 | "%3\n", 48 | "\n", 49 | "\n", 50 | "sprinkler\n", 51 | "\n", 52 | "sprinkler\n", 53 | "\n", 54 | "\n", 55 | "wet\n", 56 | "\n", 57 | "wet\n", 58 | "\n", 59 | "\n", 60 | "sprinkler->wet\n", 61 | "\n", 62 | "\n", 63 | "\n", 64 | "\n", 65 | "rain\n", 66 | "\n", 67 | "rain\n", 68 | "\n", 69 | "\n", 70 | "rain->wet\n", 71 | "\n", 72 | "\n", 73 | "\n", 74 | "\n", 75 | "slippery\n", 76 | "\n", 77 | "slippery\n", 78 | "\n", 79 | "\n", 80 | "wet->slippery\n", 81 | "\n", 82 | "\n", 83 | "\n", 84 | "\n", 85 | "season\n", 86 | "\n", 87 | "season\n", 88 | "\n", 89 | "\n", 90 | "season->sprinkler\n", 91 | "\n", 92 | "\n", 93 | "\n", 94 | "\n", 95 | "season->rain\n", 96 | "\n", 97 | "\n", 98 | "\n", 99 | "\n", 100 | "\n" 101 | ], 102 | "text/plain": [ 103 | "" 104 | ] 105 | }, 106 | "execution_count": 2, 107 | "metadata": {}, 108 | "output_type": "execute_result" 109 | } 110 | ], 111 | "source": [ 112 | "sprinkler = CausalGraphicalModel(\n", 113 | " nodes=[\"season\", \"rain\", \"sprinkler\", \"wet\", \"slippery\"],\n", 114 | " edges=[\n", 115 | " (\"season\", \"rain\"), \n", 116 | " (\"season\", \"sprinkler\"), \n", 117 | " (\"rain\", \"wet\"),\n", 118 | " (\"sprinkler\", \"wet\"), \n", 119 | " (\"wet\", \"slippery\")\n", 120 | " ]\n", 121 | ")\n", 122 | "\n", 123 | "# draw return a graphviz `dot` object, which jupyter can render\n", 124 | "sprinkler.draw()" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": 3, 130 | "metadata": {}, 131 | "outputs": [ 132 | { 133 | "name": "stdout", 134 | "output_type": "stream", 135 | "text": [ 136 | "P(season)P(sprinkler|season)P(rain|season)P(wet|rain,sprinkler)P(slippery|wet)\n" 137 | ] 138 | } 139 | ], 140 | "source": [ 141 | "# get the distribution implied by the graph\n", 142 | "print(sprinkler.get_distribution())" 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": 4, 148 | "metadata": {}, 149 | "outputs": [ 150 | { 151 | "data": { 152 | "text/plain": [ 153 | "True" 154 | ] 155 | }, 156 | "execution_count": 4, 157 | "metadata": {}, 158 | "output_type": "execute_result" 159 | } 160 | ], 161 | "source": [ 162 | "# check for d-seperation of two nodes\n", 163 | "sprinkler.is_d_separated(\"slippery\", \"season\", {\"wet\"})" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": 5, 169 | "metadata": {}, 170 | "outputs": [ 171 | { 172 | "data": { 173 | "text/plain": [ 174 | "[('sprinkler', 'rain', {'season'}),\n", 175 | " ('sprinkler', 'slippery', {'wet'}),\n", 176 | " ('sprinkler', 'slippery', {'rain', 'wet'}),\n", 177 | " ('sprinkler', 'slippery', {'season', 'wet'}),\n", 178 | " ('sprinkler', 'slippery', {'rain', 'season', 'wet'}),\n", 179 | " ('rain', 'slippery', {'wet'}),\n", 180 | " ('rain', 'slippery', {'sprinkler', 'wet'}),\n", 181 | " ('rain', 'slippery', {'season', 'wet'}),\n", 182 | " ('rain', 'slippery', {'season', 'sprinkler', 'wet'}),\n", 183 | " ('wet', 'season', {'rain', 'sprinkler'}),\n", 184 | " ('wet', 'season', {'rain', 'slippery', 'sprinkler'}),\n", 185 | " ('season', 'slippery', {'wet'}),\n", 186 | " ('season', 'slippery', {'rain', 'wet'}),\n", 187 | " ('season', 'slippery', {'sprinkler', 'wet'}),\n", 188 | " ('season', 'slippery', {'rain', 'sprinkler'}),\n", 189 | " ('season', 'slippery', {'rain', 'sprinkler', 'wet'})]" 190 | ] 191 | }, 192 | "execution_count": 5, 193 | "metadata": {}, 194 | "output_type": "execute_result" 195 | } 196 | ], 197 | "source": [ 198 | "# get all the conditional independence relationships implied by a CGM\n", 199 | "sprinkler.get_all_independence_relationships()" 200 | ] 201 | }, 202 | { 203 | "cell_type": "code", 204 | "execution_count": 6, 205 | "metadata": {}, 206 | "outputs": [ 207 | { 208 | "data": { 209 | "text/plain": [ 210 | "False" 211 | ] 212 | }, 213 | "execution_count": 6, 214 | "metadata": {}, 215 | "output_type": "execute_result" 216 | } 217 | ], 218 | "source": [ 219 | "# check backdoor adjustment set\n", 220 | "sprinkler.is_valid_backdoor_adjustment_set(\"rain\", \"slippery\", {\"wet\"})" 221 | ] 222 | }, 223 | { 224 | "cell_type": "code", 225 | "execution_count": 7, 226 | "metadata": {}, 227 | "outputs": [ 228 | { 229 | "data": { 230 | "text/plain": [ 231 | "frozenset({frozenset({'sprinkler'}),\n", 232 | " frozenset({'season'}),\n", 233 | " frozenset({'season', 'sprinkler'})})" 234 | ] 235 | }, 236 | "execution_count": 7, 237 | "metadata": {}, 238 | "output_type": "execute_result" 239 | } 240 | ], 241 | "source": [ 242 | "# get all backdoor adjustment sets\n", 243 | "sprinkler.get_all_backdoor_adjustment_sets(\"rain\", \"slippery\")" 244 | ] 245 | }, 246 | { 247 | "cell_type": "code", 248 | "execution_count": 8, 249 | "metadata": {}, 250 | "outputs": [ 251 | { 252 | "data": { 253 | "image/svg+xml": [ 254 | "\n", 255 | "\n", 257 | "\n", 259 | "\n", 260 | "\n", 262 | "\n", 263 | "%3\n", 264 | "\n", 265 | "\n", 266 | "sprinkler\n", 267 | "\n", 268 | "sprinkler\n", 269 | "\n", 270 | "\n", 271 | "wet\n", 272 | "\n", 273 | "wet\n", 274 | "\n", 275 | "\n", 276 | "sprinkler->wet\n", 277 | "\n", 278 | "\n", 279 | "\n", 280 | "\n", 281 | "rain\n", 282 | "\n", 283 | "\n", 284 | "rain\n", 285 | "\n", 286 | "\n", 287 | "rain->wet\n", 288 | "\n", 289 | "\n", 290 | "\n", 291 | "\n", 292 | "slippery\n", 293 | "\n", 294 | "slippery\n", 295 | "\n", 296 | "\n", 297 | "wet->slippery\n", 298 | "\n", 299 | "\n", 300 | "\n", 301 | "\n", 302 | "season\n", 303 | "\n", 304 | "season\n", 305 | "\n", 306 | "\n", 307 | "season->sprinkler\n", 308 | "\n", 309 | "\n", 310 | "\n", 311 | "\n", 312 | "\n" 313 | ], 314 | "text/plain": [ 315 | "" 316 | ] 317 | }, 318 | "execution_count": 8, 319 | "metadata": {}, 320 | "output_type": "execute_result" 321 | } 322 | ], 323 | "source": [ 324 | "# get the graph created by intervening on node \"rain\"\n", 325 | "do_sprinkler = sprinkler.do(\"rain\")\n", 326 | "\n", 327 | "do_sprinkler.draw()" 328 | ] 329 | }, 330 | { 331 | "cell_type": "markdown", 332 | "metadata": {}, 333 | "source": [ 334 | "# Latent Variables " 335 | ] 336 | }, 337 | { 338 | "cell_type": "code", 339 | "execution_count": 9, 340 | "metadata": {}, 341 | "outputs": [ 342 | { 343 | "data": { 344 | "image/svg+xml": [ 345 | "\n", 346 | "\n", 348 | "\n", 350 | "\n", 351 | "\n", 353 | "\n", 354 | "%3\n", 355 | "\n", 356 | "\n", 357 | "y\n", 358 | "\n", 359 | "y\n", 360 | "\n", 361 | "\n", 362 | "z\n", 363 | "\n", 364 | "z\n", 365 | "\n", 366 | "\n", 367 | "z->y\n", 368 | "\n", 369 | "\n", 370 | "\n", 371 | "\n", 372 | "x\n", 373 | "\n", 374 | "x\n", 375 | "\n", 376 | "\n", 377 | "x->z\n", 378 | "\n", 379 | "\n", 380 | "\n", 381 | "\n", 382 | "Unobserved_0\n", 383 | "\n", 384 | "\n", 385 | "\n", 386 | "Unobserved_0->y\n", 387 | "\n", 388 | "\n", 389 | "\n", 390 | "\n", 391 | "Unobserved_0->x\n", 392 | "\n", 393 | "\n", 394 | "\n", 395 | "\n", 396 | "\n" 397 | ], 398 | "text/plain": [ 399 | "" 400 | ] 401 | }, 402 | "execution_count": 9, 403 | "metadata": {}, 404 | "output_type": "execute_result" 405 | } 406 | ], 407 | "source": [ 408 | "dag_with_latent_variables = CausalGraphicalModel(\n", 409 | " nodes=[\"x\", \"y\", \"z\"],\n", 410 | " edges=[\n", 411 | " (\"x\", \"z\"),\n", 412 | " (\"z\", \"y\"), \n", 413 | " ],\n", 414 | " latent_edges=[\n", 415 | " (\"x\", \"y\")\n", 416 | " ]\n", 417 | ")\n", 418 | "\n", 419 | "dag_with_latent_variables.draw()" 420 | ] 421 | }, 422 | { 423 | "cell_type": "code", 424 | "execution_count": 10, 425 | "metadata": {}, 426 | "outputs": [ 427 | { 428 | "data": { 429 | "text/plain": [ 430 | "frozenset()" 431 | ] 432 | }, 433 | "execution_count": 10, 434 | "metadata": {}, 435 | "output_type": "execute_result" 436 | } 437 | ], 438 | "source": [ 439 | "# here there are no observed backdoor adjustment sets\n", 440 | "dag_with_latent_variables.get_all_backdoor_adjustment_sets(\"x\", \"y\")" 441 | ] 442 | }, 443 | { 444 | "cell_type": "code", 445 | "execution_count": 11, 446 | "metadata": {}, 447 | "outputs": [ 448 | { 449 | "data": { 450 | "text/plain": [ 451 | "frozenset({frozenset({'z'})})" 452 | ] 453 | }, 454 | "execution_count": 11, 455 | "metadata": {}, 456 | "output_type": "execute_result" 457 | } 458 | ], 459 | "source": [ 460 | "# but there is a frontdoor adjustment set\n", 461 | "dag_with_latent_variables.get_all_frontdoor_adjustment_sets(\"x\", \"y\")" 462 | ] 463 | }, 464 | { 465 | "cell_type": "markdown", 466 | "metadata": {}, 467 | "source": [ 468 | "# StructuralCausalModels\n", 469 | "\n", 470 | "For Structural Causal Models (SCM) we need to specify the functional form of each node:" 471 | ] 472 | }, 473 | { 474 | "cell_type": "code", 475 | "execution_count": 12, 476 | "metadata": {}, 477 | "outputs": [], 478 | "source": [ 479 | "from causalgraphicalmodels import StructuralCausalModel\n", 480 | "import numpy as np\n", 481 | "\n", 482 | "scm = StructuralCausalModel({\n", 483 | " \"x1\": lambda n_samples: np.random.binomial(n=1,p=0.7,size=n_samples),\n", 484 | " \"x2\": lambda x1, n_samples: np.random.normal(loc=x1, scale=0.1),\n", 485 | " \"x3\": lambda x2, n_samples: x2 ** 2,\n", 486 | "})" 487 | ] 488 | }, 489 | { 490 | "cell_type": "markdown", 491 | "metadata": {}, 492 | "source": [ 493 | "The only requirement on the functions are:\n", 494 | " - that variable names are consistent \n", 495 | " - each function accepts keyword variables in the form of `numpy` arrays and output numpy arrays of shape [n_samples] \n", 496 | " - that in addition to it's parents, each function takes a `n_samples` variables indicating how many samples to generate \n", 497 | " - that any function acts on each row independently. This ensure that the output samples are independent\n", 498 | " \n", 499 | "Wrapping these functions in the `StructuralCausalModel` object allows us to easily generate samples: " 500 | ] 501 | }, 502 | { 503 | "cell_type": "code", 504 | "execution_count": 13, 505 | "metadata": {}, 506 | "outputs": [ 507 | { 508 | "data": { 509 | "text/html": [ 510 | "
\n", 511 | "\n", 524 | "\n", 525 | " \n", 526 | " \n", 527 | " \n", 528 | " \n", 529 | " \n", 530 | " \n", 531 | " \n", 532 | " \n", 533 | " \n", 534 | " \n", 535 | " \n", 536 | " \n", 537 | " \n", 538 | " \n", 539 | " \n", 540 | " \n", 541 | " \n", 542 | " \n", 543 | " \n", 544 | " \n", 545 | " \n", 546 | " \n", 547 | " \n", 548 | " \n", 549 | " \n", 550 | " \n", 551 | " \n", 552 | " \n", 553 | " \n", 554 | " \n", 555 | " \n", 556 | " \n", 557 | " \n", 558 | " \n", 559 | " \n", 560 | " \n", 561 | " \n", 562 | " \n", 563 | " \n", 564 | " \n", 565 | "
x1x2x3
011.0040201.008056
110.9020450.813686
211.1503231.323242
310.9846340.969504
411.0231181.046770
\n", 566 | "
" 567 | ], 568 | "text/plain": [ 569 | " x1 x2 x3\n", 570 | "0 1 1.004020 1.008056\n", 571 | "1 1 0.902045 0.813686\n", 572 | "2 1 1.150323 1.323242\n", 573 | "3 1 0.984634 0.969504\n", 574 | "4 1 1.023118 1.046770" 575 | ] 576 | }, 577 | "execution_count": 13, 578 | "metadata": {}, 579 | "output_type": "execute_result" 580 | } 581 | ], 582 | "source": [ 583 | "ds = scm.sample(n_samples=100)\n", 584 | "\n", 585 | "ds.head()" 586 | ] 587 | }, 588 | { 589 | "cell_type": "code", 590 | "execution_count": 14, 591 | "metadata": {}, 592 | "outputs": [ 593 | { 594 | "data": { 595 | "text/plain": [ 596 | "" 597 | ] 598 | }, 599 | "execution_count": 14, 600 | "metadata": {}, 601 | "output_type": "execute_result" 602 | }, 603 | { 604 | "data": { 605 | "image/png": "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\nbdBYatDXd4Tr9XrKKysoqyintLyU/KJCsvNyKSj+3waN1lZWhAR1oGdYd/r2CKdPt164OF3dm7te\nr2fr1q0sXryYtLQ0wsPDee655+jevftVnU9R2goVGsplqqurWblyJUuXLqWsrIzbb7+dhx56iIiI\niGtuGeQXFXD67BkycjJJz8ki61w2hSXFFJcWU1RaTE1twyvmWlpY4OzojIuTM+6ubvh7++Lv7UeA\njx+hwSEEBwRdcz9KZWUlGzdu5LvvviMrK4tOnTrx+OOPM3z48GabnKgoNzMVGkqDysvLWbFiBT/+\n+CNFRUWEhIQwdepURo8ejaOj6cl9V0NKicFgQFdbQ01NDbX6WjQaDRrLui87W9sb8sYtpSQhIYFf\nfvmFTZs2odVq6d69O3/6058YOnRok/p3FKWtU6GhNEqn07FlyxaWL19OcnIyNjY2DB8+nAkTJtCv\n380zSqqpMjMz2bx5M5s3byY1NRUbGxvGjBnDvffeS48ePVTLQlGuQIWGYjYpJSdPnmTdunVs2bKF\n8vJynJycGDRoEEOGDGHAgAG4ujZtVFJzqq2tJTY2lgMHDnDw4EESEupGVYWHhzNu3DjGjRt3Q1pQ\nitKWqNBQropOp2P//v389ttv7N27l+LiulFSnTt3pl+/fvTr14/u3bvj6enZYnWsrq4mMTGRo0eP\nEh0dzYkTJ9BqtVhaWtKjRw+GDBnC2LFj1cQ8RWkCFRrKNTMYDMTHx3P48GGOHj3K8ePH0enq9g73\n9vamS5cuhISE0L59e4KCgggICMDNze269BVIKamoqCArK4vMzEwyMzM5ffo0SUlJpKWlYTTWbSsb\nEhJCnz59iIiIoH///qpFoShXSYWGct3pdDoSEhKIj48nISGBhIQEMjIyMBgMF8pYWlri6emJh4cH\nLi4uODs74+joiKOjI9bW1mg0GiwtLTEYDOj1dUNuq6qqKC8vp7y8nNLSUgoLCyksLKS6uvp3r+/j\n40NoaCihoaF07dqV8PDwm/q2maK0Jio0lGah1+vJzs4mPT2drKwsCgoKKCgoID8/n7KyMsrLy6mo\nqKCiooLaKwy7tbCwwMHBAScnJxwdHXFxccHDwwMPDw88PT3x9/cnMDCQgIAA1YpQlBvI3NBoncNj\nlJuGRqMhKCiIoKDGZ2VLKdHr9RgMBjQaDRYWFmrYq6K0Mio0lGYjhMDKygorq+u7UZOiKM1HfcxT\nFEVRzKZCQ1EURTGbCg1FURTFbC0SGkIIdyHEViFEcv2fbg2UMwghjtV/rWnueiqKoii/11ItjZeB\n7VLKzsD2+u+vRCulDK//mth81VMURVGupKVCYxLwTf3jb4B7WqgeiqIoShO0VGj4SClz6h/nAj4N\nlLMVQkQJIQ4KIVSwKIqitLAbNk9DCLEN8L3CU69d/I2UUgohGpqW3l5KmSWE6AjsEELESilTrvBa\njwOPA2ZNMlMURVGuzg0LDSnlqIaeE0KcE0L4SSlzhBB+QF4D58iq/zNVCLEL6A1cFhpSyq+Br6Fu\nGZHrUH1FURTlClrq9tQa4JH6x48Aqy8tIIRwE0LY1D/2BG4H4puthoqiKMplWio05gOjhRDJwKj6\n7xFC9BNCLKov0xWIEkIcB3YC86WUKjQURVFaUIusPSWlLARGXuHnUcCs+sf7gduauWqKoiiKCWpG\nuKIoimI2FRqKoiiK2VRoKIqiKGZToaEoiqKYTYWGoiiKYrY2t0e4ECIfONvS9TCTJ1DQ0pVoZuqa\nbx234nW35mtuL6X0aqxQmwuN1kQIEWXORu5tibrmW8eteN23wjWr21OKoiiK2VRoKIqiKGZTodGy\nvm7pCrQAdc23jlvxutv8Nas+DUVRFMVsqqWhKIqimE2FRjMSQrgLIbYKIZLr/3RroJxBCHGs/mtN\nc9fzehBCjBNCnBJCnBZCXLYHvBDCRgjxQ/3zh4QQwc1fy+vLjGt+VAiRf9Hf7ayWqOf1JIRYIoTI\nE0KcbOB5IYT4tP53ckII0ae563i9mXHNdwghSi/6e/57c9fxRlKh0bxeBrZLKTsD2+u/vxKtlDK8\n/mti81Xv+hBCWAKfA+OBbsCDQohulxSbCRRLKTsBnwDvN28try8zrxngh4v+bhdd4fnWZikwzsTz\n44HO9V+PA182Q51utKWYvmaAPRf9Pb/VDHVqNio0mtck4Jv6x98AbXXf8wjgtJQyVUpZA6yg7tov\ndvHvYhUwUgghmrGO15s519zmSCl/A4pMFJkEfCvrHARc63frbLXMuOY2TYVG8/KRUubUP84FfBoo\nZyuEiBJCHBRCtMZgCQAyLvo+s/5nVywjpdQDpYBHs9TuxjDnmgHuq79Ns0oI0a55qtaizP29tDUD\nhRDHhRAbhRDdW7oy11OLbMLUlgkhtgG+V3jqtYu/kVJKIURDQ9faSymzhBAdgR1CiFgp5WV7oyut\nzlrgeymlTgjxZ+paWiNauE7K9RdN3f/hCiHEncCv1N2eaxNUaFxnUspRDT0nhDgnhPCTUubUN9Hz\nGjhHVv2fqUKIXUBvoDWFRhZw8afowPqfXalMphBCA7gAhc1TvRui0Wuu37HyvEXAB81Qr5Zmzr+F\nNkVKWXbR4w1CiC+EEJ5Syta6JtXvqNtTzWsN8Ej940eA1ZcWEEK4CSFs6h97ArcDrW1v9CNAZyFE\nByGENTCVumu/2MW/iynADtm6Jw01es2X3MufCCQ0Y/1ayhrgj/WjqAYApRfdom2ThBC+5/vnhBAR\n1L3PtuYPRL+jWhrNaz6wUggxk7qVeB8AEEL0A56QUs4CugJfCSGM1P1jmy+lbFWhIaXUCyGeBDYD\nlsASKWWcEOItIEpKuQZYDHwnhDhNXafi1Jar8bUz85qfFkJMBPTUXfOjLVbh60QI8T1wB+AphMgE\n3gCsAKSU/wY2AHcCp4Eq4E8tU9Prx4xrngLMFkLoAS0wtZV/IPodNSNcURRFMZu6PaUoiqKYTYWG\noiiKYjYVGoqiKIrZVGgoiqIoZlOhoSiKophNhYaiNBMhRLgQ4oAQIq5+KZE/tHSdFKWp1JBbRWkm\nQohQ6laQSRZC+ANHga5SypIWrpqimE21NBTlBhBC9K9vTdgKIRyEEHGAtZQyGUBKmU3dMjJeLVpR\nRWki1dJQlBtECPEOYAvYAZlSyvcuei6CugULu0spjS1URUVpMhUainKD1K9BdQSoBgZJKQ31P/cD\ndgGP1O8xoSithro9pSg3jgfgCDhR1+JACOEMrAdeU4GhtEaqpaEoN0j9/u4rgA6AH/A8sBFYK6Vc\n0JJ1U5SrpVa5VZQbQAjxR6BWSrm8fv/w/dSt5DsU8BBCPFpf9FEp5bEWqqaiNJlqaSiKoihmU30a\niqIoitlUaCiKoihmU6GhKIqimE2FhqIoimI2FRqKoiiK2VRoKIqiKGZToaEoiqKYTYWGoiiKYrb/\nB4Zpnck22NlhAAAAAElFTkSuQmCC\n", 606 | "text/plain": [ 607 | "" 608 | ] 609 | }, 610 | "metadata": {}, 611 | "output_type": "display_data" 612 | } 613 | ], 614 | "source": [ 615 | "# and visualise the samples\n", 616 | "import seaborn as sns\n", 617 | "\n", 618 | "%matplotlib inline\n", 619 | "\n", 620 | "sns.kdeplot(\n", 621 | " data=ds.x2,\n", 622 | " data2=ds.x3,\n", 623 | ")" 624 | ] 625 | }, 626 | { 627 | "cell_type": "markdown", 628 | "metadata": {}, 629 | "source": [ 630 | "And to access the implied CGM\"" 631 | ] 632 | }, 633 | { 634 | "cell_type": "code", 635 | "execution_count": 15, 636 | "metadata": {}, 637 | "outputs": [ 638 | { 639 | "data": { 640 | "image/svg+xml": [ 641 | "\n", 642 | "\n", 644 | "\n", 646 | "\n", 647 | "\n", 649 | "\n", 650 | "%3\n", 651 | "\n", 652 | "\n", 653 | "x1\n", 654 | "\n", 655 | "x1\n", 656 | "\n", 657 | "\n", 658 | "x2\n", 659 | "\n", 660 | "x2\n", 661 | "\n", 662 | "\n", 663 | "x1->x2\n", 664 | "\n", 665 | "\n", 666 | "\n", 667 | "\n", 668 | "x3\n", 669 | "\n", 670 | "x3\n", 671 | "\n", 672 | "\n", 673 | "x2->x3\n", 674 | "\n", 675 | "\n", 676 | "\n", 677 | "\n", 678 | "\n" 679 | ], 680 | "text/plain": [ 681 | "" 682 | ] 683 | }, 684 | "execution_count": 15, 685 | "metadata": {}, 686 | "output_type": "execute_result" 687 | } 688 | ], 689 | "source": [ 690 | "scm.cgm.draw()" 691 | ] 692 | }, 693 | { 694 | "cell_type": "markdown", 695 | "metadata": {}, 696 | "source": [ 697 | "And to apply an intervention:" 698 | ] 699 | }, 700 | { 701 | "cell_type": "code", 702 | "execution_count": 16, 703 | "metadata": {}, 704 | "outputs": [ 705 | { 706 | "data": { 707 | "image/svg+xml": [ 708 | "\n", 709 | "\n", 711 | "\n", 713 | "\n", 714 | "\n", 716 | "\n", 717 | "%3\n", 718 | "\n", 719 | "\n", 720 | "x1\n", 721 | "\n", 722 | "\n", 723 | "x1\n", 724 | "\n", 725 | "\n", 726 | "x2\n", 727 | "\n", 728 | "x2\n", 729 | "\n", 730 | "\n", 731 | "x1->x2\n", 732 | "\n", 733 | "\n", 734 | "\n", 735 | "\n", 736 | "x3\n", 737 | "\n", 738 | "x3\n", 739 | "\n", 740 | "\n", 741 | "x2->x3\n", 742 | "\n", 743 | "\n", 744 | "\n", 745 | "\n", 746 | "\n" 747 | ], 748 | "text/plain": [ 749 | "" 750 | ] 751 | }, 752 | "execution_count": 16, 753 | "metadata": {}, 754 | "output_type": "execute_result" 755 | } 756 | ], 757 | "source": [ 758 | "scm_do = scm.do(\"x1\")\n", 759 | "\n", 760 | "scm_do.cgm.draw()" 761 | ] 762 | }, 763 | { 764 | "cell_type": "markdown", 765 | "metadata": {}, 766 | "source": [ 767 | "And sample from the distribution implied by this intervention:" 768 | ] 769 | }, 770 | { 771 | "cell_type": "code", 772 | "execution_count": 17, 773 | "metadata": {}, 774 | "outputs": [ 775 | { 776 | "data": { 777 | "text/html": [ 778 | "
\n", 779 | "\n", 792 | "\n", 793 | " \n", 794 | " \n", 795 | " \n", 796 | " \n", 797 | " \n", 798 | " \n", 799 | " \n", 800 | " \n", 801 | " \n", 802 | " \n", 803 | " \n", 804 | " \n", 805 | " \n", 806 | " \n", 807 | " \n", 808 | " \n", 809 | " \n", 810 | " \n", 811 | " \n", 812 | " \n", 813 | " \n", 814 | " \n", 815 | " \n", 816 | " \n", 817 | " \n", 818 | " \n", 819 | " \n", 820 | " \n", 821 | " \n", 822 | " \n", 823 | " \n", 824 | " \n", 825 | " \n", 826 | " \n", 827 | " \n", 828 | " \n", 829 | " \n", 830 | " \n", 831 | " \n", 832 | " \n", 833 | "
x1x2x3
000.2587360.066944
111.1425291.305371
221.9276653.715894
332.8789778.288507
444.09825016.795653
\n", 834 | "
" 835 | ], 836 | "text/plain": [ 837 | " x1 x2 x3\n", 838 | "0 0 0.258736 0.066944\n", 839 | "1 1 1.142529 1.305371\n", 840 | "2 2 1.927665 3.715894\n", 841 | "3 3 2.878977 8.288507\n", 842 | "4 4 4.098250 16.795653" 843 | ] 844 | }, 845 | "execution_count": 17, 846 | "metadata": {}, 847 | "output_type": "execute_result" 848 | } 849 | ], 850 | "source": [ 851 | "scm_do.sample(n_samples=5, set_values={\"x1\": np.arange(5)})" 852 | ] 853 | } 854 | ], 855 | "metadata": { 856 | "kernelspec": { 857 | "display_name": "Python 3", 858 | "language": "python", 859 | "name": "python3" 860 | }, 861 | "language_info": { 862 | "codemirror_mode": { 863 | "name": "ipython", 864 | "version": 3 865 | }, 866 | "file_extension": ".py", 867 | "mimetype": "text/x-python", 868 | "name": "python", 869 | "nbconvert_exporter": "python", 870 | "pygments_lexer": "ipython3", 871 | "version": "3.6.3" 872 | } 873 | }, 874 | "nbformat": 4, 875 | "nbformat_minor": 2 876 | } 877 | --------------------------------------------------------------------------------