Skip to contents

Collapses duplicate `from -> to` rows in an edge list using explicit, per-column aggregation semantics. This is the post-fold aggregation step intended to run *after* redirect / canonical folding has coalesced URL variants onto their representatives, and it is a loss-aware alternative to [get_unique_edges()].

Where [get_unique_edges()] dedups a `from/to` pair by **keeping the first row** (silently discarding everything else on the duplicate rows), `aggregate_edges()` combines the duplicate rows column-by-column. This matters the moment edges carry quantities: click counts, predicted click propensities, repeated links to the same destination, and conflicting follow / nofollow metadata are all preserved or combined deterministically instead of dropped.

Usage

aggregate_edges(
  edge_list_df,
  agg = list(),
  nofollow_policy = c("any", "all", "majority", "error"),
  preserve_cols = character(0),
  self_loops = c("drop", "keep"),
  from_col = "from",
  to_col = "to"
)

Arguments

edge_list_df

A data frame representing the edge list, with at least the `from_col` and `to_col` columns.

agg

A named list of per-column aggregation overrides. Names are column names; values are either a built-in aggregation string or a function. Columns not listed use the defaults described above. Default `list()`.

nofollow_policy

The default conflict policy applied to logical columns that are not explicitly listed in `agg`. One of `"any"` (default), `"all"`, `"majority"`, or `"error"`. Named `nofollow_policy` because the nofollow flag is the canonical boolean attribute, but it governs every un-overridden logical column.

preserve_cols

Character vector of columns to keep un-collapsed as per-group list-columns (e.g. placement / position features). Default `character(0)`.

self_loops

How to handle self-loops (`a -> a`). One of `"drop"` (default) or `"keep"`.

from_col

Name of the source-node column. Default `"from"`.

to_col

Name of the target-node column. Default `"to"`.

Value

A data frame with one row per unique `from/to` pair (self-loops handled per `self_loops`). `from_col` / `to_col` are coerced to character; each remaining column is aggregated per its resolved rule; `preserve_cols` become list-columns. Row order follows first appearance of each `from/to` pair in the (NA-filtered, self-loop-handled) input.

Details

## Default per-column semantics

For every column other than `from_col` / `to_col` (and any column named in `preserve_cols`), an aggregation is chosen automatically unless overridden in `agg`:

- **numeric / integer columns** (additive counts and click propensities) are summed. Repeated link instances to the same destination therefore add their propensities together: multiple slots pointing at one target produce more total propensity, which is the correct behavioral reading. - **logical columns** (boolean attributes such as `nofollow`) are resolved with an explicit *conflict policy* (see `nofollow_policy`). They are never silently first-wins. - **all other columns** (character, factor, ...) fall back to `"first"`, which reproduces the legacy keep-first behavior for non-additive identifier-like columns.

## Overriding per column

`agg` is a named list mapping a column name to either:

- one of the built-in strings `"sum"`, `"mean"`, `"max"`, `"min"`, `"first"`, `"last"`, `"any"`, `"all"`, `"majority"`, or `"error"`, or - a function taking the vector of grouped values and returning a length-1 value.

The boolean conflict policies (`"any"`, `"all"`, `"majority"`, `"error"`) may be applied to any logical column. `"error"` raises if a `from/to` group holds conflicting (mixed `TRUE`/`FALSE`) values; the others reduce to "any TRUE", "all TRUE", and the majority value (ties resolve to `TRUE`) respectively.

## Preserving placement features

Columns named in `preserve_cols` are **not** collapsed. Each surviving `from/to` group keeps the individual per-instance values as a list-column (one list element per group, holding that group's vector of values). This lets placement / position features survive aggregation so a later reasonable-surfer model can use each individual link instance.

## Backward compatibility

With no weight or extra columns (a plain `from`/`to` edge list), the result is identical to [get_unique_edges()]: NA edges dropped, self-loops handled per `self_loops`, one row per unique `from/to` pair, from/to coerced to character.

See also

[get_unique_edges()] for the lossy keep-first dedup.

Examples

# Click counts to the same destination sum instead of being dropped.
edges <- data.frame(
  from = c("A", "A", "B"),
  to = c("B", "B", "C"),
  clicks = c(3, 5, 2),
  nofollow = c(FALSE, TRUE, FALSE)
)
aggregate_edges(edges)
#>   from to clicks nofollow
#> 1    A  B      8     TRUE
#> 2    B  C      2    FALSE

# Require agreement on nofollow, erroring on a conflict.
try(aggregate_edges(edges, nofollow_policy = "error"))
#> Error : Conflicting values in column 'nofollow' for a folded from/to group (policy = "error").

# Preserve placement features as a list-column for later modeling.
edges_pos <- data.frame(
  from = c("A", "A"),
  to = c("B", "B"),
  position = c(1, 7)
)
aggregate_edges(edges_pos, preserve_cols = "position")
#>   from to position
#> 1    A  B     1, 7