Skip to contents

Computes a per-topic PageRank by running the standard [pagerank()] engine once per topic, biasing the random surfer's teleport toward each topic's seed pages, then optionally blends the per-topic scores into a single combined ranking.

This is Haveliwala's (2002) Topic-Sensitive PageRank adapted to a single site: instead of one global ranking, each "topic" is a content cluster (e.g. the *pricing* section, the *AI-Agent* product area, the *support* docs) defined by a set of seed URLs. A page can be highly authoritative for one topic and unimportant for another on the *same* link graph — the only thing that changes between runs is where the surfer teleports.

Mechanically this is pure orchestration over the existing TIPR personalization path: each topic becomes a `prior_df` handed to [pagerank()] (see [align_prior_to_vertices()]). There is no new solver and no topic inference — the caller supplies the seed sets.

Usage

topic_sensitive_pagerank(
  edge_list_df,
  topics,
  topic_weights = NULL,
  topic_url_col = "url",
  topic_weight_col = "weight",
  ...
)

Arguments

edge_list_df

A data frame representing the edge list, typically with columns like "from" and "to".

topics

A **uniquely named** list, one element per topic. Each element defines that topic's teleport seed set and is either:

a character vector of seed URLs

each seed gets equal weight `1`;

a data frame

with a URL column and a numeric weight column (see `topic_url_col` / `topic_weight_col`) for weighted seeds.

The list names become the per-topic score column names in the result, so they must be non-empty, unique, and must not be the reserved names `"node_name"` or `"blended"`. Seed URLs are canonicalized and redirect- / canonical-folded into the graph's vertex namespace by [pagerank()] before alignment, identically to any other prior.

topic_weights

Optional blend weights for the `blended` column. Either a named numeric whose names match `topics`, or an unnamed numeric of the same length as `topics` (applied in list order). Must be non-negative, finite, and sum to a positive value; they are normalized to sum to 1 internally. Default `NULL` gives every topic equal weight.

topic_url_col, topic_weight_col

Column names used when a topic is supplied as a data frame. Defaults `"url"` / `"weight"`. Ignored for topics given as plain character vectors.

...

Additional arguments forwarded to [pagerank()] and onward to `igraph::page_rank` (e.g. `redirects_df`, `canonicals_df`, `rurl_params`, `weight_col`, `prior_transform`, `prior_alpha`, `damping`). Because this function owns the teleport prior, passing `prior_df`, `prior_url_col`, or `prior_weight_col` here is an error — supply `topics` instead. Inner per-topic alignment diagnostics are silenced by default (`prior_verbose = FALSE`); pass `prior_verbose = TRUE` to re-enable them.

Value

A data frame with one row per node, sorted by `blended` descending:

node_name

Node identifier (shared vertex namespace).

<one column per topic>

The personalized PageRank score for that topic, named after the corresponding `topics` entry.

blended

The `topic_weights`-weighted combination of the per-topic scores.

Two attributes are attached: `"topic_weights"`, the normalized weights used for the blend, and `"topic_audits"`, a named list of the per-topic [transition_audit] objects from each underlying [pagerank()] run.

Details

All topics are scored on the **same** prepared graph: graph construction (URL cleaning, redirect/canonical folding, domain/host filtering, duplicate and isolate handling) depends only on `edge_list_df` and the forwarded options, never on the teleport prior, so the vertex set is identical across topics. The per-topic results are combined with a full outer join on `node_name`; any node missing from a topic (which can only happen if you opt into `prior_inject_unmatched = TRUE`, where unmatched seed URLs are injected as topic-specific isolates) is filled with score `0` for that topic.

The `blended` column is the weight-normalized linear combination \(\sum_t w_t \cdot score_t\). Each per-topic column carries the same mass semantics as a single [pagerank()] run (it can sum to less than 1 under nofollow evaporation or `robots_blocked_action = "vanish"`), and the blend inherits that — it is a weighted average of the per-topic distributions, not renormalized.

See also

[pagerank()], [align_prior_to_vertices()], [compare_pagerank()]

Examples

edges <- data.frame(
  from = c("/", "/", "/", "/ai", "/ai", "/blog", "/pricing"),
  to = c("/ai", "/blog", "/pricing", "/ai-demo", "/pricing", "/ai", "/")
)

# Two topics: the AI cluster and the pricing cluster.
res <- topic_sensitive_pagerank(
  edges,
  topics = list(
    ai_agent = c("/ai", "/ai-demo"),
    pricing = "/pricing"
  ),
  clean_edge_urls = FALSE
)
print(res)
#>   node_name  ai_agent    pricing    blended
#> 1  /pricing 0.1673078 0.36687417 0.26709099
#> 2       /ai 0.2988577 0.16345773 0.23115770
#> 3         / 0.1422116 0.31184304 0.22702734
#> 4  /ai-demo 0.3513296 0.06946953 0.21039956
#> 5     /blog 0.0402933 0.08835553 0.06432441

# Bias the blend 70/30 toward the AI cluster.
res2 <- topic_sensitive_pagerank(
  edges,
  topics = list(
    ai_agent = c("/ai", "/ai-demo"),
    pricing = "/pricing"
  ),
  topic_weights = c(ai_agent = 0.7, pricing = 0.3),
  clean_edge_urls = FALSE
)
attr(res2, "topic_weights")
#> ai_agent  pricing 
#>      0.7      0.3