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"
|
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):
|
def _format_classification_label(name):
|
||||||
"""Return canonical report label for a summary classification value."""
|
"""Return canonical report label for a summary classification value."""
|
||||||
return str(name).strip().replace("-", "_").replace("_", " ").capitalize()
|
return str(name).strip().replace("-", "_").replace("_", " ").capitalize()
|
||||||
@ -380,12 +398,38 @@ def _contrast_results(de_dir, filename, n=None):
|
|||||||
data = table
|
data = table
|
||||||
if n is not None:
|
if n is not None:
|
||||||
data = data.head(n)
|
data = data.head(n)
|
||||||
|
if "padj" in data.columns:
|
||||||
data.sort_values("padj", ascending=True, inplace=True)
|
data.sort_values("padj", ascending=True, inplace=True)
|
||||||
tables[contrast_dir.name] = data
|
tables[contrast_dir.name] = data
|
||||||
|
|
||||||
return tables
|
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):
|
def _load_bambu_qc(bambu_dir):
|
||||||
"""Load bambu QC statistics JSON."""
|
"""Load bambu QC statistics JSON."""
|
||||||
qc_file = Path(bambu_dir) / "bambu_qc_stats.json"
|
qc_file = Path(bambu_dir) / "bambu_qc_stats.json"
|
||||||
@ -911,13 +955,17 @@ def main(args):
|
|||||||
use_index=False,
|
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:
|
if tx_counts is not None and not tx_counts.empty:
|
||||||
p(
|
p(
|
||||||
"Top transcript rows from the "
|
f"Top {args.de_table_size} most abundant transcripts"
|
||||||
f"{'sample' if is_single_sample else 'cohort'} abundance table."
|
|
||||||
)
|
)
|
||||||
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:
|
if not is_single_sample:
|
||||||
with report.add_section(
|
with report.add_section(
|
||||||
@ -934,15 +982,6 @@ def main(args):
|
|||||||
use_index=False,
|
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)
|
sqanti_table = _sqanti_table(args.sqanti_dir)
|
||||||
if sqanti_table is not None and not sqanti_table.empty:
|
if sqanti_table is not None and not sqanti_table.empty:
|
||||||
with report.add_section("SQANTI3 classification", "SQANTI3"):
|
with report.add_section("SQANTI3 classification", "SQANTI3"):
|
||||||
@ -1296,7 +1335,8 @@ def main(args):
|
|||||||
strong("Note: ")
|
strong("Note: ")
|
||||||
raw(contrast_data["dtu_power_warning"])
|
raw(contrast_data["dtu_power_warning"])
|
||||||
DataTable.from_pandas(
|
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"):
|
with div(cls="clustering-info"):
|
||||||
raw(
|
raw(
|
||||||
@ -1373,7 +1413,8 @@ def main(args):
|
|||||||
if contrast_name in dtu_tables:
|
if contrast_name in dtu_tables:
|
||||||
dtu_table = dtu_tables[contrast_name]
|
dtu_table = dtu_tables[contrast_name]
|
||||||
DataTable.from_pandas(
|
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"):
|
with div(cls="clustering-info"):
|
||||||
raw(
|
raw(
|
||||||
@ -1400,11 +1441,6 @@ def argparser():
|
|||||||
parser.add_argument("report", help="Report output file.")
|
parser.add_argument("report", help="Report output file.")
|
||||||
parser.add_argument("--metadata", required=True, help="Sample metadata JSON.")
|
parser.add_argument("--metadata", required=True, help="Sample metadata JSON.")
|
||||||
parser.add_argument("--stats", nargs="+", help="Per-read stats paths.")
|
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(
|
parser.add_argument(
|
||||||
"--cohort_dir",
|
"--cohort_dir",
|
||||||
required=True,
|
required=True,
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
from workflow_glue import report
|
from workflow_glue import report
|
||||||
|
|
||||||
|
|
||||||
@ -110,11 +111,6 @@ def _build_report_args(tmp_path, de_qc=None):
|
|||||||
mod_summaries.mkdir()
|
mod_summaries.mkdir()
|
||||||
sqanti = tmp_path / "sqanti"
|
sqanti = tmp_path / "sqanti"
|
||||||
sqanti.mkdir()
|
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
|
de_dir = None
|
||||||
if de_qc is not None:
|
if de_qc is not None:
|
||||||
@ -143,8 +139,6 @@ def _build_report_args(tmp_path, de_qc=None):
|
|||||||
str(out_report),
|
str(out_report),
|
||||||
"--metadata",
|
"--metadata",
|
||||||
str(metadata),
|
str(metadata),
|
||||||
"--alignment_stats_dir",
|
|
||||||
str(alignment_stats),
|
|
||||||
"--cohort_dir",
|
"--cohort_dir",
|
||||||
str(cohort),
|
str(cohort),
|
||||||
"--ref_summary",
|
"--ref_summary",
|
||||||
@ -225,18 +219,12 @@ def test_report_main_accepts_optional_file_sentinels(monkeypatch, tmp_path):
|
|||||||
sqanti.mkdir()
|
sqanti.mkdir()
|
||||||
(sqanti / "OPTIONAL_FILE").touch()
|
(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"
|
out_report = tmp_path / "wf-transcriptomes-report.html"
|
||||||
args = report.argparser().parse_args(
|
args = report.argparser().parse_args(
|
||||||
[
|
[
|
||||||
str(out_report),
|
str(out_report),
|
||||||
"--metadata",
|
"--metadata",
|
||||||
str(metadata),
|
str(metadata),
|
||||||
"--alignment_stats_dir",
|
|
||||||
str(alignment_stats),
|
|
||||||
"--cohort_dir",
|
"--cohort_dir",
|
||||||
str(cohort),
|
str(cohort),
|
||||||
"--ref_summary",
|
"--ref_summary",
|
||||||
@ -417,8 +405,6 @@ def test_report_main_handles_degenerate_bambu_qc_and_read_summary(
|
|||||||
str(out_report),
|
str(out_report),
|
||||||
"--metadata",
|
"--metadata",
|
||||||
str(metadata),
|
str(metadata),
|
||||||
"--alignment_stats_dir",
|
|
||||||
str(alignment_stats),
|
|
||||||
"--stats",
|
"--stats",
|
||||||
str(alignment_stats),
|
str(alignment_stats),
|
||||||
"--cohort_dir",
|
"--cohort_dir",
|
||||||
@ -556,18 +542,12 @@ def test_report_main_uses_cohort_bambu_qc_for_multi_sample_inputs(
|
|||||||
sqanti.mkdir()
|
sqanti.mkdir()
|
||||||
(sqanti / "OPTIONAL_FILE").touch()
|
(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"
|
out_report = tmp_path / "wf-transcriptomes-report.html"
|
||||||
args = report.argparser().parse_args(
|
args = report.argparser().parse_args(
|
||||||
[
|
[
|
||||||
str(out_report),
|
str(out_report),
|
||||||
"--metadata",
|
"--metadata",
|
||||||
str(metadata),
|
str(metadata),
|
||||||
"--alignment_stats_dir",
|
|
||||||
str(alignment_stats),
|
|
||||||
"--cohort_dir",
|
"--cohort_dir",
|
||||||
str(cohort),
|
str(cohort),
|
||||||
"--ref_summary",
|
"--ref_summary",
|
||||||
@ -773,3 +753,64 @@ def test_report_main_tolerates_missing_statistical_fields(monkeypatch, tmp_path)
|
|||||||
]
|
]
|
||||||
assert method_tables
|
assert method_tables
|
||||||
assert "parametric" in method_tables[0].to_string()
|
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