Repository: pgmpy/pgmpy

Issue:
Inconsistent reading and writing from files because of state names
By default, pgmpy assigns an `int` type state name to variables if no state names are explicitly specified. This creates a problem when these models are written to a file and read back. Since there is no way to differentiate between an `int` type state or `str` type state (i.e whether the state was `0` or `'0'` when written) when reading from the file, it can lead to unexpected results. The reader functions currently always by default assume the state to be of `str` type, so the state `0` when written and read back will be `'0'`.

A possible solution to this is to change the default state name to `str` type instead of `int`.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/pgmpy/pgmpy/pull/1285?src=pr&el=h1) Report
> Merging [#1285](https://codecov.io/gh/pgmpy/pgmpy/pull/1285?src=pr&el=desc) into [dev](https://codecov.io/gh/pgmpy/pgmpy/commit/f379c8c3ca58651f4309b20289a09e7636fc0157&el=desc) will **increase** coverage by `0.15%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/pgmpy/pgmpy/pull/1285/graphs/tree.svg?width=650&height=150&src=pr&token=UaJMCdHaEF)](https://codecov.io/gh/pgmpy/pgmpy/pull/1285?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##              dev    #1285      +/-   ##
==========================================
+ Coverage   92.41%   92.56%   +0.15%     
==========================================
  Files         130      130              
  Lines       12838    12836       -2     
==========================================
+ Hits        11864    11882      +18     
+ Misses        974      954      -20     
```


| [Impacted Files](https://codecov.io/gh/pgmpy/pgmpy/pull/1285?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pgmpy/readwrite/BIF.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1285/diff?src=pr&el=tree#diff-cGdtcHkvcmVhZHdyaXRlL0JJRi5weQ==) | `96.33% <100.00%> (+1.37%)` | :arrow_up: |
| [pgmpy/readwrite/XMLBIF.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1285/diff?src=pr&el=tree#diff-cGdtcHkvcmVhZHdyaXRlL1hNTEJJRi5weQ==) | `96.93% <100.00%> (ø)` | |
| [pgmpy/tests/test\_readwrite/test\_BIF.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1285/diff?src=pr&el=tree#diff-cGdtcHkvdGVzdHMvdGVzdF9yZWFkd3JpdGUvdGVzdF9CSUYucHk=) | `100.00% <100.00%> (+7.76%)` | :arrow_up: |
| [pgmpy/tests/test\_readwrite/test\_XMLBIF.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1285/diff?src=pr&el=tree#diff-cGdtcHkvdGVzdHMvdGVzdF9yZWFkd3JpdGUvdGVzdF9YTUxCSUYucHk=) | `95.96% <100.00%> (+7.16%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pgmpy/pgmpy/pull/1285?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pgmpy/pgmpy/pull/1285?src=pr&el=footer). Last update [f379c8c...f3b1ee5](https://codecov.io/gh/pgmpy/pgmpy/pull/1285?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/pgmpy/readwrite/BIF.py b/pgmpy/readwrite/BIF.py
index 5d0c7e22..3095dc5c 100644
--- a/pgmpy/readwrite/BIF.py
+++ b/pgmpy/readwrite/BIF.py
@@ -345,9 +345,14 @@ class BIFReader(object):
         ]
         return edges
 
-    def get_model(self):
+    def get_model(self, state_name_type=str):
         """
-        Returns the fitted bayesian model
+        Returns the Bayesian Model read from the file/str.
+
+        Parameters
+        ----------
+        state_name_type: int, str or bool (default: str)
+            The data type to which to convert the state names of the variables.
 
         Example
         ----------
@@ -366,10 +371,10 @@ class BIFReader(object):
             for var in sorted(self.variable_cpds.keys()):
                 values = self.variable_cpds[var]
                 sn = {
-                    p_var: self.variable_states[p_var]
+                    p_var: list(map(state_name_type, self.variable_states[p_var]))
                     for p_var in self.variable_parents[var]
                 }
-                sn[var] = self.variable_states[var]
+                sn[var] = list(map(state_name_type, self.variable_states[var]))
                 cpd = TabularCPD(
                     var,
                     len(self.variable_states[var]),
diff --git a/pgmpy/readwrite/XMLBIF.py b/pgmpy/readwrite/XMLBIF.py
index 30dbc455..5ce84618 100644
--- a/pgmpy/readwrite/XMLBIF.py
+++ b/pgmpy/readwrite/XMLBIF.py
@@ -201,7 +201,19 @@ class XMLBIFReader(object):
         }
         return variable_property
 
-    def get_model(self):
+    def get_model(self, state_name_type=str):
+        """
+        Returns a Bayesian Model instance from the file/string.
+
+        Parameters
+        ----------
+        state_name_type: int, str, or bool (default: str)
+            The data type to which to convert the state names of the variables.
+
+        Returns
+        -------
+        BayesianModel instance: The read model.
+        """
         model = BayesianModel()
         model.add_nodes_from(self.variables)
         model.add_edges_from(self.edge_list)
@@ -220,7 +232,7 @@ class XMLBIFReader(object):
                 evidence=self.variable_parents[var],
                 evidence_card=evidence_card,
                 state_names={
-                    var: self.state_names[var]
+                    var: list(map(state_name_type, self.state_names[var]))
                     for var in chain([var], self.variable_parents[var])
                 },
             )


Test patch:
diff --git a/pgmpy/tests/test_readwrite/test_BIF.py b/pgmpy/tests/test_readwrite/test_BIF.py
index 8e828441..8ad9d9d7 100644
--- a/pgmpy/tests/test_readwrite/test_BIF.py
+++ b/pgmpy/tests/test_readwrite/test_BIF.py
@@ -422,11 +422,10 @@ probability ( light-on | family-out ) {
         self.maxDiff = None
         self.assertEqual(self.writer.__str__(), self.expected_string)
 
-    @unittest.skip("Fix this when #1221 is resolved")
     def test_write_read_equal(self):
         self.writer.write_bif("test_bif.bif")
         reader = BIFReader("test_bif.bif")
-        read_model = reader.get_model()
+        read_model = reader.get_model(state_name_type=int)
         self.assertEqual(sorted(self.model.nodes()), sorted(read_model.nodes()))
         self.assertEqual(sorted(self.model.edges()), sorted(read_model.edges()))
         for var in self.model.nodes():
diff --git a/pgmpy/tests/test_readwrite/test_XMLBIF.py b/pgmpy/tests/test_readwrite/test_XMLBIF.py
index 7a14d6be..a6e88c85 100644
--- a/pgmpy/tests/test_readwrite/test_XMLBIF.py
+++ b/pgmpy/tests/test_readwrite/test_XMLBIF.py
@@ -355,7 +355,7 @@ class TestXMLBIFWriterMethodsString(unittest.TestCase):
         with open("dog_problem_output.xbif", "r") as f:
             file_text = f.read()
         reader = XMLBIFReader(string=file_text)
-        model = reader.get_model()
+        model = reader.get_model(state_name_type=str)
         self.assert_models_equivelent(self.expected_model, model)
         os.remove("dog_problem_output.xbif")
 
@@ -363,21 +363,13 @@ class TestXMLBIFWriterMethodsString(unittest.TestCase):
         self.writer_stateless.write_xmlbif("grade_problem_output.xbif")
         with open("grade_problem_output.xbif", "r") as f:
             reader = XMLBIFReader(f)
-        model = reader.get_model()
+        model = reader.get_model(state_name_type=int)
         self.assert_models_equivelent(self.model_stateless, model)
         self.assertDictEqual(
-            {
-                "G": ["0", "1", "2"],
-                "I": ["0", "1"],
-                "D": ["0", "1"],
-                "S": ["0", "1"],
-                "L": ["0", "1"],
-            },
-            model.get_cpds("D").state_names,
+            {"D": [0, 1],}, model.get_cpds("D").state_names,
         )
         os.remove("grade_problem_output.xbif")
 
-    @unittest.skip("Fix this when #1221 is resolved")
     def assert_models_equivelent(self, expected, got):
         self.assertSetEqual(set(expected.nodes()), set(got.nodes()))
         for node in expected.nodes():