Repository: pgmpy/pgmpy

Issue:
[BUG] SHD calculation fails when the 2 models have different edges
**Describe the bug**
SHD computation fails when `est_model` does not have all the edges present in `true_model`, even when they have the same nodes. It's happening because the computation uses the edges of the models and fails to account for nodes that may not be present in the edges, which I think should be accounted for.

**To Reproduce**
<!--
Add a Minimal, Complete, and Verifiable example (for more details, see e.g. https://stackoverflow.com/help/mcve

If the code is too long, feel free to put it in a public gist and link it in the issue: https://gist.github.com
-->

```python
>>> from pgmpy.metrics import SHD
>>> from pgmpy.models import DiscreteBayesianNetwork    
>>> dag1 = DiscreteBayesianNetwork(ebunch=[(1,2)])
>>> dag1.add_nodes_from([3])
>>> dag2 = DiscreteBayesianNetwork(ebunch=[(1,2), (2,3)])    
>>> SHD(dag2, dag1)
.
.
.
    raise nx.NetworkXError(f"Node {n} in nodelist is not in G")
networkx.exception.NetworkXError: Node 3 in nodelist is not in G
```

**Expected behavior**
The SHD method should account for edges (from/to nodes) that are not present in the `est_model`, by adding the remaining nodes.



Discussion:
Nimish-4: The formatting checks are failing. You can take a look into `.pre-commit-config.yaml` , and configure `black`, `isort` etc. as pre commit hooks to pass the formatting checks. Could also just run them manually from the terminal.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pgmpy/pgmpy/pull/2098?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy) Report
Attention: Patch coverage is `14.28571%` with `6 lines` in your changes missing coverage. Please review.
> Project coverage is 34.12%. Comparing base [(`2bba5dc`)](https://app.codecov.io/gh/pgmpy/pgmpy/commit/2bba5dc71b8d0e0019bb28733605439352d13a79?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy) to head [(`644f492`)](https://app.codecov.io/gh/pgmpy/pgmpy/commit/644f4922342f3b06a10e92ca76097e369f49476b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy).
> Report is 10 commits behind head on dev.

| [Files with missing lines](https://app.codecov.io/gh/pgmpy/pgmpy/pull/2098?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy) | Patch % | Lines |
|---|---|---|
| [pgmpy/tests/test\_metrics/test\_metrics.py](https://app.codecov.io/gh/pgmpy/pgmpy/pull/2098?src=pr&el=tree&filepath=pgmpy%2Ftests%2Ftest_metrics%2Ftest_metrics.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy#diff-cGdtcHkvdGVzdHMvdGVzdF9tZXRyaWNzL3Rlc3RfbWV0cmljcy5weQ==) | 20.00% | [4 Missing :warning: ](https://app.codecov.io/gh/pgmpy/pgmpy/pull/2098?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy) |
| [pgmpy/metrics/metrics.py](https://app.codecov.io/gh/pgmpy/pgmpy/pull/2098?src=pr&el=tree&filepath=pgmpy%2Fmetrics%2Fmetrics.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy#diff-cGdtcHkvbWV0cmljcy9tZXRyaWNzLnB5) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/pgmpy/pgmpy/pull/2098?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy) |

> :exclamation:  There is a different number of reports uploaded between BASE (2bba5dc) and HEAD (644f492). Click for more details.
> 
> <details><summary>HEAD has 17 uploads less than BASE</summary>
>
>| Flag | BASE (2bba5dc) | HEAD (644f492) |
>|------|------|------|
>||18|1|
></details>

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##              dev    #2098       +/-   ##
===========================================
- Coverage   94.59%   34.12%   -60.48%     
===========================================
  Files         149      149               
  Lines       20155    20594      +439     
===========================================
- Hits        19065     7027    -12038     
- Misses       1090    13567    +12477     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pgmpy/pgmpy/pull/2098?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy).

yashnator: @Nimish-4 I fixed the linting errors. Let me know if there are more changes
Nimish-4: @yashnator Looks good, thank you for the fix. @ankurankan will approve the changes.

Fix (patch):
diff --git a/pgmpy/metrics/metrics.py b/pgmpy/metrics/metrics.py
index 3017b249..c9a25138 100644
--- a/pgmpy/metrics/metrics.py
+++ b/pgmpy/metrics/metrics.py
@@ -439,9 +439,11 @@ def SHD(true_model, est_model):
     nodes_list = true_model.nodes()
 
     dag_true = nx.DiGraph(true_model.edges())
+    dag_true.add_nodes_from(list(nx.isolates(true_model)))
     m1 = nx.adjacency_matrix(dag_true, nodelist=nodes_list).todense()
 
     dag_est = nx.DiGraph(est_model.edges())
+    dag_est.add_nodes_from(list(nx.isolates(est_model)))
     m2 = nx.adjacency_matrix(dag_est, nodelist=nodes_list).todense()
 
     shd = 0


Test changes:
diff --git a/pgmpy/tests/test_metrics/test_metrics.py b/pgmpy/tests/test_metrics/test_metrics.py
index 995ea2aa..6f9b8c1a 100644
--- a/pgmpy/tests/test_metrics/test_metrics.py
+++ b/pgmpy/tests/test_metrics/test_metrics.py
@@ -184,31 +184,43 @@ class TestImpliedCI(unittest.TestCase):
 
 
 class TestStructuralHammingDistance(unittest.TestCase):
-    def setUp(self):
-        self.dag_1 = DiscreteBayesianNetwork([(1, 2)])
-        self.dag_2 = DiscreteBayesianNetwork([(2, 1)])
+    def test_shd1(self):
+        dag1 = DiscreteBayesianNetwork([(1, 2)])
+        dag2 = DiscreteBayesianNetwork([(2, 1)])
+        self.assertEqual(SHD(dag1, dag2), 1)
 
-        self.dag_3 = DiscreteBayesianNetwork([(1, 2), (2, 4), (1, 3), (3, 4)])
-        self.dag_4 = DiscreteBayesianNetwork([(1, 2), (1, 3), (3, 2), (3, 4)])
+    def test_shd2(self):
+        dag1 = DiscreteBayesianNetwork([(1, 2), (2, 4), (1, 3), (3, 4)])
+        dag2 = DiscreteBayesianNetwork([(1, 2), (1, 3), (3, 2), (3, 4)])
+        self.assertEqual(SHD(dag1, dag2), 2)
 
-        self.dag_5 = DiscreteBayesianNetwork([(1, 2), (1, 3), (3, 2), (3, 5)])
+    def test_shd3(self):
+        dag1 = DiscreteBayesianNetwork([(1, 2), (1, 3), (2, 4), (3, 5), (4, 5), (5, 6)])
+        dag2 = DiscreteBayesianNetwork([(1, 2), (1, 3), (4, 2), (3, 5), (4, 6), (5, 6)])
+        self.assertEqual(SHD(dag1, dag2), 3)
 
-        self.large_dag_1 = DiscreteBayesianNetwork(
-            [(1, 2), (1, 3), (2, 4), (3, 5), (4, 5), (5, 6)]
-        )
-        self.large_dag_2 = DiscreteBayesianNetwork(
-            [(1, 2), (1, 3), (4, 2), (3, 5), (4, 6), (5, 6)]
-        )
+    def test_shd_isolated_nodes(self):
+        dag1 = DiscreteBayesianNetwork([(1, 2)])
+        dag1.add_nodes_from([3])
+        dag2 = DiscreteBayesianNetwork([(1, 2), (2, 3)])
 
-    def test_shd(self):
-        self.assertEqual(SHD(self.dag_1, self.dag_2), 1)
+        self.assertEqual(SHD(dag1, dag2), 1)
+        self.assertEqual(SHD(dag2, dag1), 1)
 
-    def test_shd(self):
-        self.assertEqual(SHD(self.dag_3, self.dag_4), 2)
+    def test_shd_mixed_differences(self):
+        dag1 = DiscreteBayesianNetwork([(1, 2), (2, 3), (2, 4), (4, 5), (6, 5), (7, 8)])
+        dag1.add_nodes_from([9, 10])
+        dag2 = DiscreteBayesianNetwork(
+            [(1, 2), (2, 4), (5, 4), (6, 5), (8, 7), (9, 10)]
+        )
+        dag2.add_nodes_from([3, 7])
 
-    def test_shd(self):
-        self.assertEqual(SHD(self.large_dag_1, self.large_dag_2), 3)
+        self.assertEqual(SHD(dag1, dag2), 4)
+        self.assertEqual(SHD(dag2, dag1), 4)
 
     def test_shd_unequal_graphs(self):
+        dag1 = DiscreteBayesianNetwork([(1, 2), (1, 3), (3, 2), (3, 4)])
+        dag2 = DiscreteBayesianNetwork([(1, 2), (1, 3), (3, 2), (3, 5)])
+
         with self.assertRaises(ValueError, msg="The graphs must have the same nodes."):
-            SHD(self.dag_4, self.dag_5)
+            SHD(dag1, dag2)