Skip to content

Commit 92b6766

Browse files
committed
Refresh tutorials with robust variance diagnostics
1 parent 51dea8c commit 92b6766

6 files changed

Lines changed: 4061 additions & 3051 deletions

File tree

docs/source/main_function/lfc.rst

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,73 @@ with effect estimates, standard errors, and BH-adjusted p-values.
99
For screens with hundreds of perturbations use :func:`gcate_lfc_batch`, which
1010
runs GCATE and LFC in batches to keep peak memory bounded.
1111

12+
Choosing the variance estimator
13+
-------------------------------
14+
15+
``LFC`` defaults to ``usevar='unequal'`` (Welch inference), which estimates
16+
the treatment and control variances separately. Retain this default for
17+
perturbation screens and for case-control, bulk, and donor-level pseudo-bulk
18+
analyses. Independence of the rows does not imply equal treatment-arm
19+
variances: disease severity, biological response, residual composition,
20+
library size, treatment imbalance, and heterogeneous expression can all make
21+
pooled inference anti-conservative.
22+
23+
Treat ``usevar='pooled'`` as an opt-in sensitivity or legacy analysis. Use it
24+
only when equal arm variances have a strong scientific and empirical
25+
justification; do not select it merely because a case-control study has a
26+
small number of independent samples. Pooled inference can produce much smaller
27+
standard errors and substantially more discoveries. For a deliberately
28+
justified batched sensitivity analysis, pass
29+
``lfc_kwargs=dict(usevar='pooled')``.
30+
31+
Donor-level independence alone does not establish equal arm variances. For
32+
example, the SEA-AD tutorial uses ``usevar='unequal'`` because disease severity,
33+
inter-individual response, residual cell composition, and library-size
34+
variation can produce different gene-wise variability between disease groups.
35+
36+
Welch inference does not itself model within-subject correlation. Repeated
37+
cells from the same donor or experimental unit should still be pseudo-bulked
38+
or handled with cluster-aware inference; ``usevar='unequal'`` only protects
39+
against unequal arm variances.
40+
41+
Propensity diagnostics
42+
----------------------
43+
44+
:func:`estimate_propensity_scores` estimates propensity scores without fitting
45+
the outcome model. Use ``K=5`` to obtain out-of-fold scores for positivity and
46+
overfitting diagnostics. :func:`summarize_propensity_scores` reports overlap,
47+
tail mass, and inverse-weight effective sample size, while
48+
:func:`plot_propensity_scores` compares treatment and control distributions.
49+
50+
Both the standalone estimator and ``LFC`` use class-balanced logistic
51+
propensity fitting by default. This preserves historical causarray behavior and
52+
ensures that standalone overlap diagnostics describe the same nuisance model
53+
used for effect estimation. Pass ``class_weight=None`` to
54+
``estimate_propensity_scores`` or ``ps_class_weight=None`` to ``LFC`` for a
55+
calibrated-probability sensitivity analysis.
56+
57+
``LFC`` uses the standard AIPW pseudo-outcome, which may be negative for
58+
individual cells even though its counterfactual mean is positive. Individual
59+
pseudo-outcomes are never clipped because doing so can bias the arm means,
60+
particularly when a large shared control group is compared with much smaller
61+
treatment groups.
62+
63+
For a calibrated-propensity sensitivity analysis, use
64+
``LFC(..., ps_class_weight=None)`` and diagnose the matching scores with
65+
``estimate_propensity_scores(..., class_weight=None)``.
66+
67+
The result includes ``mean_control``, ``mean_treated``, and ``estimable``. These
68+
are computed from the unclipped pseudo-outcomes. For numerical stability, valid
69+
aggregate arm means are floored at ``thres_diff`` only when constructing the
70+
log ratio and delta-method denominator. A pair with a nonfinite or nonpositive
71+
raw aggregate mean remains non-estimable, so the aggregate floor cannot create
72+
an extreme discovery from an invalid estimate.
73+
1274
.. automodule:: causarray.DR_learner
1375
:members:
76+
77+
.. automodule:: causarray.DR_estimation
78+
:members: estimate_propensity_scores
79+
80+
.. automodule:: causarray.diagnostics
81+
:members:

docs/source/tutorial/case_control/sea_ad_case_control.ipynb

Lines changed: 350 additions & 79 deletions
Large diffs are not rendered by default.

docs/source/tutorial/perturbseq/perturbseq-py.ipynb

Lines changed: 3263 additions & 70 deletions
Large diffs are not rendered by default.

docs/source/tutorial/perturbseq/perturbseq-r.Rmd

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,19 @@ Here, we just use a subset of the data to demonstrate the workflow of the analys
1919

2020
```{r}
2121
library(Seurat)
22-
library(caret)
2322
2423
sc.seurat <- readRDS("perturbseq-exneu.rds")
2524
26-
Y <- data.frame(t(as.matrix(sc.seurat[['RNA']]$counts))) # cell-by-gene matrix
25+
Y <- data.frame(t(as.matrix(GetAssayData(sc.seurat, assay = "RNA", layer = "counts")))) # cell-by-gene matrix
2726
metadata <- sc.seurat@meta.data
2827
2928
perturb <- metadata
3029
colnames(perturb) <- gsub("Perturbation", "trt_", colnames(perturb))
3130
perturb$trt_ <- relevel(as.factor(perturb$trt_), ref = "GFP")
32-
dmy <- dummyVars(" ~ trt_", data = perturb)
33-
A <- data.frame(predict(dmy, newdata = perturb))[,-1] # cell-by-trt matrix
31+
A <- data.frame(
32+
model.matrix(~ trt_ - 1, data = perturb)[, -1, drop = FALSE],
33+
check.names = FALSE
34+
) # cell-by-trt matrix; remove the first (GFP control) column
3435
```
3536

3637
For running causarray, we require the following inputs:
@@ -68,11 +69,12 @@ res_gate <- causarray$fit_gcate(Y, X, A, r, verbose=TRUE) # a list of results fr
6869
U <- res_gate[[2]]$U
6970
```
7071

71-
Next, we apply causarray to estimate the causal effects of perturbations on gene expression.
72+
Next, we apply causarray to estimate the causal effects of perturbations on gene expression. We use unequal (Welch) variance because treatment and control cells can have different pseudo-outcome variability; pooling those variances can be anti-conservative in a perturbation screen.
7273

7374
```{r}
7475
offsets <- log(res_gate[[2]][['kwargs_glm']][['size_factor']]) # use the precomputed size factors
75-
res <- causarray$LFC(Y, cbind(X, U), A, cbind(X_A, U), offset=offsets, verbose=TRUE)
76+
res <- causarray$LFC(Y, cbind(X, U), A, cbind(X_A, U), offset=offsets,
77+
usevar="unequal", verbose=TRUE)
7678
```
7779

7880
```{r}
@@ -104,4 +106,4 @@ ggplot(discovery_counts, aes(x = Perturbation, y = Count)) +
104106
ggtitle('Number of Discoveries (padj < 0.1) for Each Perturbation Condition') +
105107
xlab('Perturbation Condition') +
106108
ylab('Number of Discoveries')
107-
```
109+
```

docs/source/tutorial/perturbseq/perturbseq-r.md

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,25 +29,19 @@ library(Seurat)
2929
##
3030
## intersect
3131

32-
``` r
33-
library(caret)
34-
```
35-
36-
## Loading required package: ggplot2
37-
38-
## Loading required package: lattice
39-
4032
``` r
4133
sc.seurat <- readRDS("perturbseq-exneu.rds")
4234

43-
Y <- data.frame(t(as.matrix(sc.seurat[['RNA']]$counts))) # cell-by-gene matrix
35+
Y <- data.frame(t(as.matrix(GetAssayData(sc.seurat, assay = "RNA", layer = "counts")))) # cell-by-gene matrix
4436
metadata <- sc.seurat@meta.data
4537

4638
perturb <- metadata
4739
colnames(perturb) <- gsub("Perturbation", "trt_", colnames(perturb))
4840
perturb$trt_ <- relevel(as.factor(perturb$trt_), ref = "GFP")
49-
dmy <- dummyVars(" ~ trt_", data = perturb)
50-
A <- data.frame(predict(dmy, newdata = perturb))[,-1] # cell-by-trt matrix
41+
A <- data.frame(
42+
model.matrix(~ trt_ - 1, data = perturb)[, -1, drop = FALSE],
43+
check.names = FALSE
44+
) # cell-by-trt matrix; remove the first (GFP control) column
5145
```
5246

5347
For running causarray, we require the following inputs:
@@ -75,7 +69,7 @@ causarray <- import("causarray")
7569
cat(causarray$`__version__`)
7670
```
7771

78-
## 0.0.6
72+
## 0.0.7
7973

8074
``` r
8175
# (Y, A) should be either data.frame or matrix
@@ -138,11 +132,14 @@ U <- res_gate[[2]]$U
138132
```
139133

140134
Next, we apply causarray to estimate the causal effects of perturbations
141-
on gene expression.
135+
on gene expression. We use unequal (Welch) variance because treatment and
136+
control cells can have different pseudo-outcome variability; pooling those
137+
variances can be anti-conservative in a perturbation screen.
142138

143139
``` r
144140
offsets <- log(res_gate[[2]][['kwargs_glm']][['size_factor']]) # use the precomputed size factors
145-
res <- causarray$LFC(Y, cbind(X, U), A, cbind(X_A, U), offset=offsets, verbose=TRUE)
141+
res <- causarray$LFC(Y, cbind(X, U), A, cbind(X_A, U), offset=offsets,
142+
usevar="unequal", verbose=TRUE)
146143
```
147144

148145
## 'Estimating LFC...'

0 commit comments

Comments
 (0)