Repository: networkx/networkx

Issue:
fast_gnp_random_graph doesn't sample directed edges uniformly, wrong way of avoiding loop edges
I've noticed the following small mistake in the generation of the Erdős-Rényi graph.

The current way to avoid loop edges is to just take the next node if a loop edge would be generated:
https://github.com/networkx/networkx/blob/master/networkx/generators/random_graphs.py#L101

However, this way the edge `(v, v+1)` has twice the probability as any other edge. This non-uniformity can be very easily demonstrated on small graphs:
```
neighbors_of_0 = []
for i in range(1000):
    G = networkx.generators.random_graphs.fast_gnp_random_graph(4, 0.2, directed=True)
    for (i,j) in G.edges:
        if i == 0:
            neighbors_of_0.append(j)
print(np.bincount(neighbors_of_0) / 1000)

# output: array([0.   , 0.351, 0.203, 0.197])
```

Note: I don't quite understand why, but it appears empirically that the probability is not actually two times the others, but hovers around ~3.5. I think I'm missing something.
Edit*: Yes, in fact it's not exactly twice. In the loop-edges-included model, p((v,v+1)) is the sum of immediately arriving at (v,v+1) (skipping (v,v)) plus arriving at (v,v) and then at (v,v+1), while here the probability is the sum of the probability of arriving at (v,v) and the probability of arriving at (v,v+1) given that (v,v) is not included.

I believe this must have been introduced as a response to the following issue (when the generation of loop edges was excluded): https://github.com/networkx/networkx/issues/575

The cited paper [1] doesn't cover the proper way of excluding loop edges as far as I can tell. I believe one way of doing it correctly would be to generate the node `w` from `[0, n-2]` instead of `[0, n-1]` and use edge  `(v, w) if w < v else (v, w+1)`.

[1] Vladimir Batagelj and Ulrik Brandes, "Efficient generation of large random networks"


Discussion:

Resolving patch:
diff --git a/networkx/generators/random_graphs.py b/networkx/generators/random_graphs.py
--- a/networkx/generators/random_graphs.py
+++ b/networkx/generators/random_graphs.py
@@ -78,28 +78,12 @@ def fast_gnp_random_graph(n, p, seed=None, directed=False):
     if p <= 0 or p >= 1:
         return nx.gnp_random_graph(n, p, seed=seed, directed=directed)
 
-    w = -1
     lp = math.log(1.0 - p)
 
     if directed:
         G = nx.DiGraph(G)
-        # Nodes in graph are from 0,n-1 (start with v as the first node index).
-        v = 0
-        while v < n:
-            lr = math.log(1.0 - seed.random())
-            w = w + 1 + int(lr / lp)
-            if v == w:  # avoid self loops
-                w = w + 1
-            while v < n <= w:
-                w = w - n
-                v = v + 1
-                if v == w:  # avoid self loops
-                    w = w + 1
-            if v < n:
-                G.add_edge(v, w)
-    else:
-        # Nodes in graph are from 0,n-1 (start with v as the second node index).
         v = 1
+        w = -1
         while v < n:
             lr = math.log(1.0 - seed.random())
             w = w + 1 + int(lr / lp)
@@ -107,7 +91,19 @@ def fast_gnp_random_graph(n, p, seed=None, directed=False):
                 w = w - v
                 v = v + 1
             if v < n:
-                G.add_edge(v, w)
+                G.add_edge(w, v)
+
+    # Nodes in graph are from 0,n-1 (start with v as the second node index).
+    v = 1
+    w = -1
+    while v < n:
+        lr = math.log(1.0 - seed.random())
+        w = w + 1 + int(lr / lp)
+        while w >= v and v < n:
+            w = w - v
+            v = v + 1
+        if v < n:
+            G.add_edge(v, w)
     return G
 
 


Test patch:
diff --git a/networkx/generators/tests/test_random_graphs.py b/networkx/generators/tests/test_random_graphs.py
--- a/networkx/generators/tests/test_random_graphs.py
+++ b/networkx/generators/tests/test_random_graphs.py
@@ -247,6 +247,27 @@ def test_gnp(self):
                 edges += sum(1 for _ in generator(10, 0.99999, directed=True).edges())
             assert abs(edges / float(runs) - 90) <= runs * 2.0 / 100
 
+            # assert that edges are generated with correct probability
+            runs = 5000
+            n = 5
+            for p in [0.2, 0.8]:
+                for directed in [False, True]:
+                    edge_counts = [[0] * 5 for row in range(5)]
+                    for i in range(runs):
+                        G = generator(n, p, directed=directed)
+                        for (v, w) in G.edges:
+                            edge_counts[v][w] += 1
+                            if not directed:
+                                edge_counts[w][v] += 1
+                    for v in range(n):
+                        for w in range(n):
+                            if v == w:
+                                # There should be no loops
+                                assert edge_counts[v][w] == 0
+                            else:
+                                # Each edge should have been generated with probability close to p
+                                assert abs(edge_counts[v][w] / float(runs) - p) <= 0.03
+
     def test_gnm(self):
         G = nx.gnm_random_graph(10, 3)
         assert len(G) == 10