Repository: pgmpy/pgmpy

Issue:
VariableElimination ignores identical factors
### Subject of the issue
When VariableElimination generates several identical factors (same variables, same values) only one is considered and the rest are discarded. This is due to the use of sets (see e.g. "final_distribution = set()" in ExactInference.py), which do not admit repeated elements. 

This can be fixed by switching to another data structure. I would have time, but which one? Python lists, a multiset using collections.Counter?

This issue (identical factors arising during variable eliimination) will rarely be a problem for learned networks --but it can be academic examples. May be related to other inference problems (issue #856 ?).

### Environment
* pgmpy version: 0.1.10
* Python version: 3.6.10
* Operating System: Ubuntu 18.04

### Steps to reproduce
    import numpy as np
    from pgmpy.models import MarkovModel
    from pgmpy.factors.discrete import DiscreteFactor
    from pgmpy.inference import VariableElimination

    g = MarkovModel([('A', 'B'), ('A', 'C')])
    f1 = DiscreteFactor(variables=['A', 'B'], cardinality=[2, 2], values=np.eye(2) * 2)
    f2 = DiscreteFactor(variables=['A', 'C'], cardinality=[2, 2], values=np.eye(2) * 2)
    g.add_factors(f1, f2)

    print(VariableElimination(g).query(['A']))  # <- gives incorrect result: [2, 2]

    # We now modify one of the factors by a tiny amount, so they are no longer equal
    g.factors[0].values += 1e-8
    print(VariableElimination(g).query(['A']))  # <- gives correct result: [4, 4]

Discussion:
ankurankan: @rballester Thanks for the PR. I will try to review this ASAP. 

Patch:
diff --git a/Contributing.md b/Contributing.md
index 37cc4166..3a8a5c6d 100644
--- a/Contributing.md
+++ b/Contributing.md
@@ -1,48 +1,50 @@
 # Contributing to pgmpy
 
-Hi! Thanks for your interest in contributing to [pgmpy](https://pgmpy.org).
-In this document we'll try to summarize everything that you need to know to do a good job.
-
+Hi! Thanks for your interest in contributing to [pgmpy](https://pgmpy.org). This
+document summarizes everything that you need to know to get started.
 
 ## Code and Issues
 
-We use [Github](https://github.com/pgmpy/pgmpy) to host our code repositories
-and issues.You can look at [issues](https://github.com/pgmpy/pgmpy/issues) to report any
-issues related to pgmpy. Here is a [guide](https://guides.github.com/features/issues/)
-on how to report better issues.
+We use [Github](https://github.com/pgmpy/pgmpy) to host all our code. We also use github
+as our [issue tracker](https://github.com/pgmpy/pgmpy/issues). Please feel free to
+create a new issue for any bugs, questions etc. It is very helpful if you follow the
+issue template while creating new issues as it gives us enough information to reproduce
+the problem. You can also refer to github's
+[guide](https://guides.github.com/features/issues/) on how to efficiently use github
+issues.
 
 ### Git and our Branching model
 
 #### Git
 
 We use [Git](http://git-scm.com/) as our [version control
-system](http://en.wikipedia.org/wiki/Revision_control), so the best way to
-contribute is to learn how to use it and put your changes on a Git repository.
-There are plenty of documentation about Git -- you can start with the [Pro Git
-book](http://git-scm.com/book/).
-Or You can go through the [try git tutorial](https://try.github.io/levels/1/challenges/1
-)
-
-#### Forks + GitHub Pull requests
-
-We use
-[gitflow](http://nvie.com/posts/a-successful-git-branching-model/) to manage our
+system](http://en.wikipedia.org/wiki/Revision_control), so the best way to contribute is
+to learn how to use it and put your changes on a Git repository. There is plenty of
+online resources available to get started with Git:
+- Online tool to try git: [try git
+  tutorial](https://try.github.io/levels/1/challenges/1)
+- Quick intro to opening your first Pull Request:
+  https://www.freecodecamp.org/news/how-to-make-your-first-pull-request-on-github-3/
+- Git reference: [Pro Git book](http://git-scm.com/book/).
+
+#### Forks + GitHub Pull Requests
+
+We use [gitflow](http://nvie.com/posts/a-successful-git-branching-model/) to manage our
 branches.
 
 Summary of our git branching model:
-- Fork the desired repository on GitHub to your account;
-- Clone your forked repository locally
-  (`git clone git@github.com:your-username/repository-name.git`);
-- Create a new branch off of `develop` with a descriptive name (for example:
+- Fork the desired repository on GitHub to your account.
+- Clone your forked repository locally: `git clone git@github.com:your-username/repository-name.git`.
+- Create a new branch off of `dev` branch with a descriptive name (for example:
   `feature/portuguese-sentiment-analysis`, `hotfix/bug-on-downloader`). You can
-  do it by switching to `develop` branch (`git checkout develop`) and then
-  creating a new branch (`git checkout -b name-of-the-new-branch`);
+  do it by switching to `dev` branch: `git checkout dev` and then
+  creating a new branch: `git checkout -b name-of-the-new-branch`.
 - Make changes to the codebase and commit it. <b> [Imp] </b> Make sure that tests pass for each of your commits.
 - Rebase your branch on the current dev and push to your fork on GitHub (with the name as your local branch:
-  `git push origin branch-name`);
-- Create a pull request using the GitHub Web interface (asking us to pull the
-  changes from your new branch and add the changes to our `develop` branch).;
-- Wait for comments.
+  `git push origin branch-name`
+- Create a pull request using GitHub's Web interface (asking us to pull the
+  changes from your new branch and add the changes to our `dev` branch).;
+- Wait for reviews and comments.
 
 
 #### Tips
@@ -62,49 +64,42 @@ Summary of our git branching model:
 
 ### Code Guidelines
 
-- We use [PEP8](http://www.python.org/dev/peps/pep-0008/);
-- We permit 120 characters in a line, rather 79 as suggested in PEP8
+- We use `black`(https://black.readthedocs.io/en/stable/) for our code formatting.
 - Write tests for your new features (please see "Tests" topic below);
 - Always remember that [commented code is dead
   code](http://www.codinghorror.com/blog/2008/07/coding-without-comments.html);
 - Name identifiers (variables, classes, functions, module names) with readable
   names (`x` is always wrong);
-- When manipulating strings, use [Python's new-style
-  formatting](http://docs.python.org/library/string.html#format-string-syntax)
-  (`'{} = {}'.format(a, b)` instead of `'%s = %s' % (a, b)`);
+- When manipulating strings, use [Python's f-Strings](https://realpython.com/python-f-strings/)
+  (`f'{a} = {b}'` instead of `'{} = {}'.format(a, b)`);
 - When working with files use `with open(<filename>, <option>) as f` instead of
   ` f = open(<filename>, <option>)`;
 - All `#TODO` comments should be turned into issues (use our
   [GitHub issue system](https://github.com/pgmpy/pgmpy/issues));
 - Run all tests before pushing (just execute `nosetests`) so you will know if your
   changes broke something;
-- Try to write both Python 2 and Python3-friendly code so won't be a pain for
-  us to support both versions.We use [six](https://pypi.python.org/pypi/six) to help
-  us in integration across various python versions.
-
 
 ### Tests
 
-We use [Travis CI](https://travis-ci.org/) for continous integration
-and python [unittest module](https://docs.python.org/2/library/unittest.html) for writing tests.
-You should write tests for every feature you add or bug you solve in the code.
-Having automated tests for every line of our code let us make big changes
-without worries: there will always be tests to verify if the changes introduced
-bugs or lack of features. If we don't have tests we will be blind and every
-change will come with some fear of possibly breaking something.
+We use [Travis CI](https://travis-ci.org/) for continous integration for linux systems
+and [AppVeyor](https://www.appveyor.com/) for Windows systems.  We use python [unittest
+module](https://docs.python.org/2/library/unittest.html) for writing tests.  You should
+write tests for every feature you add or bug you solve in the code.  Having automated
+tests for every line of our code let us make big changes without worries: there will
+always be tests to verify if the changes introduced bugs or lack of features. If we
+don't have tests we will be blind and every change will come with some fear of possibly
+breaking something.
 
-For a better design of your code, we recommend using a technique called
-[test-driven development](https://en.wikipedia.org/wiki/Test-driven_development),
-where you write your tests **before** writing the actual code that implements
-the desired feature.
+For a better design of your code, we recommend using a technique called [test-driven
+development](https://en.wikipedia.org/wiki/Test-driven_development), where you write
+your tests **before** writing the actual code that implements the desired feature.
 
 
 ## Discussion
 
-Please feel free to contact us through mail list if
-you have any questions or suggestions.
-Connect with us at [gitter](https://gitter.im/pgmpy/pgmpy).
-Every contribution is very welcome!
+Please feel free to contact us through the mailing list if you have any questions or
+suggestions. Connect with us at [gitter](https://gitter.im/pgmpy/pgmpy).  All
+contributions are very welcome!
 
 *Mailing list* : pgmpy@googlegroups.com
 
diff --git a/pgmpy/estimators/BayesianEstimator.py b/pgmpy/estimators/BayesianEstimator.py
index 062924de..36610f41 100644
--- a/pgmpy/estimators/BayesianEstimator.py
+++ b/pgmpy/estimators/BayesianEstimator.py
@@ -3,6 +3,7 @@
 from itertools import chain
 
 import numpy as np
+import numbers
 
 from pgmpy.estimators import ParameterEstimator
 from pgmpy.factors.discrete import TabularCPD
@@ -33,9 +34,9 @@ class BayesianEstimator(ParameterEstimator):
         prior_type: 'dirichlet', 'BDeu', or 'K2'
             string indicting which type of prior to use for the model parameters.
             - If 'prior_type' is 'dirichlet', the following must be provided:
-                'pseudo_counts' = dirichlet hyperparameters; a dict containing, for each variable, a 2-D
-                 array of the shape (node_card, product of parents_card) with a "virtual" count for each
-                 variable state in the CPD, that is added to the state counts.
+                'pseudo_counts' = dirichlet hyperparameters; a single number or a dict containing, for each
+                 variable, a 2-D array of the shape (node_card, product of parents_card) with a "virtual"
+                 count for each variable state in the CPD, that is added to the state counts.
                  (lexicographic ordering of states assumed)
             - If 'prior_type' is 'BDeu', then an 'equivalent_sample_size'
                 must be specified instead of 'pseudo_counts'. This is equivalent to
@@ -74,7 +75,10 @@ class BayesianEstimator(ParameterEstimator):
                 if isinstance(equivalent_sample_size, dict)
                 else equivalent_sample_size
             )
-            _pseudo_counts = pseudo_counts[node] if pseudo_counts else None
+            if isinstance(pseudo_counts, numbers.Real):
+                _pseudo_counts = pseudo_counts
+            else:
+                _pseudo_counts = pseudo_counts[node] if pseudo_counts else None
 
             cpd = self.estimate_cpd(
                 node,
@@ -100,7 +104,7 @@ class BayesianEstimator(ParameterEstimator):
         prior_type: 'dirichlet', 'BDeu', 'K2',
             string indicting which type of prior to use for the model parameters.
             - If 'prior_type' is 'dirichlet', the following must be provided:
-                'pseudo_counts' = dirichlet hyperparameters; 2-D array of shape
+                'pseudo_counts' = dirichlet hyperparameters; a single number or 2-D array of shape
                  (node_card, product of parents_card) with a "virtual" count for
                  each variable state in the CPD.
                  The virtual counts are added to the actual state counts found in the data.
@@ -150,13 +154,17 @@ class BayesianEstimator(ParameterEstimator):
             )
             pseudo_counts = np.ones(cpd_shape, dtype=float) * alpha
         elif prior_type == "dirichlet":
-            pseudo_counts = np.array(pseudo_counts)
-            if pseudo_counts.shape != cpd_shape:
-                raise ValueError(
-                    "The shape of pseudo_counts for the node: {node} must be of shape: {shape}".format(
-                        node=node, shape=str(cpd_shape)
+            if isinstance(pseudo_counts, numbers.Real):
+                pseudo_counts = np.ones(cpd_shape, dtype=int) * pseudo_counts
+
+            else:
+                pseudo_counts = np.array(pseudo_counts)
+                if pseudo_counts.shape != cpd_shape:
+                    raise ValueError(
+                        "The shape of pseudo_counts for the node: {node} must be of shape: {shape}".format(
+                            node=node, shape=str(cpd_shape)
+                        )
                     )
-                )
         else:
             raise ValueError("'prior_type' not specified")
 
diff --git a/pgmpy/inference/ExactInference.py b/pgmpy/inference/ExactInference.py
index bbfdd6e8..ec935285 100644
--- a/pgmpy/inference/ExactInference.py
+++ b/pgmpy/inference/ExactInference.py
@@ -32,20 +32,21 @@ class VariableElimination(Inference):
         -------
         dict: Modified working factors.
         """
+
         working_factors = {
-            node: {factor for factor in self.factors[node]} for node in self.factors
+            node: {(factor, None) for factor in self.factors[node]} for node in self.factors
         }
 
         # Dealing with evidence. Reducing factors over it before VE is run.
         if evidence:
             for evidence_var in evidence:
-                for factor in working_factors[evidence_var]:
+                for factor, origin in working_factors[evidence_var]:
                     factor_reduced = factor.reduce(
                         [(evidence_var, evidence[evidence_var])], inplace=False
                     )
                     for var in factor_reduced.scope():
-                        working_factors[var].remove(factor)
-                        working_factors[var].add(factor_reduced)
+                        working_factors[var].remove((factor, origin))
+                        working_factors[var].add((factor_reduced, evidence_var))
                 del working_factors[evidence_var]
         return working_factors
 
@@ -172,23 +173,23 @@ class VariableElimination(Inference):
             # eliminated (as all the factors should be considered only once)
             factors = [
                 factor
-                for factor in working_factors[var]
+                for factor, _ in working_factors[var]
                 if not set(factor.variables).intersection(eliminated_variables)
             ]
             phi = factor_product(*factors)
             phi = getattr(phi, operation)([var], inplace=False)
             del working_factors[var]
             for variable in phi.variables:
-                working_factors[variable].add(phi)
+                working_factors[variable].add((phi, var))
             eliminated_variables.add(var)
 
         # Step 4: Prepare variables to be returned.
         final_distribution = set()
         for node in working_factors:
-            factors = working_factors[node]
-            for factor in factors:
+            for factor, origin in working_factors[node]:
                 if not set(factor.variables).intersection(eliminated_variables):
-                    final_distribution.add(factor)
+                    final_distribution.add((factor, origin))
+        final_distribution = [factor for factor, _ in final_distribution]
 
         if joint:
             if isinstance(self.model, BayesianModel):
diff --git a/pgmpy/models/BayesianModel.py b/pgmpy/models/BayesianModel.py
index 6f176823..8a6f136c 100644
--- a/pgmpy/models/BayesianModel.py
+++ b/pgmpy/models/BayesianModel.py
@@ -421,6 +421,7 @@ class BayesianModel(DAG):
         """
         moral_graph = self.moralize()
         mm = MarkovModel(moral_graph.edges())
+        mm.add_nodes_from(moral_graph.nodes())
         mm.add_factors(*[cpd.to_factor() for cpd in self.cpds])
 
         return mm


Test patch:
diff --git a/pgmpy/tests/test_estimators/test_BayesianEstimator.py b/pgmpy/tests/test_estimators/test_BayesianEstimator.py
index 72751994..46fbb599 100644
--- a/pgmpy/tests/test_estimators/test_BayesianEstimator.py
+++ b/pgmpy/tests/test_estimators/test_BayesianEstimator.py
@@ -150,6 +150,30 @@ class TestBayesianEstimator(unittest.TestCase):
             cpds,
         )
 
+    def test_get_parameters3(self):
+        pseudo_counts = 0.1
+        cpds = set(
+            [
+                self.est3.estimate_cpd(
+                    "A", prior_type="dirichlet", pseudo_counts=pseudo_counts
+                ),
+                self.est3.estimate_cpd(
+                    "B", prior_type="dirichlet", pseudo_counts=pseudo_counts
+                ),
+                self.est3.estimate_cpd(
+                    "C", prior_type="dirichlet", pseudo_counts=pseudo_counts
+                ),
+            ]
+        )
+        self.assertSetEqual(
+            set(
+                self.est3.get_parameters(
+                    prior_type="dirichlet", pseudo_counts=pseudo_counts
+                )
+            ),
+            cpds,
+        )
+
     def tearDown(self):
         del self.m1
         del self.d1
diff --git a/pgmpy/tests/test_inference/test_ExactInference.py b/pgmpy/tests/test_inference/test_ExactInference.py
index 94a53d20..28faaeb4 100644
--- a/pgmpy/tests/test_inference/test_ExactInference.py
+++ b/pgmpy/tests/test_inference/test_ExactInference.py
@@ -213,6 +213,23 @@ class TestVariableElimination(unittest.TestCase):
         del self.bayesian_model
 
 
+class TestVariableEliminationDuplicatedFactors(unittest.TestCase):
+    def setUp(self):
+        self.markov_model = MarkovModel([('A', 'B'), ('A', 'C')])
+        f1 = DiscreteFactor(variables=['A', 'B'], cardinality=[2, 2], values=np.eye(2) * 2)
+        f2 = DiscreteFactor(variables=['A', 'C'], cardinality=[2, 2], values=np.eye(2) * 2)
+        self.markov_model.add_factors(f1, f2)
+        self.markov_inference = VariableElimination(self.markov_model)
+
+    def test_duplicated_factors(self):
+        query_result = self.markov_inference.query(['A'])
+        self.assertEqual(
+            query_result,
+            DiscreteFactor(
+                variables=["A"], cardinality=[2], values=np.array([4, 4])
+            ),
+        )
+
 class TestVariableEliminationMarkov(unittest.TestCase):
     def setUp(self):
         # It is just a moralised version of the above Bayesian network so all the results are same. Only factors