Repository navigation
Add DEG F1 and Spearman z-score metrics - #84
xlancelottx wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Thanks for this! The core of the metric works well but it crashes on some of the method outputs.
I ran the metric in its image on all 8 test sets of the benchmark (1000 cells each), with predictions I generated myself. deg_f1 / spearman_z:
| prediction | GEX | ADT | ATAC |
|---|---|---|---|
| solution | 1.00 / 1.00 | 1.00 / 1.00 | 1.00 / 1.00 |
| solution / 2 | 0.38-0.60 / 1.00 | 0.39-0.62 / 1.00 | 1.00 / 1.00 |
| shuffled cells | 0.00 / -0.00-0.04 | 0.00 / -0.03--0.02 | 0.00-0.02 / 0.05-0.11 |
| mean per gene | 0.00 / 0.00 | 0.00 / 0.00 | 0.00 / 0.00 |
| zeros | 0.00 / 0.00 | 0.00 / 0.00 | 0.00 / 0.00 |
So the good and bad predictions end up where they should, and at most 4.5 GB and under a minute per prediction fits the labels fine.
Two small things outside of the diff:
- typo in the PR description: "calculted"
- the changelog will conflict with #83, whose 0.2.1 entry should end up below 0.3.0
Once the crash is fixed, it would be great to see the scores on real method outputs, to check that they differ usefully between methods.
| raise ValueError("Dataset and prediction anndata objects should have the same shape / dimensions.") | ||
|
|
||
| # the clusters are defined on the solution and applied to the prediction, so the cells must be paired | ||
| if not (ad_pred.obs_names.equals(ad_sol.obs_names) and ad_pred.var_names.equals(ad_sol.var_names)): |
There was a problem hiding this comment.
This crashes with a KeyError on predictions without names, and file_prediction.yaml doesn't require any. lm, knnr_r, novel, simple_mlp and random_predict all write theirs without, so these would go missing from the results, and on de.NBI the run can still report success. CI doesn't catch it because its test prediction has names. mse and correlation pair by position, so how about only reordering when the names match?
| if not (ad_pred.obs_names.equals(ad_sol.obs_names) and ad_pred.var_names.equals(ad_sol.var_names)): | |
| if set(ad_pred.obs_names) == set(ad_sol.obs_names) and set(ad_pred.var_names) == set(ad_sol.var_names): |
With this change a real random_predict output gets a score, and a shuffled solution that keeps its names still gets 1.0 / 1.0.
| groups = np.flatnonzero(sizes >= par["min_group_size"]) | ||
| logging.info("%d clusters, %d with at least %d cells", len(sizes), len(groups), par["min_group_size"]) | ||
| if len(groups) < 2: | ||
| raise ValueError(f"DEG recall needs at least 2 clusters with {par['min_group_size']} cells or more, found {len(groups)}") |
There was a problem hiding this comment.
Leftover from before the rename:
| raise ValueError(f"DEG recall needs at least 2 clusters with {par['min_group_size']} cells or more, found {len(groups)}") | |
| raise ValueError(f"The DEG metric needs at least 2 clusters with {par['min_group_size']} cells or more, found {len(groups)}") |
| if len(groups) < 2: | ||
| raise ValueError(f"DEG recall needs at least 2 clusters with {par['min_group_size']} cells or more, found {len(groups)}") | ||
|
|
||
| keep = np.isin(labels, groups) |
There was a problem hiding this comment.
The cells of small clusters also get removed from the rest group, while the PR description says each cluster is tested against the remaining cells. On bmmc_cite/swap this drops 7 of 18 clusters and 172 of 1000 cells, and process_dataset deliberately favours rare cell types in the test set.
Passing groups= to rank_genes_groups skips testing them but keeps their cells in the rest (I checked this against a manual Mann-Whitney z-score). The effect calculation would need to use the same rest group.
| z_sol, effect_sol, padj_sol = de_one_vs_rest(sol[keep], codes, len(groups)) | ||
| z_pred, effect_pred, padj_pred = de_one_vs_rest(pred[keep], codes, len(groups)) | ||
|
|
||
| deg_sol = (padj_sol < par["padj_threshold"]) & (effect_sol > effect_threshold) |
There was a problem hiding this comment.
The p-value is two-sided and the effect is a difference of means, so a feature that is significantly lower by rank but higher on average counts as up-regulated. None of the 31k DEGs in the real solutions do this, so it's only a cheap safeguard, also for the predictions:
| deg_sol = (padj_sol < par["padj_threshold"]) & (effect_sol > effect_threshold) | |
| deg_sol = (padj_sol < par["padj_threshold"]) & (effect_sol > effect_threshold) & (z_sol > 0) | |
| deg_pred = (padj_pred < par["padj_threshold"]) & (effect_pred > effect_threshold) & (z_pred > 0) |
| n_deg_pred = deg_pred.sum(axis=1) | ||
| n_both = (deg_sol & deg_pred).sum(axis=1) | ||
| # 2 * precision * recall / (precision + recall), which is 0 when the prediction calls no DEGs | ||
| f1_per_group = 2 * n_both[n_deg_sol > 0] / (n_deg_sol + n_deg_pred)[n_deg_sol > 0] |
There was a problem hiding this comment.
Skipping clusters without ground-truth DEGs means that DEGs a prediction calls in those clusters are never penalised. This happens for 5 clusters across 3 of the 8 test sets, all ADT or ATAC. Is that intended? If so, I'd mention it in the metric description.
| description: Minimum difference of means (cluster minus rest) on the `normalized` layer for a DEG, when the target modality is ADT. | ||
| - name: "--effect_threshold_atac" | ||
| type: double | ||
| default: 0 |
There was a problem hiding this comment.
With a threshold of 0, ATAC F1 ignores the scale of the prediction: dividing the solution by 2 scores 1.00 on ATAC, but 0.38-0.62 on GEX and ADT. That contradicts "the prediction must be on the scale of the normalized layer" in the description. Either set a threshold for ATAC, or document that ATAC is the exception.
|
|
||
| ## NEW FUNCTIONALITY | ||
|
|
||
| * `deg`: Added the `deg_f1` and `spearman_z` metrics, which compare one-vs-rest Wilcoxon DE results of Leiden clusters of the ground truth between the ground truth and the prediction (PR #XX). |
There was a problem hiding this comment.
| * `deg`: Added the `deg_f1` and `spearman_z` metrics, which compare one-vs-rest Wilcoxon DE results of Leiden clusters of the ground truth between the ground truth and the prediction (PR #XX). | |
| * `deg`: Added the `deg_f1` and `spearman_z` metrics, which compare one-vs-rest Wilcoxon DE results of Leiden clusters of the ground truth between the ground truth and the prediction (PR #84). |
Describe your changes
Adds the
degmetric component with two metrics that check whether a prediction keeps the differences of gene/protein expression between cell populations. Both metrics use the same differential-expression (DE) setup:normalizedlayer (resolution 1). The same clusters are applied to the prediction.sc.tl.rank_genes_groups, tie-corrected, Benjamini-Hochberg). The test runs on the ground truth and on the prediction separately.Two metrics are calculted:
deg_f1(0 to 1, higher is better): A feature is a DEG of a cluster when it is up-regulated, has an adjusted p-value below 0.05, and has a difference of means above the target modality's threshold. Averaged over the clusters that have ground-truth DEGs. A cluster scores 0 when the prediction calls no DEGs.spearman_z(−1 to 1, higher is better): The Spearman correlation of the ground-truth and predicted DE z-scores over all features, averaged over the clusters. It compares each cluster's whole DE profile, not just the significant DEGs.Checklist before requesting a review
I have performed a self-review of my code
Check the correct box. Does this PR contain:
Proposed changes are described in the CHANGELOG.md
CI Tests succeed and look good!