Skill Tracer
Skills › Scientific Agent Skills › bulk-rnaseq › counts-and-handoff.md

counts-and-handoff.md

168 lines · skills/bulk-rnaseq/references/counts-and-handoff.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Counts assembly and handoff to DE + enrichment
2
3The bundled bridge writes `counts.csv` (genes × samples, integer counts),
4`metadata_template.csv` (fill in biological design, then save as `metadata.csv`),
5and `counts_provenance.json` (input SHA-256 hashes, sample order, import settings).
6Keep quantifier logs, tool versions, reference/annotation release and hashes alongside
7these files; the sidecar cannot recover provenance absent from the quantification files.
8

Counts assembly and handoff to DE + enrichment

The bundled bridge writes counts.csv (genes × samples, integer counts), metadata_template.csv (fill in biological design, then save as metadata.csv), and counts_provenance.json (input SHA-256 hashes, sample order, import settings). Keep quantifier logs, tool versions, reference/annotation release and hashes alongside these files; the sidecar cannot recover provenance absent from the quantification files.

Raw lines 1 to 8
9## Choose the count model before import
11- Full-length Salmon: aggregate transcripts with `counts_from_abundance="length_scaled_tpm"`,
12 then round once for PyDESeq2. No additional transcript-length offset is applied.
13- Alternative R workflow: raw estimated counts plus effective lengths through
14 `tximport` → `DESeqDataSetFromTximport`. That function also handles bias-corrected
15 counts without adding a second length correction. Neither route is universally superior.
16- **3′ tagged/counting assays:** do not length-correct; use original counts with an
17 assay-appropriate workflow. The bundled Salmon bridge targets full-length bulk data.
18- Never submit TPM, FPKM, log counts, VST values, or batch-corrected expression to the
19 negative-binomial count model. Library-size normalization is performed by the DE engine.
21PyDESeq2 0.5.4 requires non-negative integer counts and has no tximport
22average-transcript-length-offset input. Rounding an appropriate estimated-count
23matrix is an import step, not a way to turn arbitrary normalized expression into counts.

Choose the count model before import

  • Full-length Salmon: aggregate transcripts with counts_from_abundance="length_scaled_tpm", then round once for PyDESeq2. No additional transcript-length offset is applied.
  • Alternative R workflow: raw estimated counts plus effective lengths through tximport → DESeqDataSetFromTximport. That function also handles bias-corrected counts without adding a second length correction. Neither route is universally superior.
  • 3′ tagged/counting assays: do not length-correct; use original counts with an assay-appropriate workflow. The bundled Salmon bridge targets full-length bulk data.
  • Never submit TPM, FPKM, log counts, VST values, or batch-corrected expression to the negative-binomial count model. Library-size normalization is performed by the DE engine.

PyDESeq2 0.5.4 requires non-negative integer counts and has no tximport average-transcript-length-offset input. Rounding an appropriate estimated-count matrix is an import step, not a way to turn arbitrary normalized expression into counts.

Raw lines 9 to 24
25## Salmon → gene counts (pytximport 0.13.0)
27The preferred transcript-to-gene map comes from the exact GTF used to build the
28quantification reference. This offline example replaces platform-specific awk syntax:
30```python
31from pytximport.utils import create_transcript_gene_map_from_annotation
33mapping = create_transcript_gene_map_from_annotation(
34 "annotation.gtf", source_field="transcript_id", target_field="gene_id",
36mapping.to_csv("tx2gene.tsv", sep="\t", index=False)
37```
39A live `create_transcript_gene_map(species="human")` query is not a substitute for
40matching the reference release. The helper supports human/mouse and an Ensembl
41archive `host`, but no live BioMart query was exercised in this review.
43For illustration, the underlying import API is:
45```python
46from pytximport import tximport
48txi = tximport(
49 ["quant/s1/quant.sf", "quant/s2/quant.sf"],
50 data_type="salmon",
51 transcript_gene_map="tx2gene.tsv",
52 counts_from_abundance="length_scaled_tpm",
53 ignore_transcript_version=False,
54 ignore_after_bar=False,
55 output_type="anndata",
56 return_data=True,
58counts = txi.to_df().T.round().astype("int64") # genes × samples
59```
61Use the bundled CLI for preflight checks, deterministic sample naming and provenance:
63```bash
64python scripts/build_counts_matrix.py --from salmon \
65 --quant-dir quant/ --tx2gene tx2gene.tsv --output-dir counts/
66```
68The bundled import first aggregates transcript counts, abundances and effective lengths
69to genes, then applies gene-level length scaling, then rounds once. Scaling individual
70transcripts before aggregation is a different calculation.
72It requires unique transcript IDs and the same transcript IDs/order across quant files,
73checks every quantified transcript has a map entry, and rejects ambiguous mappings.
74If the same release puts transcript versions in the FASTA but not the GTF identifiers,
75inspect the mismatch and use `--ignore-transcript-version` explicitly. It strips only
76terminal `.N` suffixes and rejects collisions; it does not fix a mismatched annotation.
77For GENCODE bar-delimited names, build the Salmon index with `--gencode`.
79On nf-core 3.27.0, preserve `*.merged.tx2gene_augmented.tsv`, the mapping actually
80used for import. Self-mapped orphan transcripts remain transcript identifiers and
81need review before interpreting rows as genes. The local bridge deliberately stops
82on missing mappings instead of silently dropping or self-mapping them.

Salmon → gene counts (pytximport 0.13.0)

The preferred transcript-to-gene map comes from the exact GTF used to build the quantification reference. This offline example replaces platform-specific awk syntax:

from pytximport.utils import create_transcript_gene_map_from_annotation

mapping = create_transcript_gene_map_from_annotation(
    "annotation.gtf", source_field="transcript_id", target_field="gene_id",
)
mapping.to_csv("tx2gene.tsv", sep="\t", index=False)

A live create_transcript_gene_map(species="human") query is not a substitute for matching the reference release. The helper supports human/mouse and an Ensembl archive host, but no live BioMart query was exercised in this review.

For illustration, the underlying import API is:

from pytximport import tximport

txi = tximport(
    ["quant/s1/quant.sf", "quant/s2/quant.sf"],
    data_type="salmon",
    transcript_gene_map="tx2gene.tsv",
    counts_from_abundance="length_scaled_tpm",
    ignore_transcript_version=False,
    ignore_after_bar=False,
    output_type="anndata",
    return_data=True,
)
counts = txi.to_df().T.round().astype("int64")  # genes × samples

Use the bundled CLI for preflight checks, deterministic sample naming and provenance:

python scripts/build_counts_matrix.py --from salmon \
  --quant-dir quant/ --tx2gene tx2gene.tsv --output-dir counts/

The bundled import first aggregates transcript counts, abundances and effective lengths to genes, then applies gene-level length scaling, then rounds once. Scaling individual transcripts before aggregation is a different calculation.

It requires unique transcript IDs and the same transcript IDs/order across quant files, checks every quantified transcript has a map entry, and rejects ambiguous mappings. If the same release puts transcript versions in the FASTA but not the GTF identifiers, inspect the mismatch and use --ignore-transcript-version explicitly. It strips only terminal .N suffixes and rejects collisions; it does not fix a mismatched annotation. For GENCODE bar-delimited names, build the Salmon index with --gencode.

On nf-core 3.27.0, preserve *.merged.tx2gene_augmented.tsv, the mapping actually used for import. Self-mapped orphan transcripts remain transcript identifiers and need review before interpreting rows as genes. The local bridge deliberately stops on missing mappings instead of silently dropping or self-mapping them.

Raw lines 25 to 83
Runs code python scripts/build_counts_matrix.py :64
84## STAR and featureCounts
86STAR `ReadsPerGene.out.tab` starts with four summary rows, then gene ID and three
87count columns. The CLI requires a verified `--strandedness`: `unstranded`, `forward`,
88or `reverse` selects zero-based column 1, 2, or 3. Gene sets must agree across samples;
89a missing row is not evidence for zero expression. Do not mix annotations or silently
90merge duplicate sample names after filename cleanup.
92featureCounts starts with a command comment followed by `Geneid, Chr, Start, End,
93Strand, Length` and one column per BAM. The bridge accepts non-negative integer
94counts, rejects duplicate sample/gene IDs, and rejects fractional output from
95`--fraction` instead of truncating it. Preserve whether paired reads were counted
96as fragments (`-p --countReadPairs`). Neither importer infers experimental design.
98Raw duplicate column labels are rejected before pandas can rename them, including
99Salmon tables, transcript maps, featureCounts, samplesheets and metadata. Resolve
100the ambiguity in the source instead of choosing one renamed column.
102All-zero genes are removed during export. Samples with no counts stop the export.

STAR and featureCounts

STAR ReadsPerGene.out.tab starts with four summary rows, then gene ID and three count columns. The CLI requires a verified --strandedness: unstranded, forward, or reverse selects zero-based column 1, 2, or 3. Gene sets must agree across samples; a missing row is not evidence for zero expression. Do not mix annotations or silently merge duplicate sample names after filename cleanup.

featureCounts starts with a command comment followed by Geneid, Chr, Start, End, Strand, Length and one column per BAM. The bridge accepts non-negative integer counts, rejects duplicate sample/gene IDs, and rejects fractional output from --fraction instead of truncating it. Preserve whether paired reads were counted as fragments (-p --countReadPairs). Neither importer infers experimental design.

Raw duplicate column labels are rejected before pandas can rename them, including Salmon tables, transcript maps, featureCounts, samplesheets and metadata. Resolve the ambiguity in the source instead of choosing one renamed column.

All-zero genes are removed during export. Samples with no counts stop the export.

Raw lines 84 to 103
104## Orientation, metadata, design and contrast
106PyDESeq2 consumes **samples × genes**. Transpose exactly once and reorder metadata
107by sample ID, never by its incidental row position:
109```python
110import pandas as pd
111from pydeseq2.dds import DeseqDataSet
112from pydeseq2.ds import DeseqStats
114counts = pd.read_csv("counts.csv", index_col=0).T
115metadata = pd.read_csv("metadata.csv", index_col=0)
116assert counts.index.is_unique and counts.columns.is_unique and metadata.index.is_unique
117assert set(metadata.index) == set(counts.index)
118metadata = metadata.loc[counts.index]
119assert metadata["condition"].notna().all()
120assert not metadata["condition"].isin(["", "CHANGE_ME"]).any()
122# Include batch only when recorded, non-missing and estimable.
123dds = DeseqDataSet(counts=counts, metadata=metadata, design="~condition", n_cpus=1)
124dds.deseq2()
125stats = DeseqStats(dds, contrast=["condition", "treated", "control"], alpha=0.05, n_cpus=1)
126stats.summary()
127stats.results_df.to_csv("deseq2_results.csv")
128```
130The positive log2 fold change above means **treated / control**. For paired samples,
131consider `~subject + condition`; for a measured crossed batch, `~batch + condition`.
132Check full matrix rank and positive residual degrees of freedom before fitting.
133Interactions change the meaning of main effects; use explicit simple effects or a
134proper numeric contrast, with the downstream skill. Technical lanes are not replicates.
136The current Python APIs and an explicit contrast were exercised on small synthetic
137data; the example paths and biological labels must be replaced for a real study.

Orientation, metadata, design and contrast

PyDESeq2 consumes samples × genes. Transpose exactly once and reorder metadata by sample ID, never by its incidental row position:

import pandas as pd
from pydeseq2.dds import DeseqDataSet
from pydeseq2.ds import DeseqStats

counts = pd.read_csv("counts.csv", index_col=0).T
metadata = pd.read_csv("metadata.csv", index_col=0)
assert counts.index.is_unique and counts.columns.is_unique and metadata.index.is_unique
assert set(metadata.index) == set(counts.index)
metadata = metadata.loc[counts.index]
assert metadata["condition"].notna().all()
assert not metadata["condition"].isin(["", "CHANGE_ME"]).any()

# Include batch only when recorded, non-missing and estimable.
dds = DeseqDataSet(counts=counts, metadata=metadata, design="~condition", n_cpus=1)
dds.deseq2()
stats = DeseqStats(dds, contrast=["condition", "treated", "control"], alpha=0.05, n_cpus=1)
stats.summary()
stats.results_df.to_csv("deseq2_results.csv")

The positive log2 fold change above means treated / control. For paired samples, consider ~subject + condition; for a measured crossed batch, ~batch + condition. Check full matrix rank and positive residual degrees of freedom before fitting. Interactions change the meaning of main effects; use explicit simple effects or a proper numeric contrast, with the downstream skill. Technical lanes are not replicates.

The current Python APIs and an explicit contrast were exercised on small synthetic data; the example paths and biological labels must be replaced for a real study.

Raw lines 104 to 138
139## DE → enrichment
141- Preranked GSEA uses all eligible tested genes with finite signed Wald statistics,
142 not only significant genes. Resolve duplicate mapped identifiers before ranking;
143 record any policy for one-to-many or many-to-one mappings and tied scores.
144- ORA uses a prespecified hit rule (for example `padj < 0.05`), separate up/down lists
145 when relevant, and the tested, detectable, mapped gene universe as background.
146- Use IDs accepted by the chosen organism/library release. Many GMT/Enrichr libraries
147 use symbols; g:Profiler also accepts Ensembl IDs. Do not force human capitalization
148 onto another species. Retain Ensembl IDs through DE and map only enrichment inputs.
150Illustrative handoffs (network enrichment was not executed in this review):
152```bash
153python ../pathway-enrichment/scripts/run_enrichment.py gsea \
154 --deseq2 deseq2_results.csv --organism human --outdir enrichment/ --seed 123
156python ../pathway-enrichment/scripts/run_enrichment.py ora \
157 --genes sig_symbols.txt --background tested_symbols.txt \
158 --organism human --outdir enrichment/
159```

DE → enrichment

  • Preranked GSEA uses all eligible tested genes with finite signed Wald statistics, not only significant genes. Resolve duplicate mapped identifiers before ranking; record any policy for one-to-many or many-to-one mappings and tied scores.
  • ORA uses a prespecified hit rule (for example padj < 0.05), separate up/down lists when relevant, and the tested, detectable, mapped gene universe as background.
  • Use IDs accepted by the chosen organism/library release. Many GMT/Enrichr libraries use symbols; g:Profiler also accepts Ensembl IDs. Do not force human capitalization onto another species. Retain Ensembl IDs through DE and map only enrichment inputs.

Illustrative handoffs (network enrichment was not executed in this review):

python ../pathway-enrichment/scripts/run_enrichment.py gsea \
  --deseq2 deseq2_results.csv --organism human --outdir enrichment/ --seed 123

python ../pathway-enrichment/scripts/run_enrichment.py ora \
  --genes sig_symbols.txt --background tested_symbols.txt \
  --organism human --outdir enrichment/
Raw lines 139 to 160
Runs code python ../pathway-enrichment/scripts/run_enrichment.py :153
161## Official sources reviewed
163- [pytximport API](https://pytximport.complextissue.com/en/stable/autoapi/pytximport/index.html)
164 and [GTF helper](https://pytximport.complextissue.com/en/stable/autoapi/pytximport/utils/index.html).
165- [tximport count/offset choices and 3′ caveat](https://bioconductor.org/packages/release/bioc/vignettes/tximport/inst/doc/tximport.html).
166- [PyDESeq2 0.5.4 dataset](https://pydeseq2.readthedocs.io/en/stable/api/docstrings/pydeseq2.dds.DeseqDataSet.html)
167 and [contrast API](https://pydeseq2.readthedocs.io/en/stable/api/docstrings/pydeseq2.ds.DeseqStats.html).
168- [nf-core/rnaseq 3.27.0 output contract](https://nf-co.re/rnaseq/3.27.0/docs/output).