pyAgrum-nightly 2.3.0.9.dev202512061764412981__cp310-abi3-macosx_11_0_arm64.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.
- pyagrum/__init__.py +165 -0
- pyagrum/_pyagrum.so +0 -0
- pyagrum/bnmixture/BNMInference.py +268 -0
- pyagrum/bnmixture/BNMLearning.py +376 -0
- pyagrum/bnmixture/BNMixture.py +464 -0
- pyagrum/bnmixture/__init__.py +60 -0
- pyagrum/bnmixture/notebook.py +1058 -0
- pyagrum/causal/_CausalFormula.py +280 -0
- pyagrum/causal/_CausalModel.py +436 -0
- pyagrum/causal/__init__.py +81 -0
- pyagrum/causal/_causalImpact.py +356 -0
- pyagrum/causal/_dSeparation.py +598 -0
- pyagrum/causal/_doAST.py +761 -0
- pyagrum/causal/_doCalculus.py +361 -0
- pyagrum/causal/_doorCriteria.py +374 -0
- pyagrum/causal/_exceptions.py +95 -0
- pyagrum/causal/_types.py +61 -0
- pyagrum/causal/causalEffectEstimation/_CausalEffectEstimation.py +1175 -0
- pyagrum/causal/causalEffectEstimation/_IVEstimators.py +718 -0
- pyagrum/causal/causalEffectEstimation/_RCTEstimators.py +132 -0
- pyagrum/causal/causalEffectEstimation/__init__.py +46 -0
- pyagrum/causal/causalEffectEstimation/_backdoorEstimators.py +774 -0
- pyagrum/causal/causalEffectEstimation/_causalBNEstimator.py +324 -0
- pyagrum/causal/causalEffectEstimation/_frontdoorEstimators.py +396 -0
- pyagrum/causal/causalEffectEstimation/_learners.py +118 -0
- pyagrum/causal/causalEffectEstimation/_utils.py +466 -0
- pyagrum/causal/notebook.py +171 -0
- pyagrum/clg/CLG.py +658 -0
- pyagrum/clg/GaussianVariable.py +111 -0
- pyagrum/clg/SEM.py +312 -0
- pyagrum/clg/__init__.py +63 -0
- pyagrum/clg/canonicalForm.py +408 -0
- pyagrum/clg/constants.py +54 -0
- pyagrum/clg/forwardSampling.py +202 -0
- pyagrum/clg/learning.py +776 -0
- pyagrum/clg/notebook.py +480 -0
- pyagrum/clg/variableElimination.py +271 -0
- pyagrum/common.py +60 -0
- pyagrum/config.py +319 -0
- pyagrum/ctbn/CIM.py +513 -0
- pyagrum/ctbn/CTBN.py +573 -0
- pyagrum/ctbn/CTBNGenerator.py +216 -0
- pyagrum/ctbn/CTBNInference.py +459 -0
- pyagrum/ctbn/CTBNLearner.py +161 -0
- pyagrum/ctbn/SamplesStats.py +671 -0
- pyagrum/ctbn/StatsIndepTest.py +355 -0
- pyagrum/ctbn/__init__.py +79 -0
- pyagrum/ctbn/constants.py +54 -0
- pyagrum/ctbn/notebook.py +264 -0
- pyagrum/defaults.ini +199 -0
- pyagrum/deprecated.py +95 -0
- pyagrum/explain/_ComputationCausal.py +75 -0
- pyagrum/explain/_ComputationConditional.py +48 -0
- pyagrum/explain/_ComputationMarginal.py +48 -0
- pyagrum/explain/_CustomShapleyCache.py +110 -0
- pyagrum/explain/_Explainer.py +176 -0
- pyagrum/explain/_Explanation.py +70 -0
- pyagrum/explain/_FIFOCache.py +54 -0
- pyagrum/explain/_ShallCausalValues.py +204 -0
- pyagrum/explain/_ShallConditionalValues.py +155 -0
- pyagrum/explain/_ShallMarginalValues.py +155 -0
- pyagrum/explain/_ShallValues.py +296 -0
- pyagrum/explain/_ShapCausalValues.py +208 -0
- pyagrum/explain/_ShapConditionalValues.py +126 -0
- pyagrum/explain/_ShapMarginalValues.py +191 -0
- pyagrum/explain/_ShapleyValues.py +298 -0
- pyagrum/explain/__init__.py +81 -0
- pyagrum/explain/_explGeneralizedMarkovBlanket.py +152 -0
- pyagrum/explain/_explIndependenceListForPairs.py +146 -0
- pyagrum/explain/_explInformationGraph.py +264 -0
- pyagrum/explain/notebook/__init__.py +54 -0
- pyagrum/explain/notebook/_bar.py +142 -0
- pyagrum/explain/notebook/_beeswarm.py +174 -0
- pyagrum/explain/notebook/_showShapValues.py +97 -0
- pyagrum/explain/notebook/_waterfall.py +220 -0
- pyagrum/explain/shapley.py +225 -0
- pyagrum/lib/__init__.py +46 -0
- pyagrum/lib/_colors.py +390 -0
- pyagrum/lib/bn2graph.py +299 -0
- pyagrum/lib/bn2roc.py +1026 -0
- pyagrum/lib/bn2scores.py +217 -0
- pyagrum/lib/bn_vs_bn.py +605 -0
- pyagrum/lib/cn2graph.py +305 -0
- pyagrum/lib/discreteTypeProcessor.py +1102 -0
- pyagrum/lib/discretizer.py +58 -0
- pyagrum/lib/dynamicBN.py +390 -0
- pyagrum/lib/explain.py +57 -0
- pyagrum/lib/export.py +84 -0
- pyagrum/lib/id2graph.py +258 -0
- pyagrum/lib/image.py +387 -0
- pyagrum/lib/ipython.py +307 -0
- pyagrum/lib/mrf2graph.py +471 -0
- pyagrum/lib/notebook.py +1821 -0
- pyagrum/lib/proba_histogram.py +552 -0
- pyagrum/lib/utils.py +138 -0
- pyagrum/pyagrum.py +31495 -0
- pyagrum/skbn/_MBCalcul.py +242 -0
- pyagrum/skbn/__init__.py +49 -0
- pyagrum/skbn/_learningMethods.py +282 -0
- pyagrum/skbn/_utils.py +297 -0
- pyagrum/skbn/bnclassifier.py +1014 -0
- pyagrum_nightly-2.3.0.9.dev202512061764412981.dist-info/LICENSE.md +12 -0
- pyagrum_nightly-2.3.0.9.dev202512061764412981.dist-info/LICENSES/LGPL-3.0-or-later.txt +304 -0
- pyagrum_nightly-2.3.0.9.dev202512061764412981.dist-info/LICENSES/MIT.txt +18 -0
- pyagrum_nightly-2.3.0.9.dev202512061764412981.dist-info/METADATA +145 -0
- pyagrum_nightly-2.3.0.9.dev202512061764412981.dist-info/RECORD +107 -0
- pyagrum_nightly-2.3.0.9.dev202512061764412981.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
############################################################################
|
|
2
|
+
# This file is part of the aGrUM/pyAgrum library. #
|
|
3
|
+
# #
|
|
4
|
+
# Copyright (c) 2005-2025 by #
|
|
5
|
+
# - Pierre-Henri WUILLEMIN(_at_LIP6) #
|
|
6
|
+
# - Christophe GONZALES(_at_AMU) #
|
|
7
|
+
# #
|
|
8
|
+
# The aGrUM/pyAgrum library is free software; you can redistribute it #
|
|
9
|
+
# and/or modify it under the terms of either : #
|
|
10
|
+
# #
|
|
11
|
+
# - the GNU Lesser General Public License as published by #
|
|
12
|
+
# the Free Software Foundation, either version 3 of the License, #
|
|
13
|
+
# or (at your option) any later version, #
|
|
14
|
+
# - the MIT license (MIT), #
|
|
15
|
+
# - or both in dual license, as here. #
|
|
16
|
+
# #
|
|
17
|
+
# (see https://agrum.gitlab.io/articles/dual-licenses-lgplv3mit.html) #
|
|
18
|
+
# #
|
|
19
|
+
# This aGrUM/pyAgrum library is distributed in the hope that it will be #
|
|
20
|
+
# useful, but WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, #
|
|
21
|
+
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES MERCHANTABILITY or FITNESS #
|
|
22
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #
|
|
23
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
|
|
24
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, #
|
|
25
|
+
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR #
|
|
26
|
+
# OTHER DEALINGS IN THE SOFTWARE. #
|
|
27
|
+
# #
|
|
28
|
+
# See LICENCES for more details. #
|
|
29
|
+
# #
|
|
30
|
+
# SPDX-FileCopyrightText: Copyright 2005-2025 #
|
|
31
|
+
# - Pierre-Henri WUILLEMIN(_at_LIP6) #
|
|
32
|
+
# - Christophe GONZALES(_at_AMU) #
|
|
33
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later OR MIT #
|
|
34
|
+
# #
|
|
35
|
+
# Contact : info_at_agrum_dot_org #
|
|
36
|
+
# homepage : http://agrum.gitlab.io #
|
|
37
|
+
# gitlab : https://gitlab.com/agrumery/agrum #
|
|
38
|
+
# #
|
|
39
|
+
############################################################################
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
This file defines a representation of a causal query in a causal model
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from collections import defaultdict
|
|
46
|
+
from typing import Union, Optional, Dict
|
|
47
|
+
|
|
48
|
+
import pyagrum
|
|
49
|
+
|
|
50
|
+
from pyagrum.causal._types import NameSet
|
|
51
|
+
from pyagrum.causal._doAST import ASTtree
|
|
52
|
+
|
|
53
|
+
# pylint: disable=unused-import
|
|
54
|
+
import pyagrum.causal # for annotations
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CausalFormula:
|
|
58
|
+
"""
|
|
59
|
+
Represents a causal query in a causal model. The query is encoded as an CausalFormula that can be evaluated in the
|
|
60
|
+
causal model : $P(on|knowing, \\overhook (doing))$
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
cm : CausalModel
|
|
65
|
+
the causal model
|
|
66
|
+
root : ASTtree
|
|
67
|
+
the syntax tree
|
|
68
|
+
on : str|Set[str]
|
|
69
|
+
the variable or the set of variables of interest
|
|
70
|
+
doing : str|Set[str]
|
|
71
|
+
the intervention variable(s)
|
|
72
|
+
knowing: None|str|Set[str]
|
|
73
|
+
the observation variable(s)
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
cm: "pyagrum.causal.CausalModel",
|
|
79
|
+
root: ASTtree,
|
|
80
|
+
on: Union[str, NameSet],
|
|
81
|
+
doing: Union[str, NameSet],
|
|
82
|
+
knowing: Optional[NameSet] = None,
|
|
83
|
+
):
|
|
84
|
+
"""
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
cm : CausalModel
|
|
88
|
+
the causal model
|
|
89
|
+
root : ASTtree
|
|
90
|
+
the syntax tree
|
|
91
|
+
on : str|Set[str]
|
|
92
|
+
the variable or the set of variables of interest
|
|
93
|
+
doing : str|Set[str]
|
|
94
|
+
the intervention variable(s)
|
|
95
|
+
knowing: None|str|Set[str]
|
|
96
|
+
the observation variable(s)
|
|
97
|
+
"""
|
|
98
|
+
self._cm = cm
|
|
99
|
+
self._root = root
|
|
100
|
+
|
|
101
|
+
if isinstance(on, str):
|
|
102
|
+
self._on = {on}
|
|
103
|
+
else:
|
|
104
|
+
self._on = on
|
|
105
|
+
|
|
106
|
+
if isinstance(doing, str):
|
|
107
|
+
self._doing = {doing}
|
|
108
|
+
else:
|
|
109
|
+
self._doing = doing
|
|
110
|
+
|
|
111
|
+
if knowing is None:
|
|
112
|
+
self._knowing = set()
|
|
113
|
+
elif isinstance(knowing, str):
|
|
114
|
+
self._knowing = {knowing}
|
|
115
|
+
else:
|
|
116
|
+
self._knowing = knowing
|
|
117
|
+
|
|
118
|
+
def _setDoing(self, doing: Union[str, NameSet]):
|
|
119
|
+
if isinstance(doing, str):
|
|
120
|
+
self._doing = {doing}
|
|
121
|
+
else:
|
|
122
|
+
self._doing = doing
|
|
123
|
+
|
|
124
|
+
def _setKnowing(self, knowing: Union[str, NameSet]):
|
|
125
|
+
if isinstance(knowing, str):
|
|
126
|
+
self._knowing = {knowing}
|
|
127
|
+
else:
|
|
128
|
+
self._knowing = knowing
|
|
129
|
+
|
|
130
|
+
def __str__(self, prefix: str = "") -> str:
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
prefix :
|
|
136
|
+
a prefix for each line of the string representation
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
str
|
|
141
|
+
the string version of the CausalFormula
|
|
142
|
+
"""
|
|
143
|
+
return self.root.__str__(prefix)
|
|
144
|
+
|
|
145
|
+
def latexQuery(self, values: Optional[Dict[str, str]] = None) -> str:
|
|
146
|
+
"""
|
|
147
|
+
Returns a string representing the query compiled by this Formula. If values, the query is annotated with the
|
|
148
|
+
values in the dictionary.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
values : None|Dict[str,str]
|
|
153
|
+
the values to add in the query representation
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
str
|
|
158
|
+
the LaTeX representation of the causal query for this CausalFormula
|
|
159
|
+
"""
|
|
160
|
+
if values is None:
|
|
161
|
+
values = {}
|
|
162
|
+
|
|
163
|
+
def _getVarRepresentation(v: str) -> str:
|
|
164
|
+
if v not in values:
|
|
165
|
+
return v
|
|
166
|
+
|
|
167
|
+
bn = self.cm.observationalBN()
|
|
168
|
+
label = bn.variable(self.cm.idFromName(v)).label(_getLabelIdx(bn, v, values[v]))
|
|
169
|
+
return v + "=" + label
|
|
170
|
+
|
|
171
|
+
# adding values when necessary
|
|
172
|
+
on = [_getVarRepresentation(k) for k in self._on]
|
|
173
|
+
doing = [_getVarRepresentation(k) for k in self._doing]
|
|
174
|
+
knowing = [_getVarRepresentation(k) for k in self._knowing]
|
|
175
|
+
|
|
176
|
+
latexOn = ",".join(on)
|
|
177
|
+
|
|
178
|
+
doOpPref = pyagrum.config["causal", "latex_do_prefix"]
|
|
179
|
+
doOpSuff = pyagrum.config["causal", "latex_do_suffix"]
|
|
180
|
+
latexDo = ""
|
|
181
|
+
if len(doing) > 0:
|
|
182
|
+
latexDo = ",".join([doOpPref + d + doOpSuff for d in doing])
|
|
183
|
+
|
|
184
|
+
latexKnw = ""
|
|
185
|
+
if len(knowing) > 0:
|
|
186
|
+
if latexDo != "":
|
|
187
|
+
latexKnw = ", "
|
|
188
|
+
latexKnw += ",".join(knowing)
|
|
189
|
+
|
|
190
|
+
return "P( " + latexOn + " \\mid " + latexDo + latexKnw + ")"
|
|
191
|
+
|
|
192
|
+
def toLatex(self) -> str:
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
Returns
|
|
196
|
+
-------
|
|
197
|
+
str
|
|
198
|
+
a LaTeX representation of the CausalFormula
|
|
199
|
+
"""
|
|
200
|
+
occur = defaultdict(int)
|
|
201
|
+
for n in self._cm.observationalBN().nodes():
|
|
202
|
+
occur[self._cm.observationalBN().variable(n).name()] = 0
|
|
203
|
+
for n in self._doing:
|
|
204
|
+
occur[n] = 1
|
|
205
|
+
for n in self._knowing:
|
|
206
|
+
occur[n] = 1
|
|
207
|
+
for n in self._on:
|
|
208
|
+
occur[n] = 1
|
|
209
|
+
|
|
210
|
+
return self.latexQuery() + " = " + self._root.toLatex(occur)
|
|
211
|
+
|
|
212
|
+
def copy(self) -> "CausalFormula":
|
|
213
|
+
"""
|
|
214
|
+
Copy theAST. Note that the causal model is just referenced. The tree is copied.
|
|
215
|
+
|
|
216
|
+
Returns
|
|
217
|
+
-------
|
|
218
|
+
CausalFormula
|
|
219
|
+
the copu
|
|
220
|
+
"""
|
|
221
|
+
return CausalFormula(self.cm, self.root.copy(), self._on, self._doing, self._knowing)
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def cm(self) -> "pyagrum.causal.CausalModel":
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
Returns
|
|
228
|
+
-------
|
|
229
|
+
CausalModel
|
|
230
|
+
the causal model
|
|
231
|
+
"""
|
|
232
|
+
return self._cm
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def root(self) -> ASTtree:
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
ASTtree
|
|
241
|
+
the causalFormula as an ASTtree
|
|
242
|
+
"""
|
|
243
|
+
return self._root
|
|
244
|
+
|
|
245
|
+
def eval(self) -> "pyagrum.Tensor":
|
|
246
|
+
"""
|
|
247
|
+
Compute the Tensor from the CausalFormula over vars using cond as value for others variables
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
pyagrum.Tensor
|
|
252
|
+
The resulting distribution
|
|
253
|
+
"""
|
|
254
|
+
return self.root.eval(self.cm.observationalBN())
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _getLabelIdx(bn: "pyagrum.BayesNet", varname: str, val: Union[int, str]) -> int:
|
|
258
|
+
"""
|
|
259
|
+
Find the index of a label in a discrete variable from a BN.
|
|
260
|
+
|
|
261
|
+
If val is an int, we keep is as is. If it is a str, we try to find the correct index in the variable
|
|
262
|
+
|
|
263
|
+
Parameters
|
|
264
|
+
----------
|
|
265
|
+
bn: pyagrum.BayesNet
|
|
266
|
+
the BN where to find the variable
|
|
267
|
+
varname : str
|
|
268
|
+
the name of the variable
|
|
269
|
+
val : int|str
|
|
270
|
+
the index or the name of the label
|
|
271
|
+
|
|
272
|
+
Returns
|
|
273
|
+
-------
|
|
274
|
+
int
|
|
275
|
+
the index of the label
|
|
276
|
+
"""
|
|
277
|
+
if not isinstance(val, str):
|
|
278
|
+
return val
|
|
279
|
+
|
|
280
|
+
return bn.variableFromName(varname).index(val)
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
############################################################################
|
|
2
|
+
# This file is part of the aGrUM/pyAgrum library. #
|
|
3
|
+
# #
|
|
4
|
+
# Copyright (c) 2005-2025 by #
|
|
5
|
+
# - Pierre-Henri WUILLEMIN(_at_LIP6) #
|
|
6
|
+
# - Christophe GONZALES(_at_AMU) #
|
|
7
|
+
# #
|
|
8
|
+
# The aGrUM/pyAgrum library is free software; you can redistribute it #
|
|
9
|
+
# and/or modify it under the terms of either : #
|
|
10
|
+
# #
|
|
11
|
+
# - the GNU Lesser General Public License as published by #
|
|
12
|
+
# the Free Software Foundation, either version 3 of the License, #
|
|
13
|
+
# or (at your option) any later version, #
|
|
14
|
+
# - the MIT license (MIT), #
|
|
15
|
+
# - or both in dual license, as here. #
|
|
16
|
+
# #
|
|
17
|
+
# (see https://agrum.gitlab.io/articles/dual-licenses-lgplv3mit.html) #
|
|
18
|
+
# #
|
|
19
|
+
# This aGrUM/pyAgrum library is distributed in the hope that it will be #
|
|
20
|
+
# useful, but WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, #
|
|
21
|
+
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES MERCHANTABILITY or FITNESS #
|
|
22
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #
|
|
23
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
|
|
24
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, #
|
|
25
|
+
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR #
|
|
26
|
+
# OTHER DEALINGS IN THE SOFTWARE. #
|
|
27
|
+
# #
|
|
28
|
+
# See LICENCES for more details. #
|
|
29
|
+
# #
|
|
30
|
+
# SPDX-FileCopyrightText: Copyright 2005-2025 #
|
|
31
|
+
# - Pierre-Henri WUILLEMIN(_at_LIP6) #
|
|
32
|
+
# - Christophe GONZALES(_at_AMU) #
|
|
33
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later OR MIT #
|
|
34
|
+
# #
|
|
35
|
+
# Contact : info_at_agrum_dot_org #
|
|
36
|
+
# homepage : http://agrum.gitlab.io #
|
|
37
|
+
# gitlab : https://gitlab.com/agrumery/agrum #
|
|
38
|
+
# #
|
|
39
|
+
############################################################################
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
This file defines a representation for causal model
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
import itertools as it
|
|
46
|
+
from typing import Union, Dict, Tuple
|
|
47
|
+
|
|
48
|
+
import pyagrum
|
|
49
|
+
|
|
50
|
+
from pyagrum.causal._types import LatentDescriptorList, NodeSet, NodeId, ArcSet, NameSet
|
|
51
|
+
from pyagrum.causal._doorCriteria import backdoor_generator, frontdoor_generator
|
|
52
|
+
|
|
53
|
+
# pylint: disable=unused-import
|
|
54
|
+
import pyagrum.causal # for annotations
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CausalModel:
|
|
58
|
+
"""
|
|
59
|
+
From an observational BNs and the description of latent variables, this class represent a complet causal model
|
|
60
|
+
obtained by adding the latent variables specified in ``latentVarsDescriptor`` to the Bayesian network ``bn``.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
bn: pyagrum.BayesNet
|
|
65
|
+
an observational Bayesian network
|
|
66
|
+
latentVarsDescriptor: List[(str,List[int])]
|
|
67
|
+
list of couples (<latent variable name>, <list of affected variables' ids>).
|
|
68
|
+
keepArcs: bool
|
|
69
|
+
By default, the arcs between variables affected by a common latent variable will be removed but this can be avoided by setting ``keepArcs`` to ``True``
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, bn: "pyagrum.BayesNet", latentVarsDescriptor: LatentDescriptorList = None, keepArcs: bool = False):
|
|
73
|
+
self.__observationalBN = bn
|
|
74
|
+
self.__latentVarsDescriptor = latentVarsDescriptor
|
|
75
|
+
self.__keepArcs = keepArcs
|
|
76
|
+
|
|
77
|
+
if latentVarsDescriptor is None:
|
|
78
|
+
latentVarsDescriptor = []
|
|
79
|
+
|
|
80
|
+
# we have to redefine those attributes since the __observationalBN may be augmented by latent variables
|
|
81
|
+
self.__causalBN = pyagrum.BayesNet()
|
|
82
|
+
|
|
83
|
+
# nodes of BN
|
|
84
|
+
for n in bn.nodes():
|
|
85
|
+
self.__causalBN.add(bn.variable(n), n)
|
|
86
|
+
|
|
87
|
+
# arcs on BN
|
|
88
|
+
for x, y in bn.arcs():
|
|
89
|
+
self.__causalBN.addArc(x, y)
|
|
90
|
+
|
|
91
|
+
# latent variables and arcs from latent variables
|
|
92
|
+
self.__lat: NodeSet = set()
|
|
93
|
+
|
|
94
|
+
self.__names = {nId: self.__causalBN.variable(nId).name() for nId in self.__causalBN.nodes()}
|
|
95
|
+
|
|
96
|
+
for n, ls in latentVarsDescriptor:
|
|
97
|
+
self.addLatentVariable(n, ls, keepArcs)
|
|
98
|
+
|
|
99
|
+
def clone(self) -> "pyagrum.causal.CausalModel":
|
|
100
|
+
"""
|
|
101
|
+
Copy a causal model
|
|
102
|
+
|
|
103
|
+
:return: the copy
|
|
104
|
+
"""
|
|
105
|
+
return CausalModel(pyagrum.BayesNet(self.__observationalBN), self.__latentVarsDescriptor, self.__keepArcs)
|
|
106
|
+
|
|
107
|
+
def addLatentVariable(self, name: str, lchild: Tuple[str, str], keepArcs: bool = False) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Add a new latent variable with a name, a tuple of children and replacing (or not) correlations between children.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
name: str
|
|
114
|
+
the name of the latent variable
|
|
115
|
+
lchild: Tuple[str,str]
|
|
116
|
+
the tuple of (2) children
|
|
117
|
+
keepArcs: bool
|
|
118
|
+
do wee keep (or not) the arc between the children ?
|
|
119
|
+
"""
|
|
120
|
+
# simplest variable to add : only 2 modalities for latent variables
|
|
121
|
+
id_latent = self.__causalBN.add(name, 2)
|
|
122
|
+
self.__lat.add(id_latent)
|
|
123
|
+
self.__names[id_latent] = name
|
|
124
|
+
|
|
125
|
+
for item in lchild:
|
|
126
|
+
j = self.__observationalBN.idFromName(item) if isinstance(item, str) else item
|
|
127
|
+
self.addCausalArc(id_latent, j)
|
|
128
|
+
|
|
129
|
+
if not keepArcs:
|
|
130
|
+
ils = {self.__observationalBN.idFromName(x) for x in lchild}
|
|
131
|
+
for ix, iy in it.combinations(ils, 2):
|
|
132
|
+
if ix in self.__causalBN.parents(iy):
|
|
133
|
+
self.eraseCausalArc(ix, iy)
|
|
134
|
+
elif iy in self.__causalBN.parents(ix):
|
|
135
|
+
self.eraseCausalArc(iy, ix)
|
|
136
|
+
|
|
137
|
+
def toDot(self) -> str:
|
|
138
|
+
"""
|
|
139
|
+
Create a dot representation of the causal model
|
|
140
|
+
|
|
141
|
+
:return: the dot representation in a string
|
|
142
|
+
"""
|
|
143
|
+
res = "digraph {"
|
|
144
|
+
|
|
145
|
+
# latent variables
|
|
146
|
+
if pyagrum.config.asBool["causal", "show_latent_names"]:
|
|
147
|
+
shap = "ellipse"
|
|
148
|
+
else:
|
|
149
|
+
shap = "point"
|
|
150
|
+
|
|
151
|
+
for n in self.nodes():
|
|
152
|
+
if n in self.latentVariablesIds():
|
|
153
|
+
res += f'''
|
|
154
|
+
"{self.names()[n]}" [fillcolor="{pyagrum.config["causal", "default_node_bgcolor"]}",
|
|
155
|
+
fontcolor="{pyagrum.config["causal", "default_node_fgcolor"]}",
|
|
156
|
+
style=filled,shape={shap}];
|
|
157
|
+
|
|
158
|
+
'''
|
|
159
|
+
|
|
160
|
+
# not latent variables
|
|
161
|
+
for n in self.nodes():
|
|
162
|
+
if n not in self.latentVariablesIds():
|
|
163
|
+
res += f'''
|
|
164
|
+
"{self.names()[n]}" [fillcolor="{pyagrum.config["causal", "default_node_bgcolor"]}",
|
|
165
|
+
fontcolor="{pyagrum.config["causal", "default_node_fgcolor"]}",
|
|
166
|
+
style=filled,shape="ellipse"];
|
|
167
|
+
|
|
168
|
+
'''
|
|
169
|
+
|
|
170
|
+
for a, b in self.arcs():
|
|
171
|
+
res += ' "' + self.names()[a] + '"->"' + self.names()[b] + '" '
|
|
172
|
+
if a in self.latentVariablesIds() or b in self.latentVariablesIds():
|
|
173
|
+
res += ' [style="dashed"];'
|
|
174
|
+
else:
|
|
175
|
+
black_color = pyagrum.config["notebook", "default_arc_color"]
|
|
176
|
+
res += ' [color="' + black_color + ":" + black_color + '"];'
|
|
177
|
+
res += "\n"
|
|
178
|
+
|
|
179
|
+
res += "\n};"
|
|
180
|
+
return res
|
|
181
|
+
|
|
182
|
+
def causalBN(self) -> "pyagrum.BayesNet":
|
|
183
|
+
"""
|
|
184
|
+
:return: the causal Bayesian network
|
|
185
|
+
|
|
186
|
+
:warning: do not infer any computations in this model. It is strictly a structural model
|
|
187
|
+
"""
|
|
188
|
+
return self.__causalBN
|
|
189
|
+
|
|
190
|
+
def observationalBN(self) -> "pyagrum.BayesNet":
|
|
191
|
+
"""
|
|
192
|
+
:return: the observational Bayesian network
|
|
193
|
+
"""
|
|
194
|
+
return self.__observationalBN
|
|
195
|
+
|
|
196
|
+
def connectedComponents(self) -> Dict[int, NodeSet]:
|
|
197
|
+
"""
|
|
198
|
+
Return a map of connected components and their nodes.
|
|
199
|
+
|
|
200
|
+
Returns
|
|
201
|
+
-------
|
|
202
|
+
Dict[int,NodeSet]:
|
|
203
|
+
thedisc of connected components
|
|
204
|
+
"""
|
|
205
|
+
return self.__causalBN.connectedComponents()
|
|
206
|
+
|
|
207
|
+
def parents(self, x: Union[NodeId, str]) -> NodeSet:
|
|
208
|
+
"""
|
|
209
|
+
From a NodeId, returns its parent (as a set of NodeId)
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
x : int
|
|
214
|
+
the node
|
|
215
|
+
|
|
216
|
+
Returns
|
|
217
|
+
-------
|
|
218
|
+
Set[int]
|
|
219
|
+
the set of parents
|
|
220
|
+
"""
|
|
221
|
+
return self.__causalBN.parents(self.__causalBN.idFromName(x) if isinstance(x, str) else x)
|
|
222
|
+
|
|
223
|
+
def children(self, x: Union[NodeId, str]) -> NodeSet:
|
|
224
|
+
"""
|
|
225
|
+
From a NodeId, returns its children (as a set of NodeId)
|
|
226
|
+
|
|
227
|
+
Parameters
|
|
228
|
+
----------
|
|
229
|
+
x : int
|
|
230
|
+
the node
|
|
231
|
+
|
|
232
|
+
Returns
|
|
233
|
+
-------
|
|
234
|
+
Set[int]
|
|
235
|
+
the set of children
|
|
236
|
+
"""
|
|
237
|
+
return self.__causalBN.children(self.__causalBN.idFromName(x) if isinstance(x, str) else x)
|
|
238
|
+
|
|
239
|
+
def names(self) -> Dict[NodeId, str]:
|
|
240
|
+
"""
|
|
241
|
+
Returns
|
|
242
|
+
-------
|
|
243
|
+
Dict[int,str]
|
|
244
|
+
the map NodeId,Name
|
|
245
|
+
"""
|
|
246
|
+
return self.__names
|
|
247
|
+
|
|
248
|
+
def idFromName(self, name: str) -> NodeId:
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
Parameters
|
|
252
|
+
----------
|
|
253
|
+
name: str
|
|
254
|
+
the name of the variable
|
|
255
|
+
|
|
256
|
+
Returns
|
|
257
|
+
-------
|
|
258
|
+
int
|
|
259
|
+
the id of the variable
|
|
260
|
+
"""
|
|
261
|
+
return self.__causalBN.idFromName(name)
|
|
262
|
+
|
|
263
|
+
def latentVariablesIds(self) -> NodeSet:
|
|
264
|
+
"""
|
|
265
|
+
Returns
|
|
266
|
+
-------
|
|
267
|
+
NodeSet
|
|
268
|
+
the set of ids of latent variables in the causal model
|
|
269
|
+
"""
|
|
270
|
+
return self.__lat
|
|
271
|
+
|
|
272
|
+
def eraseCausalArc(self, x: Union[NodeId, str], y: Union[NodeId, str]) -> None:
|
|
273
|
+
"""
|
|
274
|
+
Erase the arc x->y
|
|
275
|
+
|
|
276
|
+
Parameters
|
|
277
|
+
----------
|
|
278
|
+
x : int|str
|
|
279
|
+
the nodeId or the name of the first node
|
|
280
|
+
y : int|str
|
|
281
|
+
the nodeId or the name of the second node
|
|
282
|
+
"""
|
|
283
|
+
ix = self.__observationalBN.idFromName(x) if isinstance(x, str) else x
|
|
284
|
+
iy = self.__observationalBN.idFromName(y) if isinstance(y, str) else y
|
|
285
|
+
self.__causalBN.eraseArc(pyagrum.Arc(ix, iy))
|
|
286
|
+
|
|
287
|
+
def addCausalArc(self, x: Union[NodeId, str], y: Union[NodeId, str]) -> None:
|
|
288
|
+
"""
|
|
289
|
+
Add an arc x->y
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
x : int|str
|
|
294
|
+
the nodeId or the name of the first node
|
|
295
|
+
y : int|str
|
|
296
|
+
the nodeId or the name of the second node
|
|
297
|
+
"""
|
|
298
|
+
ix = self.__observationalBN.idFromName(x) if isinstance(x, str) else x
|
|
299
|
+
iy = self.__observationalBN.idFromName(y) if isinstance(y, str) else y
|
|
300
|
+
self.__causalBN.addArc(ix, iy)
|
|
301
|
+
|
|
302
|
+
def existsArc(self, x: Union[NodeId, str], y: Union[NodeId, str]) -> bool:
|
|
303
|
+
"""
|
|
304
|
+
Does the arc x->y exist ?
|
|
305
|
+
|
|
306
|
+
Parameters
|
|
307
|
+
----------
|
|
308
|
+
x : int|str
|
|
309
|
+
the nodeId or the name of the first node
|
|
310
|
+
y : int|str
|
|
311
|
+
the nodeId or the name of the second node
|
|
312
|
+
|
|
313
|
+
Returns
|
|
314
|
+
-------
|
|
315
|
+
bool
|
|
316
|
+
True if the arc exists.
|
|
317
|
+
"""
|
|
318
|
+
ix = self.__observationalBN.idFromName(x) if isinstance(x, str) else x
|
|
319
|
+
iy = self.__observationalBN.idFromName(y) if isinstance(y, str) else y
|
|
320
|
+
return self.__causalBN.dag().existsArc(ix, iy)
|
|
321
|
+
|
|
322
|
+
def nodes(self) -> NodeSet:
|
|
323
|
+
"""
|
|
324
|
+
:return: the set of nodes
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
return self.__causalBN.nodes()
|
|
328
|
+
|
|
329
|
+
def arcs(self) -> ArcSet:
|
|
330
|
+
"""
|
|
331
|
+
:return: the set of arcs
|
|
332
|
+
"""
|
|
333
|
+
return self.__causalBN.arcs()
|
|
334
|
+
|
|
335
|
+
def backDoor(
|
|
336
|
+
self, cause: Union[NodeId, str], effect: Union[NodeId, str], withNames: bool = True
|
|
337
|
+
) -> Union[None, NameSet, NodeSet]:
|
|
338
|
+
"""
|
|
339
|
+
Check if a backdoor exists between `cause` and `effect`
|
|
340
|
+
|
|
341
|
+
Parameters
|
|
342
|
+
----------
|
|
343
|
+
cause: int|str
|
|
344
|
+
the nodeId or the name of the cause
|
|
345
|
+
effect: int|str
|
|
346
|
+
the nodeId or the name of the effect
|
|
347
|
+
withNames: bool
|
|
348
|
+
wether we use ids (int) or names (str)
|
|
349
|
+
|
|
350
|
+
Returns
|
|
351
|
+
-------
|
|
352
|
+
None|Set[str]|Set[int]
|
|
353
|
+
None if no found backdoor. Otherwise return the found backdoors as set of ids or set of names.
|
|
354
|
+
"""
|
|
355
|
+
icause = self.__observationalBN.idFromName(cause) if isinstance(cause, str) else cause
|
|
356
|
+
ieffect = self.__observationalBN.idFromName(effect) if isinstance(effect, str) else effect
|
|
357
|
+
|
|
358
|
+
for bd in backdoor_generator(self, icause, ieffect, self.latentVariablesIds()):
|
|
359
|
+
if withNames:
|
|
360
|
+
return {self.__observationalBN.variable(i).name() for i in bd}
|
|
361
|
+
|
|
362
|
+
return bd
|
|
363
|
+
|
|
364
|
+
return None
|
|
365
|
+
|
|
366
|
+
def frontDoor(
|
|
367
|
+
self, cause: Union[NodeId, str], effect: Union[NodeId, str], withNames: bool = True
|
|
368
|
+
) -> Union[None, NameSet, NodeSet]:
|
|
369
|
+
"""
|
|
370
|
+
Check if a frontdoor exists between cause and effet
|
|
371
|
+
|
|
372
|
+
Parameters
|
|
373
|
+
----------
|
|
374
|
+
cause: int|str
|
|
375
|
+
the nodeId or the name of the cause
|
|
376
|
+
effect: int|str
|
|
377
|
+
the nodeId or the name of the effect
|
|
378
|
+
withNames: bool
|
|
379
|
+
wether we use ids (int) or names (str)
|
|
380
|
+
|
|
381
|
+
Returns
|
|
382
|
+
-------
|
|
383
|
+
None|Set[str]|Set[int]
|
|
384
|
+
None if no found frontdoot. Otherwise return the found frontdoors as set of ids or set of names.
|
|
385
|
+
"""
|
|
386
|
+
icause = self.__observationalBN.idFromName(cause) if isinstance(cause, str) else cause
|
|
387
|
+
ieffect = self.__observationalBN.idFromName(effect) if isinstance(effect, str) else effect
|
|
388
|
+
|
|
389
|
+
for fd in frontdoor_generator(self, icause, ieffect, self.latentVariablesIds()):
|
|
390
|
+
if withNames:
|
|
391
|
+
return {self.__observationalBN.variable(i).name() for i in fd}
|
|
392
|
+
|
|
393
|
+
return fd
|
|
394
|
+
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def inducedCausalSubModel(cm: CausalModel, sns: NodeSet = None) -> CausalModel:
|
|
399
|
+
"""
|
|
400
|
+
Create an causal model induced by a subset of nodes.
|
|
401
|
+
|
|
402
|
+
Parameters
|
|
403
|
+
----------
|
|
404
|
+
cm: CausalModel
|
|
405
|
+
the causal model
|
|
406
|
+
sns: Set[int]
|
|
407
|
+
the set of nodes
|
|
408
|
+
|
|
409
|
+
Returns
|
|
410
|
+
-------
|
|
411
|
+
CausalModel
|
|
412
|
+
the induced sub-causal model
|
|
413
|
+
"""
|
|
414
|
+
if sns is None:
|
|
415
|
+
sns = cm.nodes()
|
|
416
|
+
nodes = sns - cm.latentVariablesIds()
|
|
417
|
+
|
|
418
|
+
bn = pyagrum.BayesNet()
|
|
419
|
+
|
|
420
|
+
for n in nodes:
|
|
421
|
+
bn.add(cm.observationalBN().variable(n), n)
|
|
422
|
+
|
|
423
|
+
for x, y in cm.arcs():
|
|
424
|
+
if y in nodes:
|
|
425
|
+
if x in nodes:
|
|
426
|
+
bn.addArc(x, y)
|
|
427
|
+
|
|
428
|
+
names = cm.names()
|
|
429
|
+
latentVarsDescriptor = []
|
|
430
|
+
lats = cm.latentVariablesIds()
|
|
431
|
+
for latentVar in lats:
|
|
432
|
+
inters = cm.children(latentVar) & nodes
|
|
433
|
+
if len(inters) > 0:
|
|
434
|
+
latentVarsDescriptor.append((names[latentVar], list(inters)))
|
|
435
|
+
|
|
436
|
+
return CausalModel(bn, latentVarsDescriptor, True)
|