Repository: pgmpy/pgmpy

Issue:
Getting `float` values as states when fitting a `DBN` making `ApproxInference` throw an error
With the following code, I am getting states such as `0.0`, `1.0`, and so on when I print the CPD. Normally, this shouldn't be much of a problem, but it makes `ApproxInference.query` (when trying to predict future timestamps) throw an error. See the following outputs:

```python
In[1]: import numpy as np
  ...: import pandas as pd
  ...: from pgmpy.inference import ApproxInference, DBNInference
  ...: from pgmpy.models import DynamicBayesianNetwork as DBN
  ...: from sklearn.preprocessing import LabelEncoder

In[2]: data = np.random.choice(["s1", "s2"], size=(1000, 6))
  ...: colnames = []
  ...: for t in range(2):
  ...:     colnames.extend([( "A", t), ("B", t), ("C", t)])
  ...: df = pd.DataFrame(data, columns=colnames)

In[3]: df_encoded = df.copy()
  ...: for node in df.columns:
  ...:     label_encoder = LabelEncoder()
  ...:     df_encoded[node] = label_encoder.fit_transform(df[node]).astype(int)

In[4]: for node in df_encoded.columns:
  ...:     assert df_encoded[node].dtype == "int64"
  ...: print("All good!")

All good!

In[5]: model = DBN(
  ...:     [
  ...:         (("A", 0), ("B", 0)),
  ...:         (("A", 0), ("C", 0)),
  ...:         (("A", 0), ("A", 1)),
  ...:         (("B", 0), ("B", 1)),
  ...:         (("C", 0), ("C", 1)),
  ...:     ]
  ...: )

In[6]: model.fit(df_encoded)
INFO:pgmpy: Datatype (N=numerical, C=Categorical Unordered, O=Categorical Ordered) inferred from data: 
 {'C_0': 'C', 'B_0': 'C', 'A_0': 'C', 'C_1': 'C', 'B_1': 'C', 'A_1': 'C'}
```

### Get CPDs
This prints `float` values as states.
```python
In[7]: print(model.get_cpds(("A", 1)))

+-------------+--------------------+--------------------+
| (A, 0)      | (A, 0)(0.0)        | (A, 0)(1.0)        |
+-------------+--------------------+--------------------+
| (A, 1)(0.0) | 0.4657534246575342 | 0.4601226993865031 |
+-------------+--------------------+--------------------+
| (A, 1)(1.0) | 0.5342465753424658 | 0.5398773006134969 |
+-------------+--------------------+--------------------+
```

### Using DBNInference (for existing timestamps)
This works.
```python
In[1]: dbn_inf = DBNInference(model)
  ...: print(dbn_inf.query([("B", 0)], evidence={("A", 0): 1})[("B", 0)])

+-------------+-----------------+
| ('B', 0)    |   phi(('B', 0)) |
+=============+=================+
| ('B', 0)(0) |          0.5287 |
+-------------+-----------------+
| ('B', 0)(1) |          0.4713 |
+-------------+-----------------+
```
### Using DBNInference (into the future)
This works.
```python
In[2]: dbn_inf = DBNInference(model)
  ...: print(dbn_inf.query([("A", 3)], evidence={("B", 0): 1, ("C", 0): 0})[("A", 3)])

+-------------+-----------------+
| ('A', 3)    |   phi(('A', 3)) |
+=============+=================+
| ('A', 3)(0) |          0.5169 |
+-------------+-----------------+
| ('A', 3)(1) |          0.4831 |
+-------------+-----------------+
```
### Using ApproxInference (for existing timestamps)
This doesn't throw an error but has `float` as states, documented in #1968 
```python
In[3]: dbn_inf = ApproxInference(model)
  ...: print(
  ...:     dbn_inf.query(
  ...:         [("A", 0)],
  ...:         evidence={("B", 0): 0, ("C", 0): 1},
  ...:         # state_names={("A", 0): ["s1", "s2"]},
  ...:     )
  ...: )

+---------------+-----------------+
| ('A', 0)      |   phi(('A', 0)) |
+===============+=================+
| ('A', 0)(0.0) |          0.4900 |
+---------------+-----------------+
| ('A', 0)(1.0) |          0.5100 |
+---------------+-----------------+

/home/camilo/Repositorios/pgmpy/pgmpy/inference/ApproxInference.py:76: FutureWarning: The default of observed=False is deprecated and will be changed to True in a future version of pandas. Pass observed=False to retain current behavior or observed=True to adopt the future default and silence this warning.
  samples.groupby(variables).size() / samples.shape[0], state_names
```
### Using ApproxInference (into the future)
This throws an error.
```python
In[4]: dbn_inf = ApproxInference(model)
  ...: print(
  ...:     dbn_inf.query(
  ...:         [("A", 4)],
  ...:         evidence={("B", 0): 1, ("C", 0): 0},
  ...:         # state_names={("A", 0): [0, 1]},
  ...:     )
  ...: )

---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[12], line 4
      1 # Query the model
      2 dbn_inf = ApproxInference(model)
      3 print(
----> 4     dbn_inf.query(
      5         [("A", 3)],
      6         evidence={("B", 0): 1, ("C", 0): 0},
      7         # state_names={("A", 0): [0, 1]},
      8     )
      9 )

File ~/Repositorios/pgmpy/pgmpy/inference/ApproxInference.py:178, in ApproxInference.query(self, variables, n_samples, samples, evidence, virtual_evidence, joint, state_names, show_progress, seed)
    176             if cpd.variable[1] > max_time_slices:
    177                 max_time_slices = cpd.variable[2]
--> 178         samples = self.model.simulate(
    179             n_samples=n_samples,
    180             n_time_slices=max_time_slices + 1,
    181             evidence=evidence,
    182             virtual_evidence=virtual_evidence,
    183             show_progress=show_progress,
    184             seed=seed,
    185         )
    187 # Step 2: If state_names is None, infer it from samples.
    188 if state_names is None:

File ~/Repositorios/pgmpy/pgmpy/models/DynamicBayesianNetwork.py:1158, in DynamicBayesianNetwork.simulate(self, n_samples, n_time_slices, do, evidence, virtual_evidence, virtual_intervention, include_latents, seed, show_progress)
   1156     partial_df = sampled.loc[:, partial_colnames]
   1157     remaining_df = sampled.loc[:, ~sampled.columns.isin(partial_colnames)]
-> 1158     new_samples = const_bn.simulate(
   1159         n_samples=n_samples,
   1160         do={**do_dict[t_slice], **do_dict[t_slice + 1]},
   1161         evidence={**evidence_dict[t_slice], **evidence_dict[t_slice + 1]},
   1162         virtual_evidence=[
   1163             *virtual_evi_dict[t_slice],
   1164             *virtual_evi_dict[t_slice + 1],
   1165         ],
   1166         virtual_intervention=[
   1167             *virtual_inter_dict[t_slice],
   1168             *virtual_inter_dict[t_slice + 1],
   1169         ],
   1170         include_latents=True,
   1171         partial_samples=partial_df,
   1172         seed=seed,
   1173         show_progress=False,
   1174     )
   1175     sampled = pd.concat((remaining_df, new_samples), axis=1)
   1176 return self._postprocess(sampled)

File ~/Repositorios/pgmpy/pgmpy/models/BayesianNetwork.py:1433, in BayesianNetwork.simulate(self, n_samples, do, evidence, virtual_evidence, virtual_intervention, include_latents, partial_samples, seed, show_progress, missing_prob, return_full)
   1431 # Step 4: If no evidence do a forward sampling
   1432 if len(evidence) == 0:
-> 1433     samples = BayesianModelSampling(model).forward_sample(
   1434         size=n_samples,
   1435         include_latents=include_latents,
   1436         seed=seed,
   1437         show_progress=show_progress,
   1438         partial_samples=partial_samples,
   1439     )
   1441 # Step 5: If evidence; do a rejection sampling
   1442 else:
   1443     samples = BayesianModelSampling(model).rejection_sample(
   1444         size=n_samples,
   1445         evidence=[(k, v) for k, v in evidence.items()],
   (...)
   1449         partial_samples=partial_samples,
   1450     )

File ~/Repositorios/pgmpy/pgmpy/sampling/Sampling.py:114, in BayesianModelSampling.forward_sample(self, size, include_latents, seed, show_progress, partial_samples, n_jobs)
    110 unique, inverse = np.unique(
    111     evidence_values.T, axis=0, return_inverse=True
    112 )
    113 unique = [tuple(u) for u in unique]
--> 114 state_to_index, index_to_weight = self.pre_compute_reduce_maps(
    115     variable=node, evidence=evidence, state_combinations=unique
    116 )
    117 if config.get_backend() == "numpy":
    118     weight_index = np.array([state_to_index[u] for u in unique])[
    119         inverse
    120     ]

File ~/Repositorios/pgmpy/pgmpy/sampling/base.py:142, in BayesianModelInference.pre_compute_reduce_maps(self, variable, evidence, state_combinations)
    131     state_combinations = [
    132         tuple(sc)
    133         for sc in itertools.product(
    134             *[range(self.cardinality[var]) for var in evidence]
    135         )
    136     ]
    138 reduce_index = [variable_cpd.variables.index(var) for var in evidence]
    140 weights_list = compat_fns.stack(
    141     [
--> 142         BayesianModelInference._reduce_marg(variable_cpd, reduce_index, sc)
    143         for sc in state_combinations
    144     ]
    145 )
    146 unique_weights, weights_indices = compat_fns.unique(
    147     weights_list, axis=0, return_inverse=True
    148 )
    150 # convert weights to index; make mapping of state to index

File ~/Repositorios/pgmpy/pgmpy/sampling/base.py:94, in BayesianModelInference._reduce_marg(variable_cpd, reduce_index, sc)
     91 for i, index in enumerate(reduce_index):
     92     slice_[index] = sc[i]
---> 94 reduced_values = variable_cpd.values[tuple(slice_)]
     95 marg_values = compat_fns.einsum(reduced_values, range(reduced_values.ndim), [0])
     96 return marg_values / marg_values.sum()

IndexError: only integers, slices (`:`), ellipsis (`...`), numpy.newaxis (`None`) and integer or boolean arrays are valid indices
```

This doesn't occur in [master](https://github.com/pgmpy/pgmpy/tree/master) branch. There, it doesn't have floats as states and all querys work.

Discussion:
@CamiloMartinezM: A workaround is to manually turn them into `int`:

```python
In[5]: for cpd in model.get_cpds():
 ...:     print(cpd.state_names)
 ...:     for k in cpd.state_names:
 ...:         for i, value in enumerate(cpd.state_names[k]):
 ...:             cpd.state_names[k][i] = value.astype(int)
 ...:     print(cpd.state_names)

{<DynamicNode(A, 0) at 0x7fd4b786c1d0>: [np.float64(0.0), np.float64(1.0)]}
{<DynamicNode(A, 0) at 0x7fd4b786c1d0>: [np.int64(0), np.int64(1)]}
...
```@ankurankan: @CamiloMartinezM Thanks for reporting the issue. This should also be fixed in the dev branch now.

Solution patch:
diff --git a/pgmpy/utils/utils.py b/pgmpy/utils/utils.py
index 0bfcd098..16daef9a 100644
--- a/pgmpy/utils/utils.py
+++ b/pgmpy/utils/utils.py
@@ -382,7 +382,7 @@ def preprocess_data(df):
     dtypes = {}
     for col in df.columns:
         if pd.api.types.is_integer_dtype(df[col]):
-            df[col] = df[col].astype("float")
+            df[col] = df[col].astype("int")
             dtypes[col] = "N"
         elif pd.api.types.is_numeric_dtype(df[col]):
             dtypes[col] = "N"


Test patch:
diff --git a/pgmpy/tests/test_models/test_FunctionalBayesianNetwork.py b/pgmpy/tests/test_models/test_FunctionalBayesianNetwork.py
index 7d8a1afc..88e358d0 100644
--- a/pgmpy/tests/test_models/test_FunctionalBayesianNetwork.py
+++ b/pgmpy/tests/test_models/test_FunctionalBayesianNetwork.py
@@ -643,7 +643,7 @@ class TestFBNMethods(unittest.TestCase):
         self.assertIn("ibpB_alpha1", params)
 
         self.assertAlmostEqual(params["b1191_mu"], 1.273, delta=0.2)
-        self.assertAlmostEqual(params["b1191_sigma"], 0.609, delta=0.2)
+        self.assertAlmostEqual(params["b1191_sigma"], 0.609, delta=0.25)
 
         self.assertAlmostEqual(params["eutG_mu"], 1.265, delta=0.2)
         self.assertAlmostEqual(params["eutG_sigma"], 0.691, delta=0.2)