PlantPerturb-Small / scripts /preprocess_counts.py
aungkomyint's picture
Publish research draft and reproducible PCA-ridge benchmark
18410f0 verified
Raw History Blame Contribute Delete
5 kB
"""Combine technical counts, filter genes, and normalize E-MTAB-5009."""
from math import ceil
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[1]
RAW_COUNTS_PATH = (
PROJECT_ROOT / "data" / "raw" / "E-MTAB-5009" / "WP2_100acc.htseq.txt"
)
SAMPLE_SHEET_PATH = PROJECT_ROOT / "data" / "processed" / "sample_sheet.csv"
OUTPUT_DIR = PROJECT_ROOT / "data" / "processed"
MIN_CPM = 1.0
MIN_SAMPLE_FRACTION = 0.10
def load_inputs() -> tuple[pd.DataFrame, pd.DataFrame]:
counts = pd.read_csv(RAW_COUNTS_PATH, sep="\t", index_col=0)
counts.index.name = "gene_id"
sample_sheet = pd.read_csv(SAMPLE_SHEET_PATH, keep_default_na=False)
if counts.columns.duplicated().any():
raise ValueError("Raw count columns are not unique.")
if sample_sheet["count_column"].duplicated().any():
raise ValueError("Sample-sheet count columns are not unique.")
if set(counts.columns) != set(sample_sheet["count_column"]):
raise ValueError("Raw counts and sample sheet contain different columns.")
if (counts < 0).any().any():
raise ValueError("Raw counts contain negative values.")
# Put sample-sheet rows in exactly the same order as the count columns.
sample_sheet = sample_sheet.set_index("count_column").loc[counts.columns]
sample_sheet.index.name = "count_column"
sample_sheet = sample_sheet.reset_index()
return counts, sample_sheet
def combine_technical_counts(
counts: pd.DataFrame, sample_sheet: pd.DataFrame
) -> tuple[pd.DataFrame, pd.DataFrame]:
column_to_group = sample_sheet.set_index("count_column")["sample_group_id"]
# Raw sequencing counts from technical measurements are additive.
aggregated = (
counts.T.join(column_to_group)
.groupby("sample_group_id", sort=False)
.sum()
.T
)
aggregated.index.name = "gene_id"
group_sheet = (
sample_sheet.groupby("sample_group_id", sort=False)
.agg(
ecotype=("ecotype", "first"),
condition=("condition", "first"),
block=("block", "first"),
technical_count_columns=("count_column", "count"),
source_count_columns=("count_column", ";".join),
)
.reset_index()
)
if list(aggregated.columns) != group_sheet["sample_group_id"].tolist():
raise ValueError("Aggregated counts and group sheet have different orders.")
return aggregated, group_sheet
def normalize_and_filter(
aggregated: pd.DataFrame,
) -> tuple[pd.DataFrame, pd.DataFrame, int]:
library_sizes = aggregated.sum(axis=0)
if (library_sizes == 0).any():
raise ValueError("At least one sample group has a library size of zero.")
cpm = aggregated.div(library_sizes, axis="columns") * 1_000_000
minimum_samples = ceil(MIN_SAMPLE_FRACTION * aggregated.shape[1])
keep_gene = (cpm >= MIN_CPM).sum(axis=1) >= minimum_samples
filtered_counts = aggregated.loc[keep_gene]
log2_cpm = np.log2(cpm.loc[keep_gene] + 1).astype("float32")
return filtered_counts, log2_cpm, minimum_samples
def main() -> None:
counts, sample_sheet = load_inputs()
aggregated, group_sheet = combine_technical_counts(counts, sample_sheet)
filtered_counts, log2_cpm, minimum_samples = normalize_and_filter(aggregated)
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
aggregated.to_csv(
OUTPUT_DIR / "counts_aggregated.tsv.gz", sep="\t", compression="gzip"
)
filtered_counts.to_csv(
OUTPUT_DIR / "counts_filtered.tsv.gz", sep="\t", compression="gzip"
)
log2_cpm.to_csv(
OUTPUT_DIR / "expression_log2_cpm.tsv.gz", sep="\t", compression="gzip"
)
group_sheet.to_csv(OUTPUT_DIR / "sample_groups.csv", index=False)
original_libraries = counts.sum(axis=0)
combined_libraries = aggregated.sum(axis=0)
print(f"Original count columns: {counts.shape[1]:,}")
print(f"Sample groups after technical summation: {aggregated.shape[1]:,}")
print(f"Genes before filtering: {aggregated.shape[0]:,}")
print(
f"Filter: CPM >= {MIN_CPM:g} in at least "
f"{minimum_samples} of {aggregated.shape[1]} sample groups"
)
print(f"Genes retained: {filtered_counts.shape[0]:,}")
print(f"Genes removed: {aggregated.shape[0] - filtered_counts.shape[0]:,}")
print(
"Original library sizes (min / median / max): "
f"{original_libraries.min():,.0f} / "
f"{original_libraries.median():,.0f} / "
f"{original_libraries.max():,.0f}"
)
print(
"Combined library sizes (min / median / max): "
f"{combined_libraries.min():,.0f} / "
f"{combined_libraries.median():,.0f} / "
f"{combined_libraries.max():,.0f}"
)
print("Wrote counts_aggregated.tsv.gz")
print("Wrote counts_filtered.tsv.gz")
print("Wrote expression_log2_cpm.tsv.gz")
print("Wrote sample_groups.csv")
if __name__ == "__main__":
main()