Skill Tracer
Skills › Scientific Agent Skills › pydeseq2 › workflow_guide.md

workflow_guide.md

184 lines · skills/pydeseq2/references/workflow_guide.md at 5f2bc03

Open on GitHub
1# Normalization, QC, and downstream use
2

Normalization, QC, and downstream use

Raw lines 1 to 2
3## Importing counts from AnnData
4
5`obs` indexes samples and `var` indexes genes. Establish which matrix contains
6counts from preprocessing provenance: neither `.X`, `.raw.X`, nor a layer named
7`counts` guarantees that values are unnormalized. PyDESeq2's `adata=` constructor
8reads `.X`; it has no `layer=` argument. For a small dataset with a verified counts
9layer, this conversion was tested with dense and CSR inputs:
11```python
12import numpy as np
13import pandas as pd
14from scipy import sparse
16matrix = adata.layers['counts'] # verified count provenance, not inferred from name
17# Dense storage is needed downstream; check the memory cost before materializing.
18if sparse.issparse(matrix):
19 matrix = matrix.toarray()
20counts_df = pd.DataFrame(matrix, index=adata.obs_names, columns=adata.var_names)
21metadata = adata.obs.copy()
22assert counts_df.index.is_unique and counts_df.columns.is_unique
23assert np.isfinite(counts_df.to_numpy()).all()
24```
26Resolve missing annotations across **all** design variables before fitting.
27Explicitly log any sample exclusions, then perform gene filtering. Never infer
28transposition from whether the number of genes exceeds the number of samples.
30AnnData 0.13 exposes `layers[None]` as an alias for `.X`. Avoid loops assuming every
31layer key is a string, and never delete the alias as a workaround. A fitted
32PyDESeq2 0.5.4 object was successfully exported with
33`dds.to_picklable_anndata().write_h5ad(...)` and reloaded under AnnData 0.13.4.

Importing counts from AnnData

obs indexes samples and var indexes genes. Establish which matrix contains counts from preprocessing provenance: neither .X, .raw.X, nor a layer named counts guarantees that values are unnormalized. PyDESeq2's adata= constructor reads .X; it has no layer= argument. For a small dataset with a verified counts layer, this conversion was tested with dense and CSR inputs:

import numpy as np
import pandas as pd
from scipy import sparse

matrix = adata.layers['counts']  # verified count provenance, not inferred from name
# Dense storage is needed downstream; check the memory cost before materializing.
if sparse.issparse(matrix):
    matrix = matrix.toarray()
counts_df = pd.DataFrame(matrix, index=adata.obs_names, columns=adata.var_names)
metadata = adata.obs.copy()
assert counts_df.index.is_unique and counts_df.columns.is_unique
assert np.isfinite(counts_df.to_numpy()).all()

Resolve missing annotations across all design variables before fitting. Explicitly log any sample exclusions, then perform gene filtering. Never infer transposition from whether the number of genes exceeds the number of samples.

AnnData 0.13 exposes layers[None] as an alias for .X. Avoid loops assuming every layer key is a string, and never delete the alias as a workaround. A fitted PyDESeq2 0.5.4 object was successfully exported with dds.to_picklable_anndata().write_h5ad(...) and reloaded under AnnData 0.13.4.

Raw lines 3 to 34
35## Size factors and count normalization
37For the ratio method, positive-in-every-sample genes define geometric reference
38counts. PyDESeq2 computes each sample's factor as the exponential of its median
39log count/reference ratio. `normed_counts = X / size_factors[:, None]`. This is
40within-gene library/composition normalization; it is not cross-gene length correction.
42Size factors need not cluster near one. Inspect library totals, composition,
43replicate structure and external controls together; a wide factor range is a QC
44lead, not an automatic failure. Broad unidirectional expression shifts can violate
45the default reference assumption. Use prespecified invariant control genes only
46when they have defensible experimental support.
48```python
49from pydeseq2.dds import DeseqDataSet
51normalization_dds = DeseqDataSet(
52 counts=counts_df, metadata=metadata, design='~condition',
53 size_factors_fit_type='ratio', n_cpus=1,
55normalization_dds.fit_size_factors()
56sf = normalization_dds.obs['size_factors'].to_numpy()
57assert np.isfinite(sf).all() and (sf > 0).all()
58np.testing.assert_allclose(normalization_dds.layers['normed_counts'], counts_df.to_numpy() / sf[:, None])
59```
61Normalization options in 0.5.4:
63- `ratio` is default. If every gene contains a zero, `DeseqDataSet` warns and
64 switches to iterative estimation. The standalone `preprocessing.deseq2_norm`
65 does **not** provide that fallback.
66- `poscounts` uses positive entries, replacing zeros' log contribution with zero
67 when calculating gene log means, then centers size factors to geometric mean one.
68 The released implementation additionally selects genes with `logmeans > 0`:
69 all-zero **and all-zero/one** genes are excluded. A binary-only native fixture
70 produced missing factors. Check factors explicitly; do not treat this as a
71 general per-cell single-cell DE method.
72- `iterative` estimates factors with an intercept-only fit. In the released source,
73 the `control_genes` mask is not passed into this branch, including automatic
74 fallback. Do not claim control-only normalization if iteration was used.
75- `control_genes` accepts valid AnnData gene indexers for ratio/poscounts. Ensure
76 selected controls survived filtering and contain usable positive counts for
77 every sample; checking only that names exist is insufficient.
79Record the actual method and fallback warnings. Changing normalization to obtain
80more significant genes is not a validation strategy.

Size factors and count normalization

For the ratio method, positive-in-every-sample genes define geometric reference counts. PyDESeq2 computes each sample's factor as the exponential of its median log count/reference ratio. normed_counts = X / size_factors[:, None]. This is within-gene library/composition normalization; it is not cross-gene length correction.

Size factors need not cluster near one. Inspect library totals, composition, replicate structure and external controls together; a wide factor range is a QC lead, not an automatic failure. Broad unidirectional expression shifts can violate the default reference assumption. Use prespecified invariant control genes only when they have defensible experimental support.

from pydeseq2.dds import DeseqDataSet

normalization_dds = DeseqDataSet(
    counts=counts_df, metadata=metadata, design='~condition',
    size_factors_fit_type='ratio', n_cpus=1,
)
normalization_dds.fit_size_factors()
sf = normalization_dds.obs['size_factors'].to_numpy()
assert np.isfinite(sf).all() and (sf > 0).all()
np.testing.assert_allclose(normalization_dds.layers['normed_counts'], counts_df.to_numpy() / sf[:, None])

Normalization options in 0.5.4:

  • ratio is default. If every gene contains a zero, DeseqDataSet warns and switches to iterative estimation. The standalone preprocessing.deseq2_norm does not provide that fallback.
  • poscounts uses positive entries, replacing zeros' log contribution with zero when calculating gene log means, then centers size factors to geometric mean one. The released implementation additionally selects genes with logmeans > 0: all-zero and all-zero/one genes are excluded. A binary-only native fixture produced missing factors. Check factors explicitly; do not treat this as a general per-cell single-cell DE method.
  • iterative estimates factors with an intercept-only fit. In the released source, the control_genes mask is not passed into this branch, including automatic fallback. Do not claim control-only normalization if iteration was used.
  • control_genes accepts valid AnnData gene indexers for ratio/poscounts. Ensure selected controls survived filtering and contain usable positive counts for every sample; checking only that names exist is insufficient.

Record the actual method and fallback warnings. Changing normalization to obtain more significant genes is not a validation strategy.

Raw lines 35 to 81
82## Dispersion and model diagnostics
84For a negative-binomial count with mean `mu`, the dispersion convention is
85`variance = mu + dispersion * mu**2`. PyDESeq2 estimates gene-wise dispersions,
86fits a parametric trend or a mean trend, and obtains MAP dispersions. This is
87dispersion shrinkage; it is separate from optional LFC shrinkage after testing.
89The parametric trend can fail and fall back to a mean trend. This occurred in the
90small Poisson-like regression fixture and is reported, not hidden. Tiny panels
91provide little information for global dispersion estimation. Inspect nonfinite
92estimates, convergence fields, the trend and mean/count distributions. A plausible
93plot or convergence flag does not establish model adequacy or biological validity.
95```python
96import matplotlib.pyplot as plt
98dds.plot_dispersions(save_path='dispersion_plot.png')
99plt.close('all')
100print(dds.var[['genewise_dispersions', 'dispersions', '_LFC_converged']].head())
101print(dds.obs['size_factors'])
102```
104The model requires independent biological replication and residual degrees of
105freedom. The library warns on rank deficiency; the bundled driver explicitly
106stops. An interaction cannot recover information absent from a confounded design.
108Cook replacement and Cook test filtering are different operations. Default
109`min_replicates=7` governs eligible replacement; a three-versus-three experiment
110may still have Cook-filtered results without replacement. Do not delete samples
111because one gene has a large Cook distance. Inspect sample-wide QC and preserve the
112reason for any scientifically justified exclusion.

Dispersion and model diagnostics

For a negative-binomial count with mean mu, the dispersion convention is variance = mu + dispersion * mu**2. PyDESeq2 estimates gene-wise dispersions, fits a parametric trend or a mean trend, and obtains MAP dispersions. This is dispersion shrinkage; it is separate from optional LFC shrinkage after testing.

The parametric trend can fail and fall back to a mean trend. This occurred in the small Poisson-like regression fixture and is reported, not hidden. Tiny panels provide little information for global dispersion estimation. Inspect nonfinite estimates, convergence fields, the trend and mean/count distributions. A plausible plot or convergence flag does not establish model adequacy or biological validity.

import matplotlib.pyplot as plt

dds.plot_dispersions(save_path='dispersion_plot.png')
plt.close('all')
print(dds.var[['genewise_dispersions', 'dispersions', '_LFC_converged']].head())
print(dds.obs['size_factors'])

The model requires independent biological replication and residual degrees of freedom. The library warns on rank deficiency; the bundled driver explicitly stops. An interaction cannot recover information absent from a confounded design.

Cook replacement and Cook test filtering are different operations. Default min_replicates=7 governs eligible replacement; a three-versus-three experiment may still have Cook-filtered results without replacement. Do not delete samples because one gene has a large Cook distance. Inspect sample-wide QC and preserve the reason for any scientifically justified exclusion.

Raw lines 82 to 113
114## Missing tests, adjusted p-values, and plots
116All-zero genes lack estimable coefficients/tests. Extreme Cook outliers can have
117missing p-values; the original statistic may still be present. Independent
118filtering uses normalized mean count to select a rejection threshold for the
119specified `alpha`, leaving some adjusted p-values missing. It differs from the
120manual prefilter and does not remove those genes' rows from the result table.
122A p-value histogram need not be flat: true effects, discrete low-count tests,
123filtering, model errors and sample structure all affect it. Uniform null behavior
124is an idealization, not a required shape for a mixed real dataset. Compare with
125QC and study design rather than using the histogram alone to certify the analysis.
127For a volcano plot, omit missing y values and cap exact zero adjusted p-values
128only in the plotting copy:
130```python
131plot_results = wald_results.copy()
132plot_results['minus_log10_padj'] = -np.log10(plot_results.padj.clip(lower=np.finfo(float).tiny))
133ax = plot_results.plot.scatter(x='log2FoldChange', y='minus_log10_padj')
134ax.axhline(-np.log10(alpha), linestyle='--', color='gray')
135ax.figure.savefig('volcano_plot.png')
136plt.close(ax.figure)
137```
139For MA plots, display normalized mean versus effect; distinguish missing tests from
140significant results. Shrunk LFCs can be useful for visualization, but their updated
141SE is not the SE that produced the retained Wald statistic.

Missing tests, adjusted p-values, and plots

All-zero genes lack estimable coefficients/tests. Extreme Cook outliers can have missing p-values; the original statistic may still be present. Independent filtering uses normalized mean count to select a rejection threshold for the specified alpha, leaving some adjusted p-values missing. It differs from the manual prefilter and does not remove those genes' rows from the result table.

A p-value histogram need not be flat: true effects, discrete low-count tests, filtering, model errors and sample structure all affect it. Uniform null behavior is an idealization, not a required shape for a mixed real dataset. Compare with QC and study design rather than using the histogram alone to certify the analysis.

For a volcano plot, omit missing y values and cap exact zero adjusted p-values only in the plotting copy:

plot_results = wald_results.copy()
plot_results['minus_log10_padj'] = -np.log10(plot_results.padj.clip(lower=np.finfo(float).tiny))
ax = plot_results.plot.scatter(x='log2FoldChange', y='minus_log10_padj')
ax.axhline(-np.log10(alpha), linestyle='--', color='gray')
ax.figure.savefig('volcano_plot.png')
plt.close(ax.figure)

For MA plots, display normalized mean versus effect; distinguish missing tests from significant results. Shrunk LFCs can be useful for visualization, but their updated SE is not the SE that produced the retained Wald statistic.

Raw lines 114 to 142
143## VST and memory
145```python
146dds.vst(use_design=True)
147vst_values = dds.layers['vst_counts']
148assert vst_values.shape == dds.shape and np.isfinite(vst_values).all()
149```
151VST is for exploratory PCA, clustering and sample QC. `use_design=False` uses an
152intercept-only dispersion model; it is not synonymous with batch correction.
153`use_design=True` accounts for the model while estimating the transformation; it
154does not subtract fitted batch effects. Never feed VST output back as count input.
155Fit transformations on training data for predictive evaluations and explicitly
156validate the held-out transformation path for the selected normalization method.
158Do not fit independent gene batches and combine results: normalization, dispersion
159trends/priors, independent filtering and BH families change. Reduce `n_cpus`, use
160`low_memory=True`, and assess dense memory requirements. Low-memory mode removes
161some diagnostics, so decide what must be retained before discarding them.

VST and memory

dds.vst(use_design=True)
vst_values = dds.layers['vst_counts']
assert vst_values.shape == dds.shape and np.isfinite(vst_values).all()

VST is for exploratory PCA, clustering and sample QC. use_design=False uses an intercept-only dispersion model; it is not synonymous with batch correction. use_design=True accounts for the model while estimating the transformation; it does not subtract fitted batch effects. Never feed VST output back as count input. Fit transformations on training data for predictive evaluations and explicitly validate the held-out transformation path for the selected normalization method.

Do not fit independent gene batches and combine results: normalization, dispersion trends/priors, independent filtering and BH families change. Reduce n_cpus, use low_memory=True, and assess dense memory requirements. Low-memory mode removes some diagnostics, so decide what must be retained before discarding them.

Raw lines 143 to 162
163## Enrichment handoff
165For a standard two-sided zero-null Wald analysis, preserve gene IDs and select
166finite tests without thresholding on significance:
168```python
169eligible = np.isfinite(wald_results['stat']) & np.isfinite(wald_results['pvalue'])
170ranked = wald_results.loc[eligible, 'stat'].sort_values(ascending=False)
171assert ranked.index.is_unique
172ranked.to_csv('signed_wald_ranking.csv', header=['stat'])
173```
175This excludes Cook-filtered p-values while keeping genes with finite p-values and
176missing `padj` from independent filtering. State that eligibility policy. Resolve
177one-to-many/many-to-one identifier mappings before ranking and preserve the tested
178universe for ORA. These are handoff semantics; no enrichment service was called
179as part of this skill's refresh.
181Sources: [released normalization source](https://github.com/scverse/PyDESeq2/blob/v0.5.4/pydeseq2/preprocessing.py),
182[released dataset source](https://github.com/scverse/PyDESeq2/blob/v0.5.4/pydeseq2/dds.py),
183[step-by-step tutorial](https://pydeseq2.readthedocs.io/en/stable/auto_examples/plot_step_by_step.html),
184[DESeq2 statistical guidance](https://bioconductor.org/packages/release/bioc/vignettes/DESeq2/inst/doc/DESeq2.html).

Enrichment handoff

For a standard two-sided zero-null Wald analysis, preserve gene IDs and select finite tests without thresholding on significance:

eligible = np.isfinite(wald_results['stat']) & np.isfinite(wald_results['pvalue'])
ranked = wald_results.loc[eligible, 'stat'].sort_values(ascending=False)
assert ranked.index.is_unique
ranked.to_csv('signed_wald_ranking.csv', header=['stat'])

This excludes Cook-filtered p-values while keeping genes with finite p-values and missing padj from independent filtering. State that eligibility policy. Resolve one-to-many/many-to-one identifier mappings before ranking and preserve the tested universe for ORA. These are handoff semantics; no enrichment service was called as part of this skill's refresh.

Sources: released normalization source, released dataset source, step-by-step tutorial, DESeq2 statistical guidance.

Raw lines 163 to 184