Repository: pgmpy/pgmpy

Issue:
ValueError in HillClimbSearch().estimate()
### Subject of the issue
It raises ValueError when estimating the graph with the HillClimbSearch model.

### Your environment
* pgmpy version : 0.1.12
* Python version : 3.7.7
* Operating System : Ubuntu 18.04.2 LTS

### Steps to reproduce
```python3
data = pd.DataFrame(
        [[1,0,0,1,0,0,1,1,0],
         [1,0,1,0,0,1,0,1,0],
         [1,0,0,0,0,1,0,1,1],
         [1,1,0,1,0,1,1,0,0],
         [0,0,1,0,0,1,1,0,0]],
        columns=list('ABCDEFGHI')
    )
est = HillClimbSearch(data)
best_model = est.estimate(
    fixed_edges=[('A','B'), ('B', 'C')],
    white_list=[('F','C')]
)
```

### Expected behaviour
1. return model with fixed edges
or
2. return model with fixed_edges and white_list edges

### Actual behaviour
![image](https://user-images.githubusercontent.com/23306402/98773788-01f69e80-242d-11eb-904f-9295b00fc147.png)


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/pgmpy/pgmpy/pull/1349?src=pr&el=h1) Report
> Merging [#1349](https://codecov.io/gh/pgmpy/pgmpy/pull/1349?src=pr&el=desc) (0eeecb3) into [dev](https://codecov.io/gh/pgmpy/pgmpy/commit/84046ca382bff08944e05f5d3f30c262bbbd372e?el=desc) (84046ca) will **decrease** coverage by `0.02%`.
> The diff coverage is `100.00%`.

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

```diff
@@            Coverage Diff             @@
##              dev    #1349      +/-   ##
==========================================
- Coverage   93.18%   93.16%   -0.03%     
==========================================
  Files         132      132              
  Lines       13245    13199      -46     
==========================================
- Hits        12343    12297      -46     
  Misses        902      902              
```


| [Impacted Files](https://codecov.io/gh/pgmpy/pgmpy/pull/1349?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pgmpy/estimators/HillClimbSearch.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1349/diff?src=pr&el=tree#diff-cGdtcHkvZXN0aW1hdG9ycy9IaWxsQ2xpbWJTZWFyY2gucHk=) | `92.13% <ø> (ø)` | |
| [...gmpy/tests/test\_estimators/test\_HillClimbSearch.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1349/diff?src=pr&el=tree#diff-cGdtcHkvdGVzdHMvdGVzdF9lc3RpbWF0b3JzL3Rlc3RfSGlsbENsaW1iU2VhcmNoLnB5) | `98.66% <100.00%> (+0.09%)` | :arrow_up: |
| [.../test\_models/test\_LinearGaussianBayesianNetwork.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1349/diff?src=pr&el=tree#diff-cGdtcHkvdGVzdHMvdGVzdF9tb2RlbHMvdGVzdF9MaW5lYXJHYXVzc2lhbkJheWVzaWFuTmV0d29yay5weQ==) | `39.06% <0.00%> (-3.59%)` | :arrow_down: |
| [...actors/test\_continuous/test\_Linear\_Gaussain\_CPD.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1349/diff?src=pr&el=tree#diff-cGdtcHkvdGVzdHMvdGVzdF9mYWN0b3JzL3Rlc3RfY29udGludW91cy90ZXN0X0xpbmVhcl9HYXVzc2Fpbl9DUEQucHk=) | `40.00% <0.00%> (-2.86%)` | :arrow_down: |
| [pgmpy/tests/test\_estimators/test\_MmhcEstimator.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1349/diff?src=pr&el=tree#diff-cGdtcHkvdGVzdHMvdGVzdF9lc3RpbWF0b3JzL3Rlc3RfTW1oY0VzdGltYXRvci5weQ==) | `58.82% <0.00%> (-2.29%)` | :arrow_down: |
| [pgmpy/factors/continuous/discretize.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1349/diff?src=pr&el=tree#diff-cGdtcHkvZmFjdG9ycy9jb250aW51b3VzL2Rpc2NyZXRpemUucHk=) | `34.21% <0.00%> (-1.69%)` | :arrow_down: |
| [pgmpy/tests/test\_estimators/test\_SEMEstimator.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1349/diff?src=pr&el=tree#diff-cGdtcHkvdGVzdHMvdGVzdF9lc3RpbWF0b3JzL3Rlc3RfU0VNRXN0aW1hdG9yLnB5) | `79.66% <0.00%> (-1.59%)` | :arrow_down: |
| [pgmpy/factors/base.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1349/diff?src=pr&el=tree#diff-cGdtcHkvZmFjdG9ycy9iYXNlLnB5) | `84.21% <0.00%> (-0.79%)` | :arrow_down: |
| [...gmpy/factors/distributions/GaussianDistribution.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1349/diff?src=pr&el=tree#diff-cGdtcHkvZmFjdG9ycy9kaXN0cmlidXRpb25zL0dhdXNzaWFuRGlzdHJpYnV0aW9uLnB5) | `69.00% <0.00%> (-0.61%)` | :arrow_down: |
| [pgmpy/factors/continuous/ContinuousFactor.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1349/diff?src=pr&el=tree#diff-cGdtcHkvZmFjdG9ycy9jb250aW51b3VzL0NvbnRpbnVvdXNGYWN0b3IucHk=) | `86.11% <0.00%> (-0.38%)` | :arrow_down: |
| ... and [12 more](https://codecov.io/gh/pgmpy/pgmpy/pull/1349/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pgmpy/pgmpy/pull/1349?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/1349?src=pr&el=footer). Last update [84046ca...0eeecb3](https://codecov.io/gh/pgmpy/pgmpy/pull/1349?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/pgmpy/estimators/HillClimbSearch.py b/pgmpy/estimators/HillClimbSearch.py
index 66b942d9..c19cd865 100644
--- a/pgmpy/estimators/HillClimbSearch.py
+++ b/pgmpy/estimators/HillClimbSearch.py
@@ -284,7 +284,8 @@ class HillClimbSearch(StructureEstimator):
             iteration = range(int(max_iter))
 
         # Step 2: For each iteration, find the best scoring operation and
-        #         do that to the current model.
+        #         do that to the current model. If no legal operation is
+        #         possible, sets best_operation=None.
         for _ in iteration:
             best_operation, best_score_delta = max(
                 self._legal_operations(
@@ -297,6 +298,7 @@ class HillClimbSearch(StructureEstimator):
                     fixed_edges,
                 ),
                 key=lambda t: t[1],
+                default=(None, None),
             )
 
             if best_operation is None or best_score_delta < epsilon:


Test patch:
diff --git a/pgmpy/tests/test_estimators/test_HillClimbSearch.py b/pgmpy/tests/test_estimators/test_HillClimbSearch.py
index a1c147b9..3aa4a34e 100644
--- a/pgmpy/tests/test_estimators/test_HillClimbSearch.py
+++ b/pgmpy/tests/test_estimators/test_HillClimbSearch.py
@@ -206,6 +206,25 @@ class TestHillClimbEstimator(unittest.TestCase):
             in self.est_titanic2.estimate(fixed_edges=[("Pclass", "Survived")]).edges()
         )
 
+    def test_no_legal_operation(self):
+        data = pd.DataFrame(
+            [
+                [1, 0, 0, 1, 0, 0, 1, 1, 0],
+                [1, 0, 1, 0, 0, 1, 0, 1, 0],
+                [1, 0, 0, 0, 0, 1, 0, 1, 1],
+                [1, 1, 0, 1, 0, 1, 1, 0, 0],
+                [0, 0, 1, 0, 0, 1, 1, 0, 0],
+            ],
+            columns=list("ABCDEFGHI"),
+        )
+        est = HillClimbSearch(data)
+        best_model = est.estimate(
+            fixed_edges=[("A", "B"), ("B", "C")], white_list=[("F", "C")]
+        )
+        self.assertEqual(
+            set(best_model.edges()), set([("A", "B"), ("B", "C"), ("F", "C")])
+        )
+
     def tearDown(self):
         del self.rand_data
         del self.est_rand