Skip to content

Local proximity score - #152

Open
maxkarlsson wants to merge 5 commits into
mainfrom
local-proximity-score
Open

Local proximity score#152
maxkarlsson wants to merge 5 commits into
mainfrom
local-proximity-score

Conversation

@maxkarlsson

@maxkarlsson maxkarlsson commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Description

Added functionality to calculate Local Proximity Scores per each node of a graph. This can be used to calculate hot/cold spots for a given marker, or visaulize regions where multiple markers colocalize to a higher degree than expected, given their abundance.

Added

  • local_proximity to compute Local Neighborhood Enrichment (LNE)
    log2 ratios of observed vs expected UMI counts for selected marker(s).

Type of change

  • New feature (non-breaking change which adds functionality).

How Has This Been Tested?

Tests have been added.

PR checklist:

  • My changes generate no new warnings.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have documented any significant changes to the code in CHANGELOG.md

Note

Medium Risk
New graph statistics code with non-trivial null models and matrix ops on large subgraphs; risk is moderate due to numerical/assumption sensitivity and runtime for permutation mode, not due to security or data-handling paths.

Overview
Adds exported local_proximity for per-node Local Neighborhood Enrichment (LNE) on CellGraph objects: log2 ratios of observed vs expected marker UMI counts within a k-hop neighborhood (via existing expand_adjacency_matrix), with separate handling for PNA umi1/umi2 node types.

Computation options: mode all (joint colocation via min counts), any (sum across markers), or self-clustering (per-marker matrix, analytical only); method analytical (frequency × neighborhood size) or permutation (label shuffle null, converges with more iterations). Optional precomputed reachability matrix A_k, tunable k (2–10), and seed for permutations.

Package wiring: new R/local_proximity_scores.R, roxygen/man, CHANGELOG, LNE in wordlist, and testthat coverage (marker sets, modes, permutation, invalid inputs).

Reviewed by Cursor Bugbot for commit ed4fe9d. Bugbot is set up for automated code reviews on this repo. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new analysis function, local_proximity(), to compute per-node Local Neighborhood Enrichment (LNE) scores on PNA CellGraph objects, enabling hot/cold spot detection and marker co-localization scoring.

Changes:

  • Introduces local_proximity() with analytical and permutation-based expected-value computation and multiple scoring modes.
  • Exports and documents the new API (NAMESPACE + Rd), and wires it into package collation.
  • Adds testthat coverage for key modes and input validation, and updates CHANGELOG/WORDLIST.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
R/local_proximity_scores.R Implements local_proximity() and helper functions for analytical/permutation expectations.
tests/testthat/test-local_proximity.R Adds tests for single/multi-marker scoring, modes, permutation method, and invalid inputs.
NAMESPACE Exports local_proximity.
man/local_proximity.Rd Generated documentation for the new function.
DESCRIPTION Adds new R file to Collate.
inst/WORDLIST Adds “LNE” to spelling whitelist.
CHANGELOG.md Notes the new local_proximity feature.
Files not reviewed (1)
  • man/local_proximity.Rd: Generated file
Suppressed comments (1)

R/local_proximity_scores.R:299

  • This negated equality check relies on operator precedence and is hard to read/maintain. Use mode != "self-clustering" explicitly to avoid ambiguity.
  if (!mode == "self-clustering") {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread R/local_proximity_scores.R Outdated
Comment thread R/local_proximity_scores.R
Comment thread R/local_proximity_scores.R Outdated
Comment thread R/local_proximity_scores.R Outdated
Comment thread R/local_proximity_scores.R Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ed4fe9d. Configure here.

if (mode == "any") {
freq2 <- freq2 %>% sum()
}
local_stat_exp_B <- freq2 * degree_exp_B

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong analytical null for all mode

High Severity

For mode = "all" with multiple markers, the analytical expected value calculation in local_proximity is inconsistent with the observed statistic. The expected value uses the minimum of marker frequencies per UMI type, while the observed statistic takes the minimum of per-marker neighborhood sums. This mismatch biases analytical LNE scores and prevents the permutation method from converging to the analytical result as intended.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ed4fe9d. Configure here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is this something to look into?

)

# Compute log2 ratio of observed to expected statistic
log2_ratio <- log2(pmax(local_stat_obs, k - 1) / pmax(local_stat_exp, k - 1))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ratio floor scales with neighborhood k

Medium Severity

The local_proximity function applies a k-dependent floor (pmax(..., k - 1)) to both observed and expected statistics before calculating the log2 ratio. This deviates from the documented log2(O/E) formula, materially altering scores and potentially flattening weak signals. This can lead to misinterpretation of magnitudes and incorrect comparisons.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ed4fe9d. Configure here.

local_stat_exp_A <- outer(degree_exp_A, freq1)
}
if (!exists("freq2")) {
freq2 <- (counts[(nA + 1):(nA + nB), , drop = FALSE] %>% Matrix::colSums()) / nB

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frequency lookup escapes function scope

Medium Severity

.analytical_lp_exp uses exists("freq1") and exists("freq2") with default inherits = TRUE. If a caller has freq1 or freq2 in the global environment, frequency computation from counts is skipped and those external values are used silently, corrupting analytical LNE scores.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ed4fe9d. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants