-
Notifications
You must be signed in to change notification settings - Fork 10
feat: add hierarchical FDR correction for dose-response data #116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
b06003f to
1ede068
Compare
Design note: Why min p-value instead of Simes?The initial implementation 1ede068 used Simes' method for Stage 1 p-value aggregation, following Yekutieli (2008) hierarchical FDR. However, testing on real dose-response data (LINCS, 4 plates, 58 compounds × 6 doses) revealed a problem: Simes penalizes compounds for having inactive low doses - which is the expected biological behavior in dose-response data. For example, compound
The compound has a strong signal at high dose but Simes dilutes it with the inactive low doses. Min p-value is more appropriate: a compound passes Stage 1 if ANY dose is active. This matches the biological question: "Does this compound have a phenotype at any tested dose?" Results on LINCS data:
Min-p provides 88% power gain over flat BH while correctly handling the dose-response structure. When would Simes be appropriate?Simes would be better when you expect most or all group members to be active (e.g., testing replicates of the same condition). For dose-response, where only high doses are expected to be active, min-p is the right choice. If there's future demand, we could add a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic seems okay to me, but (and I consulted with @alxndrkalinin) we don't want to have an overloaded function with a bunch of arguments: Let's do composition instead.
At a general level, this means:
- Split p value calculation from statistical correction
- Isolate hierarchical statistical correction into its own function (ideally in a new file).
I'm a bit torn as to how much to modify the original mean_average_precision function, but I think it's worth isolating the two main steps to avoid code duplication, before and after the p-value is calculated this section.
Then we would have composition:
- One small function (e.g.,
get_map_pvaluethat covers the p-value calculation (the first section of mean_average_precision) - One function with hierarchical FDR correction
- refactor the function mean_average_precision into
get_map_pvalueandmultipletests, to retain backwards compatibility. - Potentially another function that wraps
get_map_pvalueand eithermultipletestsorhierarchical_fdr, if you want the convenience of map+hierarchical fdr in one.
This minimises repetition (as it is only the call to multipletests), while maximising modularity in case we have to add a different statistical correction in the future. It should be relatively simple, the code would remain modular enough, and we wouldn't start accumulating a bunch of flags and arguments on the main functions.
- Tests run on my side so far.
baff4b1 to
eea7f76
Compare
Thanks for the excellent suggestions! I've made all these changes (using Claude) but have not reviewed it carefully myself. I'll tag you again when I'm ready. |
|
Alright ready for you @afermg |
afermg
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the structure much more, but issues regarding the signature of mean_average_precision remain. We may need to bring Alex or John to give their opinion, but mine is that we should not lightly add new parameters unless we know that they are going to be widely used. I suggest instead creating another mean_average_precision function that uses hierarchical correction, or (if it doesn't add too much complexity) a function that dispatches different correction methods for the same mAP table.
src/copairs/map/map.py
Outdated
| progress_bar: bool = True, | ||
| max_workers: Optional[int] = None, | ||
| cache_dir: Optional[Union[str, Path]] = None, | ||
| hierarchical_by: Optional[List[str]] = None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My previous suggestion was:
Potentially another function that wraps get_map_pvalue and either multipletests or hierarchical_fdr, if you want the convenience of map+hierarchical fdr in one.
I don't think we should modify the signature of our core mean_average_precision function. Since hierarchical FDR is not standard (at least for now), we don't want to accumulate arguments unless strictly necessary (to avoid ending up with overloaded seaborn-like functions). I suggest instead having something like mean_average_precision_hierarchical. At the cost of duplicated documentation, we have a clear separation of behaviours. If you want one function to rule them all we can just wrap both hierarchical and non-hierarchical and pass all arguments.
This design follows the previous decision of splitting matching between monolabel and multilabel.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am trying to come up with a better solution, but perhaps using mean_average_precision as the dispatcher of subfunctions is the correct decision. I suggest we consult Alex or John before adding a parameter to a signature though. My personal preference is to keep mean_average_precision as-is, and add mean_average_precision_hierarchical as an alternative function that covers this case (since as far as I know it is rather niche).
I've resolved all the minor comments now. Please feel free to ping / discuss with Alex or John to decide what to do here |
8cebe77 to
818c198
Compare
Implements two-stage hierarchical FDR (Yekutieli 2008) to reduce over-correction when testing related hypotheses (e.g., multiple doses of the same compound). - Add `hierarchical_by` parameter to `mean_average_precision()` - Stage 1: Aggregate p-values by group using Simes' method, apply BH - Stage 2: For significant groups, apply BH within each group - Add `simes_pvalue()` function for p-value combination - Fix `silent_thread_map` bug (handle `leave` kwarg) - Add comprehensive tests for hierarchical FDR Closes #115 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Simes method penalizes compounds for having inactive low doses, which is the expected biological behavior in dose-response data. Min p-value is more appropriate: a compound passes Stage 1 if ANY dose shows activity. - Replace simes_pvalue() aggregation with simple min() - Remove unused simes_pvalue function and tests - Update docstrings to reflect the change
The silent_thread_map leave kwarg issue will be properly fixed in the fix/silent-thread-map-leave-kwarg branch. This PR should be rebased after that fix is merged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Split mean_average_precision into modular components per PR review: - get_map_pvalue(): compute mAP scores and p-values - apply_fdr_correction(): standard BH correction - apply_hierarchical_fdr(): two-stage hierarchical correction This enables composition without accumulating flags on the main function. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…tion Consistent naming pattern with apply_fdr_correction. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Change relative import to absolute import in map.py - Remove tests that only validate DataFrame structure - Add TODO for test improvements once API is finalized Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extract hierarchical FDR into a dedicated function rather than adding a parameter to mean_average_precision, following the existing pattern for monolabel/multilabel functions. This keeps the main API simple and avoids parameter accumulation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
818c198 to
b9934c7
Compare
|
Feel free to bring up any opinions (or lack of thereof). @alxndrkalinin @johnarevalo -- You don't have to read the PR/Code review, but let us know if there is any preference between adding a new argument to This additional function for hierarchical FDR is better IMO. I just added a couple of comments on the commit wrt documentation and one argument that changed position. Once those are fixed it will be good enough for me. |
Reorder parameters in mean_average_precision_hierarchical so that the first 5 required parameters match mean_average_precision exactly. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary
Implements two-stage hierarchical FDR to reduce over-correction when testing related hypotheses (e.g., multiple doses of the same compound).
hierarchical_byparameter tomean_average_precision()Usage
When
hierarchical_byis specified, the result includes additional columns:stage1_p_value: Group-level p-value (minimum p-value in group)stage1_corrected_p_value: BH-corrected Stage 1 p-valuestage1_significant: Whether the group passed Stage 1Why min p-value instead of Simes?
For dose-response data, low doses are expected to be inactive. Simes' method penalizes compounds for having inactive low doses, which is biologically normal. Min p-value is more appropriate: a compound passes Stage 1 if ANY dose shows activity.
Example on LINCS data (4 plates, 58 compounds × 6 doses):
Why hierarchical FDR matters
With 1000 compounds × 5 doses = 5000 tests, standard BH treats each as independent. But doses of the same compound test the same underlying hypothesis. Hierarchical FDR:
Test plan
Context for Broadies: See https://github.com/broadinstitute/cpg0037-oasis-broad-U2OS-data/issues/9#issuecomment-3624961526 for a real-world example of its utility
Closes #115
🤖 Generated with Claude Code