milopy 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .eggs/
milopy-0.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Lukas J. Haeuser
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
milopy-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.5
2
+ Name: milopy
3
+ Version: 0.3.0
4
+ Summary: A native Python implementation of miloR for differential abundance testing
5
+ Project-URL: Homepage, https://github.com/LuJoHae/milopy
6
+ Project-URL: Repository, https://github.com/LuJoHae/milopy.git
7
+ Author-email: LuJoHae <lukas.j.haeuser@gmail.com>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.9
11
+ Requires-Dist: anndata
12
+ Requires-Dist: edgepython
13
+ Requires-Dist: numpy
14
+ Requires-Dist: pandas
15
+ Requires-Dist: patsy
16
+ Requires-Dist: scanpy
17
+ Requires-Dist: scipy
18
+ Provides-Extra: test
19
+ Requires-Dist: anndata2ri; extra == 'test'
20
+ Requires-Dist: pytest; extra == 'test'
21
+ Requires-Dist: rpy2; extra == 'test'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # milopy
25
+
26
+ A native Python implementation of miloR for differential abundance testing of single-cell datasets.
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ pip install -e .
32
+ ```
milopy-0.3.0/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # milopy
2
+
3
+ A native Python implementation of miloR for differential abundance testing of single-cell datasets.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install -e .
9
+ ```
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "milopy"
7
+ version = "0.3.0"
8
+ description = "A native Python implementation of miloR for differential abundance testing"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "LuJoHae", email = "lukas.j.haeuser@gmail.com" }
14
+ ]
15
+ dependencies = [
16
+ "numpy",
17
+ "pandas",
18
+ "scipy",
19
+ "scanpy",
20
+ "anndata",
21
+ "edgepython",
22
+ "patsy"
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ test = [
27
+ "pytest",
28
+ "rpy2",
29
+ "anndata2ri"
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/LuJoHae/milopy"
34
+ Repository = "https://github.com/LuJoHae/milopy.git"
@@ -0,0 +1,114 @@
1
+ import matplotlib.pyplot as plt
2
+ import scanpy as sc
3
+ import numpy as np
4
+ import pandas as pd
5
+ from scipy.sparse import csr_matrix
6
+ import time
7
+ import os
8
+
9
+ # Generate synthetic dataset
10
+ np.random.seed(42)
11
+ n_cells = 3000
12
+ n_genes = 200
13
+ n_samples = 6
14
+
15
+ print("Generating synthetic data...")
16
+ # Base counts
17
+ counts = np.random.poisson(lam=1.5, size=(n_cells, n_genes))
18
+ X = csr_matrix(counts)
19
+
20
+ samples = np.random.choice([f"Sample_{i}" for i in range(n_samples)], size=n_cells)
21
+ conditions = np.array(["ConditionA" if int(s.split("_")[1]) < 3 else "ConditionB" for s in samples])
22
+
23
+ obs = pd.DataFrame({
24
+ "sample": samples,
25
+ "condition": conditions
26
+ })
27
+
28
+ adata = sc.AnnData(X=X, obs=obs)
29
+
30
+ # Let's add some artificial DA effect by modifying PCA coords for ConditionB
31
+ sc.pp.pca(adata, n_comps=30)
32
+
33
+ # Cells in ConditionB have a slight shift in PCA space to create DA
34
+ is_condB = adata.obs['condition'] == 'ConditionB'
35
+ adata.obsm['X_pca'][is_condB, 0] += 2.0
36
+
37
+ sc.pp.neighbors(adata, n_neighbors=30)
38
+ sc.tl.umap(adata)
39
+
40
+ print("Running milopy pipeline...")
41
+ import milopy
42
+ t0 = time.time()
43
+ milopy.build_graph(adata, k=30, d=30)
44
+ milopy.make_nhoods(adata, prop=0.1, k=30, d=30, random_state=42)
45
+ milopy.count_cells(adata, sample_col='sample')
46
+ milopy.calc_nhood_distance(adata, d=30)
47
+ design_df = pd.DataFrame({'condition': ['ConditionA', 'ConditionA', 'ConditionA', 'ConditionB', 'ConditionB', 'ConditionB']},
48
+ index=[f"Sample_{i}" for i in range(6)])
49
+ milopy.test_nhoods(adata, design='~ condition', design_df=design_df)
50
+ py_time = time.time() - t0
51
+ print(f"milopy took {py_time:.2f}s")
52
+
53
+ py_res = adata.uns['nhood_test_results']
54
+
55
+ print("Running miloR pipeline (via rpy2)...")
56
+ import rpy2.robjects as ro
57
+ from rpy2.robjects import pandas2ri
58
+ from rpy2.robjects import default_converter
59
+ from rpy2.robjects.conversion import localconverter
60
+ import anndata2ri
61
+ from rpy2.robjects.packages import importr
62
+
63
+ milor = importr('miloR')
64
+ scater = importr('scater')
65
+ base = importr('base')
66
+
67
+ adata_r = adata.copy()
68
+ adata_r.uns = {} # Clear uns to avoid anndata2ri conversion errors on nested dicts from scanpy PCA
69
+ t0 = time.time()
70
+ with localconverter(default_converter + pandas2ri.converter + anndata2ri.converter):
71
+ r_sce = ro.conversion.py2rpy(adata_r)
72
+ r_design_df = ro.conversion.py2rpy(design_df)
73
+
74
+ ro.globalenv['r_sce'] = r_sce
75
+ ro.globalenv['design_df'] = r_design_df
76
+
77
+ ro.r('''
78
+ library(miloR)
79
+ library(SingleCellExperiment)
80
+
81
+ milo <- Milo(r_sce)
82
+ milo <- buildGraph(milo, k=30, d=30, reduced.dim="PCA")
83
+ set.seed(42)
84
+ milo <- makeNhoods(milo, prop=0.1, k=30, d=30, refined=TRUE)
85
+ milo <- countCells(milo, meta.data=as.data.frame(colData(milo)), sample="sample")
86
+ milo <- calcNhoodDistance(milo, d=30, reduced.dim="PCA")
87
+
88
+ design_mat <- model.matrix(~ condition, data=design_df)
89
+ res <- testNhoods(milo, design=design_mat, design.df=design_df)
90
+ ''')
91
+ r_time = time.time() - t0
92
+ print(f"miloR took {r_time:.2f}s")
93
+
94
+ with localconverter(default_converter + pandas2ri.converter):
95
+ r_res = ro.conversion.rpy2py(ro.globalenv['res'])
96
+
97
+ print("Plotting comparison...")
98
+ fig, axes = plt.subplots(1, 2, figsize=(12, 5))
99
+
100
+ # Plot Py Results
101
+ axes[0].hist(py_res['logFC'], bins=30, alpha=0.7, color='blue', label='milopy')
102
+ axes[0].set_title('milopy logFC Distribution')
103
+ axes[0].set_xlabel('logFC')
104
+ axes[0].set_ylabel('Frequency')
105
+
106
+ # Plot R Results
107
+ axes[1].hist(r_res['logFC'], bins=30, alpha=0.7, color='red', label='miloR')
108
+ axes[1].set_title('miloR logFC Distribution')
109
+ axes[1].set_xlabel('logFC')
110
+ axes[1].set_ylabel('Frequency')
111
+
112
+ plt.tight_layout()
113
+ plt.savefig('/Users/halu/Code/milo/milo_comparison.png')
114
+ print("Saved comparison plot to /Users/halu/Code/milo/milo_comparison.png")
@@ -0,0 +1,122 @@
1
+ """
2
+ Correlation pipeline: Force R neighborhoods into milopy (now using edgepython)
3
+ and compare all 5 outputs.
4
+ """
5
+ import scanpy as sc
6
+ import numpy as np
7
+ import pandas as pd
8
+ from scipy.sparse import csr_matrix, csc_matrix
9
+ from scipy.stats import pearsonr
10
+
11
+ # Generate synthetic dataset
12
+ np.random.seed(42)
13
+ n_cells = 3000
14
+ n_genes = 200
15
+ n_samples = 6
16
+
17
+ print("=== Correlation Pipeline: milopy (edgepython) vs miloR (R) ===\n")
18
+
19
+ counts = np.random.poisson(lam=1.5, size=(n_cells, n_genes))
20
+ X = csr_matrix(counts)
21
+ samples = np.random.choice([f"Sample_{i}" for i in range(n_samples)], size=n_cells)
22
+ conditions = np.array(["ConditionA" if int(s.split("_")[1]) < 3 else "ConditionB" for s in samples])
23
+ obs = pd.DataFrame({"sample": samples, "condition": conditions})
24
+ adata = sc.AnnData(X=X, obs=obs)
25
+
26
+ sc.pp.pca(adata, n_comps=30)
27
+ is_condB = adata.obs['condition'] == 'ConditionB'
28
+ adata.obsm['X_pca'][is_condB, 0] += 2.0
29
+ sc.pp.neighbors(adata, n_neighbors=30)
30
+
31
+ # ---- Run miloR in R ----
32
+ print("Running miloR (R)...")
33
+ import rpy2.robjects as ro
34
+ from rpy2.robjects import pandas2ri, numpy2ri
35
+ from rpy2.robjects import default_converter
36
+ from rpy2.robjects.conversion import localconverter
37
+ import anndata2ri
38
+ from rpy2.robjects.packages import importr
39
+
40
+ milor = importr('miloR')
41
+
42
+ adata_r = adata.copy()
43
+ adata_r.uns = {}
44
+ with localconverter(default_converter + pandas2ri.converter + anndata2ri.converter):
45
+ r_sce = ro.conversion.py2rpy(adata_r)
46
+
47
+ design_df = pd.DataFrame(
48
+ {'condition': ['ConditionA']*3 + ['ConditionB']*3},
49
+ index=[f"Sample_{i}" for i in range(6)]
50
+ )
51
+ with localconverter(default_converter + pandas2ri.converter):
52
+ r_design_df = ro.conversion.py2rpy(design_df)
53
+
54
+ ro.globalenv['r_sce'] = r_sce
55
+ ro.globalenv['design_df'] = r_design_df
56
+
57
+ ro.r('''
58
+ library(miloR)
59
+ library(SingleCellExperiment)
60
+
61
+ milo <- Milo(r_sce)
62
+ milo <- buildGraph(milo, k=30, d=30, reduced.dim="PCA")
63
+ set.seed(42)
64
+ milo <- makeNhoods(milo, prop=0.1, k=30, d=30, refined=TRUE)
65
+ milo <- countCells(milo, meta.data=as.data.frame(colData(milo)), sample="sample")
66
+ milo <- calcNhoodDistance(milo, d=30, reduced.dim="PCA")
67
+
68
+ design_mat <- model.matrix(~ condition, data=design_df)
69
+ r_test_res <- testNhoods(milo, design=design_mat, design.df=design_df)
70
+
71
+ # Extract R artifacts
72
+ r_nhood_mat <- as.matrix(nhoods(milo))
73
+ r_nhood_counts_dense <- as.matrix(nhoodCounts(milo))
74
+ ''')
75
+
76
+ with localconverter(default_converter + pandas2ri.converter):
77
+ r_test_res = ro.conversion.rpy2py(ro.globalenv['r_test_res'])
78
+ with localconverter(default_converter + numpy2ri.converter):
79
+ r_nhood_mat = ro.conversion.rpy2py(ro.globalenv['r_nhood_mat'])
80
+ r_nhood_counts_dense = ro.conversion.rpy2py(ro.globalenv['r_nhood_counts_dense'])
81
+
82
+ print(f" R: {r_nhood_mat.shape[1]} neighborhoods")
83
+ print(f" R test columns: {list(r_test_res.columns)}")
84
+
85
+ # ---- Force R neighborhoods into milopy ----
86
+ print("\nForcing R neighborhoods into milopy pipeline...")
87
+ import milopy
88
+
89
+ adata_forced = adata.copy()
90
+ adata_forced.obsm['nhoods'] = csc_matrix(r_nhood_mat)
91
+ adata_forced.uns['nhood_indices'] = np.array([
92
+ np.where(r_nhood_mat[:, j] > 0)[0][0] for j in range(r_nhood_mat.shape[1])
93
+ ])
94
+
95
+ milopy.count_cells(adata_forced, sample_col='sample')
96
+ milopy.calc_nhood_distance(adata_forced, d=30)
97
+ milopy.test_nhoods(adata_forced, design='~ condition', design_df=design_df)
98
+
99
+ py_res = adata_forced.uns['nhood_test_results']
100
+ print(f" Py: {len(py_res)} neighborhoods")
101
+ print(f" Py test columns: {list(py_res.columns)}")
102
+
103
+ # ---- Compare ----
104
+ print("\n=== Correlations (forced same neighborhoods) ===")
105
+ metrics = ['logFC', 'logCPM', 'F', 'PValue', 'FDR']
106
+ for metric in metrics:
107
+ if metric in py_res.columns and metric in r_test_res.columns:
108
+ py_vals = py_res[metric].values
109
+ r_vals = r_test_res[metric].values
110
+ if len(py_vals) == len(r_vals):
111
+ corr, pval = pearsonr(py_vals, r_vals)
112
+ exact = np.allclose(py_vals, r_vals, atol=1e-10)
113
+ max_diff = np.max(np.abs(py_vals - r_vals))
114
+ print(f" {metric:8s}: r={corr:.6f}, exact={exact}, max_diff={max_diff:.2e}")
115
+ else:
116
+ print(f" {metric}: size mismatch {len(py_vals)} vs {len(r_vals)}")
117
+
118
+ # Also show first few rows side-by-side
119
+ print("\n--- R results (first 5) ---")
120
+ print(r_test_res[['logFC','logCPM','F','PValue','FDR']].head())
121
+ print("\n--- Py results (first 5) ---")
122
+ print(py_res[['logFC','logCPM','F','PValue','FDR']].head())
@@ -0,0 +1,208 @@
1
+ """
2
+ Diagnostic script: Force the SAME neighborhoods into both pipelines
3
+ to isolate whether divergence comes from:
4
+ (A) neighborhood sampling differences, or
5
+ (B) downstream logic (count_cells, calc_nhood_distance, test_nhoods)
6
+ """
7
+ import scanpy as sc
8
+ import numpy as np
9
+ import pandas as pd
10
+ from scipy.sparse import csr_matrix, csc_matrix
11
+ from scipy.stats import pearsonr
12
+ import time
13
+
14
+ # ---- Generate synthetic dataset (identical to before) ----
15
+ np.random.seed(42)
16
+ n_cells = 3000
17
+ n_genes = 200
18
+ n_samples = 6
19
+
20
+ print("=== DIAGNOSTIC: Isolating sources of divergence ===\n")
21
+
22
+ counts = np.random.poisson(lam=1.5, size=(n_cells, n_genes))
23
+ X = csr_matrix(counts)
24
+ samples = np.random.choice([f"Sample_{i}" for i in range(n_samples)], size=n_cells)
25
+ conditions = np.array(["ConditionA" if int(s.split("_")[1]) < 3 else "ConditionB" for s in samples])
26
+ obs = pd.DataFrame({"sample": samples, "condition": conditions})
27
+ adata = sc.AnnData(X=X, obs=obs)
28
+
29
+ sc.pp.pca(adata, n_comps=30)
30
+ is_condB = adata.obs['condition'] == 'ConditionB'
31
+ adata.obsm['X_pca'][is_condB, 0] += 2.0
32
+ sc.pp.neighbors(adata, n_neighbors=30)
33
+
34
+ # ---- Step 1: Run miloR to get its neighborhoods ----
35
+ print("Step 1: Running miloR to extract its neighborhoods...")
36
+ import rpy2.robjects as ro
37
+ from rpy2.robjects import pandas2ri, numpy2ri
38
+ from rpy2.robjects import default_converter
39
+ from rpy2.robjects.conversion import localconverter
40
+ import anndata2ri
41
+ from rpy2.robjects.packages import importr
42
+
43
+ milor = importr('miloR')
44
+ base = importr('base')
45
+ matrix_pkg = importr('Matrix')
46
+
47
+ adata_r = adata.copy()
48
+ adata_r.uns = {}
49
+ with localconverter(default_converter + pandas2ri.converter + anndata2ri.converter):
50
+ r_sce = ro.conversion.py2rpy(adata_r)
51
+
52
+ design_df = pd.DataFrame(
53
+ {'condition': ['ConditionA']*3 + ['ConditionB']*3},
54
+ index=[f"Sample_{i}" for i in range(6)]
55
+ )
56
+ with localconverter(default_converter + pandas2ri.converter):
57
+ r_design_df = ro.conversion.py2rpy(design_df)
58
+
59
+ ro.globalenv['r_sce'] = r_sce
60
+ ro.globalenv['design_df'] = r_design_df
61
+
62
+ ro.r('''
63
+ library(miloR)
64
+ library(SingleCellExperiment)
65
+
66
+ milo <- Milo(r_sce)
67
+ milo <- buildGraph(milo, k=30, d=30, reduced.dim="PCA")
68
+ set.seed(42)
69
+ milo <- makeNhoods(milo, prop=0.1, k=30, d=30, refined=TRUE)
70
+ milo <- countCells(milo, meta.data=as.data.frame(colData(milo)), sample="sample")
71
+ milo <- calcNhoodDistance(milo, d=30, reduced.dim="PCA")
72
+
73
+ design_mat <- model.matrix(~ condition, data=design_df)
74
+ r_test_res <- testNhoods(milo, design=design_mat, design.df=design_df)
75
+
76
+ # Extract R nhood matrix as dense
77
+ r_nhood_mat <- as.matrix(nhoods(milo))
78
+ r_nhood_counts <- nhoodCounts(milo)
79
+ r_nhood_counts_dense <- as.matrix(r_nhood_counts)
80
+
81
+ # Extract nhood distances
82
+ r_nhood_dists <- as.matrix(nhoodDistances(milo))
83
+ ''')
84
+
85
+ # Pull R results into Python
86
+ with localconverter(default_converter + pandas2ri.converter):
87
+ r_test_res = ro.conversion.rpy2py(ro.globalenv['r_test_res'])
88
+
89
+ with localconverter(default_converter + numpy2ri.converter):
90
+ r_nhood_mat = ro.conversion.rpy2py(ro.globalenv['r_nhood_mat'])
91
+ r_nhood_counts_dense = ro.conversion.rpy2py(ro.globalenv['r_nhood_counts_dense'])
92
+
93
+ r_n_nhoods = r_nhood_mat.shape[1]
94
+ print(f" R produced {r_n_nhoods} neighborhoods")
95
+ print(f" R nhood_mat shape: {r_nhood_mat.shape}")
96
+ print(f" R nhood_counts shape: {r_nhood_counts_dense.shape}")
97
+ print(f" R test_res shape: {r_test_res.shape}")
98
+ print(f" R test_res columns: {list(r_test_res.columns)}")
99
+ print(f" R test_res head:\n{r_test_res.head()}\n")
100
+
101
+ # ---- Step 2: Run milopy on same data ----
102
+ print("Step 2: Running milopy pipeline...")
103
+ import milopy
104
+ milopy.build_graph(adata, k=30, d=30)
105
+ milopy.make_nhoods(adata, prop=0.1, k=30, d=30, random_state=42)
106
+ milopy.count_cells(adata, sample_col='sample')
107
+ milopy.calc_nhood_distance(adata, d=30)
108
+ milopy.test_nhoods(adata, design='~ condition', design_df=design_df)
109
+
110
+ py_res = adata.uns['nhood_test_results']
111
+ py_nhood_mat = adata.obsm['nhoods']
112
+ if hasattr(py_nhood_mat, 'toarray'):
113
+ py_nhood_mat_dense = py_nhood_mat.toarray()
114
+ else:
115
+ py_nhood_mat_dense = py_nhood_mat
116
+ py_nhood_counts = adata.uns['nhood_counts']
117
+
118
+ py_n_nhoods = py_nhood_mat_dense.shape[1]
119
+ print(f" Py produced {py_n_nhoods} neighborhoods")
120
+ print(f" Py test_res columns: {list(py_res.columns)}")
121
+ print(f" Py test_res head:\n{py_res.head()}\n")
122
+
123
+ # ---- Step 3: Diagnose - Force R neighborhoods into Python pipeline ----
124
+ print("Step 3: Force R neighborhoods into Python pipeline and rerun downstream...")
125
+
126
+ adata_forced = adata.copy()
127
+ # Set the nhoods matrix to be the R nhoods
128
+ adata_forced.obsm['nhoods'] = csc_matrix(r_nhood_mat)
129
+ # Derive nhood indices from the R nhood matrix (index cells)
130
+ r_nhood_indices = []
131
+ for j in range(r_nhood_mat.shape[1]):
132
+ col = r_nhood_mat[:, j]
133
+ members = np.where(col > 0)[0]
134
+ r_nhood_indices.append(members[0]) # placeholder
135
+ r_nhood_indices = np.array(r_nhood_indices)
136
+ adata_forced.uns['nhood_indices'] = r_nhood_indices
137
+
138
+ # Rerun count_cells
139
+ milopy.count_cells(adata_forced, sample_col='sample')
140
+ py_forced_counts = adata_forced.uns['nhood_counts']
141
+
142
+ print(f" Forced Py nhood_counts shape: {py_forced_counts.shape}")
143
+ print(f" R nhood_counts shape: {r_nhood_counts_dense.shape}")
144
+
145
+ # Compare count matrices
146
+ # The R count matrix has samples as rows, nhoods as cols (or vice versa)
147
+ # Let's check
148
+ print(f"\n R counts sample (first 5x5):\n{r_nhood_counts_dense[:5, :5]}")
149
+ print(f" Py forced counts sample (first 5 rows):\n{py_forced_counts.head()}")
150
+
151
+ # Try to align: R nhood_counts might be samples x nhoods or nhoods x samples
152
+ # Our count_cells produces nhoods x samples
153
+ r_counts_df = pd.DataFrame(r_nhood_counts_dense)
154
+ print(f"\n R counts shape: {r_counts_df.shape}")
155
+ print(f" Py counts shape: {py_forced_counts.shape}")
156
+
157
+ # If R is samples x nhoods, transpose it
158
+ if r_counts_df.shape[0] == n_samples:
159
+ print(" R counts appears to be samples x nhoods, transposing...")
160
+ r_counts_df = r_counts_df.T
161
+
162
+ # Now both should be nhoods x samples
163
+ print(f" Aligned R counts shape: {r_counts_df.shape}")
164
+ print(f" Aligned Py counts shape: {py_forced_counts.shape}")
165
+
166
+ # Compare element-by-element
167
+ r_counts_vals = r_counts_df.values.flatten().astype(float)
168
+ py_counts_vals = py_forced_counts.values.flatten().astype(float)
169
+
170
+ if len(r_counts_vals) == len(py_counts_vals):
171
+ count_corr, count_p = pearsonr(r_counts_vals, py_counts_vals)
172
+ count_exact = np.allclose(r_counts_vals, py_counts_vals)
173
+ print(f"\n count_cells comparison (forced same nhoods):")
174
+ print(f" Pearson r = {count_corr:.6f}")
175
+ print(f" Exact match: {count_exact}")
176
+ print(f" Max abs diff: {np.max(np.abs(r_counts_vals - py_counts_vals))}")
177
+ else:
178
+ print(f" Cannot compare: different sizes {len(r_counts_vals)} vs {len(py_counts_vals)}")
179
+
180
+ # ---- Step 4: Run test_nhoods on the forced counts ----
181
+ print("\nStep 4: Running test_nhoods with forced R neighborhoods...")
182
+ milopy.calc_nhood_distance(adata_forced, d=30)
183
+ milopy.test_nhoods(adata_forced, design='~ condition', design_df=design_df)
184
+ py_forced_res = adata_forced.uns['nhood_test_results']
185
+
186
+ print(f" Py forced test_res shape: {py_forced_res.shape}")
187
+ print(f" R test_res shape: {r_test_res.shape}")
188
+
189
+ # Compare all 5 metrics
190
+ metrics = ['logFC', 'logCPM', 'F', 'PValue', 'FDR']
191
+ print(f"\n=== FINAL: Correlations with FORCED same neighborhoods ===")
192
+ for metric in metrics:
193
+ if metric in py_forced_res.columns and metric in r_test_res.columns:
194
+ py_vals = py_forced_res[metric].values
195
+ r_vals = r_test_res[metric].values
196
+ if len(py_vals) == len(r_vals):
197
+ corr, pval = pearsonr(py_vals, r_vals)
198
+ exact = np.allclose(py_vals, r_vals, atol=1e-10)
199
+ max_diff = np.max(np.abs(py_vals - r_vals))
200
+ print(f" {metric}: r={corr:.6f}, exact={exact}, max_diff={max_diff:.2e}")
201
+ else:
202
+ print(f" {metric}: size mismatch {len(py_vals)} vs {len(r_vals)}")
203
+ else:
204
+ print(f" {metric}: missing from one or both results")
205
+
206
+ # Also check if the issue is the SpatialFDR
207
+ if 'SpatialFDR' in r_test_res.columns:
208
+ print(f"\n NOTE: R has 'SpatialFDR' column. Columns in R result: {list(r_test_res.columns)}")
@@ -0,0 +1,21 @@
1
+ from .core import (
2
+ build_graph,
3
+ make_nhoods,
4
+ count_cells,
5
+ calc_nhood_distance,
6
+ test_nhoods,
7
+ )
8
+ from .meta import (
9
+ test_nhoods_meta,
10
+ test_nhoods_mixed,
11
+ )
12
+
13
+ __all__ = [
14
+ "build_graph",
15
+ "make_nhoods",
16
+ "count_cells",
17
+ "calc_nhood_distance",
18
+ "test_nhoods",
19
+ "test_nhoods_meta",
20
+ "test_nhoods_mixed",
21
+ ]