diff --git a/docs/interpreting_candidate_scores.md b/docs/interpreting_candidate_scores.md index 13c5207..c8cb533 100644 --- a/docs/interpreting_candidate_scores.md +++ b/docs/interpreting_candidate_scores.md @@ -31,6 +31,15 @@ positive components: | `mapping_confidence_score` | 3 | trust in identifier harmonization (worst mapping among contributing records) | `mapping_confidence` | | `quality_score` | 2 | study/data quality (`1 - n_quality_flags/5`, floored at 0) | `quality_flags` | +Every quality flag counts the same in `quality_score`: `batch_confounded` +(treatment fully confounded with a sequencing run, MS acquisition batch, or +extraction batch) removes the same 0.2 as `low_replication`, and counts +toward the `quality_score >= 0.4` high-priority gate the same way. Adding the +term (2026-10-16) did not change any existing score, because no registered +study declared it yet. Once a study declares it, every candidate with a +record from that study loses 0.2 `quality_score` (0.4 points of the 0-100 +score) unless the flag was already in its union. + ### Why the unbounded components saturate instead of clipping Every component whose input has no natural upper bound uses `v / (v + half)`, diff --git a/registry/controlled_vocabularies/quality_flags.yaml b/registry/controlled_vocabularies/quality_flags.yaml index de93faf..fdcc28c 100644 --- a/registry/controlled_vocabularies/quality_flags.yaml +++ b/registry/controlled_vocabularies/quality_flags.yaml @@ -24,3 +24,5 @@ terms: label: Species assignment not confirmed by genotyping (required for species in a hybridizing complex, e.g. Mytilus edulis complex, unless species_identity.basis is genotyped) - id: reference_species_mismatch label: Reads were mapped to a related species' genome; gene identifiers belong to the reference species, not the study animal + - id: batch_confounded + label: Treatment contrast fully confounded with a processing batch (sequencing run, MS acquisition batch, extraction batch); treatment effect cannot be separated from batch diff --git a/tests/test_prioritize.py b/tests/test_prioritize.py index 1d4fe79..a74b769 100644 --- a/tests/test_prioritize.py +++ b/tests/test_prioritize.py @@ -44,6 +44,19 @@ def test_compute_components_bounds_are_0_to_1(): assert 0.0 <= value <= 1.0, f"{key}={value} out of bounds" +def test_batch_confounded_costs_at_least_as_much_quality_as_low_replication(): + meta_row = { + "k_studies": 2, "total_sample_size": 24, "pooled_effect": 1.0, + "p_value": 1e-4, "direction_consistency": 1.0, "phenotype": "survival", + "distinct_tissues": 1, "distinct_life_stages": 1, "i_squared": 0.0, + } + baseline = compute_components(meta_row, ["exact"], [])["quality_score"] + low_rep = compute_components(meta_row, ["exact"], ["low_replication"])["quality_score"] + batch = compute_components(meta_row, ["exact"], ["batch_confounded"])["quality_score"] + assert batch < baseline + assert batch <= low_rep + + def test_high_priority_gate_blocks_low_direction_consistency_even_with_two_studies(isolated_reports): harmonize_all_demo_studies() meta_df = run_meta_analysis(phenotype="larval_viability", feature_type="gene") diff --git a/tests/test_schema_validation.py b/tests/test_schema_validation.py index f4a931b..df89e4c 100644 --- a/tests/test_schema_validation.py +++ b/tests/test_schema_validation.py @@ -47,6 +47,19 @@ def test_unknown_vocabulary_term_is_rejected(tmp_path): assert any("phenotype" in e for e in result.errors) +def test_batch_confounded_quality_flag_validates(tmp_path): + with open("registry/studies/GIGAS_HEAT01.yaml") as fh: + study = yaml.safe_load(fh) + study["study_id"] = "BATCH_FLAG" + study["quality_flags"] = [*(study.get("quality_flags") or []), "batch_confounded"] + path = tmp_path / "BATCH_FLAG.yaml" + with open(path, "w") as fh: + yaml.safe_dump(study, fh) + + result = validate_study_file(path) + assert result.valid, result.errors + + def test_resilience_classification_mismatch_is_a_warning_not_an_error(tmp_path): with open("registry/studies/GIGAS_HEAT01.yaml") as fh: study = yaml.safe_load(fh)