Skip to contents

Computes, from a fitted result (or any partition) plus the graph it was fitted on, every quantity the tables and figures of Ospina et al. (2026) report: modularity `Q`, the NCE leader score `H` in both its global and its community-conditioned form, recovery against a ground truth (NMI, ARI, VI, split-join), the number and size distribution of the communities, wall-clock runtime, and the search diagnostics (trace dispersion, the best iteration, and how often the lexicographic tie-break on `H` was actually decisive).

Usage

lcda_metrics(
  object,
  graph = NULL,
  truth = NULL,
  level = c("overall", "community", "leader"),
  node_score = NULL
)

Arguments

object

a fitted [lcda_grasp()], [lcda_gr()] or [lcda_ecg()] result; an igraph `communities` object (e.g. from [igraph::cluster_louvain()]); a `list(membership =, leaders =)`; or a bare membership vector. When no leaders are available they are derived as the top-eigenvector node of each community, and the output records that.

graph

the [igraph::igraph] the solution was computed on. Optional for results produced by this package, which carry their own (simplified) graph.

truth

optional ground-truth community labels (a vector of length `vcount(graph)`, or a `communities` object). Enables the recovery metrics. The labels you supply are preserved: `truth_dominant` and `truth_label` report `"X"`, not the integer code `as.factor()` gives it.

level

one of `"overall"`, `"community"`, `"leader"`; see *Levels*.

node_score

optional numeric vector of length `vcount(graph)` holding an external per-node signal against which to score the leaders; see *Validating leaders against an external signal*.

Value

A [tibble::tibble]. For `level = "overall"`, the long `algorithm`/`scope`/`metric`/`value` form; otherwise one row per community or per leader.

Details

This is the bridge between the algorithms and the reporting: a user can go from a bare [igraph::igraph] to a publication table in three calls (`lcda_gr()` -> `lcda_metrics()` -> `plot()`), without touching the precomputed datasets in [lcda_data()].

Levels

`"overall"`

One row per metric, in tidy long form with columns `algorithm`, `scope`, `metric`, `value`. `scope` groups the metrics into `"partition"`, `"leaders"`, `"recovery"` (only when `truth` is given), `"search"`, and `"consensus"` (LCDA-ECG only). The two lexicographic metrics in `"search"` appear only for [lcda_gr()], the one algorithm that instruments the tie-break; they are absent, rather than reported as a measured-looking zero, for [lcda_grasp()]. `elapsed_sec` times the search only: it starts after the graph has been converted by [as_csr()], so it is slightly smaller than timing the whole call from the outside (measured at 0.3-2 the metric set is heterogeneous and grows with the input; pivot with `tidyr::pivot_wider()` for a paper-style row.

`"community"`

One row per community: `size`, its `leader`, the internal edge weight and the cut (`boundary_edges`), internal density, conductance, and the community's additive contribution to `Q` (these sum exactly to `Q`). With `truth`, also the dominant ground-truth label and the community's purity.

`"leader"`

One row per leader: degree (total, within- and between-community), eigenvector centrality, the node-level NCE term, the participation coefficient, and the leader's rank and percentile by within-community degree, which is the form the paper's external leader validation takes. Supply `node_score` to rank the leaders by an external signal instead (see below).

Validating leaders against an external signal

The paper validates its leaders by asking where each one sits in the *within-community* distribution of an outside quantity (citations, in its OpenAlex study). Pass that quantity as `node_score`, one value per vertex, and the leader level gains `score`, `score_rank_in_community` and `score_pctile_in_community`, while the overall level gains a `leader_score` scope with the mean percentile and the top-1 / top-3 hit rates. A percentile near 0.5 means the leaders are no better than chance.

Weighted graphs

All edge sums are taken over the graph's `weight` attribute when present, so on a weighted graph the "edge" columns (`internal_edges`, `boundary_edges`, `total_edge_weight`) are weight sums and the `degree` columns are strengths. `Q` and `conductance` are weight-aware and stay in their usual ranges. Two quantities are deliberately **structural** (computed from edge counts, ignoring weights), because a weight sum in their numerator would push them out of their defining range: `internal_density`, which is a proportion of the possible pairs and therefore always in \[0, 1\], and the NCE leader score, which matches how the algorithms themselves treat weights.

See also

[lcda_grasp()], [lcda_gr()], [lcda_ecg()], [plot.lcda_grasp_result()] for the matching figure, and [lcda_data()] for the precomputed panels of the paper.

Examples

g <- igraph::make_graph("Zachary")
res <- lcda_grasp(g, B = 20, seed = 1)

m <- lcda_metrics(res)
subset(m, scope == "partition")
#> # A tibble: 11 × 4
#>    algorithm  scope     metric                 value
#>    <chr>      <chr>     <chr>                  <dbl>
#>  1 LCDA-GRASP partition n_nodes               34    
#>  2 LCDA-GRASP partition n_edges               78    
#>  3 LCDA-GRASP partition total_edge_weight     78    
#>  4 LCDA-GRASP partition n_communities          3    
#>  5 LCDA-GRASP partition Q                      0.402
#>  6 LCDA-GRASP partition size_min               5    
#>  7 LCDA-GRASP partition size_median           12    
#>  8 LCDA-GRASP partition size_mean             11.3  
#>  9 LCDA-GRASP partition size_max              17    
#> 10 LCDA-GRASP partition size_sd                6.03 
#> 11 LCDA-GRASP partition singleton_communities  0    

# per-community and per-leader breakdowns
lcda_metrics(res, level = "community")
#> # A tibble: 3 × 11
#>   algorithm  community  size leader leader_name leader_degree internal_edges
#>   <chr>          <int> <int>  <int> <chr>               <dbl>          <dbl>
#> 1 LCDA-GRASP         1    17     34 34                     17             34
#> 2 LCDA-GRASP         2    12      1 1                      16             24
#> 3 LCDA-GRASP         3     5      6 6                       4              6
#> # ℹ 4 more variables: boundary_edges <dbl>, internal_density <dbl>,
#> #   conductance <dbl>, q_contribution <dbl>
lcda_metrics(res, level = "leader")
#> # A tibble: 3 × 14
#>   algorithm  community leader leader_name community_size degree degree_within
#>   <chr>          <int>  <int> <chr>                <int>  <dbl>         <dbl>
#> 1 LCDA-GRASP         1     34 34                      17     17            14
#> 2 LCDA-GRASP         2      1 1                       12     16            10
#> 3 LCDA-GRASP         3      6 6                        5      4             3
#> # ℹ 7 more variables: degree_between <dbl>, eigen_centrality <dbl>,
#> #   nce_node <dbl>, participation <dbl>, degree_rank_in_community <int>,
#> #   degree_pctile_in_community <dbl>, source <chr>

# recovery against a ground truth, and a baseline scored the same way
truth <- c(rep(1, 17), rep(2, 17))
subset(lcda_metrics(res, truth = truth), scope == "recovery")
#> # A tibble: 6 × 4
#>   algorithm  scope    metric               value
#>   <chr>      <chr>    <chr>                <dbl>
#> 1 LCDA-GRASP recovery nmi                  0.310
#> 2 LCDA-GRASP recovery ari                  0.289
#> 3 LCDA-GRASP recovery rand                 0.647
#> 4 LCDA-GRASP recovery vi                   1.17 
#> 5 LCDA-GRASP recovery split_join          17    
#> 6 LCDA-GRASP recovery n_communities_truth  2    
subset(lcda_metrics(igraph::cluster_louvain(g), g, truth = truth),
       scope == "recovery")
#> # A tibble: 6 × 4
#>   algorithm   scope    metric               value
#>   <chr>       <chr>    <chr>                <dbl>
#> 1 multi level recovery nmi                  0.277
#> 2 multi level recovery ari                  0.139
#> 3 multi level recovery rand                 0.576
#> 4 multi level recovery vi                   1.46 
#> 5 multi level recovery split_join          25    
#> 6 multi level recovery n_communities_truth  2    

# do the leaders top their own community on an external signal?
external <- igraph::betweenness(g)
subset(lcda_metrics(res, node_score = external), scope == "leader_score")
#> # A tibble: 3 × 4
#>   algorithm  scope        metric            value
#>   <chr>      <chr>        <chr>             <dbl>
#> 1 LCDA-GRASP leader_score score_pctile_mean     1
#> 2 LCDA-GRASP leader_score score_top1_rate       1
#> 3 LCDA-GRASP leader_score score_top3_rate       1