{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# PageRank - A numpy / Jupyter / matplotlib example\n",
"\n",
"In this Jupyter notebook we consider Google's PageRank computation. Nodes correspond to webpages and edges to links from one webpage to another webpage. We consider a very simple graph with only six _nodes_ and where every node has one or two outgoing _edges_. The original description of the PageRank computation can be found in the paper below that contains an overview of the original infrastructure of the Google search engine.\n",
"\n",
"* Sergey Brin and Lawrence Page.\n",
" _The Anatomy of a Large-Scale Hypertextual Web Search Engine_.\n",
" Seventh International World-Wide Web Conference (WWW 1998), April 14-18, 1998, Brisbane, Australia.\n",
" [http://ilpubs.stanford.edu:8090/361/]\n",
" \n",
"\n",
"\n",
"## Random surfer (simplified)\n",
"The PageRank of a node (web page) is the ratio of the time one visits a node by performing an _infinite random traversal_ of a graph where one starts in node 1, and in each step performs:\n",
"\n",
" * with probability 1/6 jumps to a random page (probability 1/6 for each node)\n",
" * with probability 5/6 follows an outgoing edge to an adjacent node (selected uniformly)\n",
" \n",
"The above can be simulated by using a dice: Roll a dice. If it shows 6, jump to a random page by rolling the dice again to figure out which node to jump to. If the dice shows 1-5, follow an outgoing edge - if two outgoing edges roll the dice again and go to the lower number neighbor if it is odd."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Graph represenation - _adjacency matrix_"
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[0 1 0 0 0 0]\n",
" [0 0 0 1 0 0]\n",
" [1 1 0 0 0 0]\n",
" [0 1 0 0 1 0]\n",
" [0 1 0 0 0 1]\n",
" [0 1 0 0 0 0]]\n"
]
}
],
"source": [
"import numpy as np\n",
"\n",
"# Adjacency matrix of the above directed graph \n",
"# (note that the rows/colums are 0-indexed, whereas in the figure the nodes are 1-indexed)\n",
"\n",
"G = np.array([[0, 1, 0, 0, 0, 0],\n",
" [0, 0, 0, 1, 0, 0],\n",
" [1, 1, 0, 0, 0, 0],\n",
" [0, 1, 0, 0, 1, 0],\n",
" [0, 1, 0, 0, 0, 1],\n",
" [0, 1, 0, 0, 0, 0]])\n",
"\n",
"n = G.shape[0] # number of rows in G\n",
"\n",
"degree = np.sum(G, axis=1, keepdims=1) # creates a column vector with row sums = out-degrees\n",
"\n",
"# The below code handles sinks, i.e. nodes with outdegree zero\n",
"# this has no effect on the graph above, since no sinks\n",
"\n",
"G = G + (degree == 0) # add edges from sinks to all nodes \n",
"degree = np.sum(G, axis=1, keepdims=1)\n",
"\n",
"print(G)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Simulate random walk (random surfer model)"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[1], [3], [0, 1], [1, 4], [1, 5], [1]]\n",
"[0.039309, 0.353161, 0.027576, 0.322223, 0.162165, 0.095566]\n"
]
}
],
"source": [
"from random import randint\n",
"\n",
"STEPS = 1000000\n",
"\n",
"# adjacency_list[i] is a list of all j where (i, j) is an edge of the graph.\n",
"adjacency_list = [[col for col in range(n) if G[row, col]] for row in range(n)]\n",
"\n",
"count = [0] * n # histogram over number of node visits\n",
"state = 0 # start at node with index 0\n",
"for _ in range(STEPS):\n",
" count[state] += 1\n",
" if randint(1, 6) == 6: # original paper uses 15% instead of 1/6\n",
" state = randint(0, 5)\n",
" else:\n",
" d = len(adjacency_list[state])\n",
" state = adjacency_list[state][randint(0, d - 1)]\n",
"\n",
"print(adjacency_list, [c / STEPS for c in count], sep=\"\\n\")"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"scrolled": false
},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
"