Repository: pgmpy/pgmpy
Pull request #959 (2017-12-18 16:04:16)

Adds markov blanket

#### Changes
- Adds get children function
- Adds markov blanket feature for bayesian networks.



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

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

```diff
@@            Coverage Diff             @@
##              dev     #959      +/-   ##
==========================================
+ Coverage   94.69%   94.72%   +0.02%     
==========================================
  Files         114      114              
  Lines       11185    11201      +16     
==========================================
+ Hits        10592    10610      +18     
+ Misses        593      591       -2
```


| [Impacted Files](https://codecov.io/gh/pgmpy/pgmpy/pull/959?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pgmpy/models/BayesianModel.py](https://codecov.io/gh/pgmpy/pgmpy/pull/959/diff?src=pr&el=tree#diff-cGdtcHkvbW9kZWxzL0JheWVzaWFuTW9kZWwucHk=) | `96.03% <100%> (+0.97%)` | :arrow_up: |
| [pgmpy/base/DirectedGraph.py](https://codecov.io/gh/pgmpy/pgmpy/pull/959/diff?src=pr&el=tree#diff-cGdtcHkvYmFzZS9EaXJlY3RlZEdyYXBoLnB5) | `95.12% <100%> (+0.25%)` | :arrow_up: |
| [pgmpy/tests/test\_models/test\_BayesianModel.py](https://codecov.io/gh/pgmpy/pgmpy/pull/959/diff?src=pr&el=tree#diff-cGdtcHkvdGVzdHMvdGVzdF9tb2RlbHMvdGVzdF9CYXllc2lhbk1vZGVsLnB5) | `100% <100%> (ø)` | :arrow_up: |
| [pgmpy/tests/test\_base/test\_DirectedGraph.py](https://codecov.io/gh/pgmpy/pgmpy/pull/959/diff?src=pr&el=tree#diff-cGdtcHkvdGVzdHMvdGVzdF9iYXNlL3Rlc3RfRGlyZWN0ZWRHcmFwaC5weQ==) | `100% <100%> (ø)` | :arrow_up: |

------

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

harishkashyap: @ankurankan how does this look?
harishkashyap: @ankurankan fixed the docstrings. Thanks.
harishkashyap: I have merged the commits post rebase as well 
ankurankan: @harishkashyap This is still not ready for merge. First rule **no merge commits**. What I meant in my last comment was to do 2 separate commits for your changes for both the methods and then do a `git rebase -i HEAD~4` and the squash the respective commits. You will need to do rebase twice to squash the new commits with the original ones. 

In the end, I would like to see just 2 commits for this PR. Each having all the changes for the relevant methods.
harishkashyap: @ankurankan apologies. how does it look now?
ankurankan: @harishkashyap Looks good. Thanks.

Patch:
diff --git a/pgmpy/base/DirectedGraph.py b/pgmpy/base/DirectedGraph.py
index 39f9b800..9b6b2b89 100644
--- a/pgmpy/base/DirectedGraph.py
+++ b/pgmpy/base/DirectedGraph.py
@@ -315,3 +315,24 @@ class DirectedGraph(nx.DiGraph):
         ['A', 'E']
         """
         return [node for node, in_degree in self.in_degree().items() if in_degree == 0]
+    
+    def get_children(self, node):
+        """
+        Returns a list of children of node.
+        Throws an error if the node is not present in the graph.
+        
+        Parameters
+        ----------
+        node: string, int or any hashable python object.
+            The node whose children would be returned.
+
+        Examples
+        --------
+        >>> from pgmpy.base import DirectedGraph
+        >>> g = DirectedGraph(ebunch=[('A', 'B'), ('C', 'B'), ('B', 'D'), 
+                                      ('B', 'E'), ('B', 'F'), ('E', 'G')])
+        >>> g.children(node='B')
+        ['D', 'E', 'F']
+        """
+        return self.successors(node)
+    
diff --git a/pgmpy/models/BayesianModel.py b/pgmpy/models/BayesianModel.py
index 46bc38d9..f9585ea3 100644
--- a/pgmpy/models/BayesianModel.py
+++ b/pgmpy/models/BayesianModel.py
@@ -997,3 +997,36 @@ class BayesianModel(DirectedGraph):
         if self.cpds:
             model_copy.add_cpds(*[cpd.copy() for cpd in self.cpds])
         return model_copy
+
+    def get_markov_blanket(self, node):
+        """                                                       
+        Returns a markov blanket for a random variable. In the case
+        of Bayesian Networks, the markov blanket is the set of 
+        node's parents, its children and its children's other parents. 
+        
+        Returns
+        -------
+        list(blanket_nodes): List of nodes contained in Markov Blanket
+
+        Parameters
+        ----------
+        node: string, int or any hashable python object.
+              The node whose markov blanket would be returned.
+                                                                  
+        Examples                                                  
+        --------                                                  
+        >>> from pgmpy.models import BayesianModel
+        >>> from pgmpy.factors.discrete import TabularCPD
+        >>> G = BayesianModel([('x', 'y'), ('z', 'y'), ('y', 'w'), ('y', 'v'), ('u', 'w'), 
+                               ('s', 'v'), ('w', 't'), ('w', 'm'), ('v', 'n'), ('v', 'q')])
+        >>> bayes_model.get_markov_blanket('y') 
+        ['s', 'w', 'x', 'u', 'z', 'v']
+        """
+        children = self.get_children(node)
+        parents = self.get_parents(node)
+        blanket_nodes = children + parents
+        for child_node in children:
+            blanket_nodes.extend(self.get_parents(child_node))
+        blanket_nodes = set(blanket_nodes)
+        blanket_nodes.remove(node)
+        return list(blanket_nodes)


Test patch:
diff --git a/pgmpy/tests/test_base/test_DirectedGraph.py b/pgmpy/tests/test_base/test_DirectedGraph.py
index 4db1bd07..eff4a5ad 100644
--- a/pgmpy/tests/test_base/test_DirectedGraph.py
+++ b/pgmpy/tests/test_base/test_DirectedGraph.py
@@ -131,5 +131,9 @@ class TestDirectedGraphMoralization(unittest.TestCase):
                              [['diff', 'grade'], ['diff', 'intel'],
                               ['grade', 'intel']])
 
+    def test_get_children(self):
+        self.assertListEqual(sorted(self.graph.get_children('diff')),
+                             ['grade'])
+
     def tearDown(self):
         del self.graph
diff --git a/pgmpy/tests/test_models/test_BayesianModel.py b/pgmpy/tests/test_models/test_BayesianModel.py
index 3426b792..c2daf5db 100644
--- a/pgmpy/tests/test_models/test_BayesianModel.py
+++ b/pgmpy/tests/test_models/test_BayesianModel.py
@@ -176,6 +176,11 @@ class TestBayesianModelMethods(unittest.TestCase):
         self.assertTrue(self.G1.is_imap(JPD))
         self.assertRaises(TypeError, self.G1.is_imap, fac)
 
+    def test_markov_blanet(self):
+        G = BayesianModel([('x', 'y'), ('z', 'y'), ('y', 'w'), ('y', 'v'), ('u', 'w'), 
+                           ('s', 'v'), ('w', 't'), ('w', 'm'), ('v', 'n'), ('v', 'q')])
+        self.assertEqual(set(G.get_markov_blanket('y')), set(['s', 'w', 'x', 'u', 'z', 'v']))
+
     def test_get_immoralities(self):
         G = BayesianModel([('x', 'y'), ('z', 'y'), ('x', 'z'), ('w', 'y')])
         self.assertEqual(G.get_immoralities(), {('w', 'x'), ('w', 'z')})