Back to Graph Theory Series

PageRank

September 13, 2026 Wasil Zafar 19 min read

PageRank treats attention like probability flowing through a directed graph: important pages pass influence through their links, every vote is divided among its destinations, and occasional teleportation keeps the whole system from trapping or leaking rank.

Contents

  1. A Bit of History
  2. The Random Surfer
  3. The PageRank Equation
  4. Dangling Pages & Traps
  5. Visual Walkthrough
  6. Matrix View
  7. Why Iteration Converges
  8. Complexity Analysis
  9. Implementation
  10. Choosing Damping
  11. Applications
  12. Pitfalls
  13. Exercises

A Bit of History

Larry Page and Sergey Brin described PageRank while developing an early large-scale web search system at Stanford in the late 1990s. The central idea borrowed the spirit of academic citation: a link is a vote, but a vote from an important page should count more than one from an obscure page.

The clever step is recursive. Importance flows through a directed graph until the scores become self-consistent. That connects PageRank to Markov chains, stationary distributions, eigenvectors, and iterative numerical methods—not merely to counting incoming links.

The Random-Surfer Mental Model

Imagine a visitor who browses forever. From the current page, the visitor:

  • with probability $d$, follows one outgoing link chosen uniformly;
  • with probability $1-d$, teleports to a page selected from a fixed probability distribution.

PageRank is the long-run probability of finding this visitor on each page. Uniform teleportation chooses every page with probability $1/N$. Personalized PageRank replaces that uniform choice with a preference vector, biasing the walk toward a topic, user, or seed set.

incoming quality

Votes carry their source's rank

Ten links from negligible pages need not outweigh one link from a highly ranked page.

1 / outdegree

A page divides its vote

A page with ten destinations sends one tenth of its link-following probability to each.

1 − d

Teleportation prevents lock-in

A small restart probability lets the visitor escape cycles and reach every page.

The PageRank Equation

With uniform teleportation and no dangling pages, the score of page $v$ satisfies

$$r(v)=\frac{1-d}{N}+d\sum_{u\to v}\frac{r(u)}{L(u)},$$

where $L(u)$ is the number of outgoing links from $u$. The formula has two clearly different sources of probability:

  1. Teleportation: every page receives a base amount $(1-d)/N$.
  2. Link flow: every non-dangling page distributes $d\,r(u)$ equally among its outgoing neighbors.
  3. Dangling redistribution: rank held by pages with no outgoing links is distributed according to the teleportation vector.
  4. Repeat synchronously: compute an entirely new vector from the old one until the total change is below a tolerance.

Mass-conservation check: a correct iteration keeps every score non-negative and the total rank equal to $1$ (up to floating-point rounding).

Dangling Pages, Rank Sinks, and Teleportation

A dangling page has no outgoing links. If its rank is simply discarded, total probability shrinks every iteration—the flaw in many short PageRank examples. Treat a dangling page as linking according to the teleportation distribution. With uniform teleportation, its rank is spread evenly across all pages.

A rank sink is a closed region with no links leading out. Pure link-following can trap all probability there. Separate directed cycles can also make a walk periodic. Teleportation fixes both problems by making the transition matrix positive when the preference vector is positive.

For a general preference vector $p$ with $\sum_v p(v)=1$, the complete update is

$$r_{k+1}(v)=(1-d)p(v)+d\left[D_k p(v)+\sum_{u\to v}\frac{r_k(u)}{L(u)}\right],$$

where $D_k=\sum_{u:L(u)=0}r_k(u)$ is the dangling mass at iteration $k$.

Visual Walkthrough

Let $A$ link to $B$ and $C$, $B$ link only to $C$, and $C$ link only to $A$. Start with $r_0=(1/3,1/3,1/3)$, use $d=0.85$, and teleport uniformly. The base contribution is $(1-d)/3=0.05$ per page.

One PageRank update on a three-page directed graph Page A splits its link contribution between B and C, B sends all of its link contribution to C, and C sends all of its contribution to A. Uniform teleportation adds 0.05 to every page. Iteration 0 → 1: follow links, then add teleportation A sends 0.1417A sends 0.1417B sends 0.2833C sends 0.2833 Arank 1/3Brank 1/3Crank 1/3 Every page also receives teleportation base 0.05. New rank vector A = 0.05 + 0.2833= 0.3333B = 0.05 + 0.1417= 0.1917C = 0.05 + 0.1417 + 0.2833= 0.4750 Check: 0.3333 + 0.1917 + 0.4750 = 1 C grows because it receives a half-share from A and a full share from B.
Rank is divided at the source. A has two outgoing links, so it splits its link-following contribution. B and C each have one destination and send their entire link-following contribution there.
Iteration$r(A)$$r(B)$$r(C)$$L_1$ change
00.3333330.3333330.333333—
10.3333330.1916670.4750000.283333
20.4537500.1916670.3545830.240833
50.3948960.2178310.3872730.036975
Limit0.3877900.2148110.3974000

The Same Idea as a Matrix Equation

Construct a column-stochastic link matrix $S$: column $u$ divides one unit equally among $u$'s outgoing neighbors; a dangling column is replaced by the preference vector $p$. The Google matrix is

$$G=dS+(1-d)p\mathbf{1}^{\mathsf T},$$

and PageRank is the stationary vector satisfying $r=Gr$. Because columns sum to one, multiplying by $G$ preserves total probability. Power iteration simply computes $r_{k+1}=Gr_k$ until the vector stops changing.

Watch the Orientation

This article stores rank as a column vector, so source pages are columns and destinations are rows. Libraries that use row vectors transpose the convention. Both are correct; mixing them silently reverses the flow.

Why Power Iteration Converges

With $0\le d<1$ and a strictly positive preference vector, every entry of $G$ is positive. The resulting Markov chain is irreducible and aperiodic, so it has one positive stationary distribution.

There is also a direct numerical intuition. The stochastic link step cannot increase $L_1$ distance, and teleportation contributes the same fixed vector to every update. Therefore the PageRank update $F$ satisfies

$$\lVert F(x)-F(y)\rVert_1\le d\,\lVert x-y\rVert_1.$$

Since $d<1$, each iteration contracts differences by at least a factor of $d$. Banach's fixed-point theorem gives a unique fixed point and convergence from any starting probability vector. Smaller $d$ contracts faster; larger $d$ lets link structure dominate more strongly.

Stacked probability bars showing PageRank convergence Five stacked bars show the ranks of pages A, B, and C at iterations zero, one, two, five, and at convergence. The values settle near 0.388, 0.215, and 0.397. Probability moves, but total mass stays at 1 ABC 00.51.0 k = 0k = 1k = 2k = 5limit The bars oscillate toward a stable composition: A 0.3878 · B 0.2148 · C 0.3974.
Convergence need not be monotone page by page. A score may overshoot and fall back. The useful stopping signal is the total vector change, such as $\lVert r_{k+1}-r_k\rVert_1$, not whether one page happened to move upward.

Complexity Analysis

With adjacency lists, one synchronous iteration initializes $V$ new scores and traverses every edge once:

$$\text{Time per iteration: }O(V+E) \qquad \text{Space: }O(V+E).$$

After $k$ iterations, total time is $O(k(V+E))$. The contraction bound implies roughly $k=O(\log(1/\varepsilon)/(1-d))$ iterations to reduce error to scale $\varepsilon$, though actual convergence depends on the graph, norm, starting vector, and stopping rule. Sparse linear algebra and vertex-centric message passing make the update naturally distributable.

Implementation

These versions use synchronous updates, redistribute dangling mass, stop on $L_1$ change rather than a fixed iteration count, validate link targets, and leave the input graph unchanged. The Python version also supports a personalized teleportation vector.

def pagerank(
    outlinks, damping=0.85, tolerance=1e-12,
    max_iterations=1000, personalization=None,
):
    """Return (scores, iterations_used, final_L1_change)."""
    n = len(outlinks)
    if n == 0:
        raise ValueError("graph must contain at least one page")
    if not 0 <= damping < 1:
        raise ValueError("damping must satisfy 0 <= d < 1")
    if tolerance <= 0 or max_iterations < 1:
        raise ValueError("invalid stopping parameters")

    links = []
    for row in outlinks:
        unique = list(dict.fromkeys(row))  # treat repeated URLs as one link
        if any(v < 0 or v >= n for v in unique):
            raise ValueError("outlink target outside 0..n-1")
        links.append(unique)

    if personalization is None:
        preference = [1.0 / n] * n
    else:
        if len(personalization) != n or any(x < 0 for x in personalization):
            raise ValueError("invalid personalization vector")
        total = sum(personalization)
        if total == 0:
            raise ValueError("personalization must have positive mass")
        preference = [x / total for x in personalization]

    rank = preference.copy()
    delta = float("inf")

    for iteration in range(1, max_iterations + 1):
        dangling_mass = sum(rank[u] for u in range(n) if not links[u])
        new_rank = [
            ((1 - damping) + damping * dangling_mass) * preference[v]
            for v in range(n)
        ]

        for u in range(n):
            if links[u]:
                share = damping * rank[u] / len(links[u])
                for v in links[u]:
                    new_rank[v] += share

        delta = sum(abs(new_rank[v] - rank[v]) for v in range(n))
        rank = new_rank
        if delta <= tolerance:
            break

    return rank, iteration, delta


# A -> B, C ; B -> C ; C -> A
scores, iterations, delta = pagerank([[1, 2], [2], [0]])
print([round(x, 6) for x in scores])  # [0.38779, 0.214811, 0.3974]
print(sum(scores))                    # approximately 1.0
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <utility>
#include <vector>

struct PageRankResult {
    std::vector<double> scores;
    int iterations;
    double delta;
};

PageRankResult pagerank(std::vector<std::vector<int>> links,
                        double damping = 0.85,
                        double tolerance = 1e-12,
                        int maxIterations = 1000) {
    const int n = static_cast<int>(links.size());
    if (n == 0 || damping < 0 || damping >= 1 ||
            tolerance <= 0 || maxIterations < 1)
        throw std::invalid_argument("invalid PageRank parameters");

    for (auto& row : links) {
        for (int v : row)
            if (v < 0 || v >= n) throw std::invalid_argument("invalid target");
        std::sort(row.begin(), row.end());
        row.erase(std::unique(row.begin(), row.end()), row.end());
    }

    std::vector<double> rank(n, 1.0 / n);
    double delta = 0.0;
    for (int iteration = 1; iteration <= maxIterations; ++iteration) {
        double danglingMass = 0.0;
        for (int u = 0; u < n; ++u)
            if (links[u].empty()) danglingMass += rank[u];

        const double base = ((1.0 - damping) + damping * danglingMass) / n;
        std::vector<double> next(n, base);
        for (int u = 0; u < n; ++u) {
            if (!links[u].empty()) {
                const double share = damping * rank[u] / links[u].size();
                for (int v : links[u]) next[v] += share;
            }
        }

        delta = 0.0;
        for (int v = 0; v < n; ++v) delta += std::abs(next[v] - rank[v]);
        rank = std::move(next);
        if (delta <= tolerance) return {rank, iteration, delta};
    }
    return {rank, maxIterations, delta};
}

int main() {
    PageRankResult result = pagerank({{1, 2}, {2}, {0}});
    std::cout << std::fixed << std::setprecision(6);
    for (double score : result.scores) std::cout << score << ' ';
    std::cout << '\n'; // 0.387790 0.214811 0.397400
}
import java.util.*;

public class PageRank {
    static final class Result {
        final double[] scores;
        final int iterations;
        final double delta;
        Result(double[] scores, int iterations, double delta) {
            this.scores = scores;
            this.iterations = iterations;
            this.delta = delta;
        }
    }

    static Result compute(List<List<Integer>> outlinks,
                          double damping, double tolerance, int maxIterations) {
        int n = outlinks.size();
        if (n == 0 || damping < 0 || damping >= 1 ||
                tolerance <= 0 || maxIterations < 1)
            throw new IllegalArgumentException("invalid PageRank parameters");

        List<List<Integer>> links = new ArrayList<>();
        for (List<Integer> row : outlinks) {
            LinkedHashSet<Integer> unique = new LinkedHashSet<>();
            for (int v : row) {
                if (v < 0 || v >= n) throw new IllegalArgumentException("invalid target");
                unique.add(v);
            }
            links.add(new ArrayList<>(unique));
        }

        double[] rank = new double[n];
        Arrays.fill(rank, 1.0 / n);
        double delta = 0.0;

        for (int iteration = 1; iteration <= maxIterations; iteration++) {
            double danglingMass = 0.0;
            for (int u = 0; u < n; u++)
                if (links.get(u).isEmpty()) danglingMass += rank[u];

            double base = ((1.0 - damping) + damping * danglingMass) / n;
            double[] next = new double[n];
            Arrays.fill(next, base);
            for (int u = 0; u < n; u++) {
                if (!links.get(u).isEmpty()) {
                    double share = damping * rank[u] / links.get(u).size();
                    for (int v : links.get(u)) next[v] += share;
                }
            }

            delta = 0.0;
            for (int v = 0; v < n; v++) delta += Math.abs(next[v] - rank[v]);
            rank = next;
            if (delta <= tolerance) return new Result(rank, iteration, delta);
        }
        return new Result(rank, maxIterations, delta);
    }

    public static void main(String[] args) {
        List<List<Integer>> outlinks = List.of(
            List.of(1, 2), List.of(2), List.of(0)
        );
        Result result = compute(outlinks, 0.85, 1e-12, 1000);
        System.out.println(Arrays.toString(result.scores));
    }
}

Choosing the Damping Factor

ChoiceInterpretationPractical effect
$d=0$Always teleportScores equal the preference vector; links have no effect.
Moderate $d$Mix link structure with restartsStable ranking and reasonably fast convergence.
$d\approx1$Almost always follow linksStructure dominates, but convergence slows and traps exert more influence.
$d=1$Never teleportUniqueness and convergence from arbitrary starts are no longer guaranteed.

The familiar value $0.85$ is a convention, not a mathematical constant. Choose $d$ according to the meaning of a “restart,” the desired locality of a personalized ranking, and the convergence budget. Report it with results because changing $d$ changes the question being answered.

Where PageRank-Style Scores Help

Link analysis

Estimate structural authority in directed hyperlink or reference networks.

Citation networks

Weight a citation by the influence of the paper or patent providing it.

Personalization

Restart near a user's interests or a trusted seed set to obtain topic-sensitive scores.

Software graphs

Rank packages, symbols, or services from directed dependency and call relationships.

Risk propagation

Adapt probability flow to prioritize structurally central entities in dependency networks.

Knowledge graphs

Provide a graph-structural prior that can complement semantic relevance.

Limitations and Common Pitfalls

MistakeSymptomCorrection
Discarding dangling rankTotal score falls below $1$.Redistribute dangling mass through the preference vector.
Updating scores in placeResults depend on vertex traversal order.Use separate old and new vectors for standard synchronous power iteration.
Forgetting outdegree divisionPages with many links create extra probability.Divide each source's link contribution equally among its destinations.
Mixing matrix orientationsRank appears to flow backward.Document whether distributions are row or column vectors.
Using only a fixed iteration countWork is wasted or convergence is premature.Monitor a norm such as $L_1$ change and keep a safety cap.
Treating duplicate links inconsistentlyOne source may accidentally vote several times for the same destination.Deduplicate for a simple web graph or deliberately model a multigraph.
Reading rank as relevance or truthStructurally central but irrelevant nodes dominate.Combine PageRank with content, context, freshness, and domain signals.
Ignoring manipulation and biasLink farms or biased seed vectors distort results.Audit the graph, preference vector, and downstream use.

PageRank Is a Structural Prior

A high score means the random walk spends substantial probability on a node under the chosen graph, damping factor, dangling policy, and preference vector. It does not by itself establish quality, factual accuracy, relevance, fairness, or user value.

Exercises

Follow the Probability

  1. Trace. Reproduce iterations $1$ and $2$ of the three-page example and verify that every vector sums to $1$.
  2. Dangling node. Add page $D$ with no outlinks. Compute its contribution to the next update and confirm that no mass disappears.
  3. Personalize. Use preference vector $(0.8,0.1,0.1)$ on the example and explain how the stationary ranking changes.
  4. Orientation. Write the $3\times3$ column-stochastic matrix $S$ for the example and verify one matrix-vector update by hand.
  5. Experiment. Compare $d=0.5$, $0.85$, and $0.99$: record both the final scores and iterations needed for the same tolerance.
  6. Challenge. Implement a sparse “push” approximation for personalized PageRank that concentrates work near the seed set.

Hint for 4: column A contains $0,1/2,1/2$ because A splits its probability between B and C.