aGrUM 3.1.1
a C++ library for (probabilistic) graphical models
structuralMetrics.cpp
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
43
44
45#ifndef DOXYGEN_SHOULD_SKIP_THIS
46
47namespace {
48 // 0/0 is conventionally reported as 0 (rather than NaN) for precision/recall/f_score.
49 double _safeRatio_(double numerator, double denom) {
50 return (denom == 0.0) ? 0.0 : numerator / denom;
51 }
52
53 double _safeFScore_(double precision, double recall) {
54 return _safeRatio_(2 * precision * recall, precision + recall);
55 }
56} // namespace
57
58namespace gum {
60
63
64 void StructuralMetrics::compare(const DiGraph& ref, const DiGraph& test) {
65 if (ref.size() != test.size()) { GUM_ERROR(OperationNotAllowed, "Graphs of different sizes") }
66 for (const NodeId node: ref.asNodeSet()) {
67 if (!test.existsNode(node)) {
68 GUM_ERROR(InvalidNode, "Test doesn't contain all nodes from ref")
69 }
70 }
71 _size_ = ref.size();
72 // compute the orientation matrix
73 // no edges so these stay null
74 _true_edge_ = 0;
79 // these will be filled
80 _true_arc_ = 0;
81 _true_none_ = 0;
85
86 for (const Arc& arc: ref.arcs()) {
87 if (test.existsArc(arc)) {
88 ++_true_arc_;
89 } else if (test.existsArc(arc.head(), arc.tail())) {
91 } else {
93 }
94 }
95 for (const Arc& arc: test.arcs()) {
96 if (!ref.existsArc(arc) && !ref.existsArc(arc.head(), arc.tail())) { ++_wrong_arc_none_; }
97 }
98 // TN = #possible arcs - #existing arcs
99 _true_none_ = ref.size() * (ref.size() - 1) - _true_arc_ - _misoriented_arc_ - _wrong_arc_none_
101 }
102
103 void StructuralMetrics::compare(const UndiGraph& ref, const UndiGraph& test) {
104 if (ref.size() != test.size()) { GUM_ERROR(OperationNotAllowed, "Graphs of different sizes") }
105 for (const NodeId node: ref.asNodeSet()) {
106 if (!test.existsNode(node)) {
107 GUM_ERROR(InvalidNode, "Test doesn't contain all nodes from ref")
108 }
109 }
110 _size_ = ref.size();
111 // compute the orientation matrix
112 // no arcs so these stay null
113 _true_arc_ = 0;
119 // these will be filled
120 _true_edge_ = 0;
121 _true_none_ = 0;
124
125 for (const Edge& edge: ref.edges()) {
126 if (test.existsEdge(edge)) {
127 ++_true_edge_;
128 } else {
130 }
131 }
132 for (const Edge& edge: test.edges()) {
133 if (!ref.existsEdge(edge)) { ++_wrong_edge_none_; }
134 }
135 // TN = #possible edges - #existing edges
137 = ref.size() * (ref.size() - 1) / 2 - _true_edge_ - _wrong_edge_none_ - _wrong_none_edge_;
138 }
139
140 void StructuralMetrics::compare(const PDAG& ref, const PDAG& test) {
141 if (ref.size() != test.size()) { GUM_ERROR(OperationNotAllowed, "Graphs of different sizes") }
142 for (const NodeId node: ref.asNodeSet()) {
143 if (!test.existsNode(node)) {
144 GUM_ERROR(InvalidNode, "Test doesn't contain all nodes from ref")
145 }
146 }
147 _size_ = ref.size();
148
149 // compute the orientation matrix
150 _true_arc_ = 0;
151 _true_edge_ = 0;
152 _true_none_ = 0;
160
161 for (const Arc& arc: ref.arcs()) {
162 if (test.existsArc(arc)) {
163 ++_true_arc_;
164 } else if (test.existsArc(arc.head(), arc.tail())) {
166 } else if (test.existsEdge(arc.tail(), arc.head())) {
168 } else {
170 }
171 }
172 for (const Edge& edge: ref.edges()) {
173 if (test.existsEdge(edge)) {
174 ++_true_edge_;
175 } else if (test.existsArc(edge.first(), edge.second())
176 || test.existsArc(edge.second(), edge.first())) {
178 } else {
180 }
181 }
182 for (const Arc& arc: test.arcs()) {
183 if (!ref.existsArc(arc) && !ref.existsArc(arc.head(), arc.tail())
184 && !ref.existsEdge(arc.tail(), arc.head())) {
186 }
187 }
188 for (const Edge& edge: test.edges()) {
189 if (!ref.existsEdge(edge) && !ref.existsArc(edge.first(), edge.second())
190 && !ref.existsArc(edge.second(), edge.first())) {
192 }
193 }
194 // TN = #possible edges - #existing edges
195 _true_none_ = ref.size() * (ref.size() - 1) / 2 - _true_edge_ - _wrong_edge_none_
198 }
199
200 double StructuralMetrics::tp_skeleton() const {
202 }
203
205
207
208 double StructuralMetrics::tn_skeleton() const {
209 // Cannot simply return _true_none_: it counts ordered pairs in compare(DiGraph,DiGraph)
210 // and unordered pairs in compare(UndiGraph/UndiGraph) and compare(PDAG/PDAG),
211 // so units are inconsistent.
212 return _size_ * (_size_ - 1) / 2.0 - tp_skeleton() - fp_skeleton() - fn_skeleton();
213 }
214
216 return _safeRatio_(tp_skeleton(), tp_skeleton() + fp_skeleton());
217 }
218
220 return _safeRatio_(tp_skeleton(), tp_skeleton() + fn_skeleton());
221 }
222
224 return _safeFScore_(precision_skeleton(), recall_skeleton());
225 }
226
227 double StructuralMetrics::shd_skeleton() const { return fp_skeleton() + fn_skeleton(); }
228
229 double StructuralMetrics::tp() const { return _true_arc_ + _true_edge_; }
230
231 double StructuralMetrics::fp() const {
234 }
235
236 double StructuralMetrics::fn() const { return _wrong_none_arc_ + _wrong_none_edge_; }
237
238 double StructuralMetrics::tn() const { return _true_none_; }
239
240 double StructuralMetrics::precision() const { return _safeRatio_(tp(), tp() + fp()); }
241
242 double StructuralMetrics::recall() const { return _safeRatio_(tp(), tp() + fn()); }
243
244 double StructuralMetrics::f_score() const { return _safeFScore_(precision(), recall()); }
245
246 double StructuralMetrics::shd() const { return fp() + fn(); }
247
248 double StructuralMetrics::sid(const DAG& ref, const DAG& test) const {
249 // Validate node sets match.
250 if (ref.size() != test.size()) { GUM_ERROR(OperationNotAllowed, "Graphs of different sizes") }
251 for (const NodeId node: ref.asNodeSet()) {
252 if (!test.existsNode(node)) {
253 GUM_ERROR(InvalidNode, "test does not contain all nodes from ref")
254 }
255 }
256
257 // Pre-compute (strict) descendants in ref. DE[i] does NOT include i itself.
259 for (const NodeId i: ref.asNodeSet()) {
260 DE.insert(i, ref.descendants(i));
261 }
262
263 // Working copy of ref for the back-door mutation (mutate / restore per pair).
264 DAG G = ref;
265
266 double errors = 0.0;
267
268 for (const NodeId i: ref.asNodeSet()) {
269 const NodeSet& paG = ref.parents(i);
270 const NodeSet& paH = test.parents(i);
271
272 if (paG == paH) continue;
273
274 for (const NodeId j: ref.asNodeSet()) {
275 if (j == i) continue;
276
277 const bool ijGNull = !DE[i].contains(j);
278 const bool ijHNull = paH.contains(j);
279
280 // Case 1 : H wrongly predicts a null effect.
281 if (!ijGNull && ijHNull) {
282 errors += 1;
283 continue;
284 }
285 // Case 2 : both predict null effect → OK.
286 if (ijGNull && ijHNull) { continue; }
287
288 // Part (2a) of Lemma 5 : no child c of i on a directed path to j
289 // can be in paH nor have descendants in paH.
290 bool violated_2a = false;
291 for (const NodeId c: ref.children(i)) {
292 if (c == j || DE[c].contains(j)) {
293 // c is on a directed path i → ... → j (reflexive case if c == j)
294 if (paH.contains(c)) {
295 violated_2a = true;
296 break;
297 }
298 for (const NodeId z: paH) {
299 if (DE[c].contains(z)) {
300 violated_2a = true;
301 break;
302 }
303 }
304 if (violated_2a) break;
305 }
306 }
307 if (violated_2a) {
308 errors += 1;
309 continue;
310 }
311
312 // Part (2b) : test d-separation in the mutated G,
313 // with arcs i → c removed for every c on a directed path to j.
314 std::vector< NodeId > arcs_removed;
315 for (const NodeId c: ref.children(i)) {
316 if (c == j || DE[c].contains(j)) {
317 G.eraseArc(Arc(i, c));
318 arcs_removed.push_back(c);
319 }
320 }
321
322 if (!G.dSeparation(i, j, paH)) { errors += 1; }
323
324 // Restore the removed arcs so that G is back to ref's structure.
325 for (const NodeId c: arcs_removed) {
326 G.addArc(i, c);
327 }
328 }
329 }
330
331 return errors;
332 }
333
334} /* namespace gum */
335
336#endif /* DOXYGEN_SHOULD_SKIP_THIS */
Base class for dag.
Definition DAG.h:121
Base class for all oriented graphs.
Definition diGraph.h:132
Base class for partially directed acyclic graphs.
Definition PDAG.h:130
double _wrong_edge_arc_
Confusion matrix.
StructuralMetrics()
default constructor
double fn_skeleton() const
Confusion matrix.
double tp() const
Measures for the graphs.
double _true_arc_
Confusion matrix.
double _misoriented_arc_
Confusion matrix.
double recall_skeleton() const
Confusion matrix.
double precision() const
Confusion matrix.
double shd_skeleton() const
Confusion matrix.
double recall() const
Confusion matrix.
double _wrong_edge_none_
Confusion matrix.
double fp() const
Confusion matrix.
double _wrong_arc_edge_
Confusion matrix.
double fn() const
Confusion matrix.
double _wrong_none_arc_
Confusion matrix.
double tn() const
Confusion matrix.
double fp_skeleton() const
Confusion matrix.
double precision_skeleton() const
Confusion matrix.
double f_score_skeleton() const
Confusion matrix.
double f_score() const
Confusion matrix.
double _wrong_arc_none_
Confusion matrix.
void compare(const DiGraph &ref, const DiGraph &test)
compare two DiGraphs (nodes matched by NodeId, no alignment)
double _true_edge_
Confusion matrix.
double _wrong_none_edge_
Confusion matrix.
~StructuralMetrics()
destructor
double _true_none_
Confusion matrix.
double sid(const DAG &ref, const DAG &test) const
Confusion matrix.
Size _size_
Number of nodes in the compared graphs (set by compare()).
double shd() const
Confusion matrix.
double tp_skeleton() const
Measures for the skeleton, aka graph without orientations.
double tn_skeleton() const
Confusion matrix.
Base class for undirected graphs.
Definition undiGraph.h:130
#define GUM_ERROR(type, msg)
Definition exceptions.h:76
Size NodeId
Type for node ids.
HashTable< NodeId, VAL > NodeProperty
Property on graph elements.
Set< NodeId > NodeSet
Some typdefs and define for shortcuts ...
bool contains(std::string_view s, std::string_view needle)
true if needle in s
gum is the global namespace for all aGrUM entities
Definition agrum.h:46
A class for comparing graphs based on their structures.