From 44a7a6b5aca49595e019749e00ab4313e8ad6d50 Mon Sep 17 00:00:00 2001 From: Rohit Basu <107427918+rbasu101@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:26:50 -0500 Subject: [PATCH 1/8] feat: Add Compound Nodes to Cytoscape Graph Clusters Drugs (for gene searchs) and Genes (for drug searchs) will be put into a unique parent node based on the connected gene/drug all the nodes in the cluster share. Doing this makes it much easier to visually distinguish one cluster from another, and also allows a user to move clusters around without having to move each node individually. --- src/dgipy/graph_app.py | 2 +- src/dgipy/network_graph.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/dgipy/graph_app.py b/src/dgipy/graph_app.py index b94836f..2793848 100644 --- a/src/dgipy/graph_app.py +++ b/src/dgipy/graph_app.py @@ -197,7 +197,7 @@ def _update_cytoscape(app: dash.Dash) -> None: def update(terms: list | None, search_mode: str) -> dict: if len(terms) != 0: interactions = dgidb.get_interactions(terms, search_mode) - network_graph = ng.initalize_network(interactions, terms, search_mode) + network_graph = ng.create_network(interactions, terms, search_mode) return ng.generate_cytoscape(network_graph) return {} diff --git a/src/dgipy/network_graph.py b/src/dgipy/network_graph.py index 43d5a4a..d273759 100644 --- a/src/dgipy/network_graph.py +++ b/src/dgipy/network_graph.py @@ -97,6 +97,14 @@ def _add_node_attributes(interactions_graph: nx.Graph, search_mode: str) -> None interactions_graph.nodes[node]["node_color"] = set_color interactions_graph.nodes[node]["node_size"] = set_size + if (search_mode == "genes" and (not is_gene)) or ( + search_mode == "drugs" and is_gene + ): + neighbors = "Group: " + "-".join(list(interactions_graph.neighbors(node))) + interactions_graph.nodes[node]["group"] = neighbors + else: + interactions_graph.nodes[node]["group"] = None + def create_network( interactions: pd.DataFrame, terms: list, search_mode: str @@ -129,4 +137,14 @@ def generate_cytoscape(graph: nx.Graph) -> dict: "position": {"x": int(node_pos[0].item()), "y": int(node_pos[1].item())} } cytoscape_node_data[node].update(node_pos) + if "group" in cytoscape_node_data[node]["data"]: + cytoscape_node_data[node]["data"]["parent"] = cytoscape_node_data[node][ + "data" + ].pop("group") + groups = set() + for node in graph.nodes: + if ("group" in graph.nodes[node]) and (graph.nodes[node]["group"] is not None): + groups.add(graph.nodes[node]["group"]) + for group in groups: + cytoscape_node_data.append({"data": {"id": group}}) return cytoscape_node_data + cytoscape_edge_data From f3c34cf9511787d1bba7293a3815809c0d85aaea Mon Sep 17 00:00:00 2001 From: Rohit Basu <107427918+rbasu101@users.noreply.github.com> Date: Sat, 23 Nov 2024 10:12:40 -0500 Subject: [PATCH 2/8] test: Add tests for network_graph.py Added barebone tests for network_graph, to be expanded/formalized in the future. --- pyproject.toml | 2 +- src/dgipy/__init__.py | 3 +++ tests/test_network_graph.py | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 tests/test_network_graph.py diff --git a/pyproject.toml b/pyproject.toml index f84e33c..601608b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,7 @@ branch = true [tool.ruff] src = ["src"] -exclude = ["docs/source/conf.py", "tests/test_graph_app.py"] +exclude = ["docs/source/conf.py", "tests/test_graph_app.py", "tests/test_network_graph.py"] lint.select = [ "F", # https://docs.astral.sh/ruff/rules/#pyflakes-f "E", "W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w diff --git a/src/dgipy/__init__.py b/src/dgipy/__init__.py index 11fa409..2d24363 100644 --- a/src/dgipy/__init__.py +++ b/src/dgipy/__init__.py @@ -12,6 +12,7 @@ get_sources, ) from .graph_app import generate_app +from .network_graph import create_network, generate_cytoscape __all__ = [ "get_drugs", @@ -24,4 +25,6 @@ "get_drug_applications", "generate_app", "get_clinical_trials", + "create_network", + "generate_cytoscape", ] diff --git a/tests/test_network_graph.py b/tests/test_network_graph.py new file mode 100644 index 0000000..1bec2c5 --- /dev/null +++ b/tests/test_network_graph.py @@ -0,0 +1,18 @@ +import pytest + +from dgipy.dgidb import get_interactions +from dgipy.network_graph import create_network, generate_cytoscape + + +def test_create_network(): + interactions = get_interactions("BRAF") + terms = ["BRAF"] + search_mode = "genes" + assert create_network(interactions, terms, search_mode) + +def test_generate_cytoscape(): + interactions = get_interactions("BRAF") + terms = ["BRAF"] + search_mode = "genes" + network = create_network(interactions, terms, search_mode) + assert generate_cytoscape(network) \ No newline at end of file From 3a028b952f841b3315e790b75a470a3a5b80d640 Mon Sep 17 00:00:00 2001 From: Rohit Basu <107427918+rbasu101@users.noreply.github.com> Date: Sat, 23 Nov 2024 10:15:18 -0500 Subject: [PATCH 3/8] fix: fix node selection bug Fixed bug in which selecting a compound node caused an error. The error was due to the fact compound nodes lack a node_degree attribute (which node selection checks for). Problem was fixed by checking whether the node has a node_degree attribute --- src/dgipy/graph_app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dgipy/graph_app.py b/src/dgipy/graph_app.py index 2793848..1c7b8ae 100644 --- a/src/dgipy/graph_app.py +++ b/src/dgipy/graph_app.py @@ -262,6 +262,7 @@ def update(selected_element: str | dict) -> tuple[list, None]: if ( selected_element != "" and selected_element["group"] == "nodes" + and "node_degree" in selected_element["data"] and selected_element["data"]["node_degree"] != 1 ): neighbor_set = set() From f7857830abfa199d5a0c1328fd922dc31f60f6f7 Mon Sep 17 00:00:00 2001 From: Rohit Basu <107427918+rbasu101@users.noreply.github.com> Date: Sat, 23 Nov 2024 10:18:00 -0500 Subject: [PATCH 4/8] refactor: refactor network_graph.py Made numerous changes to improve the readability of network_graph.py, and to remove redundant code (left over from the shift to cytoscape): - Removed pandas dependency (Redundant, as functions were using dict) - Modified dict iteration in initalize_network() to improve readability - Privated initalize_network() (initalize_network serves as only one part of the overall create_network() function (which is public). initalize_network() should not be directly run itself) - Removed node color/size assignment from add_node_attributes() (With the shift to cytoscape, Node color/size assignment is now done in graph_app.py) - Moved node_degree attribute assignment to add_node_attributes() --- src/dgipy/network_graph.py | 93 ++++++++++---------------------------- 1 file changed, 23 insertions(+), 70 deletions(-) diff --git a/src/dgipy/network_graph.py b/src/dgipy/network_graph.py index d273759..3804033 100644 --- a/src/dgipy/network_graph.py +++ b/src/dgipy/network_graph.py @@ -1,50 +1,38 @@ """Provides functionality to create networkx graphs and pltoly figures for network visualization""" import networkx as nx -import pandas as pd LAYOUT_SEED = 7 -def initalize_network( - interactions: pd.DataFrame, terms: list, search_mode: str -) -> nx.Graph: - """Create a networkx graph representing interactions between genes and drugs - - :param interactions: DataFrame containing drug-gene interaction data - :param terms: List containing terms used to query interaction data - :param search_mode: String indicating whether query was gene-focused or drug-focused - :return: a networkx graph of drug-gene interactions - """ +def _initalize_network(interactions: dict, terms: list, search_mode: str) -> nx.Graph: interactions_graph = nx.Graph() graphed_terms = set() - - for index in range(len(interactions["gene_name"]) - 1): + for row in zip(*interactions.values(), strict=True): + row_dict = dict(zip(interactions.keys(), row, strict=True)) if search_mode == "genes": - graphed_terms.add(interactions["gene_name"][index]) + graphed_terms.add(row_dict["gene_name"]) if search_mode == "drugs": - graphed_terms.add(interactions["drug_name"][index]) + graphed_terms.add(row_dict["drug_name"]) interactions_graph.add_node( - interactions["gene_name"][index], - label=interactions["gene_name"][index], + row_dict["gene_name"], + label=row_dict["gene_name"], isGene=True, ) interactions_graph.add_node( - interactions["drug_name"][index], - label=interactions["drug_name"][index], + row_dict["drug_name"], + label=row_dict["drug_name"], isGene=False, ) interactions_graph.add_edge( - interactions["gene_name"][index], - interactions["drug_name"][index], - id=interactions["gene_name"][index] - + " - " - + interactions["drug_name"][index], - approval=interactions["drug_approved"][index], - score=interactions["interaction_score"][index], - attributes=interactions["interaction_attributes"][index], - sourcedata=interactions["interaction_sources"][index], - pmid=interactions["interaction_pmids"][index], + row_dict["gene_name"], + row_dict["drug_name"], + id=row_dict["gene_name"] + " - " + row_dict["drug_name"], + approval=row_dict["drug_approved"], + score=row_dict["interaction_score"], + attributes=row_dict["interaction_attributes"], + sourcedata=row_dict["interaction_sources"], + pmid=row_dict["interaction_pmids"], ) graphed_terms = set(terms).difference(graphed_terms) @@ -54,48 +42,15 @@ def initalize_network( if search_mode == "drugs": interactions_graph.add_node(term, label=term, isGene=False) - nx.set_node_attributes( - interactions_graph, dict(interactions_graph.degree()), "node_degree" - ) return interactions_graph def _add_node_attributes(interactions_graph: nx.Graph, search_mode: str) -> None: + nx.set_node_attributes( + interactions_graph, dict(interactions_graph.degree()), "node_degree" + ) for node in interactions_graph.nodes: is_gene = interactions_graph.nodes[node]["isGene"] - degree = interactions_graph.degree[node] - if search_mode == "genes": - if is_gene: - if degree > 1: - set_color = "cyan" - set_size = 10 - else: - set_color = "blue" - set_size = 10 - else: - if degree > 1: - set_color = "orange" - set_size = 7 - else: - set_color = "red" - set_size = 7 - if search_mode == "drugs": - if is_gene: - if degree > 1: - set_color = "cyan" - set_size = 7 - else: - set_color = "blue" - set_size = 7 - else: - if degree > 1: - set_color = "orange" - set_size = 10 - else: - set_color = "red" - set_size = 10 - interactions_graph.nodes[node]["node_color"] = set_color - interactions_graph.nodes[node]["node_size"] = set_size if (search_mode == "genes" and (not is_gene)) or ( search_mode == "drugs" and is_gene @@ -106,17 +61,15 @@ def _add_node_attributes(interactions_graph: nx.Graph, search_mode: str) -> None interactions_graph.nodes[node]["group"] = None -def create_network( - interactions: pd.DataFrame, terms: list, search_mode: str -) -> nx.Graph: +def create_network(interactions: dict, terms: list, search_mode: str) -> nx.Graph: """Create a networkx graph representing interactions between genes and drugs - :param interactions: DataFrame containing drug-gene interaction data + :param interactions: Dictionary containing drug-gene interaction data :param terms: List containing terms used to query interaction data :param search_mode: String indicating whether query was gene-focused or drug-focused :return: a networkx graph of drug-gene interactions """ - interactions_graph = initalize_network(interactions, terms, search_mode) + interactions_graph = _initalize_network(interactions, terms, search_mode) _add_node_attributes(interactions_graph, search_mode) return interactions_graph From 65aed7617b07eb703212d82a7ba91ab878bc4e61 Mon Sep 17 00:00:00 2001 From: Rohit Basu <107427918+rbasu101@users.noreply.github.com> Date: Sat, 23 Nov 2024 10:36:10 -0500 Subject: [PATCH 5/8] style(test_network_graph): add newline at end of file --- tests/test_network_graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_network_graph.py b/tests/test_network_graph.py index 1bec2c5..087bafd 100644 --- a/tests/test_network_graph.py +++ b/tests/test_network_graph.py @@ -15,4 +15,4 @@ def test_generate_cytoscape(): terms = ["BRAF"] search_mode = "genes" network = create_network(interactions, terms, search_mode) - assert generate_cytoscape(network) \ No newline at end of file + assert generate_cytoscape(network) From b6c26aca5fcafb261c137eef0b236b5df940f6c3 Mon Sep 17 00:00:00 2001 From: Rohit Basu <107427918+rbasu101@users.noreply.github.com> Date: Sat, 23 Nov 2024 13:47:52 -0500 Subject: [PATCH 6/8] refactor: refactor generate_cytoscape() Code was refactored so as to remove redundant iteration and variable instantiation --- src/dgipy/network_graph.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/dgipy/network_graph.py b/src/dgipy/network_graph.py index 3804033..40add63 100644 --- a/src/dgipy/network_graph.py +++ b/src/dgipy/network_graph.py @@ -84,20 +84,14 @@ def generate_cytoscape(graph: nx.Graph) -> dict: cytoscape_data = nx.cytoscape_data(graph)["elements"] cytoscape_node_data = cytoscape_data["nodes"] cytoscape_edge_data = cytoscape_data["edges"] - for node in range(len(cytoscape_node_data)): - node_pos = pos[cytoscape_node_data[node]["data"]["id"]] - node_pos = { - "position": {"x": int(node_pos[0].item()), "y": int(node_pos[1].item())} - } - cytoscape_node_data[node].update(node_pos) - if "group" in cytoscape_node_data[node]["data"]: - cytoscape_node_data[node]["data"]["parent"] = cytoscape_node_data[node][ - "data" - ].pop("group") groups = set() - for node in graph.nodes: - if ("group" in graph.nodes[node]) and (graph.nodes[node]["group"] is not None): - groups.add(graph.nodes[node]["group"]) + for node in cytoscape_node_data: + node_pos = pos[node["data"]["id"]] + node.update({"position": {"x": node_pos[0], "y": node_pos[1]}}) + if "group" in node["data"]: + group = node["data"].pop("group") + groups.add(group) + node["data"]["parent"] = group for group in groups: cytoscape_node_data.append({"data": {"id": group}}) return cytoscape_node_data + cytoscape_edge_data From 1ea0d5beab1a3cef765bade3b1ff6939b3185e07 Mon Sep 17 00:00:00 2001 From: Rohit Basu <107427918+rbasu101@users.noreply.github.com> Date: Sat, 23 Nov 2024 14:05:25 -0500 Subject: [PATCH 7/8] style(ruff): Apply ruff style to graph_app and network_graph tests --- pyproject.toml | 2 +- tests/test_graph_app.py | 2 -- tests/test_network_graph.py | 3 +-- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 601608b..328b84a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,7 @@ branch = true [tool.ruff] src = ["src"] -exclude = ["docs/source/conf.py", "tests/test_graph_app.py", "tests/test_network_graph.py"] +exclude = ["docs/source/conf.py"] lint.select = [ "F", # https://docs.astral.sh/ruff/rules/#pyflakes-f "E", "W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w diff --git a/tests/test_graph_app.py b/tests/test_graph_app.py index 7607b01..58f38d9 100644 --- a/tests/test_graph_app.py +++ b/tests/test_graph_app.py @@ -1,5 +1,3 @@ -import pytest - from dgipy.graph_app import generate_app diff --git a/tests/test_network_graph.py b/tests/test_network_graph.py index 087bafd..812b17d 100644 --- a/tests/test_network_graph.py +++ b/tests/test_network_graph.py @@ -1,5 +1,3 @@ -import pytest - from dgipy.dgidb import get_interactions from dgipy.network_graph import create_network, generate_cytoscape @@ -10,6 +8,7 @@ def test_create_network(): search_mode = "genes" assert create_network(interactions, terms, search_mode) + def test_generate_cytoscape(): interactions = get_interactions("BRAF") terms = ["BRAF"] From cc1a47aa60584ce33d2ab988da18e3532db74c00 Mon Sep 17 00:00:00 2001 From: Rohit Basu <107427918+rbasu101@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:38:20 -0500 Subject: [PATCH 8/8] fix: remove unused group Previously, any nodes without a group would create an additoinal 'None' group, which would result in the addition of a single unlabeled node in the graph. This has been fixed by removing the 'None' group. --- src/dgipy/network_graph.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dgipy/network_graph.py b/src/dgipy/network_graph.py index 40add63..873e2f6 100644 --- a/src/dgipy/network_graph.py +++ b/src/dgipy/network_graph.py @@ -92,6 +92,7 @@ def generate_cytoscape(graph: nx.Graph) -> dict: group = node["data"].pop("group") groups.add(group) node["data"]["parent"] = group + groups.remove(None) for group in groups: cytoscape_node_data.append({"data": {"id": group}}) return cytoscape_node_data + cytoscape_edge_data