aGrUM 3.0.0
a C++ library for (probabilistic) graphical models
counterfactual_tpl.h
Go to the documentation of this file.
1/****************************************************************************
2 * This file is part of the aGrUM/pyAgrum library. *
3 * *
4 * Copyright (c) 2005-2026 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-2026 *
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#pragma once
43
44#include <utility>
45
47
48namespace gum {
49
50 // ============================== CONSTRUCTORS ===============================
51
52 template < GUM_Numeric GUM_ELEMENT >
62
63 template < GUM_Numeric GUM_ELEMENT >
65 const NodeSet& onIds,
66 const NodeSet& whatifIds,
67 const HashTable< NodeId, Idx >& profileIds,
68 const HashTable< NodeId, Idx >& valuesIds) :
69 Counterfactual< GUM_ELEMENT >(cm,
70 _idsToNames_(cm, onIds),
71 _idsToNames_(cm, whatifIds),
72 _idAssignToNameAssign_(cm, profileIds),
73 _idAssignToNameAssign_(cm, valuesIds)) {}
74
75 // ============================== TWIN BUILDERS ==============================
76
77 template < GUM_Numeric GUM_ELEMENT >
82 const auto& origBN = cm.observationalBN();
83 CausalModel< GUM_ELEMENT > twincm(origBN); // copy (serves as clone)
84
85 // 1) idiosyncratic = parentless \ (whatif ∪ latent)
86 NodeSet whatifIds;
87 for (const auto& w: whatif) {
88 whatifIds.insert(cm.idFromName(w));
89 }
90 NodeSet idiosyncratic;
91 for (auto nid: origBN.nodes()) {
92 if (origBN.parents(nid).size() == 0) { idiosyncratic.insert(nid); }
93 }
94 for (auto id: whatifIds) {
95 idiosyncratic.erase(id);
96 }
97 for (auto id: cm.latentVariablesIds()) {
98 idiosyncratic.erase(id);
99 }
100
101 // 2) posterior(idiosyncratic | profile) in ORIGINAL BN
103 if (!profile.empty()) {
104 for (const auto& kv: profile) {
105 const auto& varName = kv.first;
106 const auto& valName = kv.second;
107 const NodeId nid = cm.idFromName(varName);
108 const auto& var = origBN.variable(nid);
109 const Idx valId = var.index(valName);
110 ie.addEvidence(nid, valId);
111 }
112 }
113 ie.makeInference();
114 // 3) Replace priors in the twin with posteriors from the original
115 auto& twinBN = twincm.observationalBN();
116 for (auto f: idiosyncratic) {
117 const auto& name = origBN.variable(f).name();
118 auto twinId = twinBN.idFromName(name);
119 const Tensor< GUM_ELEMENT > post = ie.posterior(f);
120 twinBN.cpt(twinId).fillWith(post);
121 }
122 return twincm;
123 }
124
125 template < GUM_Numeric GUM_ELEMENT >
128 const HashTable< NodeId, Idx >& profileIds,
129 const NodeSet& whatifIds) {
130 const auto& origBN = cm.observationalBN();
131 // 1) idiosyncratic = parentless \ (whatif ∪ latent)
132 NodeSet idiosyncratic;
133 for (auto nid: origBN.nodes()) {
134 if (origBN.parents(nid).size() == 0) { idiosyncratic.insert(nid); }
135 }
136 for (auto id: whatifIds) {
137 idiosyncratic.erase(id);
138 }
139 for (auto id: cm.latentVariablesIds()) {
140 idiosyncratic.erase(id);
141 }
142
143 // 2) posterior(idiosyncratic | profile) in ORIGINAL BN
144 CausalModel< GUM_ELEMENT > twincm(origBN);
145 {
147 if (!profileIds.empty()) {
148 for (const auto& kv: profileIds) {
149 const NodeId nid = kv.first;
150 const Idx valId = kv.second;
151 ie.addEvidence(nid, valId);
152 }
153 }
154 ie.makeInference();
155 auto& twinBN = twincm.observationalBN();
156 for (auto f: idiosyncratic) {
157 const auto& name = origBN.variable(f).name();
158 const NodeId twinId = twinBN.idFromName(name);
159 const Tensor< GUM_ELEMENT > post = ie.posterior(f);
160 twinBN.cpt(twinId).fillWith(post);
161 }
162 }
163 return twincm;
164 }
165
166 // ================================ EXECUTION ================================
167
168 template < GUM_Numeric GUM_ELEMENT >
170 // Build symbolic effect on the twin
171 _ci = std::make_unique< CausalImpact< GUM_ELEMENT > >(_twin, _on, _whatif);
172
173 // Numeric evaluation on the twin
174 Tensor< GUM_ELEMENT > adj = _ci->eval();
175
176 // Slice by specified intervention values (partial instantiation, safe if some names are absent)
177 if (!_values.empty() && adj.nbrDim() > 0) {
178 auto I = makeInstantiationFromValues(adj, _values); // keeps only vars present in `adj`
179 if (I.nbrDim() > 0) adj = adj.extract(I);
180 }
181
182 // Adapt tensor to ORIGINAL model variables
184 }
185
186 // =============================== ADAPTATION ================================
187
188 template < GUM_Numeric GUM_ELEMENT >
190 const Tensor< GUM_ELEMENT >& adj,
191 const CausalModel< GUM_ELEMENT >& cm) {
193 const auto& bn = cm.observationalBN();
194
195 // keep adj variable order
196 for (const auto& v: adj.variablesSequence()) {
197 res.add(bn.variableFromName(v->name()));
198 }
199 res.fillWith(adj);
200 return res;
201 }
202
203 // ============================ RESULT ACCESSOR ==============================
204
205 template < GUM_Numeric GUM_ELEMENT >
207 return _ci->getResult(); // public accessor in CausalImpact
208 }
209
210 // ================================= PRINT ==================================
211
212 template < GUM_Numeric GUM_ELEMENT >
214 std::stringstream os;
215 os << "[Counterfactual]\n";
216
217 os << " on = {";
218 {
219 bool first = true;
220 for (const auto& s: _on) {
221 if (!first) os << ", ";
222 os << s;
223 first = false;
224 }
225 }
226 os << "}\n whatif = {";
227 {
228 bool first = true;
229 for (const auto& s: _whatif) {
230 if (!first) os << ", ";
231 os << s;
232 first = false;
233 }
234 }
235 os << "}\n";
236
237 if (!_profile.empty()) {
238 os << " profile: ";
239 bool first = true;
240 for (const auto& [k, v]: _profile) {
241 if (!first) os << ", ";
242 os << k << "=" << v;
243 first = false;
244 }
245 os << "\n";
246 }
247
248 if (!_values.empty()) {
249 os << " values: ";
250 bool first = true;
251 for (const auto& [k, v]: _values) {
252 if (!first) os << ", ";
253 os << k << "=" << v;
254 first = false;
255 }
256 os << "\n";
257 }
258
259 os << " result (symbolic): ";
260 if (_ci && _ci->isIdentified()) os << _ciResult().toString() << "\n";
261 else os << "<unidentified>\n";
262
263 if (_adaptedValue.nbrDim() > 0) {
264 os << " value (adapted to original variables):\n";
265 os << _adaptedValue << "\n";
266 }
267
268 return os.str();
269 }
270
271 // ============================== HELPERS (IDs) ==============================
272
273 template < GUM_Numeric GUM_ELEMENT >
276 const NodeSet& ids) {
278 const auto& bn = cm.observationalBN();
279 for (auto nid: ids) {
280 const auto& var = bn.variable(nid); // reference by id
281 out.insert(var.name());
282 }
283 return out;
284 }
285
286 template < GUM_Numeric GUM_ELEMENT >
291 const HashTable< NodeId, Idx >& idAssign) {
293 const auto& bn = cm.observationalBN();
294 for (const auto& kv: idAssign) {
295 const NodeId nid = kv.first;
296 const Idx valId = kv.second;
297 const auto& var = bn.variable(nid); // reference by id
298 out.insert(var.name(), var.label(valId));
299 }
300 return out;
301 }
302
303 // ============================================================================
304 // Standalone helpers (Python-parity)
305 // ============================================================================
306
307 template < GUM_Numeric GUM_ELEMENT >
309 const Set< std::string >& on,
310 const Set< std::string >& whatif,
313 // Use the class implementation and return the adapted tensor.
314 Counterfactual< GUM_ELEMENT > cf(cm, on, whatif, profile, values);
315 return cf.value();
316 }
317
318 template < GUM_Numeric GUM_ELEMENT >
319 CausalModel< GUM_ELEMENT >
322 const Set< std::string >& whatif) {
323 const auto& origBN = cm.observationalBN();
324
325 // Clone model (copy semantics)
326 CausalModel< GUM_ELEMENT > twincm(origBN);
327
328 // what-if ids
329 NodeSet whatifIds;
330 for (const auto& w: whatif)
331 whatifIds.insert(cm.idFromName(w));
332
333 // idiosyncratic = parentless \ (whatif ∪ latent)
334 NodeSet idiosyncratic;
335 for (auto nid: origBN.nodes())
336 if (origBN.parents(nid).size() == 0) idiosyncratic.insert(nid);
337 for (auto id: whatifIds)
338 idiosyncratic.erase(id);
339 for (auto id: cm.latentVariablesIds())
340 idiosyncratic.erase(id);
341
342 // Posteriors in the original BN
344 if (!profile.empty()) {
345 for (const auto& kv: profile) {
346 const NodeId nid = cm.idFromName(kv.first);
347 const auto& var = origBN.variable(nid);
348 const Idx valId = var.index(kv.second);
349 ie.addEvidence(nid, valId);
350 }
351 }
352 ie.makeInference();
353
354 // Update priors in the twin with posteriors
355 auto& twinBN = twincm.observationalBN();
356 for (auto f: idiosyncratic) {
357 const auto& name = origBN.variable(f).name();
358 const NodeId twinId = twinBN.idFromName(name);
359 const Tensor< GUM_ELEMENT > post = ie.posterior(f);
360 twinBN.cpt(twinId).fillWith(post);
361 }
362
363 return twincm;
364 }
365
366 // ================================= ACCESSORS ==================================
367
368 template < GUM_Numeric GUM_ELEMENT >
372
373 template < GUM_Numeric GUM_ELEMENT >
377
378 template < GUM_Numeric GUM_ELEMENT >
382
383 template < GUM_Numeric GUM_ELEMENT >
387
388 template < GUM_Numeric GUM_ELEMENT >
390 return _on;
391 }
392
393 template < GUM_Numeric GUM_ELEMENT >
397
398 template < GUM_Numeric GUM_ELEMENT >
404
405 template < GUM_Numeric GUM_ELEMENT >
411
412} // namespace gum
A container for an identified causal query.
NodeSet latentVariablesIds() const
Node ids of all latent variables.
NodeId idFromName(std::string_view name) const
Node id from variable name (observed or latent).
const BayesNet< GUM_SCALAR > & observationalBN() const
Observational BN (observed variables only).
Computes a counterfactual distribution by building a twin model, then evaluating a causal effect on t...
CausalModel< GUM_ELEMENT > _twin
const Set< std::string > & on() const
std::unique_ptr< CausalImpact< GUM_ELEMENT > > _ci
HashTable< VarName, ValName > _profile
const CausalFormula< GUM_ELEMENT > & _ciResult() const
HashTable< VarName, ValName > _values
const CausalModel< GUM_ELEMENT > & twinModel() const
const CausalModel< GUM_ELEMENT > & originalModel() const
Tensor< GUM_ELEMENT > _adaptedValue
const Set< std::string > & whatif() const
static CausalModel< GUM_ELEMENT > counterFactualModel(const CausalModel< GUM_ELEMENT > &cm, const HashTable< VarName, ValName > &profile, const Set< std::string > &whatif)
static Tensor< GUM_ELEMENT > _adaptToOriginalVariables_(const Tensor< GUM_ELEMENT > &adj, const CausalModel< GUM_ELEMENT > &cm)
static HashTable< VarName, ValName > _idAssignToNameAssign_(const CausalModel< GUM_ELEMENT > &cm, const HashTable< NodeId, Idx > &idAssign)
const Tensor< GUM_ELEMENT > & value() const
Numeric result adapted to the original model’s variables.
static Set< std::string > _idsToNames_(const CausalModel< GUM_ELEMENT > &cm, const NodeSet &ids)
const HashTable< VarName, ValName > & values() const
const CausalImpact< GUM_ELEMENT > & impact() const
Symbolic result produced on the twin model.
std::string toString() const
const CausalModel< GUM_ELEMENT > & _cm
const HashTable< VarName, ValName > & profile() const
Set< std::string > _whatif
void run()
Rebuild the internal CausalImpact (ctor already calls this).
Counterfactual(const CausalModel< GUM_ELEMENT > &cm, const Set< std::string > &on, const Set< std::string > &whatif, const HashTable< VarName, ValName > &profile=HashTable< VarName, ValName >(), const HashTable< VarName, ValName > &values=HashTable< VarName, ValName >())
Construct a counterfactual query using variable names.
Set< std::string > _on
virtual void makeInference() final
perform the heavy computations needed to compute the targets' posteriors
virtual void addEvidence(NodeId id, const Idx val) final
adds a new hard evidence on node id
value_type & insert(const Key &key, const Val &val)
Adds a new element (actually a copy of this element) into the hash table.
bool empty() const noexcept
Indicates whether the hash table is empty.
const Tensor< GUM_SCALAR > & posterior(NodeId node) final
Computes and returns the posterior of a node.
Implementation of a Shafer-Shenoy's-like version of lazy propagation for inference in Bayesian networ...
const Sequence< const DiscreteVariable * > & variablesSequence() const final
void add(const DiscreteVariable &v) final
void insert(const Key &k)
Inserts a new element into the set.
Definition set_tpl.h:510
void erase(const Key &k)
Erases an element from the set.
Definition set_tpl.h:553
const Tensor< GUM_SCALAR > & fillWith(const Tensor< GUM_SCALAR > &src) const
copy a Tensor data using name of variables and labels (not necessarily the same variables in the same...
Definition tensor_tpl.h:304
Size Idx
Type for indexes.
Definition types.h:79
Size NodeId
Type for node ids.
Set< NodeId > NodeSet
Some typdefs and define for shortcuts ...
gum is the global namespace for all aGrUM entities
Definition agrum.h:46
CausalModel< GUM_ELEMENT > counterfactualModel(const CausalModel< GUM_ELEMENT > &cm, const HashTable< std::string, std::string > &profile, const Set< std::string > &whatif)
Build the twin causal model.
Tensor< GUM_ELEMENT > counterfactual(const CausalModel< GUM_ELEMENT > &cm, const Set< std::string > &on, const Set< std::string > &whatif, const HashTable< std::string, std::string > &profile=HashTable< std::string, std::string >(), const HashTable< std::string, std::string > &values=HashTable< std::string, std::string >())
Compute a counterfactual distribution using the high-level API.
Instantiation makeInstantiationFromValues(const Tensor< GUM_SCALAR > &tensor, const HashTable< std::string, std::string > &values)
Creates an instance for a tensor based on a HashTable.