Merge branch 'transcriptome_table_order' into 'dev'
Report minor fixes [CW-7281] See merge request epi2melabs/workflows/wf-transcriptomes!309
This commit is contained in:
commit
10b2013b6a
@ -112,6 +112,24 @@ def _format_ratio_value(value):
|
||||
return f"{numeric:.2f}x"
|
||||
|
||||
|
||||
def _sorted_transcript_abundance_table(tx_counts_file, sample_aliases):
|
||||
"""Get transcript abundance rows by descending total abundance of sample columns."""
|
||||
tx_counts = _read_table(tx_counts_file)
|
||||
if tx_counts is None or tx_counts.empty:
|
||||
return tx_counts
|
||||
|
||||
count_columns = [
|
||||
column for column in tx_counts.columns if column in set(sample_aliases)
|
||||
]
|
||||
if not count_columns:
|
||||
return tx_counts
|
||||
|
||||
numeric_counts = tx_counts[count_columns].apply(pd.to_numeric, errors="coerce")
|
||||
abundance = numeric_counts.sum(axis=1, min_count=1)
|
||||
order = abundance.fillna(float("-inf")).sort_values(ascending=False).index
|
||||
return tx_counts.loc[order].reset_index(drop=True)
|
||||
|
||||
|
||||
def _format_classification_label(name):
|
||||
"""Return canonical report label for a summary classification value."""
|
||||
return str(name).strip().replace("-", "_").replace("_", " ").capitalize()
|
||||
@ -380,12 +398,38 @@ def _contrast_results(de_dir, filename, n=None):
|
||||
data = table
|
||||
if n is not None:
|
||||
data = data.head(n)
|
||||
data.sort_values("padj", ascending=True, inplace=True)
|
||||
if "padj" in data.columns:
|
||||
data.sort_values("padj", ascending=True, inplace=True)
|
||||
tables[contrast_dir.name] = data
|
||||
|
||||
return tables
|
||||
|
||||
|
||||
def _round_de_table(table):
|
||||
"""Return a display-formatted copy of a DE/DTU result table."""
|
||||
rounded_columns = [
|
||||
"baseMean", "exonBaseMean", "log2FoldChange", "lfcSE", "stat", "pvalue", "padj"
|
||||
]
|
||||
|
||||
def _format_numeric(value):
|
||||
numeric_value = pd.to_numeric(value, errors="coerce")
|
||||
if pd.isna(numeric_value):
|
||||
return value
|
||||
fixed_decimal = f"{numeric_value:.3f}"
|
||||
if numeric_value != 0 and (
|
||||
abs(numeric_value) < 0.0001 or fixed_decimal in {"0.000", "-0.000"}
|
||||
):
|
||||
return f"{numeric_value:.3e}"
|
||||
return fixed_decimal
|
||||
|
||||
rounded = table.copy()
|
||||
for column in rounded_columns:
|
||||
if column not in rounded.columns:
|
||||
continue
|
||||
rounded[column] = rounded[column].map(_format_numeric)
|
||||
return rounded
|
||||
|
||||
|
||||
def _load_bambu_qc(bambu_dir):
|
||||
"""Load bambu QC statistics JSON."""
|
||||
qc_file = Path(bambu_dir) / "bambu_qc_stats.json"
|
||||
@ -911,13 +955,17 @@ def main(args):
|
||||
use_index=False,
|
||||
)
|
||||
|
||||
tx_counts = _read_table(Path(bambu_dir) / "transcript_counts.tsv")
|
||||
tx_counts = _sorted_transcript_abundance_table(
|
||||
Path(bambu_dir) / "transcript_counts.tsv",
|
||||
[item["alias"] for item in metadata]
|
||||
)
|
||||
if tx_counts is not None and not tx_counts.empty:
|
||||
p(
|
||||
"Top transcript rows from the "
|
||||
f"{'sample' if is_single_sample else 'cohort'} abundance table."
|
||||
f"Top {args.de_table_size} most abundant transcripts"
|
||||
)
|
||||
DataTable.from_pandas(tx_counts.head(20), use_index=False)
|
||||
DataTable.from_pandas(tx_counts.head(args.de_table_size), use_index=False)
|
||||
else:
|
||||
_create_warning_banner("No trancrips discovered")
|
||||
|
||||
if not is_single_sample:
|
||||
with report.add_section(
|
||||
@ -934,15 +982,6 @@ def main(args):
|
||||
use_index=False,
|
||||
)
|
||||
|
||||
if args.alignment_stats_dir and Path(args.alignment_stats_dir).exists():
|
||||
with report.add_section("Alignment statistics", "Alignments"):
|
||||
tabs = Tabs()
|
||||
for stats_file in sorted(
|
||||
Path(args.alignment_stats_dir).glob("*.flagstat.txt")
|
||||
):
|
||||
with tabs.add_tab(stats_file.stem.replace(".flagstat", "")):
|
||||
pre(stats_file.read_text())
|
||||
|
||||
sqanti_table = _sqanti_table(args.sqanti_dir)
|
||||
if sqanti_table is not None and not sqanti_table.empty:
|
||||
with report.add_section("SQANTI3 classification", "SQANTI3"):
|
||||
@ -1296,7 +1335,8 @@ def main(args):
|
||||
strong("Note: ")
|
||||
raw(contrast_data["dtu_power_warning"])
|
||||
DataTable.from_pandas(
|
||||
table.head(args.de_table_size), use_index=False
|
||||
_round_de_table(table.head(args.de_table_size)),
|
||||
use_index=False
|
||||
)
|
||||
with div(cls="clustering-info"):
|
||||
raw(
|
||||
@ -1373,7 +1413,8 @@ def main(args):
|
||||
if contrast_name in dtu_tables:
|
||||
dtu_table = dtu_tables[contrast_name]
|
||||
DataTable.from_pandas(
|
||||
dtu_table.head(args.de_table_size), use_index=False
|
||||
_round_de_table(dtu_table.head(args.de_table_size)),
|
||||
use_index=False
|
||||
)
|
||||
with div(cls="clustering-info"):
|
||||
raw(
|
||||
@ -1400,11 +1441,6 @@ def argparser():
|
||||
parser.add_argument("report", help="Report output file.")
|
||||
parser.add_argument("--metadata", required=True, help="Sample metadata JSON.")
|
||||
parser.add_argument("--stats", nargs="+", help="Per-read stats paths.")
|
||||
parser.add_argument(
|
||||
"--alignment_stats_dir",
|
||||
default=None,
|
||||
help="Alignment stats directory.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cohort_dir",
|
||||
required=True,
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
from workflow_glue import report
|
||||
|
||||
|
||||
@ -110,11 +111,6 @@ def _build_report_args(tmp_path, de_qc=None):
|
||||
mod_summaries.mkdir()
|
||||
sqanti = tmp_path / "sqanti"
|
||||
sqanti.mkdir()
|
||||
alignment_stats = tmp_path / "alignment_stats"
|
||||
alignment_stats.mkdir()
|
||||
(samples / "OPTIONAL_FILE").touch()
|
||||
(sqanti / "OPTIONAL_FILE").touch()
|
||||
(alignment_stats / "OPTIONAL_FILE").touch()
|
||||
|
||||
de_dir = None
|
||||
if de_qc is not None:
|
||||
@ -143,8 +139,6 @@ def _build_report_args(tmp_path, de_qc=None):
|
||||
str(out_report),
|
||||
"--metadata",
|
||||
str(metadata),
|
||||
"--alignment_stats_dir",
|
||||
str(alignment_stats),
|
||||
"--cohort_dir",
|
||||
str(cohort),
|
||||
"--ref_summary",
|
||||
@ -225,18 +219,12 @@ def test_report_main_accepts_optional_file_sentinels(monkeypatch, tmp_path):
|
||||
sqanti.mkdir()
|
||||
(sqanti / "OPTIONAL_FILE").touch()
|
||||
|
||||
alignment_stats = tmp_path / "alignment_stats"
|
||||
alignment_stats.mkdir()
|
||||
(alignment_stats / "OPTIONAL_FILE").touch()
|
||||
|
||||
out_report = tmp_path / "wf-transcriptomes-report.html"
|
||||
args = report.argparser().parse_args(
|
||||
[
|
||||
str(out_report),
|
||||
"--metadata",
|
||||
str(metadata),
|
||||
"--alignment_stats_dir",
|
||||
str(alignment_stats),
|
||||
"--cohort_dir",
|
||||
str(cohort),
|
||||
"--ref_summary",
|
||||
@ -417,8 +405,6 @@ def test_report_main_handles_degenerate_bambu_qc_and_read_summary(
|
||||
str(out_report),
|
||||
"--metadata",
|
||||
str(metadata),
|
||||
"--alignment_stats_dir",
|
||||
str(alignment_stats),
|
||||
"--stats",
|
||||
str(alignment_stats),
|
||||
"--cohort_dir",
|
||||
@ -556,18 +542,12 @@ def test_report_main_uses_cohort_bambu_qc_for_multi_sample_inputs(
|
||||
sqanti.mkdir()
|
||||
(sqanti / "OPTIONAL_FILE").touch()
|
||||
|
||||
alignment_stats = tmp_path / "alignment_stats"
|
||||
alignment_stats.mkdir()
|
||||
(alignment_stats / "OPTIONAL_FILE").touch()
|
||||
|
||||
out_report = tmp_path / "wf-transcriptomes-report.html"
|
||||
args = report.argparser().parse_args(
|
||||
[
|
||||
str(out_report),
|
||||
"--metadata",
|
||||
str(metadata),
|
||||
"--alignment_stats_dir",
|
||||
str(alignment_stats),
|
||||
"--cohort_dir",
|
||||
str(cohort),
|
||||
"--ref_summary",
|
||||
@ -773,3 +753,64 @@ def test_report_main_tolerates_missing_statistical_fields(monkeypatch, tmp_path)
|
||||
]
|
||||
assert method_tables
|
||||
assert "parametric" in method_tables[0].to_string()
|
||||
|
||||
|
||||
def test_round_de_table_formats_supported_numeric_columns():
|
||||
"""DE/DTU preview tables should round known numeric columns for display."""
|
||||
table = pd.DataFrame(
|
||||
{
|
||||
"GENEID": ["gene1"],
|
||||
"baseMean": [123.4567],
|
||||
"log2FoldChange": [0.00001234],
|
||||
"lfcSE": [0.98765],
|
||||
"stat": [-45.6789],
|
||||
"pvalue": [0.00001234],
|
||||
"padj": [0.123456],
|
||||
"other": [7.89123],
|
||||
}
|
||||
)
|
||||
|
||||
rounded = report._round_de_table(table)
|
||||
|
||||
assert rounded.loc[0, "baseMean"] == "123.457"
|
||||
assert rounded.loc[0, "log2FoldChange"] == "1.234e-05"
|
||||
assert rounded.loc[0, "lfcSE"] == "0.988"
|
||||
assert rounded.loc[0, "stat"] == "-45.679"
|
||||
assert rounded.loc[0, "pvalue"] == "1.234e-05"
|
||||
assert rounded.loc[0, "padj"] == "0.123"
|
||||
assert rounded.loc[0, "other"] == 7.89123
|
||||
|
||||
|
||||
def test_round_de_table_uses_scientific_notation_when_fixed_decimal_would_zero():
|
||||
"""Tiny non-zero values should not display as 0.000 in DE/DTU tables."""
|
||||
table = pd.DataFrame(
|
||||
{
|
||||
"GENEID": ["gene1"],
|
||||
"pvalue": [0.00012],
|
||||
"padj": [0.00049],
|
||||
}
|
||||
)
|
||||
|
||||
rounded = report._round_de_table(table)
|
||||
|
||||
assert rounded.loc[0, "pvalue"] == "1.200e-04"
|
||||
assert rounded.loc[0, "padj"] == "4.900e-04"
|
||||
|
||||
|
||||
def test_sorted_transcript_abundance_table_uses_sample_alias_columns(tmp_path):
|
||||
"""Transcript abundance sorting should use only real sample alias columns."""
|
||||
tx_counts_file = _write(
|
||||
tmp_path / "transcript_counts.tsv",
|
||||
(
|
||||
"TXNAME\tsampleA\tsampleB\tgene_length\tannotation_score\n"
|
||||
"tx_low\t1\t1\t10000\t999\n"
|
||||
"tx_high\t5\t5\t10\t1\n"
|
||||
"tx_mid\t2\t2\t5000\t500\n"
|
||||
),
|
||||
)
|
||||
|
||||
sorted_table = report._sorted_transcript_abundance_table(
|
||||
tx_counts_file, ["sampleA", "sampleB"]
|
||||
)
|
||||
|
||||
assert sorted_table["TXNAME"].tolist() == ["tx_high", "tx_mid", "tx_low"]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user