Skip to content

Add compare module for quantifying embedding differences (#9)#192

Merged
aronwalsh merged 2 commits into
mainfrom
compare-embeddings
May 3, 2026
Merged

Add compare module for quantifying embedding differences (#9)#192
aronwalsh merged 2 commits into
mainfrom
compare-embeddings

Conversation

@aronwalsh

@aronwalsh aronwalsh commented May 3, 2026

Copy link
Copy Markdown
Member

Summary

Re-opens the compare module work originally proposed in #182. The previous PR was closed pending review; this version rebases onto current main (post-v0.7.2) and addresses all four CodeRabbit findings from the earlier review.

Closes #9.

What's in elementembeddings.compare

Function Description
embedding_similarity Pearson/Spearman/Kendall correlation between flattened element similarity matrices
mantel_test Permutation-based significance test (two-sided p-value)
kl_divergence KL divergence between softmax-normalised similarity distributions
frobenius_distance Frobenius norm of similarity-matrix difference
pairwise_embedding_comparison Run embedding_similarity over all pairs and return a DataFrame

Includes an example script (examples/comparisons/compare_embeddings.py) that produces a comparison heatmap, UMAP map of embedding schemes, and Mantel test results across the bundled embeddings.

Changes vs PR #182

  • mantel_test now raises ValueError on unknown method instead of silently falling back to Spearman.
  • mantel_test p-value is two-sided (abs(perm_corr) >= abs(observed)) so strong negative correlations report as significant rather than ~1.0.
  • Removed unused _get_common_elements import in the example script.
  • Added "fro" (Frobenius) to the codespell ignore list.
  • New tests/test_compare.py covers self-similarity (= 1), invalid-argument paths, zero-on-self for KL and Frobenius, and pairwise-comparison shape/symmetry.

Test plan

  • pre-commit run --all-files clean (ruff, ruff-format, pyright, codespell, prettier)
  • pytest src/elementembeddings/tests — 61 passed (52 existing + 9 new)
  • End-to-end smoke test of all five public functions on magpie/mat2vec/megnet16
  • CI green on this PR

Summary by CodeRabbit

  • New Features

    • Added embedding comparison module with support for multiple similarity metrics (Pearson, Spearman, Kendall) and statistical tests including permutation-based Mantel tests.
    • New example script demonstrates comparing embedding schemes with visualization outputs (correlation heatmaps, UMAP projections) and statistical analysis.
  • Tests

    • Comprehensive test coverage added for the comparison module.

aronwalsh added 2 commits May 3, 2026 18:11
…mes (#9)

New module `elementembeddings.compare` provides:
- embedding_similarity: Pearson/Spearman/Kendall correlation between
  flattened element similarity matrices
- mantel_test: permutation-based significance test for matrix correlation
- kl_divergence: KL divergence between normalised similarity distributions
- frobenius_distance: Frobenius norm of similarity matrix difference
- pairwise_embedding_comparison: compare all pairs, return comparison matrix

Includes example script with comparison heatmap, UMAP map of embedding
schemes, and Mantel test results.

Closes #9
- Validate `method` argument in `mantel_test` (raise ValueError on
  unknown methods instead of silently using Spearman).
- Switch Mantel p-value to two-sided so strong negative correlations
  are reported as significant rather than near-1.
- Drop unused `_get_common_elements` import in the example script.
- Whitelist "fro" (Frobenius norm) in codespell to unblock CI.
- Add unit tests covering self-similarity, invalid arguments, KL/Frobenius
  zero-on-self, and pairwise comparison shape/symmetry.
@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces a new elementembeddings.compare module providing quantitative comparison utilities for embedding similarity structures via five statistical functions (correlation, permutation testing, KL divergence, Frobenius distance, and pairwise comparison). An end-to-end example script demonstrates usage by loading multiple embeddings, generating correlation heatmaps, 2D UMAP visualizations, and Mantel test results with CSV/PNG outputs.

Changes

Embedding Comparison Framework

Layer / File(s) Summary
Data Processing Helpers
src/elementembeddings/compare.py
Establishes _get_common_elements() to compute element intersection and _get_similarity_matrix() to build symmetric pairwise correlation matrices using Embedding.compute_correlation_metric().
Core Comparison APIs
src/elementembeddings/compare.py
Implements embedding_similarity() for Pearson/Spearman/Kendall correlation of flattened upper-triangle similarity vectors; mantel_test() for permutation-based significance testing; kl_divergence() for asymmetric KL divergence between softmax-normalized distributions; frobenius_distance() for matrix norm distance; and pairwise_embedding_comparison() to generate all-pairs correlation DataFrames.
Example & Integration
examples/comparisons/compare_embeddings.py
Demonstrates end-to-end workflow: loads multiple embeddings, computes pairwise similarities, generates annotated Pearson correlation heatmap, creates 2D UMAP visualization from flattened cosine-similarity features, runs Mantel tests on fixed embedding pairs, and saves results as PNG/CSV files.
Module Documentation
src/elementembeddings/__init__.py
Documents elementembeddings.compare as an exported module for comparing embedding schemes.
Tests & Configuration
src/elementembeddings/tests/test_compare.py, pyproject.toml
Test suite validates all comparison functions across three embeddings (magpie, mat2vec, megnet16) for self-comparison, method variants, error handling, and symmetry; codespell configuration adds "fro" to ignore words.

Sequence Diagram

sequenceDiagram
    participant User
    participant Script as Example Script
    participant Embeddings as Embedding<br/>System
    participant Compare as Comparison<br/>Functions
    participant Viz as Visualization<br/>Libs
    participant FS as File System

    User->>Script: Execute compare_embeddings.py
    Script->>Embeddings: load_all() for each<br/>(magpie, mat2vec, megnet16)
    Embeddings-->>Script: Return Embedding objects
    
    Script->>Compare: pairwise_embedding_comparison()
    Compare->>Embeddings: compute_correlation_metric()<br/>for all pairs
    Embeddings-->>Compare: similarity matrices
    Compare-->>Script: correlation DataFrame
    Script->>FS: Save pairwise_comparison.csv

    Script->>Compare: Compute heatmap data
    Compare-->>Script: correlation matrix
    Script->>Viz: plot_comparison_heatmap()
    Viz->>FS: Save embedding_comparison_heatmap.png

    Script->>Compare: Compute UMAP features
    Compare->>Embeddings: compute_correlation_metric()<br/>for common elements
    Embeddings-->>Compare: similarity matrices
    Compare-->>Script: flattened feature vectors
    Script->>Viz: plot_embedding_map() with UMAP
    Viz->>FS: Save embedding_umap_map.png

    Script->>Compare: run_mantel_tests() on<br/>fixed embedding pairs
    Compare->>Embeddings: compute_correlation_metric()<br/>for each pair
    Embeddings-->>Compare: similarity matrices
    Compare-->>Script: Mantel r & p-values
    Script->>FS: Save mantel_test_results.csv
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Five functions dance in harmony—
Correlate, permute, diverge with glee!
Embeddings compared from every angle,
UMAP maps them in a pretty tangle,
Statistics wrapped up neat and tidy! 📊✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a new compare module for quantifying embedding differences, with issue reference.
Description check ✅ Passed The description is detailed and well-structured, covering summary, what's included, changes vs prior PR, and test plan with checkboxes.
Linked Issues check ✅ Passed The PR implements all key requirements from #9: quantitative comparison tools (embedding_similarity, mantel_test, kl_divergence, frobenius_distance), normalization via similarity matrices, 2D visualization (UMAP), and significance testing.
Out of Scope Changes check ✅ Passed All changes are directly in scope: the compare module implementation, example script, tests, documentation updates, and codespell configuration change are all aligned with the PR objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch compare-embeddings

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🧹 Nitpick comments (2)
pyproject.toml (1)

147-152: ⚡ Quick win

Consider narrowing the codespell ignore entry for "fro"

Ignoring the short token "fro" can be a bit broad if codespell ever flags other legitimate “fro” occurrences as misspellings. If the underlying warning was specifically about the Frobenius metric shorthand, consider ignoring the more specific token that codespell reports (e.g., frobenius, fro_, or whatever exact misspelling appears) rather than the generic substring token.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pyproject.toml` around lines 147 - 152, The codespell ignore entry currently
contains the broad token "fro" in the [tool.codespell] ignore-words-list; narrow
this to the specific token reported (e.g., replace "fro" with the exact
identifier such as "frobenius", "fro_", or the precise misspelling string
codespell flagged) so you only suppress that specific false positive; update the
ignore-words-list entry accordingly in the tool.codespell section.
examples/comparisons/compare_embeddings.py (1)

19-24: Avoid teaching private helpers in a user-facing example.

This sample imports _get_similarity_matrix and _upper_triangle from the module's internals, which can change without a public API guarantee. The example uses these to build a feature matrix for UMAP dimensionality reduction. Either expose a public helper for this similarity-to-vector workflow or keep the matrix-building logic local to the example.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/comparisons/compare_embeddings.py` around lines 19 - 24, The example
imports private helpers _get_similarity_matrix and _upper_triangle which are
internal and unstable; remove those imports and stop relying on them in the
example. Fix by either (A) moving the similarity-to-vector matrix-building logic
into the example file itself (implement a local function that computes the full
similarity matrix from embeddings and extracts the upper-triangle vector) or (B)
add a new public helper in the library (e.g., get_similarity_vector or
get_similarity_matrix) and import that instead of
_get_similarity_matrix/_upper_triangle; update references to use the new local
function or the new public API and keep usage of public functions like
pairwise_embedding_comparison, mantel_test unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/elementembeddings/compare.py`:
- Around line 125-126: Add upfront validation for the n_permutations parameter:
if n_permutations < 1 raise a ValueError with a clear message (e.g.
"n_permutations must be >= 1") at the start of the function that declares
n_permutations and returns tuple[float, float], and apply the same check in the
other function that accepts n_permutations (the second occurrence referenced in
the review). This prevents division-by-zero or invalid p-value computation by
rejecting invalid inputs before any permutation logic runs.
- Around line 41-47: Change _get_common_elements to accept a min_common argument
(default 3) and use that threshold instead of the hardcoded 3; update all call
sites to pass the appropriate minimum for their metric: pass min_common=3 from
embedding_similarity() and mantel_test(), and pass min_common=2 from
kl_divergence() and frobenius_distance(). Ensure the ValueError message reflects
the passed min_common and keep the function name _get_common_elements to locate
and modify it.
- Around line 241-246: The Frobenius norm is computed over the full matrix
(diff) but the divisor n_pairs counts only unique pairs, causing a sqrt(2)
mismatch; fix by computing the norm only over the unique off-diagonal entries
(e.g., use the upper triangle of diff) and then divide by sqrt(n_pairs).
Concretely, replace the frob calculation that uses np.linalg.norm(diff, "fro")
with a norm over np.triu(diff, k=1) (or equivalent selection) so that frob and
the normalise divisor n_pairs (based on elements) measure the same set of
entries; adjust no other logic around normalise, diff, np.linalg.norm, n_pairs,
or elements.

---

Nitpick comments:
In `@examples/comparisons/compare_embeddings.py`:
- Around line 19-24: The example imports private helpers _get_similarity_matrix
and _upper_triangle which are internal and unstable; remove those imports and
stop relying on them in the example. Fix by either (A) moving the
similarity-to-vector matrix-building logic into the example file itself
(implement a local function that computes the full similarity matrix from
embeddings and extracts the upper-triangle vector) or (B) add a new public
helper in the library (e.g., get_similarity_vector or get_similarity_matrix) and
import that instead of _get_similarity_matrix/_upper_triangle; update references
to use the new local function or the new public API and keep usage of public
functions like pairwise_embedding_comparison, mantel_test unchanged.

In `@pyproject.toml`:
- Around line 147-152: The codespell ignore entry currently contains the broad
token "fro" in the [tool.codespell] ignore-words-list; narrow this to the
specific token reported (e.g., replace "fro" with the exact identifier such as
"frobenius", "fro_", or the precise misspelling string codespell flagged) so you
only suppress that specific false positive; update the ignore-words-list entry
accordingly in the tool.codespell section.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: baa0f2d1-a0de-40ed-b6c8-6fc4da573c98

📥 Commits

Reviewing files that changed from the base of the PR and between 2d9f646 and b841c42.

⛔ Files ignored due to path filters (4)
  • examples/comparisons/embedding_comparison_heatmap.png is excluded by !**/*.png
  • examples/comparisons/embedding_umap_map.png is excluded by !**/*.png
  • examples/comparisons/mantel_test_results.csv is excluded by !**/*.csv
  • examples/comparisons/pairwise_comparison.csv is excluded by !**/*.csv
📒 Files selected for processing (5)
  • examples/comparisons/compare_embeddings.py
  • pyproject.toml
  • src/elementembeddings/__init__.py
  • src/elementembeddings/compare.py
  • src/elementembeddings/tests/test_compare.py

Comment on lines +41 to +47
def _get_common_elements(emb1: Embedding, emb2: Embedding) -> list[str]:
"""Return sorted list of elements common to both embeddings."""
common = sorted(set(emb1.element_list) & set(emb2.element_list))
if len(common) < 3:
msg = f"Only {len(common)} common elements between embeddings. Need at least 3."
raise ValueError(msg)
return common

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the minimum-overlap check metric-specific.

This helper is shared by all public APIs, but the 3-element floor is only needed for the correlation-based ones. kl_divergence() and frobenius_distance() are still well-defined with exactly 2 common elements, so they currently reject valid inputs from custom embeddings.

Suggested fix
-def _get_common_elements(emb1: Embedding, emb2: Embedding) -> list[str]:
+def _get_common_elements(
+    emb1: Embedding,
+    emb2: Embedding,
+    *,
+    min_common: int = 3,
+) -> list[str]:
     """Return sorted list of elements common to both embeddings."""
     common = sorted(set(emb1.element_list) & set(emb2.element_list))
-    if len(common) < 3:
-        msg = f"Only {len(common)} common elements between embeddings. Need at least 3."
+    if len(common) < min_common:
+        msg = f"Only {len(common)} common elements between embeddings. Need at least {min_common}."
         raise ValueError(msg)
     return common

Use min_common=3 for embedding_similarity() / mantel_test(), and min_common=2 for kl_divergence() / frobenius_distance().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/elementembeddings/compare.py` around lines 41 - 47, Change
_get_common_elements to accept a min_common argument (default 3) and use that
threshold instead of the hardcoded 3; update all call sites to pass the
appropriate minimum for their metric: pass min_common=3 from
embedding_similarity() and mantel_test(), and pass min_common=2 from
kl_divergence() and frobenius_distance(). Ensure the ValueError message reflects
the passed min_common and keep the function name _get_common_elements to locate
and modify it.

Comment on lines +125 to +126
n_permutations: int = 999,
) -> tuple[float, float]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate n_permutations before computing the p-value.

Negative values currently fall through the loop and can either divide by zero (-1) or return an invalid p-value. Reject < 1 up front.

Suggested fix
 def mantel_test(
     emb1: Embedding,
     emb2: Embedding,
     metric: str = "cosine_similarity",
     method: str = "pearson",
     n_permutations: int = 999,
 ) -> tuple[float, float]:
@@
+    if n_permutations < 1:
+        msg = "n_permutations must be at least 1."
+        raise ValueError(msg)
+
     if method == "pearson":
         corr_fn = stats.pearsonr

Also applies to: 174-175

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/elementembeddings/compare.py` around lines 125 - 126, Add upfront
validation for the n_permutations parameter: if n_permutations < 1 raise a
ValueError with a clear message (e.g. "n_permutations must be >= 1") at the
start of the function that declares n_permutations and returns tuple[float,
float], and apply the same check in the other function that accepts
n_permutations (the second occurrence referenced in the review). This prevents
division-by-zero or invalid p-value computation by rejecting invalid inputs
before any permutation logic runs.

Comment on lines +241 to +246
diff = mat1 - mat2
frob = float(np.linalg.norm(diff, "fro"))

if normalise:
n_pairs = len(elements) * (len(elements) - 1) / 2
frob /= np.sqrt(n_pairs)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize the same entries you measure.

np.linalg.norm(diff, "fro") includes both symmetric halves of the matrix, but the normalisation divisor is based on unique element pairs only. That inflates the "normalise=True" result by sqrt(2) relative to the stated per-pair scaling.

Suggested fix
-    diff = mat1 - mat2
-    frob = float(np.linalg.norm(diff, "fro"))
+    diff = _upper_triangle(mat1) - _upper_triangle(mat2)
+    frob = float(np.linalg.norm(diff))
 
     if normalise:
         n_pairs = len(elements) * (len(elements) - 1) / 2
         frob /= np.sqrt(n_pairs)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/elementembeddings/compare.py` around lines 241 - 246, The Frobenius norm
is computed over the full matrix (diff) but the divisor n_pairs counts only
unique pairs, causing a sqrt(2) mismatch; fix by computing the norm only over
the unique off-diagonal entries (e.g., use the upper triangle of diff) and then
divide by sqrt(n_pairs). Concretely, replace the frob calculation that uses
np.linalg.norm(diff, "fro") with a norm over np.triu(diff, k=1) (or equivalent
selection) so that frob and the normalise divisor n_pairs (based on elements)
measure the same set of entries; adjust no other logic around normalise, diff,
np.linalg.norm, n_pairs, or elements.

@aronwalsh aronwalsh merged commit f20c012 into main May 3, 2026
12 checks passed
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.

Usability: Quantify differences between similarities

1 participant