6b7e6b44f1
gh-pages / build (push) Waiting to run
Python Publish (pypi) / Upload release to PyPI (push) Waiting to run
Spellcheck / spellcheck (push) Waiting to run
Python Build and Type Check / python-ci (ubuntu-latest, 3.11) (push) Has been cancelled
Python Build and Type Check / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Build and Type Check / python-ci (windows-latest, 3.11) (push) Has been cancelled
Python Build and Type Check / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Integration Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Integration Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Notebook Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Notebook Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Smoke Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Smoke Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Unit Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Unit Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
214 lines
6.5 KiB
Python
214 lines
6.5 KiB
Python
# Copyright (c) 2024 Microsoft Corporation.
|
|
# Licensed under the MIT License
|
|
|
|
"""Side-by-side tests for the DataFrame-based stable LCC utility."""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import networkx as nx
|
|
import pandas as pd
|
|
from graphrag.graphs.stable_lcc import stable_lcc
|
|
from pandas.testing import assert_frame_equal
|
|
|
|
from tests.unit.graphs.nx_stable_lcc import stable_largest_connected_component
|
|
|
|
FIXTURES_DIR = Path(__file__).parent / "fixtures"
|
|
|
|
|
|
def _load_fixture() -> pd.DataFrame:
|
|
"""Load the realistic graph fixture as a relationships DataFrame."""
|
|
with open(FIXTURES_DIR / "graph.json") as f:
|
|
data = json.load(f)
|
|
return pd.DataFrame(data["edges"])
|
|
|
|
|
|
def _make_relationships(*edges: tuple[str, str, float]) -> pd.DataFrame:
|
|
"""Build a relationships DataFrame from (source, target, weight) tuples."""
|
|
return pd.DataFrame([{"source": s, "target": t, "weight": w} for s, t, w in edges])
|
|
|
|
|
|
def _nx_stable_lcc_node_set(relationships: pd.DataFrame) -> set[str]:
|
|
"""Get the node set from the NX stable_largest_connected_component."""
|
|
graph = nx.from_pandas_edgelist(
|
|
relationships,
|
|
source="source",
|
|
target="target",
|
|
edge_attr=["weight"],
|
|
)
|
|
stable_graph = stable_largest_connected_component(graph)
|
|
return set(stable_graph.nodes())
|
|
|
|
|
|
def _nx_stable_lcc_edge_set(relationships: pd.DataFrame) -> set[tuple[str, str]]:
|
|
"""Get the edge set from the NX stable_largest_connected_component."""
|
|
graph = nx.from_pandas_edgelist(
|
|
relationships,
|
|
source="source",
|
|
target="target",
|
|
edge_attr=["weight"],
|
|
)
|
|
stable_graph = stable_largest_connected_component(graph)
|
|
return {(min(s, t), max(s, t)) for s, t in stable_graph.edges()}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stability tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_flipped_edges_produce_same_result():
|
|
"""Same graph with edges in different direction should produce identical output."""
|
|
rels_1 = _make_relationships(
|
|
("A", "B", 1.0),
|
|
("B", "C", 2.0),
|
|
("C", "D", 3.0),
|
|
("D", "E", 4.0),
|
|
)
|
|
rels_2 = _make_relationships(
|
|
("B", "A", 1.0),
|
|
("C", "B", 2.0),
|
|
("D", "C", 3.0),
|
|
("E", "D", 4.0),
|
|
)
|
|
result_1 = stable_lcc(rels_1)
|
|
result_2 = stable_lcc(rels_2)
|
|
assert_frame_equal(result_1, result_2)
|
|
|
|
|
|
def test_shuffled_rows_produce_same_result():
|
|
"""Different row order should produce identical output."""
|
|
rels_1 = _make_relationships(
|
|
("A", "B", 1.0),
|
|
("B", "C", 2.0),
|
|
("C", "D", 3.0),
|
|
)
|
|
rels_2 = _make_relationships(
|
|
("C", "D", 3.0),
|
|
("A", "B", 1.0),
|
|
("B", "C", 2.0),
|
|
)
|
|
result_1 = stable_lcc(rels_1)
|
|
result_2 = stable_lcc(rels_2)
|
|
assert_frame_equal(result_1, result_2)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Name normalization tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_normalizes_node_names():
|
|
"""Node names should be uppercased, stripped, and HTML-unescaped."""
|
|
rels = _make_relationships(
|
|
(" alice ", "bob", 1.0),
|
|
("bob", "carol & dave", 1.0),
|
|
)
|
|
result = stable_lcc(rels)
|
|
all_nodes = set(result["source"]).union(result["target"])
|
|
assert "ALICE" in all_nodes
|
|
assert "BOB" in all_nodes
|
|
assert "CAROL & DAVE" in all_nodes
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# LCC filtering tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_filters_to_lcc():
|
|
"""Only the largest component should remain."""
|
|
rels = _make_relationships(
|
|
("A", "B", 1.0),
|
|
("B", "C", 1.0),
|
|
("C", "A", 1.0),
|
|
("X", "Y", 1.0),
|
|
)
|
|
result = stable_lcc(rels)
|
|
all_nodes = set(result["source"]).union(result["target"])
|
|
assert all_nodes == {"A", "B", "C"}
|
|
|
|
|
|
def test_empty_relationships():
|
|
"""Empty input should return empty output."""
|
|
rels = pd.DataFrame(columns=["source", "target", "weight"])
|
|
result = stable_lcc(rels)
|
|
assert result.empty
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Side-by-side with NX implementation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_node_set_matches_nx():
|
|
"""LCC node set should match the NX stable_largest_connected_component."""
|
|
rels = _make_relationships(
|
|
("A", "B", 1.0),
|
|
("B", "C", 1.0),
|
|
("C", "D", 1.0),
|
|
("D", "E", 1.0),
|
|
("X", "Y", 1.0),
|
|
)
|
|
nx_nodes = _nx_stable_lcc_node_set(rels)
|
|
df_result = stable_lcc(rels)
|
|
df_nodes = set(df_result["source"]).union(df_result["target"])
|
|
assert df_nodes == nx_nodes
|
|
|
|
|
|
def test_edge_set_matches_nx():
|
|
"""LCC edge set should match the NX stable_largest_connected_component."""
|
|
rels = _make_relationships(
|
|
("A", "B", 1.0),
|
|
("B", "C", 1.0),
|
|
("C", "D", 1.0),
|
|
("D", "E", 1.0),
|
|
("X", "Y", 1.0),
|
|
)
|
|
nx_edges = _nx_stable_lcc_edge_set(rels)
|
|
df_result = stable_lcc(rels)
|
|
df_edges = {
|
|
(min(s, t), max(s, t))
|
|
for s, t in zip(df_result["source"], df_result["target"], strict=True)
|
|
}
|
|
assert df_edges == nx_edges
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixture tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_fixture_node_set_matches_nx():
|
|
"""Fixture LCC nodes should match NX stable LCC."""
|
|
rels = _load_fixture()
|
|
nx_nodes = _nx_stable_lcc_node_set(rels)
|
|
df_result = stable_lcc(rels)
|
|
df_nodes = set(df_result["source"]).union(df_result["target"])
|
|
assert df_nodes == nx_nodes
|
|
|
|
|
|
def test_fixture_edge_set_matches_nx():
|
|
"""Fixture LCC edges should match NX stable LCC."""
|
|
rels = _load_fixture()
|
|
nx_edges = _nx_stable_lcc_edge_set(rels)
|
|
df_result = stable_lcc(rels)
|
|
df_edges = {
|
|
(min(s, t), max(s, t))
|
|
for s, t in zip(df_result["source"], df_result["target"], strict=True)
|
|
}
|
|
assert df_edges == nx_edges
|
|
|
|
|
|
def test_fixture_edges_are_sorted():
|
|
"""Output edges should be sorted with source <= target and rows in order."""
|
|
rels = _load_fixture()
|
|
result = stable_lcc(rels)
|
|
# Every source should be <= target
|
|
assert (result["source"] <= result["target"]).all()
|
|
# Rows should be sorted
|
|
is_sorted = (
|
|
result[["source", "target"]].apply(tuple, axis=1).is_monotonic_increasing
|
|
)
|
|
assert is_sorted
|