411 lines
9.6 KiB
Markdown
411 lines
9.6 KiB
Markdown
---
|
|
title: "Troubleshooting Guide"
|
|
task: ""
|
|
lineage_type: import
|
|
upstream_source: https://github.com/mims-harvard/ToolUniverse/blob/e2520a96/skills/tooluniverse-rnaseq-deseq2/references/troubleshooting.md
|
|
upstream_sha: e2520a96
|
|
imported_at: 2026-06-26
|
|
prompt_class: prompt
|
|
upstream_changes: accepted
|
|
author: upstream
|
|
validated: false
|
|
---
|
|
|
|
# Troubleshooting Guide
|
|
|
|
Common issues and solutions for RNA-seq analysis with PyDESeq2.
|
|
|
|
## Data Loading Issues
|
|
|
|
### "No matching samples between counts and metadata"
|
|
|
|
**Cause**: Sample names don't match between count matrix and metadata.
|
|
|
|
**Solutions**:
|
|
|
|
```python
|
|
# Check sample names
|
|
print("Count samples:", list(counts.index)[:5])
|
|
print("Metadata samples:", list(metadata.index)[:5])
|
|
|
|
# Try case-insensitive matching
|
|
counts.index = counts.index.str.lower()
|
|
metadata.index = metadata.index.str.lower()
|
|
|
|
# Remove whitespace
|
|
counts.index = counts.index.str.strip()
|
|
metadata.index = metadata.index.str.strip()
|
|
|
|
# Try transpose
|
|
if set(counts.columns) & set(metadata.index):
|
|
counts = counts.T
|
|
```
|
|
|
|
### "Non-integer counts detected"
|
|
|
|
**Cause**: Data is normalized (FPKM, TPM) or has floating point values.
|
|
|
|
**Solutions**:
|
|
|
|
```python
|
|
# Option 1: Round to integers (acceptable if close to integers)
|
|
counts = counts.round().astype(int)
|
|
|
|
# Option 2: Use t-test instead of DESeq2 for normalized data
|
|
from scipy import stats
|
|
stat, pval = stats.ttest_ind(group1, group2)
|
|
|
|
# Check if data is truly raw counts
|
|
print(counts.head())
|
|
print("Min value:", counts.min().min())
|
|
print("Has decimals:", (counts % 1 != 0).any().any())
|
|
```
|
|
|
|
### Matrix orientation wrong
|
|
|
|
**Cause**: Genes are rows instead of columns.
|
|
|
|
**Solution**:
|
|
|
|
```python
|
|
# Check shape
|
|
print(f"Counts shape: {counts.shape}") # Should be (n_samples, n_genes)
|
|
print(f"Samples >> genes? {counts.shape[0] > counts.shape[1] * 5}")
|
|
|
|
# Transpose if needed
|
|
if counts.shape[0] > counts.shape[1] * 5:
|
|
counts = counts.T
|
|
```
|
|
|
|
## DESeq2 Execution Issues
|
|
|
|
### "Dispersion trend did not converge"
|
|
|
|
**Cause**: Small sample size or low variation.
|
|
|
|
**Solution**:
|
|
|
|
```python
|
|
# Use mean fit instead of parametric
|
|
dds = DeseqDataSet(
|
|
counts=counts,
|
|
metadata=metadata,
|
|
design="~condition",
|
|
fit_type='mean', # Instead of 'parametric'
|
|
quiet=True
|
|
)
|
|
dds.deseq2()
|
|
```
|
|
|
|
### "Contrast not found"
|
|
|
|
**Cause**: Wrong factor/level names in contrast.
|
|
|
|
**Solution**:
|
|
|
|
```python
|
|
# Check available levels
|
|
print("Available factors:", metadata.columns)
|
|
print("Condition levels:", metadata['condition'].unique())
|
|
|
|
# Verify exact names (case-sensitive)
|
|
contrast = ['condition', 'treatment', 'control'] # Must match exactly
|
|
```
|
|
|
|
### "All genes filtered out"
|
|
|
|
**Cause**: Too strict pre-filtering or all counts are zero.
|
|
|
|
**Solution**:
|
|
|
|
```python
|
|
# Check data quality
|
|
print("Zero genes:", (counts.sum(axis=0) == 0).sum())
|
|
print("Low count genes:", (counts.sum(axis=0) < 10).sum())
|
|
|
|
# Remove only zero genes
|
|
nonzero = counts.sum(axis=0) > 0
|
|
counts = counts.loc[:, nonzero]
|
|
|
|
# Don't pre-filter too aggressively
|
|
# DESeq2 handles low counts internally
|
|
```
|
|
|
|
### "Single replicate per condition"
|
|
|
|
**Cause**: Only 1 sample per condition - cannot estimate dispersion.
|
|
|
|
**Solution**:
|
|
|
|
```python
|
|
# Cannot run DESeq2 with single replicates
|
|
# Use fold-change only
|
|
mean_A = counts[samples_A].mean(axis=1)
|
|
mean_B = counts[samples_B].mean(axis=1)
|
|
log2fc = np.log2((mean_B + 1) / (mean_A + 1))
|
|
|
|
# Or pool replicates from similar conditions
|
|
```
|
|
|
|
## Reference Level Issues
|
|
|
|
### Wrong reference level
|
|
|
|
**Cause**: Reference level not set correctly.
|
|
|
|
**Solution**:
|
|
|
|
```python
|
|
# In PyDESeq2, FIRST category is reference
|
|
metadata['condition'] = pd.Categorical(
|
|
metadata['condition'],
|
|
categories=['control', 'treatment'] # Control first = reference
|
|
)
|
|
|
|
# Verify
|
|
print("Categories:", metadata['condition'].cat.categories)
|
|
print("First category (reference):", metadata['condition'].cat.categories[0])
|
|
```
|
|
|
|
## LFC Shrinkage Issues
|
|
|
|
### "Coefficient not found for shrinkage"
|
|
|
|
**Cause**: Wrong coefficient name format.
|
|
|
|
**Solution**:
|
|
|
|
```python
|
|
# Check available coefficients
|
|
print("Available coefficients:", list(dds.varm['LFC'].columns))
|
|
|
|
# Standard format: factor[T.level]
|
|
coeff = 'condition[T.treatment]'
|
|
|
|
# Verify before shrinking
|
|
if coeff in dds.varm['LFC'].columns:
|
|
stat_res.lfc_shrink(coeff=coeff)
|
|
else:
|
|
print(f"ERROR: '{coeff}' not found")
|
|
print("Skipping shrinkage")
|
|
```
|
|
|
|
## Result Extraction Issues
|
|
|
|
### "NaN in padj column"
|
|
|
|
**Cause**: Independent filtering removed genes with insufficient evidence.
|
|
|
|
**Solution**:
|
|
|
|
```python
|
|
# This is EXPECTED behavior
|
|
# Remove NaN before counting DEGs
|
|
sig_genes = results.dropna(subset=['padj'])
|
|
sig_genes = sig_genes[sig_genes['padj'] < 0.05]
|
|
|
|
# Don't include NaN genes in counts
|
|
answer = len(sig_genes) # Correct
|
|
# NOT: len(results[results['padj'] < 0.05]) # Wrong, includes NaN
|
|
```
|
|
|
|
### "Gene not found in results"
|
|
|
|
**Cause**: Gene name case mismatch or gene filtered out.
|
|
|
|
**Solution**:
|
|
|
|
```python
|
|
# Case-insensitive search
|
|
def find_gene(results_df, gene_name):
|
|
# Exact match
|
|
if gene_name in results_df.index:
|
|
return gene_name
|
|
|
|
# Case-insensitive
|
|
idx_lower = {g.lower(): g for g in results_df.index}
|
|
if gene_name.lower() in idx_lower:
|
|
return idx_lower[gene_name.lower()]
|
|
|
|
# Partial match
|
|
matches = [g for g in results_df.index if gene_name.lower() in g.lower()]
|
|
if len(matches) == 1:
|
|
return matches[0]
|
|
elif len(matches) > 1:
|
|
print(f"Multiple matches: {matches}")
|
|
|
|
return None
|
|
|
|
gene = find_gene(results, "TP53")
|
|
if gene:
|
|
lfc = results.loc[gene, 'log2FoldChange']
|
|
```
|
|
|
|
## Enrichment Analysis Issues
|
|
|
|
### "No enrichment results"
|
|
|
|
**Cause**: Gene list too small or wrong organism/library.
|
|
|
|
**Solution**:
|
|
|
|
```python
|
|
# Check gene list
|
|
print(f"Gene list size: {len(gene_list)}")
|
|
print(f"Sample genes: {gene_list[:5]}")
|
|
|
|
# Try different libraries
|
|
libraries = [
|
|
'GO_Biological_Process_2023',
|
|
'GO_Biological_Process_2021',
|
|
'KEGG_2021_Human',
|
|
'KEGG_2019_Mouse'
|
|
]
|
|
|
|
for lib in libraries:
|
|
try:
|
|
enr = gp.enrich(gene_list=gene_list, gene_sets=lib, outdir=None, no_plot=True)
|
|
if len(enr.results) > 0:
|
|
print(f"✓ {lib}: {len(enr.results)} results")
|
|
break
|
|
except Exception as e:
|
|
print(f"✗ {lib}: {e}")
|
|
```
|
|
|
|
### "gseapy organism parameter deprecated"
|
|
|
|
**Cause**: Using old gseapy syntax with `organism` parameter.
|
|
|
|
**Solution**:
|
|
|
|
```python
|
|
# OLD (doesn't work in gseapy >= 1.1)
|
|
# enr = gp.enrich(gene_list=gene_list, organism='human', gene_sets='GO_Biological_Process')
|
|
|
|
# NEW (correct)
|
|
enr = gp.enrich(
|
|
gene_list=gene_list,
|
|
gene_sets='GO_Biological_Process_2023', # Organism in library name
|
|
outdir=None
|
|
)
|
|
```
|
|
|
|
## Memory Issues
|
|
|
|
### "Memory error with large gene set"
|
|
|
|
**Cause**: Too many genes (60K+ genes with many samples).
|
|
|
|
**Solution**:
|
|
|
|
```python
|
|
# Pre-filter low-count genes
|
|
min_count = 10
|
|
keep = counts.sum(axis=0) >= min_count
|
|
counts_filtered = counts.loc[:, keep]
|
|
print(f"Kept {keep.sum()} / {len(keep)} genes")
|
|
|
|
# Use sparse matrices (if applicable)
|
|
from scipy.sparse import csr_matrix
|
|
counts_sparse = csr_matrix(counts.values)
|
|
```
|
|
|
|
## Multi-Factor Design Issues
|
|
|
|
### "Singular design matrix"
|
|
|
|
**Cause**: Confounded factors (e.g., batch perfectly correlated with condition).
|
|
|
|
**Solution**:
|
|
|
|
```python
|
|
# Check confounding
|
|
print(pd.crosstab(metadata['batch'], metadata['condition']))
|
|
|
|
# If confounded, cannot separate effects
|
|
# Remove confounded factor from design
|
|
design = "~condition" # Remove batch if confounded
|
|
```
|
|
|
|
## Performance Issues
|
|
|
|
### "DESeq2 takes too long"
|
|
|
|
**Cause**: Large dataset or many factors.
|
|
|
|
**Solutions**:
|
|
|
|
```python
|
|
# Use parallel processing (if available)
|
|
import multiprocessing
|
|
n_cpus = multiprocessing.cpu_count()
|
|
# Note: PyDESeq2 doesn't directly support n_cpus, but numpy may use multiple cores
|
|
|
|
# Pre-filter more aggressively
|
|
min_count = 10
|
|
min_samples = 3
|
|
keep = (counts >= min_count).sum(axis=0) >= min_samples
|
|
counts_filtered = counts.loc[:, keep]
|
|
|
|
# Use mean fit instead of parametric (faster)
|
|
dds = DeseqDataSet(counts=counts, metadata=metadata, design="~condition",
|
|
fit_type='mean', quiet=True)
|
|
```
|
|
|
|
## Validation Issues
|
|
|
|
### Results don't match expected answer
|
|
|
|
**Checks**:
|
|
|
|
```python
|
|
# 1. Check reference level
|
|
print("Reference:", metadata['condition'].cat.categories[0])
|
|
|
|
# 2. Check contrast direction
|
|
print("Contrast:", contrast) # [factor, numerator, denominator]
|
|
|
|
# 3. Check filtering thresholds
|
|
print(f"padj < {padj_threshold}")
|
|
print(f"|log2FC| > {lfc_threshold}")
|
|
|
|
# 4. Check if shrinkage was applied
|
|
print("Shrinkage applied:", 'lfcSE' in results.columns)
|
|
|
|
# 5. Check for NaN handling
|
|
print("Genes with NaN padj:", results['padj'].isna().sum())
|
|
```
|
|
|
|
## Debugging Checklist
|
|
|
|
Before reporting issues, verify:
|
|
|
|
- [ ] Count matrix has samples as rows, genes as columns
|
|
- [ ] Counts are non-negative integers
|
|
- [ ] Metadata index matches count matrix index exactly
|
|
- [ ] Design formula references valid column names in metadata
|
|
- [ ] Reference level is set correctly (first category in Categorical)
|
|
- [ ] Contrast factor and levels exist in metadata
|
|
- [ ] LFC shrinkage coefficient name matches pydeseq2 format
|
|
- [ ] Filtering thresholds match question exactly
|
|
- [ ] NaN values in padj are excluded from DEG counts
|
|
|
|
## Getting Help
|
|
|
|
```python
|
|
# Print diagnostic information
|
|
print("=" * 50)
|
|
print("DIAGNOSTIC INFORMATION")
|
|
print("=" * 50)
|
|
print(f"Counts shape: {counts.shape}")
|
|
print(f"Metadata shape: {metadata.shape}")
|
|
print(f"Design: {design}")
|
|
print(f"Reference level: {metadata['condition'].cat.categories[0]}")
|
|
print(f"Contrast: {contrast}")
|
|
print(f"PyDESeq2 version: {pydeseq2.__version__}")
|
|
print(f"\nSample counts:\n{counts.iloc[:3, :3]}")
|
|
print(f"\nMetadata:\n{metadata.head(3)}")
|
|
print(f"\nResults:\n{results.head(3)}")
|
|
print("=" * 50)
|
|
```
|