Repository: networkx/networkx

Issue:
Personalization in PageRank
In https://github.com/networkx/networkx/blob/master/networkx/algorithms/link_analysis/pagerank_alg.py in all algorithms, the personalization dict accepted needs a value for every node. In my personal application, most values are 0. I suspect that in a lot of applications, including the default, most nodes will have the same probability.
Furthermore, for all but the PageRank algorithm, the docstring suggests that each of these values needs to be non-zero. As far as I know, there is no code-technical or mathematical basis for this and it should be "at least one non-zero value".

My proposal:

- Add a parameter specifying a default value for personalization.
- Do not force presence of all keys in personalization, instead use default value for non-existant values.
- Rewrite docstrings to specify "at least one non-zero value"

I'll be happy to implement this. Feedback appreciated. :)


Discussion:
I'm not sure why the documentation is unclear.  It should be "at least one non-zero value" as you suggest.

I'm less excited  about adding another keyword parameter for a default value.  In your use case the default is 0.  How about just allowing a dictionary with only some of the node keys and the rest get set to zero inside the pagerank functions?  If a user requires a different default from 0 then the full dictionary would need to be specified.  
The use case for a default value would be "values x, y, z for these nodes, then uniform over the rest".

I do agree with your reluctance for another parameter, and I suppose above use case is uncommon enough to require a full dict for it. I'll adopt your suggestion when implementing.
It's reasonably easy to do that using dict.fromkeys()

```python
In [1]: import networkx as nx

In [2]: G = nx.path_graph(10)

In [3]: d = dict.fromkeys(G,0.5)

In [4]: d
Out[4]: 
{0: 0.5,
 1: 0.5,
 2: 0.5,
 3: 0.5,
 4: 0.5,
 5: 0.5,
 6: 0.5,
 7: 0.5,
 8: 0.5,
 9: 0.5}

In [5]: d.update({0:1,2:1})

In [6]: d
Out[6]: {0: 1, 1: 0.5, 2: 1, 3: 0.5, 4: 0.5, 5: 0.5, 6: 0.5, 7: 0.5, 8: 0.5, 9: 0.5}
```

Resolving patch:
diff --git a/networkx/algorithms/link_analysis/pagerank_alg.py b/networkx/algorithms/link_analysis/pagerank_alg.py
--- a/networkx/algorithms/link_analysis/pagerank_alg.py
+++ b/networkx/algorithms/link_analysis/pagerank_alg.py
@@ -35,8 +35,9 @@ def pagerank(G, alpha=0.85, personalization=None,
 
     personalization: dict, optional
       The "personalization vector" consisting of a dictionary with a
-      key for every graph node and personalization value for each node.
+      key some subset of graph nodes and personalization value each of those.
       At least one personalization value must be non-zero.
+      If not specfiied, a nodes personalization value will be zero.
       By default, a uniform distribution is used.
 
     max_iter : integer, optional
@@ -129,11 +130,6 @@ def pagerank(G, alpha=0.85, personalization=None,
         # Assign uniform personalization vector if not given
         p = dict.fromkeys(W, 1.0 / N)
     else:
-        missing = set(G) - set(personalization)
-        if missing:
-            raise NetworkXError('Personalization dictionary '
-                                'must have a value for every node. '
-                                'Missing nodes %s' % missing)
         s = float(sum(personalization.values()))
         p = dict((k, v / s) for k, v in personalization.items())
 
@@ -141,11 +137,6 @@ def pagerank(G, alpha=0.85, personalization=None,
         # Use personalization vector if dangling vector not specified
         dangling_weights = p
     else:
-        missing = set(G) - set(dangling)
-        if missing:
-            raise NetworkXError('Dangling node dictionary '
-                                'must have a value for every node. '
-                                'Missing nodes %s' % missing)
         s = float(sum(dangling.values()))
         dangling_weights = dict((k, v/s) for k, v in dangling.items())
     dangling_nodes = [n for n in W if W.out_degree(n, weight=weight) == 0.0]
@@ -160,7 +151,7 @@ def pagerank(G, alpha=0.85, personalization=None,
             # doing a left multiply x^T=xlast^T*W
             for nbr in W[n]:
                 x[nbr] += alpha * xlast[n] * W[n][nbr][weight]
-            x[n] += danglesum * dangling_weights[n] + (1.0 - alpha) * p[n]
+            x[n] += danglesum * dangling_weights.get(n,0) + (1.0 - alpha) * p.get(n,0)
         # check convergence, l1 norm
         err = sum([abs(x[n] - xlast[n]) for n in x])
         if err < N*tol:
@@ -183,7 +174,9 @@ def google_matrix(G, alpha=0.85, personalization=None,
 
     personalization: dict, optional
       The "personalization vector" consisting of a dictionary with a
-      key for every graph node and nonzero personalization value for each node.
+      key some subset of graph nodes and personalization value each of those.
+      At least one personalization value must be non-zero.
+      If not specfiied, a nodes personalization value will be zero.
       By default, a uniform distribution is used.
 
     nodelist : list, optional
@@ -238,25 +231,15 @@ def google_matrix(G, alpha=0.85, personalization=None,
     if personalization is None:
         p = np.repeat(1.0 / N, N)
     else:
-        missing = set(nodelist) - set(personalization)
-        if missing:
-            raise NetworkXError('Personalization vector dictionary '
-                                'must have a value for every node. '
-                                'Missing nodes %s' % missing)
-        p = np.array([personalization[n] for n in nodelist], dtype=float)
+        p = np.array([personalization.get(n, 0) for n in nodelist], dtype=float)
         p /= p.sum()
 
     # Dangling nodes
     if dangling is None:
         dangling_weights = p
     else:
-        missing = set(nodelist) - set(dangling)
-        if missing:
-            raise NetworkXError('Dangling node dictionary '
-                                'must have a value for every node. '
-                                'Missing nodes %s' % missing)
         # Convert the dangling dictionary into an array in nodelist order
-        dangling_weights = np.array([dangling[n] for n in nodelist],
+        dangling_weights = np.array([dangling.get(n, 0) for n in nodelist],
                                     dtype=float)
         dangling_weights /= dangling_weights.sum()
     dangling_nodes = np.where(M.sum(axis=1) == 0)[0]
@@ -288,9 +271,11 @@ def pagerank_numpy(G, alpha=0.85, personalization=None, weight='weight',
       Damping parameter for PageRank, default=0.85.
 
     personalization: dict, optional
-       The "personalization vector" consisting of a dictionary with a
-       key for every graph node and nonzero personalization value for each
-       node. By default, a uniform distribution is used.
+      The "personalization vector" consisting of a dictionary with a
+      key some subset of graph nodes and personalization value each of those.
+      At least one personalization value must be non-zero.
+      If not specfiied, a nodes personalization value will be zero.
+      By default, a uniform distribution is used.
 
     weight : key, optional
       Edge data key to use as weight.  If None weights are set to 1.
@@ -370,9 +355,11 @@ def pagerank_scipy(G, alpha=0.85, personalization=None,
       Damping parameter for PageRank, default=0.85.
 
     personalization: dict, optional
-       The "personalization vector" consisting of a dictionary with a
-       key for every graph node and nonzero personalization value for each
-       node. By default, a uniform distribution is used.
+      The "personalization vector" consisting of a dictionary with a
+      key some subset of graph nodes and personalization value each of those.
+      At least one personalization value must be non-zero.
+      If not specfiied, a nodes personalization value will be zero.
+      By default, a uniform distribution is used.
 
     max_iter : integer, optional
       Maximum number of iterations in power method eigenvalue solver.
@@ -452,26 +439,15 @@ def pagerank_scipy(G, alpha=0.85, personalization=None,
     if personalization is None:
         p = scipy.repeat(1.0 / N, N)
     else:
-        missing = set(nodelist) - set(personalization)
-        if missing:
-            raise NetworkXError('Personalization vector dictionary '
-                                'must have a value for every node. '
-                                'Missing nodes %s' % missing)
-        p = scipy.array([personalization[n] for n in nodelist],
-                        dtype=float)
+        p = scipy.array([personalization.get(n, 0) for n in nodelist], dtype=float)
         p = p / p.sum()
 
     # Dangling nodes
     if dangling is None:
         dangling_weights = p
     else:
-        missing = set(nodelist) - set(dangling)
-        if missing:
-            raise NetworkXError('Dangling node dictionary '
-                                'must have a value for every node. '
-                                'Missing nodes %s' % missing)
         # Convert the dangling dictionary into an array in nodelist order
-        dangling_weights = scipy.array([dangling[n] for n in nodelist],
+        dangling_weights = scipy.array([dangling.get(n, 0) for n in nodelist],
                                        dtype=float)
         dangling_weights /= dangling_weights.sum()
     is_dangling = scipy.where(S == 0)[0]


Test patch:
diff --git a/networkx/algorithms/link_analysis/tests/test_pagerank.py b/networkx/algorithms/link_analysis/tests/test_pagerank.py
--- a/networkx/algorithms/link_analysis/tests/test_pagerank.py
+++ b/networkx/algorithms/link_analysis/tests/test_pagerank.py
@@ -71,13 +71,7 @@ def test_google_matrix(self):
         p = numpy.array(ev[:, 0] / ev[:, 0].sum())[:, 0]
         for (a, b) in zip(p, self.G.pagerank.values()):
             assert_almost_equal(a, b)
-
-        personalize = dict((n, random.random()) for n in G)
-        M = networkx.google_matrix(G, alpha=0.9, personalization=personalize)
-        personalize.pop(1)
-        assert_raises(networkx.NetworkXError, networkx.google_matrix, G,
-                      personalization=personalize)
-
+        
     def test_personalization(self):
         G = networkx.complete_graph(4)
         personalize = {0: 1, 1: 1, 2: 4, 3: 4}
@@ -85,9 +79,6 @@ def test_personalization(self):
         p = networkx.pagerank(G, alpha=0.85, personalization=personalize)
         for n in G:
             assert_almost_equal(p[n], answer[n], places=4)
-        personalize.pop(0)
-        assert_raises(networkx.NetworkXError, networkx.pagerank, G,
-                      personalization=personalize)
 
     def test_zero_personalization_vector(self):
         G = networkx.complete_graph(4)
@@ -102,7 +93,15 @@ def test_one_nonzero_personalization_value(self):
         p = networkx.pagerank(G, alpha=0.85, personalization=personalize)
         for n in G:
             assert_almost_equal(p[n], answer[n], places=4)
-
+            
+    def test_incomplete_personalization(self):
+        G = networkx.complete_graph(4)
+        personalize = {3: 1}
+        answer = {0: 0.22077931820379187, 1: 0.22077931820379187, 2: 0.22077931820379187, 3: 0.3376620453886241}
+        p = networkx.pagerank(G, alpha=0.85, personalization=personalize)
+        for n in G:
+            assert_almost_equal(p[n], answer[n], places=4)
+       
     def test_dangling_matrix(self):
         """
         Tests that the google_matrix doesn't change except for the dangling