|
| 1 | +"""DIA box plots must hand MultiQC summary statistics, not raw points, on large runs (#717). |
| 2 | +
|
| 3 | +On PXD030304 (5,798 runs) draw_dia_ms1_area serialised every raw MS1 area into the |
| 4 | +box plot and polars panicked on a >4 GiB buffer; draw_dia_intensity_std carried |
| 5 | +42.7 M points. Above the flat threshold both must pass {min,q1,median,q3,max,mean}. |
| 6 | +""" |
| 7 | + |
| 8 | +import numpy as np |
| 9 | +import pandas as pd |
| 10 | +import pytest |
| 11 | + |
| 12 | +from pmultiqc.modules.common.plots import dia as dia_plots |
| 13 | +from pmultiqc.modules.common.plots import general |
| 14 | + |
| 15 | + |
| 16 | +@pytest.fixture |
| 17 | +def capture_box(monkeypatch): |
| 18 | + seen = [] |
| 19 | + monkeypatch.setattr(dia_plots.box, "plot", lambda list_of_data_by_sample, pconfig=None: seen.append(list_of_data_by_sample) or "html") |
| 20 | + monkeypatch.setattr(dia_plots, "add_sub_section", lambda **kw: None) |
| 21 | + monkeypatch.setattr(dia_plots, "plot_html_check", lambda h: h, raising=False) |
| 22 | + return seen |
| 23 | + |
| 24 | + |
| 25 | +def _big_ms1(n_runs=20, per_run=None): |
| 26 | + per_run = per_run or (general.FLAT_THRESHOLD // n_runs + 1) |
| 27 | + rng = np.random.default_rng(0) |
| 28 | + return pd.DataFrame({ |
| 29 | + "Run": np.repeat([f"run{i}" for i in range(n_runs)], per_run), |
| 30 | + "log_ms1_area": rng.normal(20, 2, n_runs * per_run), |
| 31 | + }) |
| 32 | + |
| 33 | + |
| 34 | +def test_ms1_area_uses_summary_stats_above_threshold(capture_box): |
| 35 | + dia_plots.draw_dia_ms1_area(None, _big_ms1()) |
| 36 | + (data,) = capture_box |
| 37 | + assert data, "box.plot received no data" |
| 38 | + for run, stats in data.items(): |
| 39 | + assert isinstance(stats, dict), f"{run}: raw list reached box.plot" |
| 40 | + assert {"min", "q1", "median", "q3", "max", "mean"} <= set(stats) |
| 41 | + |
| 42 | + |
| 43 | +def test_ms1_area_keeps_raw_points_below_threshold(capture_box): |
| 44 | + dia_plots.draw_dia_ms1_area(None, _big_ms1(n_runs=2, per_run=10)) |
| 45 | + (data,) = capture_box |
| 46 | + assert all(isinstance(v, list) for v in data.values()), "small reports should keep raw points" |
| 47 | + |
| 48 | + |
| 49 | +def test_intensity_std_uses_summary_stats_above_threshold(capture_box, monkeypatch): |
| 50 | + n = general.FLAT_THRESHOLD + 10 |
| 51 | + fake = [{"Sample 1": list(np.linspace(0, 1, n))}] |
| 52 | + monkeypatch.setattr(dia_plots, "calculate_dia_intensity_std", lambda df, sdrf: fake) |
| 53 | + dia_plots.draw_dia_intensity_std(None, pd.DataFrame(), pd.DataFrame()) |
| 54 | + (data,) = capture_box |
| 55 | + ds = data[0] if isinstance(data, list) else data |
| 56 | + assert isinstance(ds["Sample 1"], dict) |
| 57 | + assert {"min", "q1", "median", "q3", "max", "mean"} <= set(ds["Sample 1"]) |
| 58 | + |
| 59 | + |
| 60 | +def test_box_stats_by_group_matches_summarise_box_data(): |
| 61 | + """Vectorised per-group statistics must equal the list-based summary (#717).""" |
| 62 | + rng = np.random.default_rng(7) |
| 63 | + n = general.FLAT_THRESHOLD + 500 |
| 64 | + keys = rng.choice([f"r{i}" for i in range(12)], n) |
| 65 | + vals = np.concatenate([rng.normal(20, 2, n - 40), np.full(20, 20.0), rng.normal(20, 40, 20)]) # ties + outliers |
| 66 | + vals[:5] = np.nan |
| 67 | + df = pd.DataFrame({"Run": pd.Categorical(keys), "x": vals}) |
| 68 | + lists = {str(k): g["x"].dropna().tolist() for k, g in df.groupby("Run", observed=True)} |
| 69 | + expected = general.summarise_box_data(lists) |
| 70 | + got = general.box_stats_by_group(df, "x", "Run") |
| 71 | + assert got.keys() == expected.keys() |
| 72 | + for k in expected: |
| 73 | + for stat in ("min", "q1", "median", "q3", "max", "mean"): |
| 74 | + assert np.isclose(got[k][stat], expected[k][stat]), (k, stat, got[k][stat], expected[k][stat]) |
| 75 | + |
| 76 | + |
| 77 | +def test_intensity_dis_by_sample_without_merge(capture_box): |
| 78 | + n = general.FLAT_THRESHOLD + 10 |
| 79 | + df = pd.DataFrame({ |
| 80 | + "Run": pd.Categorical(np.repeat(["a", "b"], n // 2)), |
| 81 | + "Modified.Sequence": pd.Categorical(["P"] * n), |
| 82 | + "Protein.Group": pd.Categorical(["G"] * n), |
| 83 | + "log_intensity": np.linspace(10, 30, n), |
| 84 | + }) |
| 85 | + sdrf = pd.DataFrame({"Run": ["a", "b"], "Sample": [1, 1]}) |
| 86 | + dia_plots.draw_dia_intensity_dis(None, df, sdrf) |
| 87 | + (data,) = capture_box |
| 88 | + assert isinstance(data, list) and len(data) == 2 |
| 89 | + assert set(data[0]) == {"a", "b"} and set(data[1]) == {"Sample 1"} |
| 90 | + assert all(isinstance(v, dict) for ds in data for v in ds.values()) |
0 commit comments