Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions tests/unit/helpers/test_jaccard.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Unit tests for Jaccard similarity helpers."""

import logging

import numpy as np
import pandas as pd
import pytest
Expand All @@ -8,6 +10,7 @@


def b(*vals: int) -> np.ndarray:
"""Construct a 1-D boolean ndarray from integer literals."""
return np.array(vals, dtype=bool)


Expand All @@ -17,38 +20,44 @@ def b(*vals: int) -> np.ndarray:


def test_similarity_identical_vectors() -> None:
"""Identical vectors should have similarity of 1."""
x = b(1, 1, 0, 1)
assert jaccard.similarity(x, x) == pytest.approx(1.0)


def test_similarity_disjoint_vectors() -> None:
"""Disjoint vectors should have similarity of 0."""
x = b(1, 1, 0, 0)
y = b(0, 0, 1, 1)
assert jaccard.similarity(x, y) == pytest.approx(0.0)


def test_similarity_partial_overlap() -> None:
"""Vectors with partial overlap should return intersect/union."""
x = b(1, 1, 1, 0)
y = b(1, 1, 0, 1)
# intersect=2, union=4 → 0.5
assert jaccard.similarity(x, y) == pytest.approx(0.5)


def test_similarity_all_zeros_returns_nan() -> None:
"""All-zero vectors produce a zero denominator; result should be NaN."""
x = b(0, 0, 0)
y = b(0, 0, 0)
result = jaccard.similarity(x, y)
assert np.isnan(result)


def test_similarity_with_explicit_px_py() -> None:
"""Explicitly supplied px/py should be used instead of the sample mean."""
x = b(1, 0, 1, 0)
y = b(1, 0, 1, 0)
result = jaccard.similarity(x, y, px=0.5, py=0.5)
assert result == pytest.approx(1.0)


def test_similarity_centered() -> None:
"""Centered similarity should subtract the expected overlap under independence."""
x = b(1, 1, 0, 0)
y = b(1, 1, 0, 0)
# j=1, px=py=0.5, denominator=0.75, centered = 1 - (0.25/0.75)
Expand All @@ -57,18 +66,21 @@ def test_similarity_centered() -> None:


def test_similarity_raises_on_2d_input() -> None:
"""Non-1-D arrays should raise IndexError."""
x = np.array([[True, False], [True, False]])
y = np.array([[True, False], [True, False]])
with pytest.raises(IndexError):
jaccard.similarity(x, y)


def test_similarity_raises_on_length_mismatch() -> None:
"""Arrays of different lengths should raise IndexError."""
with pytest.raises(IndexError):
jaccard.similarity(b(1, 0), b(1, 0, 1))


def test_similarity_raises_on_non_boolean_dtype() -> None:
"""Integer arrays should raise TypeError."""
x = np.array([1, 0, 1], dtype=int)
y = np.array([1, 0, 1], dtype=int)
with pytest.raises(TypeError):
Expand All @@ -81,23 +93,27 @@ def test_similarity_raises_on_non_boolean_dtype() -> None:


def test_distance_identical_vectors() -> None:
"""Identical vectors should have distance of 0."""
x = b(1, 0, 1)
assert jaccard.distance(x, x) == pytest.approx(0.0)


def test_distance_disjoint_vectors() -> None:
"""Disjoint vectors should have distance of 1."""
x = b(1, 1, 0, 0)
y = b(0, 0, 1, 1)
assert jaccard.distance(x, y) == pytest.approx(1.0)


def test_distance_partial_overlap() -> None:
"""Distance should equal 1 minus the Jaccard similarity."""
x = b(1, 1, 1, 0)
y = b(1, 1, 0, 1)
assert jaccard.distance(x, y) == pytest.approx(0.5)


def test_distance_with_explicit_px_py() -> None:
"""Explicit px/py should be forwarded to similarity correctly."""
x = b(1, 0, 1, 0)
y = b(1, 0, 1, 0)
result = jaccard.distance(x, y, px=0.5, py=0.5)
Expand All @@ -110,6 +126,7 @@ def test_distance_with_explicit_px_py() -> None:


def test_bootstrap_returns_series_with_correct_index() -> None:
"""Bootstrap should return a Series with J-sim and p-val labels."""
x = b(1, 0, 1, 0, 1, 0)
y = b(1, 1, 0, 0, 1, 0)
result = jaccard.bootstrap(x, y, seed=0)
Expand All @@ -118,35 +135,35 @@ def test_bootstrap_returns_series_with_correct_index() -> None:


def test_bootstrap_p_value_in_range() -> None:
"""Bootstrap p-value should be in [0, 1]."""
x = b(1, 0, 1, 0, 1, 0)
y = b(1, 1, 0, 0, 1, 0)
result = jaccard.bootstrap(x, y, n=200, seed=42)
assert 0.0 <= result["p-val"] <= 1.0


def test_bootstrap_degenerate_all_ones(caplog: pytest.LogCaptureFixture) -> None:
"""All-ones vector should trigger the degenerate warning and return p-val=1."""
x = b(1, 1, 1, 1)
y = b(1, 0, 1, 0)
import logging

with caplog.at_level(logging.WARNING, logger="lta.helpers.jaccard"):
result = jaccard.bootstrap(x, y)
assert result["p-val"] == pytest.approx(1.0)
assert "degenerate" in caplog.text


def test_bootstrap_degenerate_all_zeros(caplog: pytest.LogCaptureFixture) -> None:
"""All-zeros vector should trigger the degenerate warning and return p-val=1."""
x = b(0, 0, 0, 0)
y = b(1, 0, 1, 0)
import logging

with caplog.at_level(logging.WARNING, logger="lta.helpers.jaccard"):
result = jaccard.bootstrap(x, y)
assert result["p-val"] == pytest.approx(1.0)
assert "degenerate" in caplog.text


def test_bootstrap_deterministic_with_seed() -> None:
"""Same seed should produce identical J-sim and p-val across calls."""
x = b(1, 0, 1, 0, 1, 0, 1, 0)
y = b(0, 1, 0, 1, 1, 0, 1, 0)
r1 = jaccard.bootstrap(x, y, n=500, seed=7)
Expand All @@ -156,6 +173,7 @@ def test_bootstrap_deterministic_with_seed() -> None:


def test_bootstrap_jsim_matches_similarity() -> None:
"""J-sim in bootstrap result should match the plain similarity value."""
x = b(1, 0, 1, 0, 1, 0)
y = b(1, 1, 0, 0, 1, 0)
result = jaccard.bootstrap(x, y, seed=42)
Expand Down
Loading