From 85712e9a8ac56a674ef2d7dde02669fbd649bf47 Mon Sep 17 00:00:00 2001 From: Vicente Date: Mon, 2 Jun 2025 11:25:57 -0600 Subject: [PATCH 1/9] Adding Unit Tests for next release --- .gitignore | 2 +- README.md | 2 +- .../downstream_task/subject_representation.py | 16 +- .../network_embedding/gnn_embedding.py | 5 +- bioneuralnet/utils/graph.py | 23 ++- docs/source/TCGA-BRCA_Dataset.ipynb | 21 --- docs/source/index.rst | 4 +- tests/__init__.py | 13 +- tests/test_correlated_louvain.py | 69 --------- tests/test_correlated_pagerank.py | 41 ------ tests/test_dpmon.py | 69 +++++---- tests/test_gnn_embedding.py | 114 +++------------ tests/test_graph_gen.py | 104 +++++++++++++ tests/test_hybrid_louvain.py | 108 ++++++++------ tests/test_metrics.py | 127 ---------------- tests/test_smccnet.py | 137 ------------------ tests/test_subject_representation.py | 71 +++++---- tests/test_utils.py | 48 ------ 18 files changed, 317 insertions(+), 657 deletions(-) delete mode 100644 tests/test_correlated_louvain.py delete mode 100644 tests/test_correlated_pagerank.py create mode 100644 tests/test_graph_gen.py delete mode 100644 tests/test_metrics.py delete mode 100644 tests/test_smccnet.py delete mode 100644 tests/test_utils.py diff --git a/.gitignore b/.gitignore index c83e39d..9df706a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,7 @@ doc_examples_outdated dpmon_output .enviroment TCGA-BRCA_Dataset_testing*.ipynb - +testsOLD # Other example data and tests not needed in the repo. Output** diff --git a/README.md b/README.md index 4845aee..eb7220f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# BioNeuralNet: Multi-Omics Integration with Graph Neural Networks +# BioNeuralNet: A Graph Neural Network based Multi-Omics Network Data Analysis Tool [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/UCD-BDLab/BioNeuralNet/blob/main/LICENSE) [![PyPI](https://img.shields.io/pypi/v/bioneuralnet)](https://pypi.org/project/bioneuralnet/) diff --git a/bioneuralnet/downstream_task/subject_representation.py b/bioneuralnet/downstream_task/subject_representation.py index 27f66dd..d0d1d74 100644 --- a/bioneuralnet/downstream_task/subject_representation.py +++ b/bioneuralnet/downstream_task/subject_representation.py @@ -61,15 +61,18 @@ def __init__( if omics_data is None or omics_data.empty: raise ValueError("Omics data must be non-empty.") - - if embeddings is None or embeddings.empty: - self.logger.info( - "No embeddings provided, please review documentation to see how to generate embeddings.") + + if embeddings is None: + self.logger.warning("No embeddings provided, please review documentation to see how to generate embeddings.") raise ValueError("Embeddings must be non-empty.") if not isinstance(embeddings, pd.DataFrame): raise ValueError("Embeddings must be provided as a pandas DataFrame.") + if embeddings.empty: + self.logger.warning("No embeddings provided, please review documentation to see how to generate embeddings.") + raise ValueError("Embeddings must be non-empty.") + if tune and phenotype_data is None: raise ValueError("Phenotype data must be provided for classification-based tuning.") @@ -260,6 +263,9 @@ def _integrate_embeddings(self, reduced: pd.DataFrame, method="multiply", alpha= This is so at least 50% of the final output is influenced by the computed weight """ + if not isinstance(reduced, (pd.DataFrame, pd.Series)): + raise ValueError("Reduced embeddings must be a pandas DataFrame or Series.") + missing_features = set(self.omics_data.columns) - set(reduced.index) if missing_features: self.logger.warning(f"Missing {len(missing_features)} features in reduced embeddings: {list(missing_features)[:5]}") @@ -277,8 +283,6 @@ def _integrate_embeddings(self, reduced: pd.DataFrame, method="multiply", alpha= weight_series = reduced.mean(axis=1) elif isinstance(reduced, pd.Series): weight_series = (reduced - reduced.min()) / (reduced.max() - reduced.min()) - else: - raise ValueError("Reduced embeddings must be a pandas DataFrame or Series.") weight_series = weight_series.fillna(0) ranks = weight_series.rank(method="average") diff --git a/bioneuralnet/network_embedding/gnn_embedding.py b/bioneuralnet/network_embedding/gnn_embedding.py index 1a98923..dde0d1f 100644 --- a/bioneuralnet/network_embedding/gnn_embedding.py +++ b/bioneuralnet/network_embedding/gnn_embedding.py @@ -89,8 +89,9 @@ def __init__( if omics_data.empty: raise ValueError("Omics data cannot be empty.") if clinical_data is not None and clinical_data.empty: - raise ValueError("Clinical data was provided but is empty.") - + self.logger.warning("Clinical data was provided but is empty, setting to None.") + clinical_data = None + if adjacency_matrix.shape[0] != adjacency_matrix.shape[1]: raise ValueError("Adjacency matrix must be square.") if not adjacency_matrix.index.equals(adjacency_matrix.columns): diff --git a/bioneuralnet/utils/graph.py b/bioneuralnet/utils/graph.py index eac85b8..587b407 100644 --- a/bioneuralnet/utils/graph.py +++ b/bioneuralnet/utils/graph.py @@ -4,6 +4,9 @@ from typing import Optional import torch.nn.functional as F from sklearn.covariance import GraphicalLasso +from .logger import get_logger + +logger = get_logger(__name__) def gen_similarity_graph(X:pd.DataFrame, k:int = 15, metric:str = "cosine", mutual:bool = False, per_node:bool = True, self_loops:bool = True) -> pd.DataFrame: """ @@ -30,6 +33,7 @@ def gen_similarity_graph(X:pd.DataFrame, k:int = 15, metric:str = "cosine", mutu raise TypeError("X must be a pandas.DataFrame") N = x_torch.size(0) + k = min(k, N-1) # full similarity matrix if metric == "cosine": @@ -280,18 +284,25 @@ def gen_lasso_graph(X: pd.DataFrame, alpha: float = 0.01, self_loops: bool = Tru """ if isinstance(X, pd.DataFrame): nodes = X.index - x_numpy = X.values + x_numpy = X.values.T else: raise TypeError("X must be a pandas.DataFrame") - model = GraphicalLasso(alpha=alpha, max_iter=200) - model.fit(x_numpy) + try: + model = GraphicalLasso(alpha=alpha, max_iter=1000) + model.fit(x_numpy) + + P = torch.from_numpy(model.precision_).abs() + W = (P + P.t()) / 2 - P = torch.from_numpy(model.precision_).abs() - W = (P + P.t()) / 2 + except FloatingPointError: + logger.warning("Graphical Lasso failed to converge, using identity matrix instead.") + N = len(nodes) + W = torch.eye(N, dtype=torch.float32) if self_loops: - W.fill_diagonal_(W.diagonal() + 1.0) + diag_indices = torch.arange(W.size(0), device=W.device) + W[diag_indices, diag_indices] += 1.0 W = F.normalize(W, p=1, dim=1) final_graph = pd.DataFrame(W.cpu().numpy(), index=nodes, columns=nodes) diff --git a/docs/source/TCGA-BRCA_Dataset.ipynb b/docs/source/TCGA-BRCA_Dataset.ipynb index f25bb3d..3d8d13b 100644 --- a/docs/source/TCGA-BRCA_Dataset.ipynb +++ b/docs/source/TCGA-BRCA_Dataset.ipynb @@ -60,27 +60,6 @@ "- [Direct Download BRCA](http://firebrowse.org/?cohort=BRCA&download_dialog=true)\n" ] }, - { - "cell_type": "code", - "execution_count": 1, - "id": "60a6b53c", - "metadata": {}, - "outputs": [], - "source": [ - "# adjusting global pandas options for better display on web documentation\n", - "import pandas as pd\n", - "import warnings\n", - "import logging\n", - "\n", - "pd.set_option(\"display.max_columns\", 5)\n", - "pd.set_option(\"display.expand_frame_repr\", False)\n", - "warnings.filterwarnings(\"ignore\", category=UserWarning)\n", - "warnings.filterwarnings(\"ignore\", category=DeprecationWarning)\n", - "logging.getLogger(\"ray\").setLevel(logging.ERROR)\n", - "logging.getLogger(\"ray.tune\").setLevel(logging.ERROR)\n", - "logging.getLogger(\"torch_geometric\").setLevel(logging.ERROR)" - ] - }, { "cell_type": "markdown", "id": "c9698b74", diff --git a/docs/source/index.rst b/docs/source/index.rst index 9ee6979..e1d3d37 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,5 +1,5 @@ -BioNeuralNet - Multi-Omics Integration with Graph Neural Networks -================================================================= +BioNeuralNet: A Graph Neural Network based Multi-Omics Network Data Analysis Tool +================================================================================= .. image:: https://img.shields.io/badge/license-MIT-blue.svg :target: https://github.com/UCD-BDLab/BioNeuralNet/blob/main/LICENSE diff --git a/tests/__init__.py b/tests/__init__.py index b7e2985..7c35f70 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,9 +1,10 @@ from .test_gnn_embedding import * -from .test_metrics import * -from .test_utils import * from .test_subject_representation import * -from .test_utils import * -from .test_smccnet import * -from .test_correlated_louvain import * -from .test_correlated_pagerank import * from .test_hybrid_louvain import * +from .test_dpmon import * + +#from .test_metrics import * +#from .test_utils import * +#from .test_smccnet import * +#from .test_correlated_louvain import * +#from .test_correlated_pagerank import * \ No newline at end of file diff --git a/tests/test_correlated_louvain.py b/tests/test_correlated_louvain.py deleted file mode 100644 index fa60587..0000000 --- a/tests/test_correlated_louvain.py +++ /dev/null @@ -1,69 +0,0 @@ -import unittest -import networkx as nx -import pandas as pd -import numpy as np -from bioneuralnet.clustering import CorrelatedLouvain - -class TestCorrelatedLouvain(unittest.TestCase): - def setUp(self): - self.G = nx.Graph() - nodes = [] - nodes.append("node1") - nodes.append("node2") - nodes.append("node3") - nodes.append("node4") - nodes.append("node5") - i = 0 - while i < len(nodes): - self.G.add_node(nodes[i]) - i = i + 1 - self.G.add_edge("node1", "node2", weight=1.0) - self.G.add_edge("node2", "node3", weight=1.0) - self.G.add_edge("node3", "node4", weight=1.0) - self.G.add_edge("node4", "node5", weight=1.0) - self.G.add_edge("node5", "node1", weight=1.0) - data = {} - i = 0 - while i < len(nodes): - col = nodes[i] - vals = [] - j = 0 - while j < 10: - vals.append(np.random.rand()) - j = j + 1 - data[col] = vals - i = i + 1 - self.B = pd.DataFrame(data) - phenos = [] - i = 0 - while i < 10: - phenos.append(np.random.rand()) - i = i + 1 - self.Y = pd.DataFrame({"phenotype": phenos}) - - def test_run_partition(self): - cl = CorrelatedLouvain(self.G, self.B, self.Y, k3=0.2, k4=0.8, weight="weight", tune=False) - part = cl.run(as_dfs=False) - self.assertIsInstance(part, dict) - keys = [] - for k in part: - keys.append(k) - self.assertTrue(len(keys) > 0) - - def test_run_dfs(self): - cl = CorrelatedLouvain(self.G, self.B, self.Y, k3=0.2, k4=0.8, weight="weight", tune=False) - dfs = cl.run(as_dfs=True) - self.assertIsInstance(dfs, list) - i = 0 - while i < len(dfs): - self.assertIsInstance(dfs[i], pd.DataFrame) - i = i + 1 - - def test_get_quality(self): - cl = CorrelatedLouvain(self.G, self.B, self.Y, k3=0.2, k4=0.8, weight="weight", tune=False) - cl.run(as_dfs=False) - q = cl.get_quality() - self.assertIsInstance(q, float) - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_correlated_pagerank.py b/tests/test_correlated_pagerank.py deleted file mode 100644 index 222ba43..0000000 --- a/tests/test_correlated_pagerank.py +++ /dev/null @@ -1,41 +0,0 @@ -import unittest -import networkx as nx -import pandas as pd -import numpy as np -from bioneuralnet.clustering import CorrelatedPageRank - -class TestCorrelatedPageRank(unittest.TestCase): - def setUp(self): - self.G = nx.complete_graph(4, create_using=nx.DiGraph()) - nodes = list(self.G.nodes()) - - data = {node: np.random.rand(10) for node in nodes} - self.B = pd.DataFrame(data) - - self.Y = pd.DataFrame({"phenotype": np.random.rand(10)}) - - def test_run_valid(self): - cp = CorrelatedPageRank( - self.G, self.B, self.Y, - alpha=0.9, max_iter=100, tol=1e-6, k=0.5, tune=False - ) - - seed_nodes = [list(self.G.nodes())[0], list(self.G.nodes())[1]] - res = cp.run(seed_nodes) - expected_keys = [ - "cluster_nodes", "cluster_size", "conductance", - "correlation", "composite_score", "correlation_pvalue" - ] - for key in expected_keys: - self.assertIn(key, res) - - def test_run_empty_seed(self): - cp = CorrelatedPageRank( - self.G, self.B, self.Y, - alpha=0.9, max_iter=100, tol=1e-6, k=0.5, tune=False - ) - with self.assertRaises(ValueError): - cp.run([]) - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_dpmon.py b/tests/test_dpmon.py index 5ab74fc..a1c14d5 100644 --- a/tests/test_dpmon.py +++ b/tests/test_dpmon.py @@ -1,74 +1,91 @@ import unittest from unittest.mock import patch import pandas as pd +import tempfile +import shutil from bioneuralnet.downstream_task import DPMON +import warnings -class TestDPMON(unittest.TestCase): +class TestDPMON(unittest.TestCase): def setUp(self): - self.adjacency_matrix = pd.DataFrame( - [[1.0, 0.3, 0.1], [0.3, 1.0, 0.05], [0.1, 0.05, 1.0]], - index=["gene1", "gene2", "gene3"], - columns=["gene1", "gene2", "gene3"], - ) + self.adjacency_matrix = pd.DataFrame([[1.0, 0.3, 0.1], [0.3, 1.0, 0.05], [0.1, 0.05, 1.0]],index=["gene1", "gene2", "gene3"],columns=["gene1", "gene2", "gene3"],) + self.tempfolder = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.tempfolder) self.omics_data1 = pd.DataFrame( {"gene1": [1, 2], "gene2": [3, 4]}, index=["sample1", "sample2"]) self.omics_data2 = pd.DataFrame({"gene3": [5, 6]}, index=["sample1", "sample2"]) self.phenotype_data = pd.DataFrame({"phenotype": [0, 1]}, index=["sample1", "sample2"]) - self.features_data = pd.DataFrame( {"age": [30, 45], "bmi": [22.5, 28.0]}, index=["sample1", "sample2"]) + self.clinical_data = pd.DataFrame( {"age": [30, 45], "bmi": [22.5, 28.0]}, index=["sample1", "sample2"]) @patch("bioneuralnet.downstream_task.dpmon.run_standard_training") def test_run_without_tune(self, mock_standard): - mock_standard.return_value = pd.DataFrame( - {"Actual": [2, 3], "Predicted": [2, 2]}, index=["sample1", "sample2"] - ) + warnings.filterwarnings("ignore") + + mock_standard.return_value = (pd.DataFrame({"Actual": [2, 3], "Predicted": [2, 2]}, index=["sample1", "sample2"]), 0.89) dpmon = DPMON( adjacency_matrix=self.adjacency_matrix, omics_list=[self.omics_data1, self.omics_data2], phenotype_data=self.phenotype_data, - clinical_data=self.features_data, - model="GCN", + clinical_data=self.clinical_data, tune=False, gpu=False, - output_dir="test_output", + output_dir=self.tempfolder ) predictions = dpmon.run() mock_standard.assert_called_once() - self.assertIn("Actual", predictions.columns) - self.assertIn("Predicted", predictions.columns) - self.assertEqual(predictions.shape, (2, 2)) + self.assertIsInstance(predictions, tuple) + self.assertIsInstance(predictions[0], pd.DataFrame) + self.assertIsInstance(predictions[1], float) + @patch("bioneuralnet.downstream_task.dpmon.run_standard_training") @patch("bioneuralnet.downstream_task.dpmon.run_hyperparameter_tuning") - def test_run_with_tune(self, mock_tune): + def test_run_with_tune(self, mock_tune, mock_standard): + warnings.filterwarnings("ignore") + best_config_df = pd.DataFrame([{ + "gnn_hidden_dim": 64, + "gnn_layer_num": 2, + "nn_hidden_dim1": 128, + "nn_hidden_dim2": 64, + "num_epochs": 10, + }]) + mock_tune.return_value = best_config_df + mock_standard.return_value = (pd.DataFrame({"Actual": [0, 1], "Predicted": [0, 1]}), 0.95) + dpmon = DPMON( adjacency_matrix=self.adjacency_matrix, omics_list=[self.omics_data1, self.omics_data2], phenotype_data=self.phenotype_data, - clinical_data=self.features_data, - model="GAT", + clinical_data=self.clinical_data, tune=True, gpu=False, + output_dir=self.tempfolder, ) - predictions = dpmon.run() + + df, score = dpmon.run() mock_tune.assert_called_once() - self.assertIsInstance(predictions, tuple) - self.assertIsInstance(predictions[0], pd.DataFrame) - self.assertIsInstance(predictions[1], float) + mock_standard.assert_called_once() + self.assertIsInstance(df, pd.DataFrame) + self.assertIsInstance(score, float) - def test_empty_clinical_data(self): + @patch("bioneuralnet.downstream_task.dpmon.run_standard_training") + def test_empty_clinical_data(self, mock_run_standard_training): + warnings.filterwarnings("ignore") + mock_run_standard_training.return_value = None empty_clinical = pd.DataFrame() - DPMON( + dpmon = DPMON( adjacency_matrix=self.adjacency_matrix, omics_list=[self.omics_data1, self.omics_data2], phenotype_data=self.phenotype_data, clinical_data=empty_clinical, - model="SAGE", tune=False, gpu=False, ) + predictions = dpmon.run() if __name__ == "__main__": + warnings.filterwarnings("ignore") unittest.main() diff --git a/tests/test_gnn_embedding.py b/tests/test_gnn_embedding.py index 6c1d5bc..3212576 100644 --- a/tests/test_gnn_embedding.py +++ b/tests/test_gnn_embedding.py @@ -7,33 +7,12 @@ class TestGNNEmbedding(unittest.TestCase): def setUp(self): - self.adjacency_matrix = pd.DataFrame( - { - "gene1": [1.0, 1.0, 0.0], - "gene2": [1.0, 1.0, 1.0], - "gene3": [0.0, 1.0, 1.0], - }, - index=["gene1", "gene2", "gene3"], - ) - - self.omics_data = pd.DataFrame( - {"gene1": [1, 2], "gene2": [3, 4], "gene3": [5, 6]}, - index=["sample1", "sample2"], - ) - - self.clinical_data = pd.DataFrame( - {"age": [30, 45], "bmi": [22.5, 28.0]}, index=["sample1", "sample2"] - ) - - self.phenotype_data = pd.DataFrame( - {"phenotype": [0, 1]}, index=["sample1", "sample2"] - ) + self.adjacency_matrix = pd.DataFrame([[1.0, 0.3, 0.1], [0.3, 1.0, 0.05], [0.1, 0.05, 1.0]],index=["gene1", "gene2", "gene3"],columns=["gene1", "gene2", "gene3"],) + self.omics_data = pd.DataFrame({"gene1": [1, 2], "gene2": [3, 4], "gene3": [5, 6]},index=["sample1", "sample2"]) + self.clinical_data = pd.DataFrame({"age": [30, 45], "bmi": [22.5, 28.0]}, index=["sample1", "sample2"]) + self.phenotype_data = pd.DataFrame({"phenotype": [0, 1]}, index=["sample1", "sample2"]) - @patch.object( - GNNEmbedding, - "embed", - return_value=torch.tensor([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]]), - ) + @patch.object(GNNEmbedding,"embed",return_value=torch.tensor([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])) def test_fit_and_embed_with_clinical(self, mock_embed): gnn = GNNEmbedding( adjacency_matrix=self.adjacency_matrix, @@ -41,15 +20,7 @@ def test_fit_and_embed_with_clinical(self, mock_embed): phenotype_data=self.phenotype_data, clinical_data=self.clinical_data, phenotype_col="phenotype", - model_type="GCN", - hidden_dim=2, - layer_num=2, - dropout=True, - num_epochs=10, - lr=1e-3, - weight_decay=1e-4, gpu=False, - seed=42, tune=False, ) gnn.fit() @@ -65,42 +36,27 @@ def test_embed_without_fit(self): phenotype_data=self.phenotype_data, clinical_data=self.clinical_data, phenotype_col="phenotype", - model_type="SAGE", - hidden_dim=2, - layer_num=2, - dropout=True, - num_epochs=10, - lr=1e-3, - weight_decay=1e-4, - gpu=False, - seed=42, - tune=False, ) with self.assertRaises(ValueError): gnn.embed() - def test_initialization_with_empty_clinical_data(self): + @patch.object(GNNEmbedding,"embed",return_value=torch.tensor([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])) + def test_fit_and_embed_without_clinical(self, mock_embed): empty_clinical = pd.DataFrame() - with self.assertRaises(ValueError) as cm: - gnn = GNNEmbedding( - adjacency_matrix=self.adjacency_matrix, - omics_data=self.omics_data, - phenotype_data=self.phenotype_data, - clinical_data=empty_clinical, - phenotype_col="phenotype", - model_type="GIN", - hidden_dim=2, - layer_num=2, - dropout=False, - num_epochs=10, - lr=1e-3, - weight_decay=1e-4, - gpu=False, - seed=42, - tune=False, - ) - self.assertEqual(str(cm.exception), "Clinical data cannot be empty.") + gnn = GNNEmbedding( + adjacency_matrix=self.adjacency_matrix, + omics_data=self.omics_data, + phenotype_data=self.phenotype_data, + clinical_data=empty_clinical, + phenotype_col="phenotype", + ) + gnn.fit() + embeddings = gnn.embed() + + mock_embed.assert_called_once() + self.assertIsInstance(embeddings, torch.Tensor) + self.assertEqual(embeddings.shape, (3, 2)) def test_empty_adjacency_matrix(self): empty_adj = pd.DataFrame() @@ -111,16 +67,6 @@ def test_empty_adjacency_matrix(self): phenotype_data=self.phenotype_data, clinical_data=self.clinical_data, phenotype_col="phenotype", - model_type="GCN", - hidden_dim=2, - layer_num=2, - dropout=True, - num_epochs=10, - lr=1e-3, - weight_decay=1e-4, - gpu=False, - seed=42, - tune=False, ) def test_empty_omics_data(self): @@ -132,16 +78,6 @@ def test_empty_omics_data(self): phenotype_data=self.phenotype_data, clinical_data=self.clinical_data, phenotype_col="phenotype", - model_type="GCN", - hidden_dim=2, - layer_num=2, - dropout=True, - num_epochs=10, - lr=1e-3, - weight_decay=1e-4, - gpu=False, - seed=42, - tune=False, ) def test_empty_phenotype_data(self): @@ -153,16 +89,6 @@ def test_empty_phenotype_data(self): phenotype_data=empty_pheno, clinical_data=self.clinical_data, phenotype_col="phenotype", - model_type="GCN", - hidden_dim=2, - layer_num=2, - dropout=True, - num_epochs=10, - lr=1e-3, - weight_decay=1e-4, - gpu=False, - seed=42, - tune=False, ) if __name__ == "__main__": diff --git a/tests/test_graph_gen.py b/tests/test_graph_gen.py new file mode 100644 index 0000000..5f5f0ec --- /dev/null +++ b/tests/test_graph_gen.py @@ -0,0 +1,104 @@ +import unittest +import pandas as pd +import numpy as np + +from bioneuralnet.utils.graph import gen_similarity_graph +from bioneuralnet.utils.graph import gen_correlation_graph +from bioneuralnet.utils.graph import gen_threshold_graph +from bioneuralnet.utils.graph import gen_gaussian_knn_graph +from bioneuralnet.utils.graph import gen_lasso_graph +from bioneuralnet.utils.graph import gen_mst_graph +from bioneuralnet.utils.graph import gen_snn_graph + +class TestGraphGeneration(unittest.TestCase): + def setUp(self): + # for testing we use a small dataframe, 3 rows (nodes) and 2 columns (features) each. + # distinct values so that results are deterministic + self.X = pd.DataFrame({"f1": [1.0, 2.0, 3.0], "f2": [4.0, 5.0, 6.0]}, index=["n1", "n2", "n3"]) + self.N = 3 + + def _basic_checks(self, G: pd.DataFrame): + """ + Shared checks for any generated graph: + + - Must be a DataFrame of shape (N, N) + - Rows sum to 1 (with small tolerance) + - Diagonal entries (self-loops) are > 0 + + """ + + self.assertIsInstance(G, pd.DataFrame) + self.assertEqual(G.shape, (self.N, self.N)) + row_sums = G.sum(axis=1).values + self.assertTrue(np.allclose(row_sums, np.ones(self.N), atol=1e-6)) + diag = np.diag(G.values) + self.assertTrue(np.all(diag > 0)) + + def test_gen_similarity_graph_default(self): + G = gen_similarity_graph(self.X) + + self._basic_checks(G) + self.assertEqual(set(G.index), set(self.X.index)) + self.assertEqual(set(G.columns), set(self.X.index)) + + def test_gen_similarity_graph_type_error(self): + with self.assertRaises(TypeError): + gen_similarity_graph([1, 2, 3]) + + def test_gen_correlation_graph_default(self): + G = gen_correlation_graph(self.X, k=2, method="pearson") + self._basic_checks(G) + self.assertTrue((G.values >= 0).all()) + + def test_gen_correlation_graph_type_error(self): + with self.assertRaises(TypeError): + gen_correlation_graph("not a df") + + def test_gen_threshold_graph_default(self): + G = gen_threshold_graph(self.X, b=2.0, k=2) + self._basic_checks(G) + self.assertTrue((G.values >= 0).all()) + + def test_gen_threshold_graph_type_error(self): + with self.assertRaises(TypeError): + gen_threshold_graph(None) + + def test_gen_gaussian_knn_graph_default(self): + G = gen_gaussian_knn_graph(self.X, k=2) + self._basic_checks(G) + self.assertTrue((G.values >= 0).all()) + + def test_gen_gaussian_knn_graph_type_error(self): + with self.assertRaises(TypeError): + gen_gaussian_knn_graph(123) + + def test_gen_lasso_graph_default(self): + G = gen_lasso_graph(self.X, alpha=0.01) + self._basic_checks(G) + self.assertTrue((G.values >= 0).all()) + + def test_gen_lasso_graph_type_error(self): + with self.assertRaises(TypeError): + gen_lasso_graph([1, 2, 3]) + + def test_gen_mst_graph_default(self): + G = gen_mst_graph(self.X) + self._basic_checks(G) + self.assertTrue((G.values >= 0).all()) + + def test_gen_mst_graph_type_error(self): + with self.assertRaises(TypeError): + gen_mst_graph(5.0) + + def test_gen_snn_graph_default(self): + G = gen_snn_graph(self.X, k=2) + self._basic_checks(G) + self.assertTrue((G.values >= 0).all()) + + def test_gen_snn_graph_type_error(self): + with self.assertRaises(TypeError): + gen_snn_graph("oops, not a df") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_hybrid_louvain.py b/tests/test_hybrid_louvain.py index 5e7d4d4..d7cffff 100644 --- a/tests/test_hybrid_louvain.py +++ b/tests/test_hybrid_louvain.py @@ -1,55 +1,73 @@ import unittest +from unittest.mock import patch, MagicMock import networkx as nx import pandas as pd -import numpy as np -from bioneuralnet.clustering import HybridLouvain +from bioneuralnet.clustering.hybrid_louvain import HybridLouvain class TestHybridLouvain(unittest.TestCase): def setUp(self): self.G = nx.Graph() - nodes = [] - nodes.append("a") - nodes.append("b") - nodes.append("c") - nodes.append("d") - nodes.append("e") - i = 0 - while i < len(nodes): - self.G.add_node(nodes[i]) - i = i + 1 - self.G.add_edge("a", "b", weight=1.0) - self.G.add_edge("b", "c", weight=1.0) - self.G.add_edge("c", "d", weight=1.0) - self.G.add_edge("d", "e", weight=1.0) - self.G.add_edge("e", "a", weight=1.0) - data = {} - i = 0 - while i < len(nodes): - col = nodes[i] - vals = [] - j = 0 - while j < 10: - vals.append(np.random.rand()) - j = j + 1 - data[col] = vals - i = i + 1 - self.B = pd.DataFrame(data) - phenos = [] - i = 0 - while i < 10: - phenos.append(np.random.rand()) - i = i + 1 - self.Y = pd.DataFrame({"phenotype": phenos}) - - def test_run(self): - hl = HybridLouvain(self.G, self.B, self.Y, k3=0.2, k4=0.8, max_iter=5, weight="weight", tune=False) - res = hl.run() - keys = [] - for key in res: - keys.append(key) - self.assertIn("curr", keys) - self.assertIn("clus", keys) - self.assertIsInstance(res["clus"], dict) + self.G.add_nodes_from(["a", "b", "c"]) + self.B = pd.DataFrame({"a": [1.0, 2.0],"b": [3.0, 4.0],"c": [5.0, 6.0],},index=["sample1", "sample2"],) + self.Y = pd.Series({"a": 0.0, "b": 1.0, "c": 2.0},name="phenotype") + + @patch("bioneuralnet.clustering.hybrid_louvain.CorrelatedLouvain",autospec=True) + @patch("bioneuralnet.clustering.hybrid_louvain.CorrelatedPageRank",autospec=True) + def test_run_returns_partition_and_clusters_dict(self,mock_page_rank_cls,mock_louvain_cls): + fake_louvain = MagicMock() + fake_louvain.run.return_value = {"a": 0, "b": 0, "c": 0} + fake_louvain.get_quality.return_value = 0.5 + + def fake_compute_corr(nodes): + return (0.7, None) + + fake_louvain._compute_community_correlation.side_effect = fake_compute_corr + mock_louvain_cls.return_value = fake_louvain + + fake_pagerank = MagicMock() + def fake_pr_run(best_seed): + return {"cluster_nodes": best_seed} + + fake_pagerank.run.side_effect = fake_pr_run + mock_page_rank_cls.return_value = fake_pagerank + + hybrid = HybridLouvain(G=self.G, B=self.B, Y=self.Y.to_frame()) + result = hybrid.run(as_dfs=False) + + expected_partition = {"a": 0, "b": 0, "c": 0} + self.assertIn("curr", result) + self.assertEqual(result["curr"], expected_partition) + + self.assertIn("clus", result) + self.assertEqual(set(result["clus"].keys()), {0}) + self.assertEqual(result["clus"][0], ["a", "b", "c"]) + + mock_louvain_cls.assert_called() + mock_page_rank_cls.assert_called() + + @patch("bioneuralnet.clustering.hybrid_louvain.CorrelatedLouvain",autospec=True) + @patch("bioneuralnet.clustering.hybrid_louvain.CorrelatedPageRank",autospec=True) + def test_run_as_dfs_returns_list_of_dataframes(self,mock_page_rank_cls,mock_louvain_cls): + fake_louvain = MagicMock() + fake_louvain.run.return_value = {"a": 0, "b": 0, "c": 0} + fake_louvain.get_quality.return_value = 0.5 + fake_louvain._compute_community_correlation.side_effect = lambda nodes: (0.7, None) + mock_louvain_cls.return_value = fake_louvain + + fake_pagerank = MagicMock() + fake_pagerank.run.side_effect = lambda best_seed: {"cluster_nodes": best_seed} + mock_page_rank_cls.return_value = fake_pagerank + + hybrid = HybridLouvain(G=self.G, B=self.B, Y=self.Y) + dfs_list = hybrid.run(as_dfs=True) + + self.assertIsInstance(dfs_list, list) + self.assertEqual(len(dfs_list), 1) + + df0 = dfs_list[0] + self.assertIsInstance(df0, pd.DataFrame) + self.assertEqual(set(df0.columns), {"a", "b", "c"}) + pd.testing.assert_frame_equal(df0, self.B.loc[:, ["a", "b", "c"]]) if __name__ == "__main__": unittest.main() diff --git a/tests/test_metrics.py b/tests/test_metrics.py deleted file mode 100644 index 4a9d562..0000000 --- a/tests/test_metrics.py +++ /dev/null @@ -1,127 +0,0 @@ -import unittest -import numpy as np -import pandas as pd -import matplotlib -matplotlib.use('Agg') -import matplotlib.pyplot as plt -from matplotlib.figure import Figure - -from bioneuralnet.metrics import omics_correlation, cluster_correlation, louvain_to_adjacency -from bioneuralnet.metrics import evaluate_rf -from bioneuralnet.metrics import plot_variance_distribution, plot_variance_by_feature, plot_performance, plot_embeddings, plot_network, compare_clusters - -class TestCorrelationFunctions(unittest.TestCase): - def test_omics_correlation_valid(self): - df_omics = pd.DataFrame(np.random.rand(100, 5), columns=["gene0", "gene1", "gene2", "gene3", "gene4"]) - df_pheno = pd.DataFrame(np.random.rand(100), columns=["phenotype"]) - corr, pval = omics_correlation(df_omics, df_pheno) - self.assertIsInstance(corr, float) - self.assertIsInstance(pval, float) - - def test_omics_correlation_empty(self): - df_omics = pd.DataFrame() - df_pheno = pd.DataFrame([1, 2, 3], columns=["phenotype"]) - with self.assertRaises(ValueError): - omics_correlation(df_omics, df_pheno) - - def test_omics_correlation_mismatched_length(self): - df_omics = pd.DataFrame(np.random.rand(10, 3)) - df_pheno = pd.DataFrame(np.random.rand(5), columns=["phenotype"]) - with self.assertRaises(ValueError): - omics_correlation(df_omics, df_pheno) - - def test_cluster_correlation_small_cluster(self): - df_cluster = pd.DataFrame({"A": [1, 2, 3, 4]}) - df_pheno = pd.DataFrame({"phenotype": [1, 2, 3, 4]}) - size, corr = cluster_correlation(df_cluster, df_pheno) - self.assertEqual(size, 1) - self.assertIsNone(corr) - - def test_louvain_to_adjacency(self): - df = pd.DataFrame({"A": [1, 2, 3], "B": [2, 3, 1], "C": [3, 1, 2]}) - adj = louvain_to_adjacency(df) - self.assertEqual(adj.shape, df.shape) - np.testing.assert_array_equal(np.diag(adj.values), np.zeros(adj.shape[0])) - -class TestEvaluationFunction(unittest.TestCase): - def test_evaluate_rf_regression(self): - from sklearn.datasets import make_regression - X, y = make_regression(n_samples=100, n_features=5, noise=0.1, random_state=0) - r2 = evaluate_rf(X, y, n=10, mode="regression") - self.assertIsInstance(r2, float) - self.assertTrue(-1.0 <= r2 <= 1.0) - - def test_evaluate_rf_classification(self): - from sklearn.datasets import make_classification - X, y = make_classification(n_samples=100, n_features=5, n_informative=3, random_state=0) - acc = evaluate_rf(X, y, n=10, mode="classification") - self.assertIsInstance(acc, float) - self.assertTrue(0.0 <= acc <= 1.0) - - def test_evaluate_rf_invalid_mode(self): - X = [[1]] - y = [1] - with self.assertRaises(ValueError): - evaluate_rf(X, y, mode="invalid") - -class TestPlotFunctions(unittest.TestCase): - def test_plot_variance_distribution(self): - df = pd.DataFrame({"A": np.random.rand(100), "B": np.random.rand(100)}) - fig = plot_variance_distribution(df, bins=10) - self.assertIsInstance(fig, Figure) - plt.close(fig) - - def test_plot_variance_by_feature(self): - df = pd.DataFrame({"A": np.random.rand(100), "B": np.random.rand(100)}) - fig = plot_variance_by_feature(df) - self.assertIsInstance(fig, Figure) - plt.close(fig) - - def test_plot_performance(self): - embedding_result = pd.DataFrame({"Actual": [0, 1, 1, 0], "Predicted": [0, 1, 0, 0]}) - raw_rf_acc = 0.75 - orig_show = plt.show - plt.show = lambda: None - try: - plot_performance(embedding_result, raw_rf_acc, title="Test Performance") - finally: - plt.show = orig_show - - def test_plot_embeddings(self): - embeddings = np.random.rand(50, 10) - orig_show = plt.show - plt.show = lambda: None - try: - plot_embeddings(embeddings) - finally: - plt.show = orig_show - - def test_plot_network(self): - df = pd.DataFrame({"A": [1, 0.5, 0.2], "B": [0.5, 1, 0.3], "C": [0.2, 0.3, 1]}, index=["A", "B", "C"]) - orig_show = plt.show - plt.show = lambda: None - try: - mapping_df = plot_network(df, weight_threshold=0.1, show_labels=False, show_edge_weights=False) - self.assertIsInstance(mapping_df, pd.DataFrame) - finally: - plt.show = orig_show - - def test_compare_clusters(self): - cluster1 = pd.DataFrame({"gene1": [1, 2, 3], "gene2": [2, 3, 4]}, index=[0, 1, 2]) - cluster2 = pd.DataFrame({"gene3": [1, 2, 3], "gene4": [3, 4, 5]}, index=[0, 1, 2]) - pheno = pd.DataFrame({"phenotype": [1, 2, 3]}) - omics_merged = pd.DataFrame({"gene3": [1, 2, 3], "gene4": [3, 4, 5]}, index=[0, 1, 2]) - orig_show = plt.show - plt.show = lambda: None - try: - df_results = compare_clusters([cluster1], [cluster2], pheno, omics_merged, label1="Test1", label2="Test2") - expected_cols = ["Cluster", "Louvain Size", "Louvain Correlation", "SMCCNET Size", "SMCCNET Correlation"] - i = 0 - while i < len(expected_cols): - self.assertIn(expected_cols[i], df_results.columns) - i = i + 1 - finally: - plt.show = orig_show - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_smccnet.py b/tests/test_smccnet.py deleted file mode 100644 index b1ef313..0000000 --- a/tests/test_smccnet.py +++ /dev/null @@ -1,137 +0,0 @@ -import unittest -from unittest.mock import patch, MagicMock -import pandas as pd -from bioneuralnet.external_tools import SmCCNet -import subprocess - -class TestSmCCNet(unittest.TestCase): - - def setUp(self): - self.phenotype_df = pd.DataFrame( - { - "ID": ["S1", "S2", "S3", "S4"], - "Phenotype": [0, 1, 0, 1], - } - ) - self.omics_df1 = pd.DataFrame( - { - "ID": ["S1", "S2", "S3", "S4"], - "GeneA": [1.2, 2.3, 3.1, 4.0], - "GeneB": [2.1, 3.4, 1.2, 3.3], - "GeneC": [3.3, 1.5, 2.2, 4.1], - } - ) - self.omics_df2 = pd.DataFrame( - { - "ID": ["S1", "S2", "S3", "S4"], - "ProtD": [4.2, 5.3, 6.1, 7.0], - "ProtE": [5.1, 6.4, 4.2, 6.3], - "ProtF": [6.3, 4.5, 5.2, 7.1], - } - ) - - self.omics_dfs = [self.omics_df1, self.omics_df2] - self.data_types = ["Genomics", "Proteomics"] - - @patch("bioneuralnet.external_tools.smccnet.pd.read_csv") - @patch("bioneuralnet.external_tools.smccnet.subprocess.run") - def test_smccnet_successful_run(self, mock_run, mock_read_csv): - mock_completed_process = MagicMock() - mock_completed_process.returncode = 0 - mock_run.return_value = mock_completed_process - - mock_adjacency = pd.DataFrame( - [ - [1.0, 0.8, 0.5, 0.2, 0.3, 0.4], - [0.8, 1.0, 0.6, 0.3, 0.4, 0.5], - [0.5, 0.6, 1.0, 0.4, 0.5, 0.6], - [0.2, 0.3, 0.4, 1.0, 0.7, 0.4], - [0.3, 0.4, 0.5, 0.7, 1.0, 0.5], - [0.4, 0.5, 0.6, 0.4, 0.5, 1.0], - ], - index=["GeneA", "GeneB", "GeneC", "ProtD", "ProtE", "ProtF"], - columns=["GeneA", "GeneB", "GeneC", "ProtD", "ProtE", "ProtF"], - ) - mock_read_csv.return_value = mock_adjacency - - smccnet = SmCCNet( - phenotype_df=self.phenotype_df, - omics_dfs=self.omics_dfs, - data_types=self.data_types, - kfold=5, - summarization="PCA", - seed=732, - ) - result = smccnet.run() - adjacency_matrix = result[0] - self.assertIsInstance(adjacency_matrix, pd.DataFrame) - self.assertIsInstance(adjacency_matrix, pd.DataFrame) - self.assertFalse(adjacency_matrix.isnull().values.any()) - self.assertEqual(adjacency_matrix.shape, (6, 6)) - self.assertListEqual( - list(adjacency_matrix.columns), - ["GeneA", "GeneB", "GeneC", "ProtD", "ProtE", "ProtF"], - ) - self.assertListEqual( - list(adjacency_matrix.index), - ["GeneA", "GeneB", "GeneC", "ProtD", "ProtE", "ProtF"], - ) - self.assertAlmostEqual(adjacency_matrix.loc["GeneA", "GeneB"], 0.8) - self.assertAlmostEqual(adjacency_matrix.loc["ProtD", "ProtF"], 0.4) - - @patch("bioneuralnet.external_tools.smccnet.subprocess.run") - def test_smccnet_run_failure(self, mock_run): - mock_run.side_effect = subprocess.CalledProcessError( - returncode=1, - cmd="Rscript SmCCNet.R", - stderr='Some Error"\nExecution halted', - ) - - smccnet = SmCCNet( - phenotype_df=self.phenotype_df, - omics_dfs=self.omics_dfs, - data_types=self.data_types, - kfold=5, - summarization="PCA", - seed=732, - ) - - with self.assertRaises(subprocess.CalledProcessError): - smccnet.run() - - def test_mismatched_omics_and_data_types(self): - with self.assertRaises(ValueError): - SmCCNet( - phenotype_df=self.phenotype_df, - omics_dfs=self.omics_dfs, - data_types=["Transcriptomics"], - kfold=5, - summarization="PCA", - seed=732, - ) - - @patch("bioneuralnet.external_tools.smccnet.subprocess.run") - def test_no_valid_samples(self, mock_run): - mock_run.side_effect = subprocess.CalledProcessError( - returncode=1, - cmd="Rscript SmCCNet.R", - stderr="Error: No valid samples after preprocessing.\nExecution halted", - ) - - self.omics_dfs[0].iloc[0, 1] = pd.NA - self.omics_dfs[1].iloc[1, 2] = pd.NA - - smccnet = SmCCNet( - phenotype_df=self.phenotype_df, - omics_dfs=self.omics_dfs, - data_types=self.data_types, - kfold=5, - summarization="PCA", - seed=732, - ) - - with self.assertRaises(subprocess.CalledProcessError): - smccnet.run() - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_subject_representation.py b/tests/test_subject_representation.py index a908550..c220072 100644 --- a/tests/test_subject_representation.py +++ b/tests/test_subject_representation.py @@ -5,45 +5,66 @@ class TestSubjectRepresentation(unittest.TestCase): def setUp(self): - self.omics_data = pd.DataFrame( - {"gene1": [1, 2, 3], "gene2": [4, 5, 6], "gene3": [7, 8, 9]}, - index=["sample1", "sample2", "sample3"], - ) - self.phenotype_data = pd.DataFrame( - {"phenotype": [0, 1, 2]}, index=["sample1", "sample2", "sample3"] - ) - self.precomputed_embeddings = pd.DataFrame( - {"dim1": [0.1, 0.2, 0.3], "dim2": [0.4, 0.5, 0.6], "dim3": [0.7, 0.8, 0.9]}, - index=["gene1", "gene2", "gene3"], - ) + self.omics_data = pd.DataFrame({"gene1": [1, 2, 3],"gene2": [4, 5, 6],"gene3": [7, 8, 9],},index=["sample1", "sample2", "sample3"],) + self.phenotype_data = pd.DataFrame({"phenotype": [0, 1, 2]}, index=["sample1", "sample2", "sample3"]) + self.precomputed_embeddings = pd.DataFrame({"dim1": [0.1, 0.2, 0.3],"dim2": [0.4, 0.5, 0.6],"dim3": [0.7, 0.8, 0.9],},index=["gene1", "gene2", "gene3"],) def test_run_with_precomputed_embeddings(self): - graph_embed = SubjectRepresentation( + sr = SubjectRepresentation( omics_data=self.omics_data, phenotype_data=self.phenotype_data, embeddings=self.precomputed_embeddings, reduce_method="PCA", tune=False, ) - enhanced_omics_data = graph_embed.run() - self.assertIsInstance(enhanced_omics_data, pd.DataFrame) - self.assertEqual(enhanced_omics_data.shape[0], 3) - - def test_integrate_embeddings(self): - node_embedding_values = pd.Series( - {"gene1": 0.5, "gene2": 0.6, "gene3": 0.7}, - index=["gene1", "gene2", "gene3"], - ) - with self.assertRaises(ValueError) as context: - graph_embed = SubjectRepresentation( + + enhanced = sr.run() + self.assertIsInstance(enhanced, pd.DataFrame) + self.assertEqual(enhanced.shape[0], 3) + self.assertEqual(set(enhanced.columns), {"gene1", "gene2", "gene3"}) + + def test_init_without_embeddings(self): + # Case A: embeddings=None + with self.assertRaises(ValueError): + SubjectRepresentation( omics_data=self.omics_data, phenotype_data=self.phenotype_data, embeddings=None, reduce_method="PCA", tune=False, ) - graph_embed.integrate_embeddings(node_embedding_values) - self.assertEqual(str(context.exception), "Embeddings must be provided as a pandas DataFrame.") + + with self.assertRaises(ValueError): + SubjectRepresentation( + omics_data=self.omics_data, + phenotype_data=self.phenotype_data, + embeddings=pd.DataFrame(), + reduce_method="PCA", + tune=False, + ) + + def test_init_with_wrong_embedding_type(self): + bad_embeddings = ["gene1", "gene2", "gene3"] + with self.assertRaises(ValueError): + SubjectRepresentation( + omics_data=self.omics_data, + phenotype_data=self.phenotype_data, + embeddings=bad_embeddings, + reduce_method="PCA", + tune=False, + ) + + def test_integrate_embeddings_wrong_type(self): + sr = SubjectRepresentation( + omics_data=self.omics_data, + phenotype_data=self.phenotype_data, + embeddings=self.precomputed_embeddings, + reduce_method="PCA", + tune=False, + ) + + with self.assertRaises(ValueError): + sr._integrate_embeddings(reduced=123, method="multiply", alpha=1.0, beta=0.5) if __name__ == "__main__": unittest.main() diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index be5be9c..0000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,48 +0,0 @@ -# import unittest -# import pandas as pd -# from bioneuralnet.utils.data_summary import ( -# network_remove_low_variance, -# network_remove_high_zero_fraction, -# network_filter, -# ) - -# class TestVarianceFunctions(unittest.TestCase): - -# def test_network_remove_low_variance(self): -# df = pd.DataFrame( -# {"A": [1, 1, 1], "B": [1, 2, 3], "C": [2, 3, 4]}, -# index=["A", "B", "C"], -# ) -# filtered = network_remove_low_variance(df, threshold=0.1) -# self.assertNotIn("A", filtered.columns) - -# def test_network_remove_high_zero_fraction(self): -# df = pd.DataFrame( -# {"A": [0, 0, 0], "B": [1, 2, 3], "C": [2, 3, 4]}, -# index=["A", "B", "C"], -# ) -# filtered = network_remove_high_zero_fraction(df, threshold=0.66) -# self.assertNotIn("A", filtered.columns) -# self.assertIn("B", filtered.columns) -# self.assertIn("C", filtered.columns) - -# def test_network_filter_variance(self): -# df = pd.DataFrame( -# {"A": [1, 1, 1], "B": [1, 2, 3], "C": [2, 3, 4]}, -# index=["A", "B", "C"], -# ) -# filtered = network_filter(df, threshold=0.1, filter_type="variance") -# self.assertNotIn("A", filtered.columns) - -# def test_network_filter_zero_fraction(self): -# df = pd.DataFrame( -# {"A": [0, 0, 0], "B": [1, 2, 3], "C": [2, 3, 4]}, -# index=["A", "B", "C"], -# ) -# filtered = network_filter(df, threshold=0.66, filter_type="zero_fraction") -# self.assertNotIn("A", filtered.columns) -# self.assertListEqual(list(filtered.columns), ["B", "C"]) - - -# if __name__ == "__main__": -# unittest.main() From eeb6ddba935a359fa5ba05976603e337efba6f5f Mon Sep 17 00:00:00 2001 From: Vicente Date: Tue, 3 Jun 2025 11:07:07 -0600 Subject: [PATCH 2/9] Added remaining unittests and re-enabled pre-commmit hooks in github actions --- .github/workflows/pre-commit.yml | 19 +- .github/workflows/python-app.yml | 15 +- .pre-commit-config.yaml | 12 +- .../downstream_task/subject_representation.py | 4 +- .../network_embedding/gnn_embedding.py | 2 +- tests/__init__.py | 7 +- tests/test_correlation_metrics.py | 85 ++++++++ tests/test_data_utils.py | 119 +++++++++++ tests/test_dataset_loader.py | 67 +++++++ tests/test_dpmon.py | 2 +- tests/test_graph_gen.py | 1 - tests/test_hybrid_louvain.py | 4 +- tests/test_preprocess.py | 188 ++++++++++++++++++ 13 files changed, 488 insertions(+), 37 deletions(-) create mode 100644 tests/test_correlation_metrics.py create mode 100644 tests/test_data_utils.py create mode 100644 tests/test_dataset_loader.py create mode 100644 tests/test_preprocess.py diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index cf3aa65..bb63b3d 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -42,6 +42,16 @@ jobs: pip install torch pip install torch_geometric shell: bash + + - name: Cache pre-commit hooks + uses: actions/cache@v3 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: pre-commit-${{ runner.os }}- + + - name: Run Pre-Commit Checks + run: pre-commit run --all-files --show-diff-on-failure # - name: Install R # uses: r-lib/actions/setup-r@v2 @@ -57,13 +67,4 @@ jobs: # Rscript -e "install.packages('WGCNA', repos='https://cran.r-project.org')" # shell: bash - - name: Cache pre-commit hooks - uses: actions/cache@v3 - with: - path: ~/.cache/pre-commit - key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} - restore-keys: pre-commit-${{ runner.os }}- - - name: Run Pre-Commit Checks - if: matrix.os != 'ubuntu-latest' - run: pre-commit run --all-files --show-diff-on-failure diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 5b79ee4..d6e5a30 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -40,7 +40,7 @@ jobs: pip install torch pip install torch_geometric shell: bash - + # - name: Install R # uses: r-lib/actions/setup-r@v2 # with: @@ -54,16 +54,3 @@ jobs: # Rscript -e "install.packages('SmCCNet', repos='https://cran.r-project.org')" # Rscript -e "install.packages('WGCNA', repos='https://cran.r-project.org')" # shell: bash - - # - name: Run tests with pytest - # run: | - # find . -name ".coverage*" -delete - # pytest --cov=bioneuralnet --cov-report=xml tests/ - - # - name: Upload coverage to Codecov - # uses: codecov/codecov-action@v3 - # with: - # token: ${{ secrets.CODECOV_TOKEN }} - # files: ./coverage.xml - # flags: unittests - # name: codecov-umbrella diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 082d203..828c6c1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - id: clean-coverage-files name: Remove stale .coverage files - entry: bash -c "find . -name '.coverage*' -delete" + entry: bash -c "find . -name '.coverage*' -exec rm -f {} + 2>/dev/null || true" language: system stages: [pre-commit] @@ -56,8 +56,8 @@ repos: language: system stages: [pre-commit] -# - id: run-tests -# name: Run Tests with Pytest -# entry: pytest --ignore=docs/source/examples --cov=bioneuralnet --cov-report=term-missing || true -# language: system -# types: [python] + - id: run-tests + name: Run Tests with Pytest + entry: bash -c "pytest tests --cov=bioneuralnet --cov-report=term-missing || true" + language: system + types: [python] diff --git a/bioneuralnet/downstream_task/subject_representation.py b/bioneuralnet/downstream_task/subject_representation.py index d0d1d74..6fd9954 100644 --- a/bioneuralnet/downstream_task/subject_representation.py +++ b/bioneuralnet/downstream_task/subject_representation.py @@ -61,7 +61,7 @@ def __init__( if omics_data is None or omics_data.empty: raise ValueError("Omics data must be non-empty.") - + if embeddings is None: self.logger.warning("No embeddings provided, please review documentation to see how to generate embeddings.") raise ValueError("Embeddings must be non-empty.") @@ -265,7 +265,7 @@ def _integrate_embeddings(self, reduced: pd.DataFrame, method="multiply", alpha= """ if not isinstance(reduced, (pd.DataFrame, pd.Series)): raise ValueError("Reduced embeddings must be a pandas DataFrame or Series.") - + missing_features = set(self.omics_data.columns) - set(reduced.index) if missing_features: self.logger.warning(f"Missing {len(missing_features)} features in reduced embeddings: {list(missing_features)[:5]}") diff --git a/bioneuralnet/network_embedding/gnn_embedding.py b/bioneuralnet/network_embedding/gnn_embedding.py index dde0d1f..f583685 100644 --- a/bioneuralnet/network_embedding/gnn_embedding.py +++ b/bioneuralnet/network_embedding/gnn_embedding.py @@ -91,7 +91,7 @@ def __init__( if clinical_data is not None and clinical_data.empty: self.logger.warning("Clinical data was provided but is empty, setting to None.") clinical_data = None - + if adjacency_matrix.shape[0] != adjacency_matrix.shape[1]: raise ValueError("Adjacency matrix must be square.") if not adjacency_matrix.index.equals(adjacency_matrix.columns): diff --git a/tests/__init__.py b/tests/__init__.py index 7c35f70..dffa5cb 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -2,9 +2,14 @@ from .test_subject_representation import * from .test_hybrid_louvain import * from .test_dpmon import * +from .test_graph_gen import * +from .test_preprocess import * +from .test_data_utils import * +from .test_correlation_metrics import * +from .test_dataset_loader import * #from .test_metrics import * #from .test_utils import * #from .test_smccnet import * #from .test_correlated_louvain import * -#from .test_correlated_pagerank import * \ No newline at end of file +#from .test_correlated_pagerank import * diff --git a/tests/test_correlation_metrics.py b/tests/test_correlation_metrics.py new file mode 100644 index 0000000..72b0579 --- /dev/null +++ b/tests/test_correlation_metrics.py @@ -0,0 +1,85 @@ +import unittest +import pandas as pd +import numpy as np +from scipy.stats import pearsonr + +from bioneuralnet.metrics import omics_correlation +from bioneuralnet.metrics import cluster_correlation +from bioneuralnet.metrics import louvain_to_adjacency + +class TestDataFunctions(unittest.TestCase): + def setUp(self): + self.omics = pd.DataFrame({ + "g1": [1.0, 2.0, 3.0, 4.0], + "g2": [2.0, 4.0, 6.0, 8.0] + }, index=["s1", "s2", "s3", "s4"]) + self.pheno = pd.DataFrame({"phen": [10.0, 20.0, 30.0, 40.0]}, index=["s1", "s2", "s3", "s4"]) + + def test_omics_correlation_valid(self): + corr, pval = omics_correlation(self.omics, self.pheno) + expected_pc1 = np.array([ -1.34164, -0.44721, 0.44721, 1.34164 ]) + expected_corr, _ = pearsonr(expected_pc1, self.pheno["phen"].values) + self.assertAlmostEqual(corr, expected_corr, places=5) + self.assertTrue(0 <= pval <= 1) + + def test_omics_correlation_empty(self): + empty = pd.DataFrame() + with self.assertRaises(ValueError): + omics_correlation(empty, self.pheno) + with self.assertRaises(ValueError): + omics_correlation(self.omics, pd.DataFrame()) + + def test_omics_correlation_mismatch(self): + pheno_short = pd.DataFrame({"phen": [1.0, 2.0]}, index=["s1", "s2"]) + with self.assertRaises(ValueError): + omics_correlation(self.omics, pheno_short) + + def test_cluster_correlation_size_one(self): + df = pd.DataFrame({"a": [1.0, 2.0, 3.0]}, index=["x", "y", "z"]) + size, corr = cluster_correlation(df, pd.DataFrame({"p": [1.0, 2.0, 3.0]}, index=["x", "y", "z"])) + self.assertEqual(size, 1) + self.assertIsNone(corr) + + def test_cluster_correlation_zero_variance(self): + df = pd.DataFrame({ + "c1": [1.0, 1.0, 1.0], + "c2": [2.0, 2.0, 2.0] + }, index=["i1", "i2", "i3"]) + size, corr = cluster_correlation(df, pd.DataFrame({"p": [1.0, 2.0, 3.0]}, index=["i1", "i2", "i3"])) + self.assertEqual(size, 2) + self.assertIsNone(corr) + + def test_cluster_correlation_insufficient_samples(self): + df = pd.DataFrame({ + "f1": [1.0, 2.0], + "f2": [2.0, 4.0] + }, index=["s1", "s2"]) + ph = pd.DataFrame({"p": [5.0, 10.0]}, index=["s1", "s2"]) + size, corr = cluster_correlation(df, ph) + self.assertEqual(size, 2) + self.assertIsNone(corr) + + def test_cluster_correlation_valid(self): + df = pd.DataFrame({ + "f1": [1.0, 2.0, 3.0, 4.0], + "f2": [2.0, 4.0, 6.0, 8.0] + }, index=["s1", "s2", "s3", "s4"]) + ph = pd.DataFrame({"p": [5.0, 10.0, 15.0, 20.0]}, index=["s1", "s2", "s3", "s4"]) + size, corr = cluster_correlation(df, ph) + self.assertEqual(size, 2) + self.assertAlmostEqual(corr, 1.0, places=5) + + def test_louvain_to_adjacency(self): + df = pd.DataFrame({ + "a": [1.0, 2.0, 3.0], + "b": [2.0, 4.0, 6.0], + "c": [1.0, 0.0, 1.0] + }, index=["x", "y", "z"]) + adj = louvain_to_adjacency(df) + self.assertEqual(set(adj.columns), {"a", "b", "c"}) + self.assertEqual(set(adj.index), {"a", "b", "c"}) + self.assertTrue((np.diag(adj.values) == 0).all()) + self.assertFalse(adj.isna().any().any()) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_data_utils.py b/tests/test_data_utils.py new file mode 100644 index 0000000..04c8703 --- /dev/null +++ b/tests/test_data_utils.py @@ -0,0 +1,119 @@ +import unittest +import pandas as pd +import numpy as np +import io +import sys +from bioneuralnet.utils.data import variance_summary +from bioneuralnet.utils.data import zero_fraction_summary +from bioneuralnet.utils.data import expression_summary +from bioneuralnet.utils.data import correlation_summary +from bioneuralnet.utils.data import explore_data_stats + +class TestDataUtils(unittest.TestCase): + def setUp(self): + self.df_var = pd.DataFrame({ + "A": [1.0, 3.0, 5.0], + "B": [2.0, 4.0, 6.0], + }) + self.df_zero = pd.DataFrame({ + "X": [0, 1, 2, 0], + "Y": [1, 1, 1, 1], + "Z": [0, 0, 0, 0], + }) + self.df_expr = pd.DataFrame({ + "P": [10.0, 20.0, 30.0], + "Q": [1.0, 1.0, 1.0], + "R": [0.0, 5.0, 0.0], + }) + self.df_corr = pd.DataFrame({ + "U": [1.0, 2.0, 3.0], + "V": [3.0, 2.0, 1.0], + "W": [1.0, 0.0, 1.0], + }) + + def test_variance_summary_no_threshold(self): + stats = variance_summary(self.df_var, low_var_threshold=None) + + self.assertAlmostEqual(stats["variance_mean"], 4.0) + self.assertAlmostEqual(stats["variance_median"], 4.0) + self.assertAlmostEqual(stats["variance_min"], 4.0) + self.assertAlmostEqual(stats["variance_max"], 4.0) + self.assertAlmostEqual(stats["variance_std"], 0.0) + + self.assertNotIn("num_low_variance_features", stats) + + def test_variance_summary_with_threshold(self): + stats = variance_summary(self.df_var, low_var_threshold=5.0) + + self.assertIn("num_low_variance_features", stats) + self.assertEqual(stats["num_low_variance_features"], 2) + + def test_zero_fraction_summary_no_threshold(self): + stats = zero_fraction_summary(self.df_zero, high_zero_threshold=None) + + self.assertAlmostEqual(stats["zero_fraction_mean"], 0.5) + self.assertAlmostEqual(stats["zero_fraction_median"], 0.5) + self.assertAlmostEqual(stats["zero_fraction_min"], 0.0) + self.assertAlmostEqual(stats["zero_fraction_max"], 1.0) + + expected_std = pd.Series([0.5, 0.0, 1.0]).std() + + self.assertAlmostEqual(stats["zero_fraction_std"], expected_std) + self.assertNotIn("num_high_zero_features", stats) + + def test_zero_fraction_summary_with_threshold(self): + stats = zero_fraction_summary(self.df_zero, high_zero_threshold=0.5) + + self.assertIn("num_high_zero_features", stats) + self.assertEqual(stats["num_high_zero_features"], 1) + + def test_expression_summary(self): + stats = expression_summary(self.df_expr) + mean_exprs = pd.Series({"P": 20.0, "Q": 1.0, "R": (0.0 + 5.0 + 0.0) / 3}) + + self.assertAlmostEqual(stats["expression_mean"], mean_exprs.mean()) + self.assertAlmostEqual(stats["expression_median"], mean_exprs.median()) + self.assertAlmostEqual(stats["expression_min"], mean_exprs.min()) + self.assertAlmostEqual(stats["expression_max"], mean_exprs.max()) + self.assertAlmostEqual(stats["expression_std"], mean_exprs.std()) + + def test_correlation_summary(self): + stats = correlation_summary(self.df_corr) + corr_abs = self.df_corr.corr().abs() + + np.fill_diagonal(corr_abs.values, 0.0) + max_corr = corr_abs.max() + + self.assertAlmostEqual(max_corr["U"], 1.0) + self.assertAlmostEqual(max_corr["V"], 1.0) + + self.assertAlmostEqual(stats["max_corr_mean"], max_corr.mean()) + self.assertAlmostEqual(stats["max_corr_median"], max_corr.median()) + self.assertAlmostEqual(stats["max_corr_min"], max_corr.min()) + self.assertAlmostEqual(stats["max_corr_max"], max_corr.max()) + self.assertAlmostEqual(stats["max_corr_std"], max_corr.std()) + + def test_explore_data_stats_prints_all_sections(self): + buf = io.StringIO() + old_stdout = sys.stdout + sys.stdout = buf + + try: + explore_data_stats(self.df_corr, name="TestDF") + finally: + sys.stdout = old_stdout + + output = buf.getvalue() + + self.assertIn("Statistics for TestDF:", output) + self.assertIn("Variance Summary:", output) + self.assertIn("Zero Fraction Summary:", output) + self.assertIn("Expression Summary:", output) + self.assertIn("Correlation Summary:", output) + self.assertIn("variance_mean", output) + self.assertIn("zero_fraction_mean", output) + self.assertIn("expression_mean", output) + self.assertIn("max_corr_mean", output) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_dataset_loader.py b/tests/test_dataset_loader.py new file mode 100644 index 0000000..7a6d0f4 --- /dev/null +++ b/tests/test_dataset_loader.py @@ -0,0 +1,67 @@ +import unittest +import pandas as pd +from pathlib import Path +from bioneuralnet.datasets.dataset_loader import DatasetLoader + +class TestDatasetLoader(unittest.TestCase): + def test_example1_loads(self): + loader = DatasetLoader("example1") + keys = set(loader.data.keys()) + self.assertEqual(keys, {"X1", "X2", "Y", "clinical_data"}) + + for df in loader.data.values(): + self.assertIsInstance(df, pd.DataFrame) + self.assertGreater(df.shape[0], 0) + self.assertGreater(df.shape[1], 0) + + for name, shape in loader.shape.items(): + self.assertIsInstance(shape, tuple) + self.assertEqual(len(shape), 2) + + def test_monet_loads(self): + loader = DatasetLoader("monet") + keys = set(loader.data.keys()) + + self.assertEqual(keys, {"gene_data", "mirna_data", "phenotype", "rppa_data", "clinical_data"}) + + for df in loader.data.values(): + self.assertIsInstance(df, pd.DataFrame) + self.assertGreater(df.shape[0], 0) + self.assertGreater(df.shape[1], 0) + + for name, shape in loader.shape.items(): + self.assertIsInstance(shape, tuple) + self.assertEqual(len(shape), 2) + + def test_brca_loads(self): + loader = DatasetLoader("brca") + keys = set(loader.data.keys()) + self.assertEqual(keys, {"mirna", "pam50", "clinical", "rna", "meth"}) + for df in loader.data.values(): + self.assertIsInstance(df, pd.DataFrame) + self.assertGreater(df.shape[0], 0) + self.assertGreater(df.shape[1], 0) + for name, shape in loader.shape.items(): + self.assertIsInstance(shape, tuple) + self.assertEqual(len(shape), 2) + + def test_invalid_folder_raises(self): + with self.assertRaises(FileNotFoundError): + DatasetLoader("nonexistent_folder") + + def test_unrecognized_name_raises(self): + base = Path(__file__).parent.parent / "bioneuralnet" / "datasets" + dummy = base / "dummy" + dummy.mkdir(exist_ok=True) + + (dummy / "placeholder.csv").write_text("a,b\n1,2") + + with self.assertRaises(ValueError): + DatasetLoader("dummy") + + for child in dummy.iterdir(): + child.unlink() + dummy.rmdir() + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_dpmon.py b/tests/test_dpmon.py index a1c14d5..fcdd8fa 100644 --- a/tests/test_dpmon.py +++ b/tests/test_dpmon.py @@ -21,7 +21,7 @@ def setUp(self): @patch("bioneuralnet.downstream_task.dpmon.run_standard_training") def test_run_without_tune(self, mock_standard): warnings.filterwarnings("ignore") - + mock_standard.return_value = (pd.DataFrame({"Actual": [2, 3], "Predicted": [2, 2]}, index=["sample1", "sample2"]), 0.89) dpmon = DPMON( diff --git a/tests/test_graph_gen.py b/tests/test_graph_gen.py index 5f5f0ec..e2e6c2b 100644 --- a/tests/test_graph_gen.py +++ b/tests/test_graph_gen.py @@ -99,6 +99,5 @@ def test_gen_snn_graph_type_error(self): with self.assertRaises(TypeError): gen_snn_graph("oops, not a df") - if __name__ == "__main__": unittest.main() diff --git a/tests/test_hybrid_louvain.py b/tests/test_hybrid_louvain.py index d7cffff..8cd8920 100644 --- a/tests/test_hybrid_louvain.py +++ b/tests/test_hybrid_louvain.py @@ -20,14 +20,14 @@ def test_run_returns_partition_and_clusters_dict(self,mock_page_rank_cls,mock_lo def fake_compute_corr(nodes): return (0.7, None) - + fake_louvain._compute_community_correlation.side_effect = fake_compute_corr mock_louvain_cls.return_value = fake_louvain fake_pagerank = MagicMock() def fake_pr_run(best_seed): return {"cluster_nodes": best_seed} - + fake_pagerank.run.side_effect = fake_pr_run mock_page_rank_cls.return_value = fake_pagerank diff --git a/tests/test_preprocess.py b/tests/test_preprocess.py new file mode 100644 index 0000000..1e96c3b --- /dev/null +++ b/tests/test_preprocess.py @@ -0,0 +1,188 @@ +import unittest +import pandas as pd +import numpy as np + +from bioneuralnet.utils.preprocess import preprocess_clinical +from bioneuralnet.utils.preprocess import clean_inf_nan +from bioneuralnet.utils.preprocess import select_top_k_variance +from bioneuralnet.utils.preprocess import select_top_k_correlation +from bioneuralnet.utils.preprocess import select_top_randomforest +from bioneuralnet.utils.preprocess import top_anova_f_features +from bioneuralnet.utils.preprocess import prune_network +from bioneuralnet.utils.preprocess import prune_network_by_quantile +from bioneuralnet.utils.preprocess import network_remove_low_variance +from bioneuralnet.utils.preprocess import network_remove_high_zero_fraction + +class TestPreprocessFunctions(unittest.TestCase): + def setUp(self): + self.clinical = pd.DataFrame({ + "age": [25, np.inf, 45, 60], + "bmi": [22.0, 28.5, np.nan, 30.0], + "gender": ["M", "F", None, "F"], + "smoker": [True, False, True, False], + "ignore": [1, 1, 1, 1] + }, index=["s1", "s2", "s3", "s4"]) + + self.y_clin = pd.Series([0, 1, 0, 1], index=["s1", "s2", "s3", "s4"]) + + self.X_small = pd.DataFrame({ + "f1": [1.0, 2.0, 3.0, 4.0], + "f2": [1.0, 1.0, 1.0, 1.0], + "f3": [4.0, 3.0, 2.0, 1.0], + "f4": [10.0, 20.0, 30.0, 40.0], + }, index=["i1", "i2", "i3", "i4"]) + + self.y_reg = pd.Series([0.1, 0.4, 0.5, 0.8], index=self.X_small.index) + + adj = np.array([ + [1.0, 0.2, 0.0, 0.8], + [0.2, 1.0, 0.1, 0.0], + [0.0, 0.1, 1.0, 0.05], + [0.8, 0.0, 0.05, 1.0], + ], dtype=float) + + self.adj_df = pd.DataFrame(adj, index=["a", "b", "c", "d"], columns=["a", "b", "c", "d"]) + + def test_clean_inf_nan_replaces_and_drops(self): + df = pd.DataFrame({"x": [1.0, np.inf, 3.0, -np.inf], "y": [np.nan, 2.0, np.nan, 4.0], "z": [5.0, 5.0, 5.0, 5.0]}) + cleaned = clean_inf_nan(df) + + self.assertTrue(np.allclose(cleaned["x"].values, [1.0, 2.0, 3.0, 2.0])) + self.assertTrue(np.allclose(cleaned["y"].values, [3.0, 2.0, 3.0, 4.0])) + + self.assertNotIn("z", cleaned.columns) + self.assertFalse(cleaned.isin([np.inf, -np.inf]).any().any()) + self.assertFalse(cleaned.isna().any().any()) + + def test_preprocess_clinical_basic(self): + result = preprocess_clinical(X=self.clinical,y=self.y_clin,top_k=2,scale=False,ignore_columns=["ignore"]) + + self.assertIsInstance(result, pd.DataFrame) + self.assertEqual(result.shape[1], 2) + self.assertNotIn("ignore", result.columns) + + def test_preprocess_clinical_errors(self): + bad_y = pd.DataFrame({"a": [1, 2], "b": [3, 4]}) + + with self.assertRaises(ValueError): + preprocess_clinical(self.clinical.iloc[:2, :], bad_y, top_k=1) + + with self.assertRaises(KeyError): + preprocess_clinical(self.clinical, self.y_clin, ignore_columns=["nonexistent"]) + + def test_select_top_k_variance(self): + top2 = select_top_k_variance(self.X_small, k=2) + + self.assertEqual(set(top2.columns), {"f4", "f1", "f3"}.intersection(set(top2.columns))) + self.assertEqual(top2.shape[1], 2) + self.assertTrue((top2.var(axis=0) > 0).all()) + + def test_select_top_k_correlation_unsupervised(self): + unsup = select_top_k_correlation(self.X_small, y=None, top_k=2) + + self.assertEqual(unsup.shape[1], 2) + self.assertNotIn("f2", unsup.columns) + + def test_select_top_k_correlation_supervised(self): + sup = select_top_k_correlation(self.X_small, y=self.y_reg, top_k=2) + + self.assertEqual(sup.shape[1], 2) + + bad_y = pd.DataFrame({"a": [1, 2], "b": [3, 4]}) + + with self.assertRaises(ValueError): + select_top_k_correlation(self.X_small.iloc[:2, :], bad_y, top_k=1) + + def test_select_top_randomforest(self): + X_num = pd.DataFrame({ + "a": [1, 2, 1, 2], + "b": [3, 3, 4, 4], + "c": [0, 1, 0, 1] + }, index=["p1", "p2", "p3", "p4"]) + + y_bin = pd.Series([0, 1, 0, 1], index=X_num.index) + top_rf = select_top_randomforest(X_num, y_bin, top_k=2, seed=0) + + self.assertEqual(top_rf.shape[1], 2) + + X_mixed = X_num.copy() + X_mixed["d"] = ["x", "y", "z", "w"] + + with self.assertRaises(ValueError): + select_top_randomforest(X_mixed, y_bin, top_k=1) + + def test_top_anova_f_features_classification(self): + X = pd.DataFrame({ + "f1": [0, 1, 0, 1], + "f2": [5, 5, 5, 5], + "f3": [2, 3, 2, 3] + }, index=["s1", "s2", "s3", "s4"]) + + y_bin = pd.Series([0, 1, 0, 1], index=X.index) + top_feats = top_anova_f_features(X, y_bin, max_features=2, alpha=0.05, task="classification") + + self.assertEqual(top_feats.shape[1], 2) + self.assertIn("f1", top_feats.columns) + + X_reg = pd.DataFrame({ + "f1": [1.0, 2.0, 3.0, 4.0], + "f2": [10.0, 20.0, 30.0, 40.0], + "f3": [0.1, 0.2, 0.3, 0.4] + }, index=["a", "b", "c", "d"]) + + y_cont = pd.Series([1.0, 2.0, 3.0, 4.0], index=X_reg.index) + top_feats_reg = top_anova_f_features(X_reg, y_cont, max_features=1, alpha=0.05, task="regression") + + self.assertEqual(top_feats_reg.shape[1], 1) + + with self.assertRaises(ValueError): + top_anova_f_features(X_reg, y_cont, max_features=1, alpha=0.05, task="invalid") + + def test_prune_network_threshold(self): + pruned = prune_network(self.adj_df, weight_threshold=0.15) + + self.assertEqual(set(pruned.index), set(self.adj_df.index)) + self.assertEqual(set(pruned.columns), set(self.adj_df.columns)) + + full = prune_network(self.adj_df, weight_threshold=0.0) + + self.assertEqual(set(full.index), set(self.adj_df.index)) + + def test_prune_network_by_quantile(self): + pruned_q = prune_network_by_quantile(self.adj_df, quantile=0.5) + + self.assertEqual(set(pruned_q.index), set(self.adj_df.index)) + + empty_adj = pd.DataFrame(np.zeros((3, 3)), index=["x", "y", "z"], columns=["x", "y", "z"]) + pruned_empty = prune_network_by_quantile(empty_adj, quantile=0.5) + + self.assertTrue(pruned_empty.equals(empty_adj)) + + def test_network_remove_low_variance(self): + net = pd.DataFrame({ + "n1": [1.0, 2.0, 1.0], + "n2": [1.0, 1.0, 1.0], + "n3": [3.0, 4.0, 5.0] + }, index=["n1", "n2", "n3"]) + + filtered = network_remove_low_variance(net, threshold=0.5) + + self.assertNotIn("n2", filtered.index) + self.assertNotIn("n2", filtered.columns) + self.assertEqual(set(filtered.index), {"n3"}) + + def test_network_remove_high_zero_fraction(self): + net = pd.DataFrame({ + "c1": [1.0, 0.5, 0.0], + "c2": [1.0, 0.0, 0.0], + "c3": [0.2, 0.2, 0.2] + }, index=["c1", "c2", "c3"]) + + filtered = network_remove_high_zero_fraction(net, threshold=0.5) + + self.assertNotIn("c2", filtered.index) + self.assertNotIn("c2", filtered.columns) + self.assertEqual(set(filtered.index), {"c1", "c3"}) + +if __name__ == "__main__": + unittest.main() From b0fbd6f99548faeffb53c371ab669fcb64e67d65 Mon Sep 17 00:00:00 2001 From: Vicente Date: Tue, 3 Jun 2025 11:12:14 -0600 Subject: [PATCH 3/9] Ran pre-commits before this push --- .github/workflows/pre-commit.yml | 4 +--- .github/workflows/python-app.yml | 2 +- tests/test_data_utils.py | 4 ++-- tests/test_dataset_loader.py | 8 ++++---- tests/test_preprocess.py | 2 +- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index bb63b3d..fea118f 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -42,7 +42,7 @@ jobs: pip install torch pip install torch_geometric shell: bash - + - name: Cache pre-commit hooks uses: actions/cache@v3 with: @@ -66,5 +66,3 @@ jobs: # Rscript -e "install.packages('SmCCNet', repos='https://cran.r-project.org')" # Rscript -e "install.packages('WGCNA', repos='https://cran.r-project.org')" # shell: bash - - diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index d6e5a30..95849d3 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -40,7 +40,7 @@ jobs: pip install torch pip install torch_geometric shell: bash - + # - name: Install R # uses: r-lib/actions/setup-r@v2 # with: diff --git a/tests/test_data_utils.py b/tests/test_data_utils.py index 04c8703..28c5536 100644 --- a/tests/test_data_utils.py +++ b/tests/test_data_utils.py @@ -39,7 +39,7 @@ def test_variance_summary_no_threshold(self): self.assertAlmostEqual(stats["variance_min"], 4.0) self.assertAlmostEqual(stats["variance_max"], 4.0) self.assertAlmostEqual(stats["variance_std"], 0.0) - + self.assertNotIn("num_low_variance_features", stats) def test_variance_summary_with_threshold(self): @@ -97,7 +97,7 @@ def test_explore_data_stats_prints_all_sections(self): buf = io.StringIO() old_stdout = sys.stdout sys.stdout = buf - + try: explore_data_stats(self.df_corr, name="TestDF") finally: diff --git a/tests/test_dataset_loader.py b/tests/test_dataset_loader.py index 7a6d0f4..6747f0b 100644 --- a/tests/test_dataset_loader.py +++ b/tests/test_dataset_loader.py @@ -8,12 +8,12 @@ def test_example1_loads(self): loader = DatasetLoader("example1") keys = set(loader.data.keys()) self.assertEqual(keys, {"X1", "X2", "Y", "clinical_data"}) - + for df in loader.data.values(): self.assertIsInstance(df, pd.DataFrame) self.assertGreater(df.shape[0], 0) self.assertGreater(df.shape[1], 0) - + for name, shape in loader.shape.items(): self.assertIsInstance(shape, tuple) self.assertEqual(len(shape), 2) @@ -23,12 +23,12 @@ def test_monet_loads(self): keys = set(loader.data.keys()) self.assertEqual(keys, {"gene_data", "mirna_data", "phenotype", "rppa_data", "clinical_data"}) - + for df in loader.data.values(): self.assertIsInstance(df, pd.DataFrame) self.assertGreater(df.shape[0], 0) self.assertGreater(df.shape[1], 0) - + for name, shape in loader.shape.items(): self.assertIsInstance(shape, tuple) self.assertEqual(len(shape), 2) diff --git a/tests/test_preprocess.py b/tests/test_preprocess.py index 1e96c3b..9942b40 100644 --- a/tests/test_preprocess.py +++ b/tests/test_preprocess.py @@ -22,7 +22,7 @@ def setUp(self): "smoker": [True, False, True, False], "ignore": [1, 1, 1, 1] }, index=["s1", "s2", "s3", "s4"]) - + self.y_clin = pd.Series([0, 1, 0, 1], index=["s1", "s2", "s3", "s4"]) self.X_small = pd.DataFrame({ From 9c4cace6fa458c1fcc0bfd0139893ae4d73d2625 Mon Sep 17 00:00:00 2001 From: Vicente Date: Tue, 3 Jun 2025 11:41:44 -0600 Subject: [PATCH 4/9] Optimized pre-commit and python-app actions to run tests in parallel and on check precommits once --- .github/workflows/pre-commit.yml | 39 +++++--------------------------- .github/workflows/python-app.yml | 3 +++ .pre-commit-config.yaml | 2 +- requirements-dev.txt | 4 ++-- tests/test_dpmon.py | 1 - 5 files changed, 12 insertions(+), 37 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index fea118f..46f4f48 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -12,8 +12,8 @@ jobs: pre-commit: strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.10", "3.11", "3.12"] + os: [ubuntu-latest] + python-version: ["3.10"] runs-on: ${{ matrix.os }} @@ -27,21 +27,11 @@ jobs: python-version: ${{ matrix.python-version }} check-latest: true - - name: Cache pip dependencies - uses: actions/cache@v3 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', '**/requirements-dev.txt') }} - restore-keys: ${{ runner.os }}-pip- - - - name: Install Python dependencies + - name: Install pre-commit run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -r requirements-dev.txt - pip install torch - pip install torch_geometric - shell: bash + pip install --upgrade pip + pip install pre-commit + pip install black flake8 mypy - name: Cache pre-commit hooks uses: actions/cache@v3 @@ -49,20 +39,3 @@ jobs: path: ~/.cache/pre-commit key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} restore-keys: pre-commit-${{ runner.os }}- - - - name: Run Pre-Commit Checks - run: pre-commit run --all-files --show-diff-on-failure - - # - name: Install R - # uses: r-lib/actions/setup-r@v2 - # with: - # r-version: "latest" - - # - name: Install R packages - # run: | - # Rscript -e "if (!requireNamespace('BiocManager', quietly = TRUE)) install.packages('BiocManager', repos='https://cran.r-project.org')" - # Rscript -e "install.packages(c('dplyr', 'jsonlite'), repos='https://cran.r-project.org')" - # Rscript -e "BiocManager::install(c('impute', 'preprocessCore', 'GO.db', 'AnnotationDbi'), update=FALSE, ask=FALSE)" - # Rscript -e "install.packages('SmCCNet', repos='https://cran.r-project.org')" - # Rscript -e "install.packages('WGCNA', repos='https://cran.r-project.org')" - # shell: bash diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 95849d3..b8e2236 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -41,6 +41,9 @@ jobs: pip install torch_geometric shell: bash + - name: Run pytest + run: pytest -n auto tests/ + # - name: Install R # uses: r-lib/actions/setup-r@v2 # with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 828c6c1..7502cf6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -58,6 +58,6 @@ repos: - id: run-tests name: Run Tests with Pytest - entry: bash -c "pytest tests --cov=bioneuralnet --cov-report=term-missing || true" + entry: bash -c "pytest tests" language: system types: [python] diff --git a/requirements-dev.txt b/requirements-dev.txt index 67c6656..846dfd5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -32,10 +32,10 @@ build #others ipywidgets -cptac +#cptac dtt tensorboardX debugpy PyYAML -node2vec +#node2vec wheel diff --git a/tests/test_dpmon.py b/tests/test_dpmon.py index fcdd8fa..a35554e 100644 --- a/tests/test_dpmon.py +++ b/tests/test_dpmon.py @@ -6,7 +6,6 @@ from bioneuralnet.downstream_task import DPMON import warnings - class TestDPMON(unittest.TestCase): def setUp(self): self.adjacency_matrix = pd.DataFrame([[1.0, 0.3, 0.1], [0.3, 1.0, 0.05], [0.1, 0.05, 1.0]],index=["gene1", "gene2", "gene3"],columns=["gene1", "gene2", "gene3"],) From ad9ee9c3468f83d164cf151efd774ce800a38f62 Mon Sep 17 00:00:00 2001 From: Vicente Date: Tue, 3 Jun 2025 11:45:09 -0600 Subject: [PATCH 5/9] removed auto from pyhthon-app.yml line 45 --- .github/workflows/python-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index b8e2236..c4f5090 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -42,7 +42,7 @@ jobs: shell: bash - name: Run pytest - run: pytest -n auto tests/ + run: pytest tests/ # - name: Install R # uses: r-lib/actions/setup-r@v2 From 1497c8424a7977d1d815619e27ddafb0a67042c0 Mon Sep 17 00:00:00 2001 From: Vicente Date: Tue, 3 Jun 2025 12:14:58 -0600 Subject: [PATCH 6/9] Release v1.0.9 --- CHANGELOG.md | 2 +- README.md | 2 +- bioneuralnet/__init__.py | 2 +- docs/jupyter_execute/Quick_Start.ipynb | 2 +- docs/source/Quick_Start.ipynb | 2 +- docs/source/conf.py | 2 +- setup.cfg | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a426520..b19115b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,7 +66,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/). - **Updated Tutorials and Documentation**: New end to end jupiter notebook example. - **Updated Test**: All test have been updated and new ones have been added. -## [1.0.1] to [1.0.8] - 2025-04-24 +## [1.0.1] to [1.0.9] - 2025-04-24 - **BUG**: A bug related to rdata files missing - **New realease**: A new release will include documentation for the other updates. (1.1.0) diff --git a/README.md b/README.md index eb7220f..5cf63c3 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Documentation](https://img.shields.io/badge/docs-read%20the%20docs-blue.svg)](https://bioneuralnet.readthedocs.io/en/latest/) -## Welcome to BioNeuralNet 1.0.8 +## Welcome to BioNeuralNet 1.0.9 ![BioNeuralNet Logo](assets/LOGO_WB.png) diff --git a/bioneuralnet/__init__.py b/bioneuralnet/__init__.py index 2f79194..17af74f 100644 --- a/bioneuralnet/__init__.py +++ b/bioneuralnet/__init__.py @@ -29,7 +29,7 @@ - `datasets`: Contains example (synthetic) datasets for testing and demonstration purposes. """ -__version__ = "1.0.8" +__version__ = "1.0.9" from .network_embedding import GNNEmbedding from .downstream_task import SubjectRepresentation diff --git a/docs/jupyter_execute/Quick_Start.ipynb b/docs/jupyter_execute/Quick_Start.ipynb index 076863b..827cc56 100644 --- a/docs/jupyter_execute/Quick_Start.ipynb +++ b/docs/jupyter_execute/Quick_Start.ipynb @@ -913,7 +913,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "BioNeuralNet version: 1.0.8\n" + "BioNeuralNet version: 1.0.9\n" ] } ], diff --git a/docs/source/Quick_Start.ipynb b/docs/source/Quick_Start.ipynb index a60a81f..5c93cfe 100644 --- a/docs/source/Quick_Start.ipynb +++ b/docs/source/Quick_Start.ipynb @@ -913,7 +913,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "BioNeuralNet version: 1.0.8\n" + "BioNeuralNet version: 1.0.9\n" ] } ], diff --git a/docs/source/conf.py b/docs/source/conf.py index 238088d..40953d5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -7,7 +7,7 @@ try: release = metadata.version("bioneuralnet") except metadata.PackageNotFoundError: - release = "1.0.8" + release = "1.0.9" project = "BioNeuralNet" version = release diff --git a/setup.cfg b/setup.cfg index cbfbadd..703b93e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = bioneuralnet -version = 1.0.8 +version = 1.0.9 author = Vicente Ramos author_email = vicente.ramos@ucdenver.edu description = A comprehensive framework for integrating multi-omics data with neural network embeddings. From ee52681f1424a408cb8b0f56ffc008c9d2352960 Mon Sep 17 00:00:00 2001 From: Vicente Date: Tue, 3 Jun 2025 12:48:09 -0600 Subject: [PATCH 7/9] Release v1.0.9 Github Action publish.yml added --- .github/workflows/publish.yml | 43 +++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..c794fb0 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,43 @@ +name: Publish + +on: + push: + tags: + - "v*.*.*" + +jobs: + build-and-publish: + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install build tools + run: | + python -m pip install --upgrade pip setuptools wheel twine + + - name: Build source & wheel + run: | + python setup.py sdist bdist_wheel + + - name: Publish to GitHub Packages + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + run: | + twine upload \ + --repository-url https://upload.pkg.github.com/${{ github.repository }} \ + dist/* + + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: | + twine upload dist/* From 30d04c47f4faa65a183613096a84cea81d6f77c9 Mon Sep 17 00:00:00 2001 From: Vicente Date: Tue, 3 Jun 2025 12:52:16 -0600 Subject: [PATCH 8/9] Release v1.0.9 Github Action publish.yml added --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c794fb0..784cd7a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,4 @@ -name: Publish +name: Publish to PyPI and GitHub Packages on: push: From 7cfc9f36e17abf80a6da6aa32793c90c98563692 Mon Sep 17 00:00:00 2001 From: Vicente Date: Tue, 3 Jun 2025 13:23:17 -0600 Subject: [PATCH 9/9] updated repo-url --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 784cd7a..5093b87 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -32,7 +32,7 @@ jobs: TWINE_PASSWORD: ${{ secrets.GITHUB_TOKEN }} run: | twine upload \ - --repository-url https://upload.pkg.github.com/${{ github.repository }} \ + --repository-url https://upload.pypi.github.io/UCD-BDLab/BioNeuralNet \ dist/* - name: Publish to PyPI