From 615ac498d366d8b510e5b443f142f5f60cd97650 Mon Sep 17 00:00:00 2001 From: Jordan Matelsky Date: Mon, 27 Jul 2026 10:40:09 -0400 Subject: [PATCH] fix: derive edge-only DataFrame nodes correctly --- grand/backends/_dataframe.py | 30 ++++++++++++++++++++---------- grand/backends/test_backends.py | 24 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/grand/backends/_dataframe.py b/grand/backends/_dataframe.py index ac2b47d..6d94bf0 100644 --- a/grand/backends/_dataframe.py +++ b/grand/backends/_dataframe.py @@ -146,12 +146,17 @@ def all_nodes_as_iterable(self, include_metadata: bool = False): ] else: + nodes = pd.unique( + pd.concat( + [ + self._edge_df[self._edge_df_source_column], + self._edge_df[self._edge_df_target_column], + ], + ignore_index=True, + ) + ) return [ - (node_id, {}) if include_metadata else node_id - for node_id in self._edge_df[self._edge_df_source_column] - ] + [ - (node_id, {}) if include_metadata else node_id - for node_id in self._edge_df[self._edge_df_target_column] + (node_id, {}) if include_metadata else node_id for node_id in nodes ] def has_node(self, u: Hashable) -> bool: @@ -167,8 +172,8 @@ def has_node(self, u: Hashable) -> bool: if self._node_df is not None: return u in self._node_df.index - return u in (self._edge_df[self._edge_df_source_column]) or u in ( - self._edge_df[self._edge_df_target_column] + return u in self._edge_df[self._edge_df_source_column].values or u in ( + self._edge_df[self._edge_df_target_column].values ) def add_edge(self, u: Hashable, v: Hashable, metadata: dict): @@ -469,10 +474,15 @@ def get_node_count(self) -> int: """ if self._node_df is not None: return len(self._node_df) - # Return number of unique sources intersected with number of unique targets return len( - set(self._edge_df[self._edge_df_source_column]).intersection( - set(self._edge_df[self._edge_df_target_column]) + pd.unique( + pd.concat( + [ + self._edge_df[self._edge_df_source_column], + self._edge_df[self._edge_df_target_column], + ], + ignore_index=True, + ) ) ) diff --git a/grand/backends/test_backends.py b/grand/backends/test_backends.py index 5a640bd..93368e2 100644 --- a/grand/backends/test_backends.py +++ b/grand/backends/test_backends.py @@ -565,6 +565,30 @@ def test_can_create_from_int_dataframes(self): assert b.get_edge_count() == 5 assert b.get_node_count() == 5 + def test_edge_only_nodes_are_unique_members_and_counted_from_union(self): + edges = pd.DataFrame( + { + "source": ["A", "B", "A"], + "target": ["B", "C", "B"], + } + ) + backend = DataFrameBackend( + edge_df=edges, + edge_df_source_column="source", + edge_df_target_column="target", + ) + + assert backend.all_nodes_as_iterable() == ["A", "B", "C"] + assert backend.all_nodes_as_iterable(include_metadata=True) == [ + ("A", {}), + ("B", {}), + ("C", {}), + ] + assert backend.has_node("A") + assert backend.has_node("C") + assert not backend.has_node("missing") + assert backend.get_node_count() == 3 + def test_networkx_can_ingest_edgelist_dataframe(): backend = NetworkXBackend(directed=True)