TrustRank: seed-biased PageRank in pagerankr
Bart Turczynski
2026-07-11
Source:vignettes/trustrank.Rmd
trustrank.RmdWhat TrustRank is
TrustRank (Gyöngyi, Garcia-Molina & Pedersen, 2004) is ordinary PageRank with one change: instead of the random surfer teleporting to a uniform random page, it teleports back into a set of trusted seed pages. Trust then propagates outward along links and attenuates with distance — the further a page is from the trusted core, the less trust reaches it. The PageRank damping factor is exactly that attenuation mechanism, so no extra machinery is needed.
The original motivation was web spam: pick a small set of human-vetted “good” pages, and let trust flow from them so that link farms — which good pages rarely link to — stay low. The same mechanic is useful on a single site for any “authority radiates from these known-good pages” question: your editorial hubs, your hand-picked cornerstone content, or a manually curated quality set.
This is seed-biased PageRank, not a spam classifier.
pagerankr supplies the biased-propagation core;
which pages are trustworthy is your call.
It reuses the personalization path — no new solver
A trusted-seed set is just a teleport (personalization) prior.
pagerankr already builds personalized PageRank from a
per-URL prior via pagerank(prior_df = ...) (the TIPR path).
TrustRank is therefore two small helpers over that engine:
-
trust_seed_prior()turns a seed set into aprior_df. -
trustrank()is the one-call convenience wrapper.
A worked example
edges <- data.frame(
from = c("/", "/", "/hub", "/hub", "/good", "/spam", "/spam"),
to = c("/hub", "/good", "/good", "/deep", "/hub", "/sink", "/good")
)Here / and /hub are our trusted editorial
core. /spam links into the good neighborhood (a classic
spam tactic) but nothing trusted links to it, and it feeds an
off-topic /sink.
Build the seed prior explicitly
prior <- trust_seed_prior(c("/", "/hub"))
prior
#> url weight
#> 1 / 1
#> 2 /hub 1Equal weights reproduce TrustRank’s uniform distribution over the
trusted set. Run it through pagerank() like any other
prior:
pr_manual <- pagerank(edges, prior_df = prior, clean_edge_urls = FALSE)
#> TIPR prior aligned: 2/6 real vertices carry authority; transform='none', alpha=0 (uniform mass ~0.0%). 0 prior URL(s) (sum weight 0) did not fold onto any vertex and were dropped.
pr_manual[order(-pr_manual$pagerank), c("node_name", "pagerank")]
#> node_name pagerank
#> 4 /hub 0.4238061
#> 3 /good 0.2445263
#> 2 /deep 0.1801176
#> 1 / 0.1515500
#> 5 /sink 0.0000000
#> 6 /spam 0.0000000Or use the convenience wrapper
tr <- trustrank(edges, c("/", "/hub"), clean_edge_urls = FALSE)
#> TIPR prior aligned: 2/6 real vertices carry authority; transform='none', alpha=0 (uniform mass ~0.0%). 0 prior URL(s) (sum weight 0) did not fold onto any vertex and were dropped.
tr[order(-tr$pagerank), c("node_name", "pagerank", "prior_weight")]
#> node_name pagerank prior_weight
#> 4 /hub 0.4238061 0.5
#> 3 /good 0.2445263 0.0
#> 2 /deep 0.1801176 0.0
#> 1 / 0.1515500 0.5
#> 5 /sink 0.0000000 0.0
#> 6 /spam 0.0000000 0.0trustrank() is identical to the manual call — it just
builds the prior for you. Two things to read in the output:
-
prior_weightis the teleport mass each page receives. The seeds carry it;/spamand/sink, outside the trusted neighborhood, get exactly0. - A non-seed page reachable from the trusted core
(e.g.
/deep, linked from/hub) still earns trust through inheritance, while the seed-unreachable/spamregion stays suppressed relative to plain PageRank.
Compare against uniform PageRank
uni <- pagerank(edges, clean_edge_urls = FALSE)
compare_pagerank(uni, tr)[, c("node_name", "pagerank_a", "pagerank_b", "delta")]
#> node_name pagerank_a pagerank_b delta
#> 1 /hub 0.31375833 0.4238061 0.11004777
#> 2 /sink 0.09527563 0.0000000 -0.09527563
#> 3 / 0.06686009 0.1515500 0.08468988
#> 4 /spam 0.06686009 0.0000000 -0.06686009
#> 5 /deep 0.20020738 0.1801176 -0.02008979
#> 6 /good 0.25703846 0.2445263 -0.01251213pagerank_a is uniform PageRank, pagerank_b
is TrustRank. The trusted core and what it links to gain; the untrusted
region loses.
Graded trust and a teleport floor
Trust need not be all-or-nothing. Give some seeds more weight than others:
trustrank(
edges,
data.frame(url = c("/", "/hub"), weight = c(3, 1)),
clean_edge_urls = FALSE
)[, c("node_name", "pagerank", "prior_weight")]
#> TIPR prior aligned: 2/6 real vertices carry authority; transform='none', alpha=0 (uniform mass ~0.0%). 0 prior URL(s) (sum weight 0) did not fold onto any vertex and were dropped.
#> node_name pagerank prior_weight
#> 1 / 0.2142415 0.75
#> 2 /deep 0.1595946 0.00
#> 3 /good 0.2506472 0.00
#> 4 /hub 0.3755166 0.25
#> 5 /sink 0.0000000 0.00
#> 6 /spam 0.0000000 0.00By default untrusted, seed-unreachable pages receive no teleport mass
at all. To give every page a small floor (a blend of trust teleport and
uniform teleport), pass prior_alpha:
trustrank(
edges, c("/", "/hub"),
prior_alpha = 0.15, clean_edge_urls = FALSE
)[, c("node_name", "pagerank", "prior_weight")]
#> TIPR prior aligned: 2/6 real vertices carry authority; transform='none', alpha=0.15 (uniform mass ~15.0%). 0 prior URL(s) (sum weight 0) did not fold onto any vertex and were dropped.
#> node_name pagerank prior_weight
#> 1 / 0.141586436 0.450
#> 2 /deep 0.182481103 0.025
#> 3 /good 0.245998351 0.025
#> 4 /hub 0.410859270 0.450
#> 5 /sink 0.011208926 0.025
#> 6 /spam 0.007865913 0.025prior_alpha = 0 (the default) is pure trust teleport;
prior_alpha = 1 reproduces uniform PageRank.
Notes
- Seed weights are an additive trust budget: if two
seed URLs fold onto the same vertex (redirect or canonical variants),
their weights sum — consistent with the prior contract in
pagerank()andalign_prior_to_vertices(). - Everything
pagerank()accepts flows throughtrustrank()via...: redirects, canonicals, URL cleaning, domain/host filtering, edge weights, and duplicate-edge policy. Trusted seeds are canonicalized and folded into the same vertex namespace as the edges before alignment. - For topic-biased authority (multiple content clusters,
blended), see
topic_sensitive_pagerank(); it uses the same personalization path with a per-topic prior instead of a single trusted set.