pyAgrum-nightly 2.3.1.9.dev202512261765915415__cp310-abi3-macosx_10_15_x86_64.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.
Files changed (107) hide show
  1. pyagrum/__init__.py +165 -0
  2. pyagrum/_pyagrum.so +0 -0
  3. pyagrum/bnmixture/BNMInference.py +268 -0
  4. pyagrum/bnmixture/BNMLearning.py +376 -0
  5. pyagrum/bnmixture/BNMixture.py +464 -0
  6. pyagrum/bnmixture/__init__.py +60 -0
  7. pyagrum/bnmixture/notebook.py +1058 -0
  8. pyagrum/causal/_CausalFormula.py +280 -0
  9. pyagrum/causal/_CausalModel.py +436 -0
  10. pyagrum/causal/__init__.py +81 -0
  11. pyagrum/causal/_causalImpact.py +356 -0
  12. pyagrum/causal/_dSeparation.py +598 -0
  13. pyagrum/causal/_doAST.py +761 -0
  14. pyagrum/causal/_doCalculus.py +361 -0
  15. pyagrum/causal/_doorCriteria.py +374 -0
  16. pyagrum/causal/_exceptions.py +95 -0
  17. pyagrum/causal/_types.py +61 -0
  18. pyagrum/causal/causalEffectEstimation/_CausalEffectEstimation.py +1175 -0
  19. pyagrum/causal/causalEffectEstimation/_IVEstimators.py +718 -0
  20. pyagrum/causal/causalEffectEstimation/_RCTEstimators.py +132 -0
  21. pyagrum/causal/causalEffectEstimation/__init__.py +46 -0
  22. pyagrum/causal/causalEffectEstimation/_backdoorEstimators.py +774 -0
  23. pyagrum/causal/causalEffectEstimation/_causalBNEstimator.py +324 -0
  24. pyagrum/causal/causalEffectEstimation/_frontdoorEstimators.py +396 -0
  25. pyagrum/causal/causalEffectEstimation/_learners.py +118 -0
  26. pyagrum/causal/causalEffectEstimation/_utils.py +466 -0
  27. pyagrum/causal/notebook.py +172 -0
  28. pyagrum/clg/CLG.py +658 -0
  29. pyagrum/clg/GaussianVariable.py +111 -0
  30. pyagrum/clg/SEM.py +312 -0
  31. pyagrum/clg/__init__.py +63 -0
  32. pyagrum/clg/canonicalForm.py +408 -0
  33. pyagrum/clg/constants.py +54 -0
  34. pyagrum/clg/forwardSampling.py +202 -0
  35. pyagrum/clg/learning.py +776 -0
  36. pyagrum/clg/notebook.py +480 -0
  37. pyagrum/clg/variableElimination.py +271 -0
  38. pyagrum/common.py +60 -0
  39. pyagrum/config.py +319 -0
  40. pyagrum/ctbn/CIM.py +513 -0
  41. pyagrum/ctbn/CTBN.py +573 -0
  42. pyagrum/ctbn/CTBNGenerator.py +216 -0
  43. pyagrum/ctbn/CTBNInference.py +459 -0
  44. pyagrum/ctbn/CTBNLearner.py +161 -0
  45. pyagrum/ctbn/SamplesStats.py +671 -0
  46. pyagrum/ctbn/StatsIndepTest.py +355 -0
  47. pyagrum/ctbn/__init__.py +79 -0
  48. pyagrum/ctbn/constants.py +54 -0
  49. pyagrum/ctbn/notebook.py +264 -0
  50. pyagrum/defaults.ini +199 -0
  51. pyagrum/deprecated.py +95 -0
  52. pyagrum/explain/_ComputationCausal.py +75 -0
  53. pyagrum/explain/_ComputationConditional.py +48 -0
  54. pyagrum/explain/_ComputationMarginal.py +48 -0
  55. pyagrum/explain/_CustomShapleyCache.py +110 -0
  56. pyagrum/explain/_Explainer.py +176 -0
  57. pyagrum/explain/_Explanation.py +70 -0
  58. pyagrum/explain/_FIFOCache.py +54 -0
  59. pyagrum/explain/_ShallCausalValues.py +204 -0
  60. pyagrum/explain/_ShallConditionalValues.py +155 -0
  61. pyagrum/explain/_ShallMarginalValues.py +155 -0
  62. pyagrum/explain/_ShallValues.py +296 -0
  63. pyagrum/explain/_ShapCausalValues.py +208 -0
  64. pyagrum/explain/_ShapConditionalValues.py +126 -0
  65. pyagrum/explain/_ShapMarginalValues.py +191 -0
  66. pyagrum/explain/_ShapleyValues.py +298 -0
  67. pyagrum/explain/__init__.py +81 -0
  68. pyagrum/explain/_explGeneralizedMarkovBlanket.py +152 -0
  69. pyagrum/explain/_explIndependenceListForPairs.py +146 -0
  70. pyagrum/explain/_explInformationGraph.py +264 -0
  71. pyagrum/explain/notebook/__init__.py +54 -0
  72. pyagrum/explain/notebook/_bar.py +142 -0
  73. pyagrum/explain/notebook/_beeswarm.py +174 -0
  74. pyagrum/explain/notebook/_showShapValues.py +97 -0
  75. pyagrum/explain/notebook/_waterfall.py +220 -0
  76. pyagrum/explain/shapley.py +225 -0
  77. pyagrum/lib/__init__.py +46 -0
  78. pyagrum/lib/_colors.py +390 -0
  79. pyagrum/lib/bn2graph.py +299 -0
  80. pyagrum/lib/bn2roc.py +1026 -0
  81. pyagrum/lib/bn2scores.py +217 -0
  82. pyagrum/lib/bn_vs_bn.py +605 -0
  83. pyagrum/lib/cn2graph.py +305 -0
  84. pyagrum/lib/discreteTypeProcessor.py +1102 -0
  85. pyagrum/lib/discretizer.py +58 -0
  86. pyagrum/lib/dynamicBN.py +390 -0
  87. pyagrum/lib/explain.py +57 -0
  88. pyagrum/lib/export.py +84 -0
  89. pyagrum/lib/id2graph.py +258 -0
  90. pyagrum/lib/image.py +387 -0
  91. pyagrum/lib/ipython.py +307 -0
  92. pyagrum/lib/mrf2graph.py +471 -0
  93. pyagrum/lib/notebook.py +1821 -0
  94. pyagrum/lib/proba_histogram.py +552 -0
  95. pyagrum/lib/utils.py +138 -0
  96. pyagrum/pyagrum.py +31495 -0
  97. pyagrum/skbn/_MBCalcul.py +242 -0
  98. pyagrum/skbn/__init__.py +49 -0
  99. pyagrum/skbn/_learningMethods.py +282 -0
  100. pyagrum/skbn/_utils.py +297 -0
  101. pyagrum/skbn/bnclassifier.py +1014 -0
  102. pyagrum_nightly-2.3.1.9.dev202512261765915415.dist-info/LICENSE.md +12 -0
  103. pyagrum_nightly-2.3.1.9.dev202512261765915415.dist-info/LICENSES/LGPL-3.0-or-later.txt +304 -0
  104. pyagrum_nightly-2.3.1.9.dev202512261765915415.dist-info/LICENSES/MIT.txt +18 -0
  105. pyagrum_nightly-2.3.1.9.dev202512261765915415.dist-info/METADATA +145 -0
  106. pyagrum_nightly-2.3.1.9.dev202512261765915415.dist-info/RECORD +107 -0
  107. pyagrum_nightly-2.3.1.9.dev202512261765915415.dist-info/WHEEL +4 -0
@@ -0,0 +1,81 @@
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
+ Causality in pyAgrum mainly consists in the ability to build a causal model, i.e. a (observational) Bayesian network
43
+ and a set of latent variables and their relation with observation variables and in the ability to compute using
44
+ do-calculus the causal impact in such a model.
45
+ """
46
+
47
+ __author__ = "Pierre-Henri Wuillemin, Paul Alam, Ibrahim Merad"
48
+ __copyright__ = "(c) 2019-2024 PARIS"
49
+
50
+ from ._causalImpact import causalImpact, counterfactualModel, counterfactual
51
+ from ._doorCriteria import backdoor_generator, frontdoor_generator
52
+ from ._doAST import ASTtree, ASTjointProba, ASTmult, ASTdiv, ASTsum, ASTposteriorProba, ASTminus, ASTplus, ASTBinaryOp
53
+ from ._doCalculus import doCalculusWithObservation, identifyingIntervention
54
+ from ._CausalFormula import CausalFormula
55
+ from ._CausalModel import CausalModel
56
+ from ._exceptions import HedgeException, UnidentifiableException
57
+ from .causalEffectEstimation import CausalEffectEstimation
58
+ import sys
59
+
60
+ __all__ = [
61
+ "CausalModel",
62
+ "CausalFormula",
63
+ "CausalEffectEstimation",
64
+ "causalImpact",
65
+ "counterfactualModel",
66
+ "counterfactual",
67
+ "doCalculusWithObservation",
68
+ "identifyingIntervention",
69
+ "backdoor_generator",
70
+ "HedgeException",
71
+ "UnidentifiableException",
72
+ "ASTtree",
73
+ "ASTjointProba",
74
+ "ASTmult",
75
+ "ASTdiv",
76
+ "ASTsum",
77
+ "ASTposteriorProba",
78
+ "ASTminus",
79
+ "ASTplus",
80
+ "ASTBinaryOp",
81
+ ]
@@ -0,0 +1,356 @@
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 gives an API for causal inference
43
+ """
44
+
45
+ from typing import Union, Optional, Dict, Tuple, Set
46
+
47
+ import pyagrum
48
+
49
+ from pyagrum.causal._types import NameSet
50
+ from pyagrum.causal._dSeparation import isDSep
51
+ from pyagrum.causal._doAST import ASTposteriorProba
52
+
53
+ from pyagrum.causal._exceptions import HedgeException
54
+
55
+ from pyagrum.causal._CausalModel import CausalModel
56
+ from pyagrum.causal._CausalFormula import CausalFormula, _getLabelIdx
57
+ from pyagrum.causal._doCalculus import doCalculusWithObservation, doCalculus, getFrontDoorTree, getBackDoorTree
58
+
59
+ # pylint: disable=unused-import
60
+ import pyagrum.causal # for annotations
61
+
62
+
63
+ def causalImpact(
64
+ cm: CausalModel,
65
+ on: Union[str, NameSet],
66
+ doing: Union[str, NameSet],
67
+ knowing: Optional[NameSet] = None,
68
+ values: Optional[Dict[str, int]] = None,
69
+ ) -> Tuple["pyagrum.causal.CausalFormula", "pyagrum.Tensor", str]:
70
+ """
71
+ Determines the causal impact of interventions.
72
+
73
+ Determines the causal impact of the interventions specified in ``doing`` on the single or list of variables ``on``
74
+ knowing the states of the variables in ``knowing`` (optional). These last parameters is dictionary <variable
75
+ name>:<value>. The causal impact is determined in the causal DAG ``cm``.
76
+ This function returns a triplet with a latex format formula used to compute the causal impact, a tensor
77
+ representing the probability distribution of ``on`` given the interventions and observations as parameters,
78
+ and an explanation of the method allowing the identification. If there is no impact, the joint probability of
79
+ ``on`` is simply returned. If the impact is not identifiable the formula and the adjustment will be ``None`` but an
80
+ explanation is still given.
81
+
82
+ Parameters
83
+ ----------
84
+ cm: CausalModel
85
+ the causal model
86
+ on: str|NameSet
87
+ variable name or variable names set of interest
88
+ doing: str|NameSet
89
+ the interventions
90
+ knowing: str|NameSet
91
+ the observations
92
+ values: Dict[str,int] default=None
93
+ the values of interventions and observations
94
+
95
+ Returns
96
+ -------
97
+ Tuple[CausalFormula,pyagrum.Tensor,str]
98
+ the CausalFormula, the computation, the explanation
99
+ """
100
+ # Checking the args
101
+ son = {on} if isinstance(on, str) else on
102
+ sdoing = {doing} if isinstance(doing, str) else doing
103
+ sk = set() if knowing is None else knowing
104
+
105
+ # verifies that when len()==0, args are empty sets
106
+ if len(son) == 0:
107
+ son = set()
108
+ if len(sdoing) == 0:
109
+ sdoing = set()
110
+ if len(sk) == 0:
111
+ sk = set()
112
+
113
+ total = {cm.observationalBN().variable(cm.observationalBN().idFromName(i)).name() for i in son | sdoing | sk}
114
+
115
+ if values is not None:
116
+ for k in values.keys():
117
+ if k not in total:
118
+ raise ValueError(f"{k} is not in the query arguments.")
119
+
120
+ if len(son & sdoing & sk) > 0:
121
+ raise ValueError("The 3 parts of the query (on, doing, knowing) must not intersect.")
122
+
123
+ formula, tensor, explanation = _causalImpact(cm, son, sdoing, sk)
124
+
125
+ # no need to contextualize the tensor
126
+ if tensor is None or values is None:
127
+ potfinal = tensor
128
+ else:
129
+ sv = set(tensor.names)
130
+ extract_values = {k: _getLabelIdx(cm.observationalBN(), k, v) for k, v in values.items() if k in sv}
131
+ potfinal = tensor.extract(extract_values)
132
+
133
+ # doCalculous can change doing and knowing
134
+ if formula is not None:
135
+ formula._setDoing(sdoing)
136
+ formula._setKnowing(sk)
137
+
138
+ return formula, potfinal, explanation
139
+
140
+
141
+ def _causalImpact(
142
+ cm: CausalModel, on: Union[str, NameSet], doing: Union[str, NameSet], knowing: Union[str, NameSet]
143
+ ) -> Tuple["pyagrum.causal.CausalFormula", "pyagrum.Tensor", str]:
144
+ """
145
+ Determines the causal impact of interventions.
146
+
147
+ Determines the causal impact of the interventions specified in ``doing`` on the single or list of variables ``on``
148
+ knowing the states of the variables in ``knowing`` (optional). The causal impact is determined in the causal DAG
149
+ ``cm``.
150
+ This function returns a triplet with a latex format formula used to compute the causal impact, a tensor
151
+ representing the probability distribution of ``on`` given the interventions and observations as parameters,
152
+ and an explanation of the method allowing the identification. If there is no impact, the joint probability of
153
+ ``on`` is simply returned. If the impact is not identifiable the formula and the adjustment will be ``None`` but an
154
+ explanation is still given.
155
+
156
+ Parameters
157
+ ----------
158
+ cm: CausalModel
159
+ the causal model
160
+ on: str|Set[str]
161
+ targeted variable(s)
162
+ doing: str|Set[str]
163
+ interventions
164
+ knowing: str|Set[str]
165
+ observations
166
+
167
+ Returns
168
+ -------
169
+ Tuple[CausalFormula,pyagrum.Tensor,str]
170
+ the latex representation, the computation, the explanation
171
+ """
172
+ nY = list(on)
173
+ iY = [cm.observationalBN().idFromName(i) for i in nY]
174
+
175
+ nDo = list(doing)
176
+ iDo = [cm.observationalBN().idFromName(i) for i in nDo]
177
+
178
+ if knowing is None:
179
+ nK = []
180
+ else:
181
+ nK = list(knowing)
182
+ sK = {cm.observationalBN().idFromName(i) for i in nK}
183
+
184
+ # Null causal effect (different from non identifiable causal effect)
185
+ if isDSep(cm, set(iDo), set(iY), sK | cm.latentVariablesIds()):
186
+ explain = "No causal effect of X on Y, because they are d-separated "
187
+ explain += "(conditioning on the observed variables if any)."
188
+ ar = CausalFormula(cm, ASTposteriorProba(cm.causalBN(), set(nY), set(nK)), on, doing, knowing)
189
+ adj = ar.eval()
190
+ return ar, adj.reorganize([v for v in nY + nDo + nK if v in adj.names]), explain
191
+
192
+ # Front or Back door
193
+ if len(iDo) == 1 and len(nY) == 1 and len(nK) == 0:
194
+ # for bd in backdoor_generator(cm, iDo[0], iY[0], cm.latentVariablesIds()):
195
+ bd = cm.backDoor(iDo[0], iY[0], withNames=False)
196
+ if bd is not None:
197
+ ar = CausalFormula(cm, getBackDoorTree(cm, nDo[0], nY[0], bd), on, doing, knowing)
198
+ adj = ar.eval()
199
+ explain = "backdoor " + str([cm.causalBN().variable(i).name() for i in bd]) + " found."
200
+ return ar, adj.reorganize([v for v in nY + nDo + nK if v in adj.names]), explain
201
+
202
+ # for fd in frontdoor_generator(cm, iDo[0], iY[0], cm.latentVariablesIds()):
203
+ fd = cm.frontDoor(iDo[0], iY[0], withNames=False)
204
+ if fd is not None:
205
+ ar = CausalFormula(cm, getFrontDoorTree(cm, nDo[0], nY[0], fd), on, doing, knowing)
206
+ adj = ar.eval()
207
+ explain = "frontdoor " + str([cm.causalBN().variable(i).name() for i in fd]) + " found."
208
+ return ar, adj.reorganize([v for v in nY + nDo + nK if v in adj.names]), explain
209
+
210
+ # Go for do-calculus
211
+ try:
212
+ if len(nK) == 0:
213
+ ar = doCalculus(cm, on, set(nDo))
214
+ else:
215
+ ar = doCalculusWithObservation(cm, on, set(nDo), set(nK))
216
+ except HedgeException as h:
217
+ return None, None, h.message
218
+
219
+ adj = ar.eval()
220
+ lsum = nY + nDo + nK
221
+ lv = [v for v in lsum if v in adj.names]
222
+
223
+ # todo : check why it is possible that some variables are in names and
224
+ # not in lsum ... (see for instance p213, book of why and
225
+ # https://twitter.com/analisereal/status/1022277416205475841 : should
226
+ # really z be in the last formula ?)
227
+ ssum = set(lsum)
228
+ lv += [v for v in adj.names if v not in ssum]
229
+
230
+ adj = adj.reorganize(lv) # sumIn(lv).reorganize(lv)
231
+ explain = "Do-calculus computations"
232
+ return ar, adj, explain
233
+
234
+
235
+ def counterfactualModel(
236
+ cm: CausalModel, profile: Union[Dict[str, int], type(None)], whatif: Union[str, Set[str]]
237
+ ) -> CausalModel:
238
+ """
239
+ Determines the estimation of the twin model following the three steps algorithm from "The Book Of Why" (Pearl 2018) chapter 8 page 253.
240
+
241
+ This is done according to the following algorithm:
242
+ -Step 1: calculate the posterior probabilities of idiosyncratic nodes (parentless nodes - whatif-latent variables) in the BN with which we created the causal model with "profile" as evidence.
243
+ -Step 2 : We replace in the original BN the prior probabilities of idiosyncratic nodes with tensors calculated in step 1 (it will spread to the causal model)
244
+
245
+ This function returns the twin CausalModel
246
+
247
+ Parameters
248
+ ----------
249
+ cm: CausalModel
250
+ profile: Dict[str,int] default=None
251
+ evidence
252
+ whatif: str|Set[str]
253
+ idiosyncratic nodes
254
+
255
+ Returns
256
+ -------
257
+ CausalModel
258
+ the twin CausalModel
259
+ """
260
+ # Step 1 : calculate the posterior probabilities of idiosyncratic nodes knowing the profil
261
+
262
+ # whatif can be a string or a set of strings
263
+ if isinstance(whatif, str):
264
+ idWhatif = {whatif}
265
+ else:
266
+ idWhatif = whatif
267
+ idWhatif = set(map(cm.idFromName, idWhatif))
268
+
269
+ # get nodes without parents in the causal model
270
+ parentless = set()
271
+ # nodes of the causal model
272
+ nodes = cm.names().keys()
273
+ for node in nodes:
274
+ # if nb parents is equal to zero => parentless node
275
+ if len(cm.parents(node)) == 0:
276
+ parentless.add(node)
277
+
278
+ # idiosyncratic factors (specific to and representative of the profile) are parentless - (whatif+latent variables)
279
+ idiosyncratic = parentless.difference(idWhatif)
280
+ idiosyncratic = idiosyncratic.difference(cm.latentVariablesIds())
281
+
282
+ # copying the causal model
283
+ twincm = cm.clone()
284
+ bn = twincm.observationalBN()
285
+
286
+ # calculate the posterior probability of each idiosyncratic factor knowing the profile in the original BN
287
+ # posteriors will be a dict {factor : posterior probability knowing the profile}
288
+ posteriors = dict.fromkeys(idiosyncratic)
289
+ ie = pyagrum.LazyPropagation(bn)
290
+ ie.setEvidence(profile)
291
+ ie.makeInference()
292
+ for factor in idiosyncratic:
293
+ posteriors[factor] = ie.posterior(factor)
294
+
295
+ # Step 2 : We replace the prior probabilities of idiosyncratic nodes with tensors calculated in step 1 in the BN
296
+ # Saving the original CPTs of idiosyncratic variables
297
+ for factor in idiosyncratic:
298
+ bn.cpt(factor).fillWith(posteriors[factor])
299
+
300
+ return twincm
301
+
302
+
303
+ def counterfactual(
304
+ cm: CausalModel,
305
+ profile: Union[Dict[str, int], type(None)],
306
+ on: Union[str, Set[str]],
307
+ whatif: Union[str, Set[str]],
308
+ values: Union[Dict[str, int], type(None)] = None,
309
+ ) -> "pyagrum.Tensor":
310
+ """
311
+ Determines the estimation of a counterfactual query following the the three steps algorithm from "The Book Of Why"
312
+ (Pearl 2018) chapter 8 page 253.
313
+
314
+ Determines the estimation of the counterfactual query: Given the "profile" (dictionary <variable name>:<value>),what
315
+ would variables in "on" (single or list of variables) be if variables in "whatif" (single or list of variables) had
316
+ been as specified in "values" (dictionary <variable name>:<value>)(optional).
317
+
318
+ This is done according to the following algorithm:
319
+ -Step 1-2: compute the twin causal model
320
+ -Step 3 : determine the causal impact of the interventions specified in "whatif" on the single or list of
321
+ variables "on" in the causal model.
322
+
323
+ This function returns the tensor calculated in step 3, representing the probability distribution of "on" given
324
+ the interventions "whatif", if it had been as specified in "values" (if "values" is omitted, every possible value of
325
+ "whatif")
326
+
327
+ Parameters
328
+ ----------
329
+ cm: CausalModel
330
+ profile: Dict[str,int] default=None
331
+ evidence
332
+ on: variable name or variable names set
333
+ the variable(s) of interest
334
+ whatif: str|Set[str]
335
+ idiosyncratic nodes
336
+ values: Dict[str,int]
337
+ values for certain variables in whatif.
338
+
339
+ Returns
340
+ -------
341
+ pyagrum.Tensor
342
+ the computed counterfactual impact
343
+ """
344
+ # Step 1 and 2 : create the twin causal model
345
+ twincm = counterfactualModel(cm, profile, whatif)
346
+
347
+ # Step 3 : operate the intervention in the causal model based on bn
348
+ _, adj, _ = causalImpact(twincm, on=on, doing=whatif, values=values)
349
+ # cslnb.showCausalImpact(cm,on = on,whatif=whatif,values=values)
350
+
351
+ # adj is using variables from twincm. We copy it in a Tensor using variables of cm
352
+ res = pyagrum.Tensor()
353
+ for v in adj.names:
354
+ res.add(cm.observationalBN().variableFromName(v))
355
+ res.fillWith(adj)
356
+ return res