# 6Ω of Separation¶

@ApiMethod(name = "kevinBacon")
public Notice kevinBacon() {
ArrayList<Profile> profiles = getAll(PROFILE);    // Get all users from database
String result = getTimestamp() + ":";             // Initialize result string with current date
for (Profile p : profiles)                        // Append user's id and ids of all contacts (=following)
result += p.getId().toString() + "." + p.getFollowing() + ";";
return new Notice(result + ";");                  // Notice is just a wrapper class for String
}

The returned String looks like this:

timestamp:uid1.[uid11, uid12, uid13, ...];uid2.[uid21, uid22, uid23, ...];...;;

With uidxy being the user-id of the y-th person user x follows (contacts are also included in following, used synonymously from here on).

These strings are then concatenated in a file called "data" which then looks like this:

timestamp1:...;;timestamp2:...;;...
In [1]:
import random
from pprint import pprint
import networkx as nx
import matplotlib.pyplot as plt
import numpy as np
from math import inf, nan
import json
import pickle


First things first: We need to read the data into our python program.

In [2]:
data = open("data").read()
entries = [x for x in data.split(";;") if x != ""]    # Divide data by date and remove empty entries
entry = entries[-1]                                   # The last entry, will be used here for demonstration


Because the entry is still just a string, it needs to be split up into an array containing all its data. To make some stuff a bit more digestable, we will also randomly pick 50 users to be used later for demonstrations.

In [3]:
def split_entry(entry):
dt = entry.split(":")[0]    # The entry's timestamp
users = {}                  # Users' contacts {uid1: [uid11, uid12, uid13, ...], uid2: [uid21, uid22, ...], ...}

# Split entry into single users and save their ids and their contacts' ids into users.
for user in entry.split(":")[1].split(";"):
users[user.split(".")[0]] = [x for x in user.split(".")[1][1:-1].split(", ") if x != ""]

return dt, users

In [4]:
dt, users = split_entry(entry)
uids = list(users.keys())
users_test = random.sample([u for u in users if users[u]], 10)    # Pick 10 random connected users for demonstration
users_test_idx = [uids.index(u) for u in users_test]              # Also save indices of users_test
pprint(dict(random.sample(users.items(), 1)))                     # Print 1 random sample from users

{'5747976207073280': []}


To better understand the data, it is helpful to visualize the network. This is done here using a directed graph with the arrows pointing from a user to its contacts.

In [5]:
def visualize_network(network, special=set(), labels=False, small=False):
G = nx.DiGraph()    # Directed Graph -> the thicker parts of lines are "arrow heads", {a: [b]} => (a -> b)

for n in network.items():
for c in n[1]:
G.add_edge(n[0], c)    # Because only edges are added to G, isolated nodes (=users) won't be shown

node_colors = ['r' if x in special else 'b' for x in G.nodes()]

plt.rc('figure', figsize=((6, 4) if small else (15, 10)))
nx.draw_networkx(G, with_labels=labels, node_color=node_colors, node_size=150, width=0.5)
plt.show()

print(f"Network of ePotato users on {dt}:")
visualize_network(users, set(users_test))

Network of ePotato users on 20171123153627:


That's it for data preprocessing, now for the fun part.

## Separation¶

The matix whose elements $s_{X\rightarrow Y}$ are the degrees of separation from user $X$ to user $Y$ is called the separation matrix. It is organized in the following fashion:

$$S = \begin{bmatrix} s_{A\rightarrow A} & s_{A\rightarrow B} & s_{A\rightarrow C} & \dots & s_{A\rightarrow N} \\ s_{B\rightarrow A} & s_{B\rightarrow B} & s_{B\rightarrow C} & \dots & s_{B\rightarrow N} \\ s_{C\rightarrow A} & s_{C\rightarrow B} & s_{C\rightarrow C} & \dots & s_{C\rightarrow N} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ s_{N\rightarrow A} & s_{N\rightarrow B} & s_{N\rightarrow C} & \dots & s_{N\rightarrow N} \end{bmatrix}$$

Note that this matrix will not be symmetrical when considering all contacts of a user (directed separation), because if $X$ follows $Y$, the separation is 1°, but if $Y$ does not follow $X$, the separation $s_{Y\rightarrow X}$ is not 1°!

Because of this, it also follows that the separation matrix for the must be symmetrical when only considering mutual friends ('$X$ follows $Y$' $\Leftrightarrow$ '$Y$ follows $X$') because then $s_{X\rightarrow Y} = s_{Y\rightarrow X} = s_{X\leftrightarrow Y}$ (undirected separation).

The diagonal elements $s_{X\leftrightarrow X}$ of both matrices are equal to zero.

### Directed Separation¶

The separation matrix S will first be initialized to contain inf as every element, symbolizing an infinite separation.

For actually calculating the separations, we need to loop over all users (=nodes) of the network and take a look at their contacts (=network[node]) to build up that user's row in the matrix by setting up an array called levels with the first level being equal to the id of the current user (Separation = 0°)

Then for the next levels:

• Level 1 will contain the ids in network[node] (the contacts / outgoing connections) of the user, Separation = 1°
• Level 2 will contain the contacts of the users in level[1], Separation = 2°
• Level 3 will contain the contacts of the users in level[2], Separation = 3°
• ...

This will go on until no new users are in the last level's users' contacts. Finally, the current user's row in the separation matrix will be filled by assigning the number of the level the column's user is in to that column. This continues for every user until the the matrix is filled.

In [6]:
def separation(network):
S = np.full((len(network), len(network)), inf)    # Initialize S to infinite separations
nodes = list(network.keys())

# Loop over all nodes, each time building up that node's row in the matrix.
for node in network:
levels = [[node]]    # Node X in levels[n] => Separation node -> user X: n°
known = [node]       # Every node only has to be sorted into levels once (the lowest possible level)

# Fill up levels until there are no new nodes to sort
for level in levels:
for node_ in level:
for node__ in network[node_]:
if node__ not in known:
if level is levels[-1]:
levels.append([])
known.append(node__)
levels[-1].append(node__)

# Write out level-numbers as columns in node's row in S
for i in range(len(levels)):
for node_ in levels[i]:
S[nodes.index(node), nodes.index(node_)] = i

return S

In [7]:
def visualize_matrix(mat):
plt.rc('figure', figsize=(15.0, 10.0))
plt.imshow(mat, cmap="jet")
plt.colorbar()
plt.show()

S = separation(users)
S_ = S[users_test_idx, :][:, users_test_idx]
print(f"Directed Separation Matrix (Extract):\n\n{S_}\n\n\nRepresentation:")
visualize_matrix(S_)

Directed Separation Matrix (Extract):

[[  0.   2.   2.   2.   2.   2.   2.  inf  inf   2.]
[  1.   0.   2.   2.   2.   2.   2.  inf  inf   2.]
[  2.   2.   0.   2.   1.   2.   1.  inf  inf   2.]
[  3.   3.   2.   0.   1.   3.   2.  inf  inf   3.]
[  2.   2.   1.   1.   0.   2.   1.  inf  inf   2.]
[  3.   3.   3.   3.   3.   0.   3.  inf  inf   3.]
[  2.   2.   1.   2.   1.   2.   0.  inf  inf   2.]
[  2.   2.   2.   2.   2.   2.   2.   0.  inf   2.]
[  1.   2.   2.   2.   2.   2.   2.  inf   0.   2.]
[  2.   2.   2.   2.   2.   2.   2.  inf  inf   0.]]

Representation:


### Undirected Separation¶

To find the undirected seperation between two users, we must first remove all direction from the network. This is done using either one of two functions: misdirect or loosen. While misdirect removes all directed edges from the network, so that only "mutual friends" remain, loosen converts every edge into an undirected edge, so that the whole network is more closely connected.

In [8]:
def misdirect(network):
# Remove all directed edges from network
net = {k: [c for c in network[k] if c in network and k in network[c]] for k in network}
return net

In [9]:
S_tight = separation(misdirect(users))
S_ = S_tight[users_test_idx, :][:, users_test_idx]
print(f"Undirected (Misdirected) Separation Matrix (Extract):\n\n{S_}\n\n\nRepresentation:")
visualize_matrix(S_)

Undirected (Misdirected) Separation Matrix (Extract):

[[  0.   2.   2.   3.   2.   3.   2.  inf  inf   2.]
[  2.   0.   2.   3.   2.   3.   2.  inf  inf   2.]
[  2.   2.   0.   2.   1.   3.   1.  inf  inf   2.]
[  3.   3.   2.   0.   1.   4.   2.  inf  inf   3.]
[  2.   2.   1.   1.   0.   3.   1.  inf  inf   2.]
[  3.   3.   3.   4.   3.   0.   3.  inf  inf   3.]
[  2.   2.   1.   2.   1.   3.   0.  inf  inf   2.]
[ inf  inf  inf  inf  inf  inf  inf   0.  inf  inf]
[ inf  inf  inf  inf  inf  inf  inf  inf   0.  inf]
[  2.   2.   2.   3.   2.   3.   2.  inf  inf   0.]]

Representation:

In [10]:
def loosen(network):
net = network.copy()
for n, cs in list(net.items()):
for c in cs:
net[c] = list(set(net.get(c, [])) | {n})    # Add node to all connected nodes
return net

In [11]:
S_loose = separation(loosen(users))
S_ = S_loose[users_test_idx, :][:, users_test_idx]
print(f"Undirected (Loose) Separation Matrix (Extract):\n\n{S_}\n\n\nRepresentation:")
visualize_matrix(S_)

Undirected (Loose) Separation Matrix (Extract):

[[ 0.  1.  2.  2.  2.  2.  2.  2.  1.  2.]
[ 1.  0.  2.  2.  2.  2.  2.  2.  2.  2.]
[ 2.  2.  0.  2.  1.  2.  1.  2.  2.  2.]
[ 2.  2.  2.  0.  1.  2.  2.  2.  2.  2.]
[ 2.  2.  1.  1.  0.  2.  1.  2.  2.  2.]
[ 2.  2.  2.  2.  2.  0.  2.  2.  2.  2.]
[ 2.  2.  1.  2.  1.  2.  0.  2.  2.  2.]
[ 2.  2.  2.  2.  2.  2.  2.  0.  2.  2.]
[ 1.  2.  2.  2.  2.  2.  2.  2.  0.  2.]
[ 2.  2.  2.  2.  2.  2.  2.  2.  2.  0.]]

Representation:


Note that I will sometimes refer to a "misdirected" network as a "tightened" network. This is because misdirect came first, then loosen and after that tighten is just a cooler name. But I'm still not going to change it. You do it.

## Resistance Distance¶

The next step is to calculate the resistance between the users (=nodes). For this, every edge (=connection) in the network is assigned a resistance of 1 Ohm. To find out the resulting resistance between two individual users, the whole network has to be simplified to only one edge using the following techniques:

• Simplification
• Series Circuits (3 nodes $\rightarrow$ 2 nodes)
• Parallel Circuits (2 edges $\rightarrow$ 1 edge)
• Star-Mesh Transform ($n + 1$ nodes $\rightarrow$ $n$ nodes, $n$ edges $\rightarrow$ $\binom{n}{2}$ edges)

### Series Circuits¶

The formula for calculating the resulting resistor of a series circuit of $n$ resistors is very simple:

$$R = R_1 + R_2 + ... + R_n$$

but because it is computationally simpler to just regard a series of two resistors at a time, we will only use the case where $n = 2$:

$$R = R_1 + R_2$$

### Parallel Circuits¶

The formula for calculating the resulting resistor of $n$ parallel resistors is also simple:

$$\frac{1}{R} = \frac{1}{R_1} + \frac{1}{R_2} + ... + \frac{1}{R_n}$$

again, because it is simpler, we will just use the case where $n$ = 2, so that

$$R = \frac{R_1 \cdot R_2}{R_1 + R_2}$$

Because it is not possible for parallel connections to exist between two nodes, parallel circuits will only play a role after simplifying a series of edges to one edge. In a network of three nodes A, B and C, where A is connected to B and C and B also is connected to C, there are no parallel edges. If we however simplify the connection A - B - C to A - C by eliminating the middle node B, we need to consider the initial edge A - C as well. This is then done using the above formula to calculate the resulting resistor.

Because of this, both these forms of simplification are bundled in one function called simplify:

In [12]:
def simplify(network, resistors, a, b):
net = network.copy()
rs = resistors.copy()

again = True
while again:
again = False      # Will be set to True if simplification was found
active = {a, b}    # Active nodes: Those that are important for the resistance

# Active nodes need to have incoming and outgoing connections.
for ns in net.values():
active |= {n for n in ns if n in net and net[n]}

net_ = net.copy()    # Backup to check for changes

# Remove all non-active nodes from the network
net = {k: [c for c in net[k] if c in active and c != a] for k in active if k != b}
rs = {k: rs[k] for k in rs if set(k) <= active and k[0] != b and k[1] != a}

# Network has changed -> repeat from the top
if net != net_:
again = True
continue

# Loop over nodes with only one outgoing connection -> check for incoming connections
for n, cs in [(n, cs) for n, cs in net.items() if len(cs) == 1 and n != a]:
# Incoming nodes that are different from the outgoing node
incoming = [x[0] for x in net.items() if n in x[1] and x[0] != cs[0]]

# No incoming connections found -> delete semi-isolated node and repeat from the top
if not incoming and n != b:
del net[n]
again = True
break

# One incoming connection -> redundand node, can be simplified as series connection
if len(incoming) == 1:
i = incoming[0]; o = cs[0]
R = rs[i, n] + rs[n, o]    # Resulting resistor of series circuit

# There is no existing edge -> Just simplify the series circuit, add new edge to network
if (i, o) not in rs:
rs[i, o] = R
net[i].append(o)

# There is an existing edge -> Simplify series and parallel circuits
else:
rs[i, o] = R * rs[i, o] / (R + rs[i, o])

# Delete redundand node from network and repeat from the top
del net[n]
again = True
break

return net, rs


To supply the above function with a network of resistors, a function build_resistors will be defined, initializing every edge in a network to 1 Ohm:

In [13]:
def build_resistors(network):
resistors = {}
# Loop over edges, initialize every resistor to 1 Ohm
for node in network.items():
for node_ in node[1]:
resistors[node[0], node_] = 1    # resistors: {(node1, node2): R}
return resistors


### Star-Mesh Transform¶

Because only very simple networks can be simplified to a single edge using series and parallel circuits, we need to use another tool for simplifying the network, called Star-Mesh transform. It is a generalization of a technique called Y$\Delta$-transform and allows us to eliminate one node from a "star" by adding $\binom{n}{2} - n$ edges, $n$ being the number of edges in the initial star (so $n = 5$ in the picture). The value of these new resistors is given by the formula:

$$R_{\nu\mu} = R_{\nu 0} R_{\mu 0} \sum_{k=1}^{n} \frac{1}{R_{k 0}}$$

where $n$ is the number of edges of the star, 0 is the middle node of the star that is being eliminated and $R_{\nu\mu}$ is the resistor between nodes $\nu$ and $\mu$.

Because this technique does not work for directed connections (or maybe it does, I just couldn't figure out how), we first need to misdirect or loosen the network using the functions defined above before commencing the transformation. Because the resistors are also stored in a directed manner, those need to be undirected as well. This latter task is done using the function undirect.

As a result of this, true directed resistances can only be calculated if the network is so simple that star-mesh transform is no option. However, because simplify does compute directed resistances, we can calculate a semi-directed (from here on just "directed") resistance by first simplifying the directed network and the loosening it for star-mesh transformations.

In [14]:
def star_mesh_transform(network, resistors, a, b):
net = network.copy()
rs = resistors.copy()

# n0: Middle node of star, cs: Edges of star
n0, cs = next((n0, cs) for n0, cs in net.items() if n0 not in {a, b})
Rs = {}

# Calculate new resistors and reconnect network
for n in list(cs):
for m in [c for c in cs if c != n]:
# Calculate R_nm with star-mesh formula
R = rs[n, n0] * rs[m, n0] * sum([1 / R for R in [R for k, R in rs.items() if k[0] == n0]])

net[n] = list(set(net.get(n, [])) | {m})
net[m] = list(set(net.get(m, [])) | {n})

# Condense resistors
rs[n, m] = rs[m, n] = R * rs[n, m] / (R + rs[n, m]) if (n, m) in rs else R

# Remove star-edges
net[n].remove(n0)
net[n0].remove(n)

# Remove old resistors
rs = undirect(rs, net)

return net, rs

In [15]:
def undirect(resistors, network):
rs = {}
# Add all resistors (double-sided) of which the edges are found in the network
for k, R in [(k, R) for k, R in list(resistors.items()) if k[1] in network[k[0]]]:
rs[k[0], k[1]] = rs[k[1], k[0]] = R
return rs


Now that all important functions for calculating the resistance between two nodes in a network are defined, we can create a single function for the computation called resistance. This function will loop over the functions simplify and star_mesh_transform until the network cannot be simplified any more and, if the network was simplified to one edge, return the resulting resistor:

In [16]:
def resistance(network, a, b, resistors=None, iterations=False, visualize=False):
# Check if a = b
if a == b:
return (0, 0) if iterations else 0

net = network.copy()

# If resistors are not predefined, initialize them all to 1 Ohm (directed)
if not resistors:
rs = build_resistors(network)
else:
rs = resistors.copy()

net_ = net.copy()    # Backup to check for changes
i = 0                # Iterations counter

while True:
# Simplification
net, rs = simplify(net, rs, a, b)

if visualize:
print("Simplification (Directed):")
visualize_network(net, {a, b}, small=True, labels=True)

# No changes or single connection: finished
if net == net_ or set(net.keys()) <= {a, b}:
break

net_ = net.copy()    # Backup to check for changes

# Star-Mesh Transform
net = loosen(net)
rs = undirect(rs, net)
net, rs = star_mesh_transform(net, rs, a, b)

i += 1

if visualize:
print(f"Star-Mesh Transform (Loosened Network) #{i}:")
visualize_network(net, {a, b}, small=True, labels=True)

R = nan    # Initialize R to NaN for when it can not be computed using star-mesh transform and simplification
if len(rs) == 1:
R = list(rs.values())[0]
elif len(rs) == 0:
R = inf

return (R, i) if iterations else R


As an example, take a look at the following (small) network and its simplification to one edge:

In [17]:
net = {1: [2, 3], 2: [3, 1, 5, 7], 3: [4, 2], 4: [3, 1, 5, 6], 5: [2, 6], 6: [2, 4], 7: [3]}
a = 1; b = 6

print("Network to be analyzed:")
visualize_network(net, {a, b}, labels=True)

print("R = %8.6fΩ (%d iterations) <- Directed Resistance 1 -> 6" % resistance(net, a, b, iterations=True, visualize=True))
print("R = %8.6fΩ (%d iterations) <- Directed Resistance 6 -> 1" % resistance(net, b, a, iterations=True))
print("R = %8.6fΩ (%d iterations) <- Misdirected Resistance" % resistance(misdirect(net), a, b, iterations=True))
print("R = %8.6fΩ (%d iterations) <- Loose Resistance" % resistance(loosen(net), a, b, iterations=True))

Network to be analyzed:

Simplification (Directed):

Star-Mesh Transform (Loosened Network) #1:

Simplification (Directed):

Star-Mesh Transform (Loosened Network) #2:

Simplification (Directed):

Star-Mesh Transform (Loosened Network) #3:

Simplification (Directed):

R = 1.500000Ω (3 iterations) <- Directed Resistance 1 -> 6
R = 1.000000Ω (2 iterations) <- Directed Resistance 6 -> 1
R = 4.000000Ω (0 iterations) <- Misdirected Resistance
R = 0.743750Ω (4 iterations) <- Loose Resistance


### Resistance Distance Matrix¶

Now we can create a resistance distance matrix $R$, similar to the separation matrix $S$:

$$R = \begin{bmatrix} R_{A\rightarrow A} & R_{A\rightarrow B} & R_{A\rightarrow C} & \dots & R_{A\rightarrow N} \\ R_{B\rightarrow A} & R_{B\rightarrow B} & R_{B\rightarrow C} & \dots & R_{B\rightarrow N} \\ R_{C\rightarrow A} & R_{C\rightarrow B} & R_{C\rightarrow C} & \dots & R_{C\rightarrow N} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ R_{N\rightarrow A} & R_{N\rightarrow B} & R_{N\rightarrow C} & \dots & R_{N\rightarrow N} \end{bmatrix}$$

with the following function:

In [18]:
def resistance_distance(network):
R = np.empty((len(network), len(network)))    # No intialization necessary, resistance always returns a value
nodes = list(network.keys())

for a in network:
for b in network:
R[nodes.index(a), nodes.index(b)] = resistance(network, a, b)

return R

In [19]:
R = resistance_distance(users)
R_ = R[users_test_idx, :][:, users_test_idx]
print(f"Resistance Distance Matrix (Directed, Extract):\n\n{R_}\n\n\nRepresentation:")
visualize_matrix(R_)

Resistance Distance Matrix (Directed, Extract):

[[ 0.          0.66666667  0.84444444  0.91111111  0.8         1.13333333
0.84444444         inf         inf  1.46666667]
[ 0.4         0.          0.77777778  0.84444444  0.73333333  1.06666667
0.77777778         inf         inf  1.4       ]
[ 0.77777778  0.84444444  0.          0.6         0.37777778  1.04444444
0.4                inf         inf  1.37777778]
[ 1.8         1.86666667  1.4         0.          1.          2.06666667
1.4                inf         inf  2.4       ]
[ 0.8         0.86666667  0.4         0.44444444  0.          1.06666667
0.4                inf         inf  1.4       ]
[ 2.4         2.46666667  2.37777778  2.44444444  2.33333333  0.
2.37777778         inf         inf  3.        ]
[ 0.77777778  0.84444444  0.4         0.6         0.37777778  1.04444444
0.                 inf         inf  1.37777778]
[ 0.8         0.86666667  0.96111111  1.02777778  0.91666667  1.25
0.96111111  0.                 inf  1.58333333]
[ 0.375       0.6         0.75277778  0.81944444  0.70833333  1.04166667
0.75277778         inf  0.          1.375     ]
[ 1.          1.06666667  0.97777778  1.04444444  0.93333333  1.26666667
0.97777778         inf         inf  0.        ]]

Representation:

In [20]:
R_tight = resistance_distance(misdirect(users))
R_ = R_tight[users_test_idx, :][:, users_test_idx]
print(f"Resistance Distance Matrix (Misdirected, Extract):\n\n{R_}\n\n\nRepresentation:")
visualize_matrix(R_)

Resistance Distance Matrix (Misdirected, Extract):

[[ 0.          0.66666667  0.86666667  1.86666667  0.86666667  2.46666667
0.86666667         inf         inf  1.46666667]
[ 0.66666667  0.          0.86666667  1.86666667  0.86666667  2.46666667
0.86666667         inf         inf  1.46666667]
[ 0.86666667  0.86666667  0.          1.4         0.4         2.4         0.4
inf         inf  1.4       ]
[ 1.86666667  1.86666667  1.4         0.          1.          3.4         1.4
inf         inf  2.4       ]
[ 0.86666667  0.86666667  0.4         1.          0.          2.4         0.4
inf         inf  1.4       ]
[ 2.46666667  2.46666667  2.4         3.4         2.4         0.          2.4
inf         inf  3.        ]
[ 0.86666667  0.86666667  0.4         1.4         0.4         2.4         0.
inf         inf  1.4       ]
[        inf         inf         inf         inf         inf         inf
inf  0.                 inf         inf]
[        inf         inf         inf         inf         inf         inf
inf         inf  0.                 inf]
[ 1.46666667  1.46666667  1.4         2.4         1.4         3.          1.4
inf         inf  0.        ]]

Representation:

In [21]:
R_loose = resistance_distance(loosen(users))
R_ = R_loose[users_test_idx, :][:, users_test_idx]
print(f"Resistance Distance Matrix (Loose, Extract):\n\n{R_}\n\n\nRepresentation:")
visualize_matrix(R_)

Resistance Distance Matrix (Loose, Extract):

[[ 0.          0.375       0.69920635  0.76587302  0.6547619   0.98809524
0.69920635  0.75        0.375       0.92142857]
[ 0.375       0.          0.74087302  0.80753968  0.69642857  1.0297619
0.74087302  0.79166667  0.5         0.96309524]
[ 0.69920635  0.74087302  0.          0.6         0.37777778  1.04444444
0.4         0.94920635  0.74087302  0.97777778]
[ 0.76587302  0.80753968  0.6         0.          0.44444444  1.11111111
0.6         1.01587302  0.80753968  1.04444444]
[ 0.6547619   0.69642857  0.37777778  0.44444444  0.          1.
0.37777778  0.9047619   0.69642857  0.93333333]
[ 0.98809524  1.0297619   1.04444444  1.11111111  1.          0.
1.04444444  1.23809524  1.0297619   1.26666667]
[ 0.69920635  0.74087302  0.4         0.6         0.37777778  1.04444444
0.          0.94920635  0.74087302  0.97777778]
[ 0.75        0.79166667  0.94920635  1.01587302  0.9047619   1.23809524
0.94920635  0.          0.79166667  1.17142857]
[ 0.375       0.5         0.74087302  0.80753968  0.69642857  1.0297619
0.74087302  0.79166667  0.          0.96309524]
[ 0.92142857  0.96309524  0.97777778  1.04444444  0.93333333  1.26666667
0.97777778  1.17142857  0.96309524  0.        ]]

Representation:


## Whole Network Analysis¶

There's still some information concerning the whole network that is waiting to be extracted from the data. The two thigs we are going to analyse are:

• The size and number of single subnets (that are unconnecterd)
• The average distances between users.

For the former, we are using a function called propagate. This function will begin at a node and spread out from there, walking along the arrows and "collecting" all nodes it reaches into one subnet. The function is called several times (from a function called subnets) until there are no new subnets.

Because the point here is mainly to find big clusters, we will first loosen the network so that every user who is connected to the network will be part of it. If we don't do this, every user who is not "accessible" from the cluster, but does him-/herself access the cluster, would be its on "subnet" including the whole cluster, if that makes sense.

In [22]:
def propagate(network, a):
net = {a: network[a]}      # The subnet to be built up
history = set()            # Nodes already seen
cache = set(network[a])    # Nodes to be crawled

while cache:
for node in cache.copy():
net[node] = list(set(net.get(node, []) + network[node]))    # Add node to subnet
cache |= {n for n in net[node] if n not in history}         # Add all newly accessible nodes to cache

# We're done with this node
cache.remove(node)

return net

In [23]:
def subnets(network):
nets = []        # The subnets to be found
nodes = set()    # Already processed nodes

for node in network:
if node not in nodes:
subnet = propagate(network, node)
nets.append(subnet)                  # Add the subnet reachable from node
nodes.update(subnet.keys())          # A node can only be in one subnet -> ignore all nodes in subnet

return nets

In [24]:
nets = subnets(loosen(users))
largest = 0, 0
connected = 0
isolated = 0

for i, net in enumerate(nets):
if len(net) > largest[1]:
largest = i, len(net)
if len(net) == 1:
isolated += 1

cluster = nets[largest[0]]
percent = largest[1] / len(users) * 100

S_ = separation(cluster)
s = S_.sum() / (S_.size - len(S_))    # Average separation, remove 0° separations from calculation

R_ = resistance_distance(cluster)
r = R_.sum() / (R_.size - len(R_))    # Average resistance, remove 0° separations from calculation

print(f"""
There are {len(nets)} subnets in the network, {isolated} of which are isolated (population: 1 user).
The biggest subnet has {largest[1]} members, making up {percent}% of the whole network.
The average separation S of this largest cluster and its average resistance distance R are:
S = {s}
R = {r}Ω.
""")

There are 9 subnets in the network, 8 of which are isolated (population: 1 user).
The biggest subnet has 24 members, making up 75.0% of the whole network.
The average separation S of this largest cluster and its average resistance distance R are:
S = 2.0217391304347827
R = 1.1016908212560383Ω.



### Export Data¶

To use all this data on the website, we need to export everything computed here as a pickle file.

In [25]:
users = json.load(open('users.json'))    # Conversion Table: {username: user-id} (Created with API-method kevinBacon)

with open("kevinBacon.pickle", 'wb') as f:
pickle.dump({
'dt': dt,
'users': users,
'uids': uids,
'S': S.tolist(),
'S_loose': S_loose.tolist(),
'S_tight': S_tight.tolist(),
'R': R.tolist(),
'R_loose': R_loose.tolist(),
'R_tight': R_tight.tolist(),
's': float(s),
'r': float(r),
'subnets': len(nets),
'isolated': isolated,
'largest': largest[1],
'percent': percent,
}, f, protocol=2)