Skip to content

Commit 47eb98a

Browse files
committed
Update tests
1 parent ec90ca3 commit 47eb98a

7 files changed

Lines changed: 216 additions & 361 deletions

tests/test_DR_learner.py

Lines changed: 48 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,74 +3,63 @@
33
import pytest
44
from causarray.DR_learner import LFC
55

6-
# package/tests/test_DR_learner.py
7-
86

97
@pytest.fixture
108
def sample_data():
11-
np.random.seed(0)
12-
Y = np.random.poisson(5, (100, 10))
13-
W = np.random.normal(0, 1, (100, 5))
14-
A = np.random.binomial(1, 0.5, 100)
15-
Y += np.random.poisson(1, (100, 10)) * A[:, None]
9+
rng = np.random.default_rng(0)
10+
Y = rng.poisson(5, (100, 10)).astype(float)
11+
W = rng.standard_normal((100, 5))
12+
A = rng.binomial(1, 0.5, 100)
13+
Y += rng.poisson(1, (100, 10)) * A[:, None]
1614
return Y, W, A
1715

18-
def test_LFC_basic(sample_data):
19-
Y, W, A = sample_data
20-
result, estimation = LFC(Y, W, A)
21-
assert isinstance(result, pd.DataFrame)
22-
assert 'tau' in result.columns
23-
assert 'std' in result.columns
24-
assert 'stat' in result.columns
25-
assert 'rej' in result.columns
26-
assert 'pvalue' in result.columns
27-
assert 'padj' in result.columns
28-
assert 'pvalue_emp_null_adj' in result.columns
29-
assert 'padj_emp_null_adj' in result.columns
3016

31-
def test_LFC_with_offset(sample_data):
32-
Y, W, A = sample_data
33-
offset = np.log(np.random.poisson(5, 100))
34-
result, estimation = LFC(Y, W, A, offset=offset)
35-
assert isinstance(result, pd.DataFrame)
36-
assert 'tau' in result.columns
17+
class TestLFCOutputSchema:
18+
def test_output_columns(self, sample_data):
19+
Y, W, A = sample_data
20+
result, estimation = LFC(Y, W, A)
21+
assert isinstance(result, pd.DataFrame)
22+
for col in ('tau', 'std', 'stat', 'rej', 'pvalue', 'padj',
23+
'pvalue_emp_null_adj', 'padj_emp_null_adj'):
24+
assert col in result.columns
3725

38-
# def test_LFC_with_cross_est(sample_data):
39-
# Y, W, A = sample_data
40-
# result, estimation = LFC(Y, W, A, cross_est=True)
41-
# assert isinstance(result, pd.DataFrame)
42-
# assert 'tau' in result.columns
26+
def test_with_offset(self, sample_data):
27+
Y, W, A = sample_data
28+
rng = np.random.default_rng(1)
29+
offset = np.log(rng.poisson(5, 100).clip(1))
30+
result, estimation = LFC(Y, W, A, offset=offset)
31+
assert isinstance(result, pd.DataFrame)
32+
assert 'tau' in result.columns
4333

44-
def test_LFC_with_fdx(sample_data):
45-
Y, W, A = sample_data
46-
result, estimation = LFC(Y, W, A, fdx=True)
47-
assert isinstance(result, pd.DataFrame)
48-
assert 'tau' in result.columns
34+
def test_with_fdx(self, sample_data):
35+
Y, W, A = sample_data
36+
result, estimation = LFC(Y, W, A, fdx=True)
37+
assert isinstance(result, pd.DataFrame)
38+
assert 'tau' in result.columns
4939

50-
def test_LFC_with_custom_family(sample_data):
51-
Y, W, A = sample_data
52-
result, estimation = LFC(Y, W, A, family='poisson')
53-
assert isinstance(result, pd.DataFrame)
54-
assert 'tau' in result.columns
40+
def test_with_custom_family(self, sample_data):
41+
Y, W, A = sample_data
42+
result, estimation = LFC(Y, W, A, family='poisson')
43+
assert isinstance(result, pd.DataFrame)
44+
assert 'tau' in result.columns
5545

5646

57-
def test_LFC_multi_treatment():
58-
"""Test LFC with multiple perturbations (exercises per-perturbation fast path)."""
59-
np.random.seed(42)
60-
n, p, a = 200, 50, 3
61-
W = np.random.normal(0, 1, (n, 3))
62-
# One-hot treatment: first 50 cells = control, rest split among 3 perturbations
63-
A = np.zeros((n, a))
64-
A[50:100, 0] = 1
65-
A[100:150, 1] = 1
66-
A[150:200, 2] = 1
67-
Y = np.random.poisson(5, (n, p))
68-
# Add treatment effects for perturbation 0
69-
Y[50:100] += np.random.poisson(2, (50, p))
47+
class TestLFCMultiTreatment:
48+
def test_multi_treatment(self):
49+
"""LFC with multiple perturbations exercises the per-perturbation fast path."""
50+
np.random.seed(42)
51+
n, p, a = 200, 50, 3
52+
W = np.random.normal(0, 1, (n, 3))
53+
A = np.zeros((n, a))
54+
A[50:100, 0] = 1
55+
A[100:150, 1] = 1
56+
A[150:200, 2] = 1
57+
Y = np.random.poisson(5, (n, p))
58+
Y[50:100] += np.random.poisson(2, (50, p))
7059

71-
result, estimation = LFC(Y, W, A, family='nb')
72-
assert isinstance(result, pd.DataFrame)
73-
assert 'trt' in result.columns
74-
assert len(result) == p * a
75-
assert result['tau'].notna().all()
76-
assert result['padj'].notna().all()
60+
result, estimation = LFC(Y, W, A, family='nb')
61+
assert isinstance(result, pd.DataFrame)
62+
assert 'trt' in result.columns
63+
assert len(result) == p * a
64+
assert result['tau'].notna().all()
65+
assert result['padj'].notna().all()

tests/test_batch_fitting.py

Lines changed: 36 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -48,52 +48,44 @@ def _make_data(n=200, p=40, a=6, intercept=True, seed=0, family='nb'):
4848

4949

5050
# ---------------------------------------------------------------------------
51-
# Tests: subsample_ctrl_cells
51+
# Tests: subsample utilities
5252
# ---------------------------------------------------------------------------
5353

54-
def test_subsample_ctrl_cells_smaller_pool():
55-
ctrl_idx = np.arange(500)
56-
result = subsample_ctrl_cells(ctrl_idx, n_ctrl=2000, random_state=0)
57-
np.testing.assert_array_equal(result, ctrl_idx)
58-
59-
60-
def test_subsample_ctrl_cells_larger_pool():
61-
ctrl_idx = np.arange(5000)
62-
result = subsample_ctrl_cells(ctrl_idx, n_ctrl=2000, random_state=0)
63-
assert result.shape == (2000,)
64-
assert np.all(result[:-1] < result[1:]), "Result should be sorted"
65-
assert np.all(np.isin(result, ctrl_idx)), "All indices should be in ctrl_idx"
66-
67-
68-
def test_subsample_ctrl_cells_reproducible():
69-
ctrl_idx = np.arange(5000)
70-
r1 = subsample_ctrl_cells(ctrl_idx, n_ctrl=1000, random_state=42)
71-
r2 = subsample_ctrl_cells(ctrl_idx, n_ctrl=1000, random_state=42)
72-
np.testing.assert_array_equal(r1, r2)
73-
74-
75-
# ---------------------------------------------------------------------------
76-
# Tests: subsample_pert_cells
77-
# ---------------------------------------------------------------------------
78-
79-
def test_subsample_pert_cells_within_budget():
80-
pert_idx = np.arange(100)
81-
result = subsample_pert_cells(pert_idx, max_cells=2000)
82-
np.testing.assert_array_equal(result, pert_idx)
83-
84-
85-
def test_subsample_pert_cells_over_budget():
86-
pert_idx = np.arange(4000)
87-
result = subsample_pert_cells(pert_idx, max_cells=2000)
88-
assert len(result) == 2000
89-
assert np.all(result[:-1] < result[1:])
90-
91-
92-
def test_subsample_pert_cells_no_cap():
93-
"""max_cells=None keeps all pert cells."""
94-
pert_idx = np.arange(5000)
95-
result = subsample_pert_cells(pert_idx, max_cells=None)
96-
np.testing.assert_array_equal(result, pert_idx)
54+
class TestSubsampleUtils:
55+
def test_ctrl_cells_smaller_pool(self):
56+
ctrl_idx = np.arange(500)
57+
result = subsample_ctrl_cells(ctrl_idx, n_ctrl=2000, random_state=0)
58+
np.testing.assert_array_equal(result, ctrl_idx)
59+
60+
def test_ctrl_cells_larger_pool(self):
61+
ctrl_idx = np.arange(5000)
62+
result = subsample_ctrl_cells(ctrl_idx, n_ctrl=2000, random_state=0)
63+
assert result.shape == (2000,)
64+
assert np.all(result[:-1] < result[1:]), "Result should be sorted"
65+
assert np.all(np.isin(result, ctrl_idx)), "All indices should be in ctrl_idx"
66+
67+
def test_ctrl_cells_reproducible(self):
68+
ctrl_idx = np.arange(5000)
69+
r1 = subsample_ctrl_cells(ctrl_idx, n_ctrl=1000, random_state=42)
70+
r2 = subsample_ctrl_cells(ctrl_idx, n_ctrl=1000, random_state=42)
71+
np.testing.assert_array_equal(r1, r2)
72+
73+
def test_pert_cells_within_budget(self):
74+
pert_idx = np.arange(100)
75+
result = subsample_pert_cells(pert_idx, max_cells=2000)
76+
np.testing.assert_array_equal(result, pert_idx)
77+
78+
def test_pert_cells_over_budget(self):
79+
pert_idx = np.arange(4000)
80+
result = subsample_pert_cells(pert_idx, max_cells=2000)
81+
assert len(result) == 2000
82+
assert np.all(result[:-1] < result[1:])
83+
84+
def test_pert_cells_no_cap(self):
85+
"""max_cells=None keeps all pert cells."""
86+
pert_idx = np.arange(5000)
87+
result = subsample_pert_cells(pert_idx, max_cells=None)
88+
np.testing.assert_array_equal(result, pert_idx)
9789

9890

9991
# ---------------------------------------------------------------------------

tests/test_fit_glm_numpy.py

Lines changed: 0 additions & 64 deletions
This file was deleted.

tests/test_gcate.py

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -96,37 +96,33 @@ def test_fit_gcate_near_singular_X():
9696

9797

9898
# ---------------------------------------------------------------------------
99-
# G10 – backend toggle propagates through gcate (module-qualified import)
99+
# Backend integration via fit_gcate
100100
# ---------------------------------------------------------------------------
101101

102-
def test_g10_backend_toggle_propagates_through_fit_gcate():
103-
"""_USE_FAST_BACKEND=False must cause fit_gcate to use statsmodels path."""
104-
np.random.seed(0)
105-
n, p = 60, 20
106-
Y = np.random.negative_binomial(5, 0.5, (n, p))
107-
X = np.column_stack([np.ones(n), np.random.randn(n)])
108-
A = np.random.binomial(1, 0.3, (n, 1)).astype(float)
109-
110-
with gcate_glm._backend_override("original"):
111-
with patch.object(gcate_glm, "fit_glm", wraps=gcate_glm.fit_glm) as mock_fg:
112-
fit_gcate(Y, X, A, r=1, family="nb", offset=False, backend="original")
113-
assert mock_fg.called, (
114-
"fit_glm (statsmodels) was not called despite backend='original'"
115-
)
116-
117-
118-
# ---------------------------------------------------------------------------
119-
# G9 – backend parameter accepted without error
120-
# ---------------------------------------------------------------------------
121-
122-
def test_g9_fit_gcate_backend_original_small():
123-
"""fit_gcate(..., backend='original') must complete and return dicts."""
124-
np.random.seed(1)
125-
n, p = 60, 20
126-
Y = np.random.negative_binomial(5, 0.5, (n, p))
127-
X = np.column_stack([np.ones(n), np.random.randn(n)])
128-
A = np.random.binomial(1, 0.3, (n, 1)).astype(float)
129-
130-
res_1, res_2 = fit_gcate(Y, X, A, r=1, family="nb", offset=False, backend="original")
131-
assert isinstance(res_1, dict)
132-
assert isinstance(res_2, dict)
102+
class TestBackendIntegration:
103+
def test_backend_toggle_propagates_through_fit_gcate(self):
104+
"""_USE_FAST_BACKEND=False must cause fit_gcate to use statsmodels path."""
105+
np.random.seed(0)
106+
n, p = 60, 20
107+
Y = np.random.negative_binomial(5, 0.5, (n, p))
108+
X = np.column_stack([np.ones(n), np.random.randn(n)])
109+
A = np.random.binomial(1, 0.3, (n, 1)).astype(float)
110+
111+
with gcate_glm._backend_override("original"):
112+
with patch.object(gcate_glm, "fit_glm", wraps=gcate_glm.fit_glm) as mock_fg:
113+
fit_gcate(Y, X, A, r=1, family="nb", offset=False, backend="original")
114+
assert mock_fg.called, (
115+
"fit_glm (statsmodels) was not called despite backend='original'"
116+
)
117+
118+
def test_backend_original_param_accepted(self):
119+
"""fit_gcate(..., backend='original') must complete and return dicts."""
120+
np.random.seed(1)
121+
n, p = 60, 20
122+
Y = np.random.negative_binomial(5, 0.5, (n, p))
123+
X = np.column_stack([np.ones(n), np.random.randn(n)])
124+
A = np.random.binomial(1, 0.3, (n, 1)).astype(float)
125+
126+
res_1, res_2 = fit_gcate(Y, X, A, r=1, family="nb", offset=False, backend="original")
127+
assert isinstance(res_1, dict)
128+
assert isinstance(res_2, dict)

tests/test_gcate_convergence.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
"""
2-
Unit tests for GCATE confounder estimation quality.
3-
4-
Tests T1–T6 use simulated NB/Poisson data with known latent structure so
5-
that confounder recovery can be evaluated objectively.
6-
"""
1+
"""Unit tests for GCATE confounder estimation quality using simulated NB/Poisson data with known latent structure."""
72

83
import numpy as np
94
import pytest

0 commit comments

Comments
 (0)