Load gene expression count matrices from various quantification tools (featureCounts, Salmon, kallisto, STAR, HTSeq, 10X) into pandas DataFrames for downstream analysis.
pip install pandas numpy scipy
Tell your AI agent what you want to do: - "Load my featureCounts output file into a pandas DataFrame" - "Combine Salmon quantification files from multiple samples" - "Import STAR gene counts with reverse strand specificity"
"Load the featureCounts file at counts.txt and clean up the sample names"
"Read all Salmon quant.sf files from the salmon_quants directory into a combined count matrix"
"Import STAR ReadsPerGene.out.tab files with reverse strandedness"
"Load my count matrix and show me basic QC statistics"
"Check my count matrix for duplicate gene IDs and sum them"
"Figure out what delimiter my counts file uses and load it"
| Source | Format | Key Columns | Notes |
|---|---|---|---|
| featureCounts | TSV | Geneid + 5 meta cols + counts | Integer counts; discards multi-mapped reads by default |
| Salmon | quant.sf per sample | NumReads, TPM | Fractional estimates from EM; use tximport for gene-level |
| kallisto | abundance.tsv per sample | est_counts, tpm | Fractional estimates; use tximport for gene-level |
| STAR | ReadsPerGene.out.tab | gene_id + 3 strand columns | Must select correct strandedness column |
| 10X | matrix.mtx + features/barcodes | Sparse format | Use scanpy or cellranger |
| HTSeq | TSV | gene_id, count | Last 5 rows are summary stats (prefixed __) |
Salmon and kallisto produce transcript-level estimates. Loading NumReads/est_counts directly and summing to gene level introduces length bias (genes switching isoforms appear falsely DE). Always use tximport (R) or equivalent offset handling. See rna-quantification/tximport-workflow.
STAR ReadsPerGene.out.tab columns: gene_id | unstranded | sense | antisense. For Illumina TruSeq stranded libraries, the antisense column (column 4) is correct. Verify by comparing total counts in columns 3 vs 4 -- the larger total indicates the correct strand.
import pandas as pd
import numpy as np
from pathlib import Path
class CountMatrixLoader:
@staticmethod
def from_featurecounts(filepath):
df = pd.read_csv(filepath, sep='\t', comment='#')
counts = df.set_index('Geneid').iloc[:, 5:]
counts.columns = [c.replace('.bam', '').split('/')[-1] for c in counts.columns]
return counts
@staticmethod
def from_salmon_dir(base_dir):
base = Path(base_dir)
samples = [d.name for d in base.iterdir() if d.is_dir() and (d / 'quant.sf').exists()]
dfs = {}
for sample in samples:
sf = pd.read_csv(base / sample / 'quant.sf', sep='\t', index_col=0)
dfs[sample] = sf['NumReads']
return pd.DataFrame(dfs)
@staticmethod
def from_star_genecounts(filepaths, strandedness='reverse'):
# File cols (1-indexed): 1=gene_id, 2=unstranded, 3=forward, 4=reverse
# After index_col=0, remaining cols are 0=unstranded, 1=forward, 2=reverse
col_map = {'unstranded': 0, 'forward': 1, 'reverse': 2}
col_idx = col_map[strandedness]
dfs = {}
for fp in filepaths:
sample = Path(fp).name.replace('_ReadsPerGene.out.tab', '')
df = pd.read_csv(fp, sep='\t', header=None, index_col=0)
dfs[sample] = df.iloc[4:, col_idx]
return pd.DataFrame(dfs)
counts = CountMatrixLoader.from_featurecounts('counts.txt')
counts = CountMatrixLoader.from_salmon_dir('salmon_quants/')
def check_count_matrix(counts):
print(f'Shape: {counts.shape[0]} genes x {counts.shape[1]} samples')
print(f'Total counts per sample:\n{counts.sum().describe()}')
print(f'Genes with zero counts: {(counts.sum(axis=1) == 0).sum()}')
print(f'Any NaN values: {counts.isna().any().any()}')
print(f'Library sizes range: {counts.sum().min():.0f} - {counts.sum().max():.0f}')
return counts
counts = check_count_matrix(counts)
| Organism | Ensembl Format | Example |
|---|---|---|
| Human | ENSG | ENSG00000141510 |
| Mouse | ENSMUSG | ENSMUSG00000059552 |
| Zebrafish | ENSDARG | ENSDARG00000002354 |
| Fly | FBgn | FBgn0000008 |
| Worm | WBGene | WBGene00000001 |
sample_ids = counts.index[:5].tolist()
print(sample_ids)
if counts.index.str.startswith('ENSG').any():
print('Human Ensembl gene IDs')
elif counts.index.str.startswith('ENSMUSG').any():
print('Mouse Ensembl gene IDs')
if counts.index.duplicated().any():
print(f'Duplicate IDs: {counts.index.duplicated().sum()}')
counts = counts.groupby(counts.index).sum()
expected = ['sample1', 'sample2', 'sample3']
actual = counts.columns.tolist()
missing = set(expected) - set(actual)
if missing:
print(f'Missing samples: {missing}')
import csv
with open('counts.txt', 'r') as f:
dialect = csv.Sniffer().sniff(f.read(1024))
print(f'Detected delimiter: {repr(dialect.delimiter)}')
-p --countReadPairs since Subread v2.0.2; the -p flag alone counts each mate separately and inflates counts ~2xcountsFromAbundance='lengthScaledTPM' returns count-scale values for limma-voom; despite the name, the output is NOT TPM and should not be reported as suchcountsFromAbundance='no' WITHOUT length offset -- length bias is negligible