Download scripts/preprocess_counts.py from aungkomyint/PlantPerturb-Small: direct link, hf CLI and curl.
- Browser
- Download file 5 kB
-
https://huggingface.co/aungkomyint/PlantPerturb-Small/resolve/main/scripts/preprocess_counts.py
- Command line
-
hf download hf://aungkomyint/PlantPerturb-Small/scripts/preprocess_counts.py
-
curl -L -o preprocess_counts.py https://huggingface.co/aungkomyint/PlantPerturb-Small/resolve/main/scripts/preprocess_counts.py
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() | |