Repository: pgmpy/pgmpy

Issue:
CausalInference misbehaves when state_name is integer.
get_all_backdoor_adjustment_sets is behaving inconsistently when state_name is integer.

```
BN1 = BayesianNetwork([(1,2),(1,3),(2,4),(1,4),(1,5)])
CI1 = CausalInference(BN1)
CI1.get_all_backdoor_adjustment_sets(2,4)
```
gives an error as below:

```
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[11], [line 3](vscode-notebook-cell:?execution_count=11&line=3)
      [1](vscode-notebook-cell:?execution_count=11&line=1) BN1 = BayesianNetwork([(1,2),(1,3),(2,4),(1,4),(1,5)])
      [2](vscode-notebook-cell:?execution_count=11&line=2) CI1 = CausalInference(BN1)
----> [3](vscode-notebook-cell:?execution_count=11&line=3) CI1.get_all_backdoor_adjustment_sets(2,4)

File ~/codes/pgmpy/pgmpy/inference/CausalInference.py:158, in CausalInference.get_all_backdoor_adjustment_sets(self, X, Y)
    [156](https://file+.vscode-resource.vscode-cdn.net/Users/jihyeseo/codes/pgmpy/~/codes/pgmpy/pgmpy/inference/CausalInference.py:156)     if any(super_of_complete):
    [157](https://file+.vscode-resource.vscode-cdn.net/Users/jihyeseo/codes/pgmpy/~/codes/pgmpy/pgmpy/inference/CausalInference.py:157)         continue
--> [158](https://file+.vscode-resource.vscode-cdn.net/Users/jihyeseo/codes/pgmpy/~/codes/pgmpy/pgmpy/inference/CausalInference.py:158)     if self.is_valid_backdoor_adjustment_set(X, Y, s):
    [159](https://file+.vscode-resource.vscode-cdn.net/Users/jihyeseo/codes/pgmpy/~/codes/pgmpy/pgmpy/inference/CausalInference.py:159)         valid_adjustment_sets.append(frozenset(s))
    [161](https://file+.vscode-resource.vscode-cdn.net/Users/jihyeseo/codes/pgmpy/~/codes/pgmpy/pgmpy/inference/CausalInference.py:161) if len(valid_adjustment_sets) == 0:

File ~/codes/pgmpy/pgmpy/inference/CausalInference.py:95, in CausalInference.is_valid_backdoor_adjustment_set(self, X, Y, Z)
     [65](https://file+.vscode-resource.vscode-cdn.net/Users/jihyeseo/codes/pgmpy/~/codes/pgmpy/pgmpy/inference/CausalInference.py:65) def is_valid_backdoor_adjustment_set(self, X, Y, Z=[]):
     [66](https://file+.vscode-resource.vscode-cdn.net/Users/jihyeseo/codes/pgmpy/~/codes/pgmpy/pgmpy/inference/CausalInference.py:66)     """
     [67](https://file+.vscode-resource.vscode-cdn.net/Users/jihyeseo/codes/pgmpy/~/codes/pgmpy/pgmpy/inference/CausalInference.py:67)     Test whether Z is a valid backdoor adjustment set for estimating the causal impact of X on Y.
     [68](https://file+.vscode-resource.vscode-cdn.net/Users/jihyeseo/codes/pgmpy/~/codes/pgmpy/pgmpy/inference/CausalInference.py:68) 
   (...)
     [93](https://file+.vscode-resource.vscode-cdn.net/Users/jihyeseo/codes/pgmpy/~/codes/pgmpy/pgmpy/inference/CausalInference.py:93)     True
     [94](https://file+.vscode-resource.vscode-cdn.net/Users/jihyeseo/codes/pgmpy/~/codes/pgmpy/pgmpy/inference/CausalInference.py:94)     """
---> [95](https://file+.vscode-resource.vscode-cdn.net/Users/jihyeseo/codes/pgmpy/~/codes/pgmpy/pgmpy/inference/CausalInference.py:95)     Z_ = _variable_or_iterable_to_set(Z)
     [97](https://file+.vscode-resource.vscode-cdn.net/Users/jihyeseo/codes/pgmpy/~/codes/pgmpy/pgmpy/inference/CausalInference.py:97)     observed = [X] + list(Z_)
     [98](https://file+.vscode-resource.vscode-cdn.net/Users/jihyeseo/codes/pgmpy/~/codes/pgmpy/pgmpy/inference/CausalInference.py:98)     parents_d_sep = []
...
     [27](https://file+.vscode-resource.vscode-cdn.net/Users/jihyeseo/codes/pgmpy/~/codes/pgmpy/pgmpy/utils/sets.py:27)         f"{x} is expected to be either a string, set of strings, or an iterable of strings"
     [28](https://file+.vscode-resource.vscode-cdn.net/Users/jihyeseo/codes/pgmpy/~/codes/pgmpy/pgmpy/utils/sets.py:28)     )
     [30](https://file+.vscode-resource.vscode-cdn.net/Users/jihyeseo/codes/pgmpy/~/codes/pgmpy/pgmpy/utils/sets.py:30) return frozenset(x)

ValueError: (1,) is expected to be either a string, set of strings, or an iterable of strings
```

If I pass all state_name as string, I get a different result.

```
BN2 = BayesianNetwork([('1','2'),('1','3'),('2','4'),('1','4'),('1','5')])
CI2 = CausalInference(BN2)
CI2.get_all_backdoor_adjustment_sets('2','4')
# frozenset({frozenset({'1'})})
```

Might be related to https://github.com/pgmpy/pgmpy/issues/1877

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pgmpy/pgmpy/pull/1945?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 `23.80952%` with `16 lines` in your changes missing coverage. Please review.
> Project coverage is 34.38%. Comparing base [(`720b0ae`)](https://app.codecov.io/gh/pgmpy/pgmpy/commit/720b0ae4fd57b4f8a9ebeb076ea860e799f4d39d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy) to head [(`cce2705`)](https://app.codecov.io/gh/pgmpy/pgmpy/commit/cce27052e63878d47a11b11b4256d0c515941a53?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy).
> Report is 23 commits behind head on dev.

| [Files with missing lines](https://app.codecov.io/gh/pgmpy/pgmpy/pull/1945?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\_base/test\_DAG.py](https://app.codecov.io/gh/pgmpy/pgmpy/pull/1945?src=pr&el=tree&filepath=pgmpy%2Ftests%2Ftest_base%2Ftest_DAG.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy#diff-cGdtcHkvdGVzdHMvdGVzdF9iYXNlL3Rlc3RfREFHLnB5) | 25.00% | [6 Missing :warning: ](https://app.codecov.io/gh/pgmpy/pgmpy/pull/1945?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy) |
| [pgmpy/base/DAG.py](https://app.codecov.io/gh/pgmpy/pgmpy/pull/1945?src=pr&el=tree&filepath=pgmpy%2Fbase%2FDAG.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy#diff-cGdtcHkvYmFzZS9EQUcucHk=) | 20.00% | [4 Missing :warning: ](https://app.codecov.io/gh/pgmpy/pgmpy/pull/1945?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy) |
| [pgmpy/inference/CausalInference.py](https://app.codecov.io/gh/pgmpy/pgmpy/pull/1945?src=pr&el=tree&filepath=pgmpy%2Finference%2FCausalInference.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy#diff-cGdtcHkvaW5mZXJlbmNlL0NhdXNhbEluZmVyZW5jZS5weQ==) | 0.00% | [3 Missing :warning: ](https://app.codecov.io/gh/pgmpy/pgmpy/pull/1945?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy) |
| [pgmpy/tests/test\_inference/test\_CausalInference.py](https://app.codecov.io/gh/pgmpy/pgmpy/pull/1945?src=pr&el=tree&filepath=pgmpy%2Ftests%2Ftest_inference%2Ftest_CausalInference.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy#diff-cGdtcHkvdGVzdHMvdGVzdF9pbmZlcmVuY2UvdGVzdF9DYXVzYWxJbmZlcmVuY2UucHk=) | 40.00% | [3 Missing :warning: ](https://app.codecov.io/gh/pgmpy/pgmpy/pull/1945?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 (720b0ae) and HEAD (cce2705). Click for more details.
> 
> <details><summary>HEAD has 17 uploads less than BASE</summary>
>
>| Flag | BASE (720b0ae) | HEAD (cce2705) |
>|------|------|------|
>||18|1|
></details>

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


```diff
@@             Coverage Diff             @@
##              dev    #1945       +/-   ##
===========================================
- Coverage   93.61%   34.38%   -59.23%     
===========================================
  Files         143      146        +3     
  Lines       18455    18930      +475     
===========================================
- Hits        17277     6510    -10767     
- Misses       1178    12420    +11242     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pgmpy/pgmpy/pull/1945?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).


Patch:
diff --git a/pgmpy/base/DAG.py b/pgmpy/base/DAG.py
index 8ab2af63..2218c071 100644
--- a/pgmpy/base/DAG.py
+++ b/pgmpy/base/DAG.py
@@ -1244,6 +1244,15 @@ class DAG(nx.DiGraph):
         bn.add_cpds(*cpds_list)
         return bn
 
+    def _variable_name_contains_non_string(self):
+        """
+        Checks if the variable names contain any non-string values. Used only for CausalInference class.
+        """
+        for node in list(self.nodes()):
+            if not isinstance(node, str):
+                return (node, type(node))
+        return False
+
 
 class PDAG(nx.DiGraph):
     """
diff --git a/pgmpy/factors/discrete/CPD.py b/pgmpy/factors/discrete/CPD.py
index 4a42e46b..bd7714a6 100644
--- a/pgmpy/factors/discrete/CPD.py
+++ b/pgmpy/factors/discrete/CPD.py
@@ -6,6 +6,7 @@ from itertools import chain, product
 from shutil import get_terminal_size
 from warnings import warn
 
+import pandas as pd
 import numpy as np
 import torch
 
@@ -314,6 +315,45 @@ class TabularCPD(DiscreteFactor):
             writer = csv.writer(f)
             writer.writerows(self._make_table_str(tablefmt="grid", return_list=True))
 
+    def to_dataframe(self):
+        """
+        Exports the CPD as a pandas dataframe.
+
+        Examples
+        --------
+        >>> from pgmpy.utils import get_example_model
+        >>> model = get_example_model("insurance")
+        >>> cpd = model.get_cpds("ThisCarCost")
+        >>> df = cpd.to_dataframe()
+        >>> df.query("CarValue=='FiftyThou' and Theft == 'True'")
+        ThisCarCost                 HundredThou  Million   TenThou  Thousand
+        ThisCarDam CarValue  Theft
+        Mild       FiftyThou True      0.950000      0.0  0.020000  0.030000
+        Moderate   FiftyThou True      0.998000      0.0  0.001000  0.001000
+        None       FiftyThou True      0.950000      0.0  0.010000  0.040000
+        Severe     FiftyThou True      0.999998      0.0  0.000001  0.000001
+        >>> # Probability sums up to zero, for every combination of evidence variables
+        >>> df.sum(axis=1)
+        ThisCarDam  CarValue    Theft
+        Mild        FiftyThou   False    1.0
+                                True     1.0
+                    FiveThou    False    1.0
+                                True     1.0
+                    Million     False    1.0
+                                True     1.0
+        """
+        state_combinations_with_all_variables = pd.MultiIndex.from_product(
+            [self.state_names[var] for var in self.variables], names=self.variables
+        )
+        df_with_1_column = pd.DataFrame(
+            {"probability": self.values.flatten()},
+            index=state_combinations_with_all_variables,
+        )
+        df_with_prob_rowsum_to_1 = df_with_1_column["probability"].unstack(
+            self.variable
+        )
+        return df_with_prob_rowsum_to_1
+
     def copy(self):
         """
         Returns a copy of the `TabularCPD` object.
diff --git a/pgmpy/inference/CausalInference.py b/pgmpy/inference/CausalInference.py
index e7f0e187..d24857fb 100644
--- a/pgmpy/inference/CausalInference.py
+++ b/pgmpy/inference/CausalInference.py
@@ -52,6 +52,11 @@ class CausalInference(object):
             raise NotImplementedError(
                 "Causal Inference is only implemented for BayesianNetworks at this time."
             )
+        bad_variable = model._variable_name_contains_non_string()
+        if bad_variable != False:
+            raise NotImplementedError(
+                f"Causal Inference is only implemented for a model with variable names with string type. Found {bad_variable[0]} with type {bad_variable[1]}. Convert them to string to proceed."
+            )
         self.model = model
         self.set_nodes = _variable_or_iterable_to_set(set_nodes)
         self.observed_variables = frozenset(self.model.nodes()).difference(


Test patch:
diff --git a/pgmpy/tests/test_base/test_DAG.py b/pgmpy/tests/test_base/test_DAG.py
index 9e00086c..9498dc3b 100644
--- a/pgmpy/tests/test_base/test_DAG.py
+++ b/pgmpy/tests/test_base/test_DAG.py
@@ -60,6 +60,16 @@ class TestDAGCreation(unittest.TestCase):
         self.assertListEqual(sorted(self.graph.nodes()), [0, 1])
         self.assertEqual(self.graph.adj[0][1]["weight"], {"weight": 3})  # None
 
+    def test_variable_name_contains_non_string_adj_matrix(self):
+        df = pd.DataFrame([[0, 3], [0, 0]])
+        self.graph = DAG(df)
+        self.assertEqual(self.graph._variable_name_contains_non_string(), (0, int))
+
+    def test_variable_name_contains_non_string_mixed_types(self):
+        self.graph = DAG([("a", "b"), ("b", "c"), ("a", 3.2)])
+        self.graph.nodes()
+        self.assertEqual(self.graph._variable_name_contains_non_string(), (3.2, float))
+
     def test_add_node_string(self):
         self.graph = DAG()
         self.graph.add_node("a")
diff --git a/pgmpy/tests/test_factors/test_discrete/test_Factor.py b/pgmpy/tests/test_factors/test_discrete/test_Factor.py
index ddf52bb0..9fbd3362 100644
--- a/pgmpy/tests/test_factors/test_discrete/test_Factor.py
+++ b/pgmpy/tests/test_factors/test_discrete/test_Factor.py
@@ -300,6 +300,18 @@ class TestFactorMethods(unittest.TestCase):
             )
             self.assertEqual(phi, phi_copy)
 
+    def test_to_dataframe(self):
+        model = get_example_model("sachs")
+        cpd = model.get_cpds("Mek")
+        df = cpd.to_dataframe()
+        self.assertEqual(df.shape, (27, 3))
+        self.assertEqual(df.index.shape, (27,))
+        self.assertEqual(
+            df.query('PKA=="AVG" and Raf =="HIGH" and PKC=="LOW"')["LOW"].values,
+            0.8652899,
+        )
+        np_test.assert_array_almost_equal(df.sum(axis=1).values, np.ones(27))
+
     def test_set_value(self):
         model = get_example_model("asia")
         cpd = model.get_cpds("either")
diff --git a/pgmpy/tests/test_inference/test_CausalInference.py b/pgmpy/tests/test_inference/test_CausalInference.py
index e15940ca..9e06477d 100644
--- a/pgmpy/tests/test_inference/test_CausalInference.py
+++ b/pgmpy/tests/test_inference/test_CausalInference.py
@@ -48,6 +48,13 @@ class TestCausalGraphMethods(unittest.TestCase):
         )
 
 
+class TestCausalInferenceInit(unittest.TestCase):
+    def test_integer_variable_name(self):
+        df = pd.DataFrame([[0, 1], [0, 0]])
+        self.model = BayesianNetwork(df)
+        self.assertRaises(NotImplementedError, CausalInference, self.model)
+
+
 class TestAdjustmentSet(unittest.TestCase):
     def setUp(self):
         # Model example taken from Constructing Separators and Adjustment Sets