Skip to main content

choir_queue/
envelope.rs

1//! What precision a detector can reach at *our* prevalence (D23).
2//!
3//! Published detector numbers are measured on corpora that are roughly
4//! one-third positive, because they sample same-declaration parallel
5//! edits. Deployed against a mainline where bad merges are well under
6//! one percent, the same detector reports the same recall and a
7//! completely different precision — the false positives are drawn from a
8//! vastly larger pool of negatives.
9//!
10//! That transformation is the difference between "60% recall at 43%
11//! precision, promising" and "one true alarm per 160", so it should be
12//! computed rather than eyeballed. Recall and specificity are properties
13//! of the detector and carry across; precision is a property of the
14//! detector *and the population* and does not.
15//!
16//! # What the prevalence here actually is
17//!
18//! The measured 0.004 is `choir-queue::corpus`'s revert-labelled bad-merge
19//! rate on rust-lang/rust — merges that history took back. That is not
20//! the same event as "merges containing a semantic conflict": it misses
21//! defects fixed forward and includes reverts that were not defects. It
22//! is used because it is the only prevalence anyone has measured on a
23//! real mainline, and because the operational question is about flagging
24//! merges, not about conflicts in the abstract.
25
26/// A detector's prevalence-independent behaviour.
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct Detector {
29    /// Fraction of genuinely bad merges it flags. Carries across
30    /// populations.
31    pub recall: f64,
32    /// Fraction of genuinely good merges it flags anyway. Carries across
33    /// populations, and is the number that decides everything at low
34    /// prevalence.
35    pub false_positive_rate: f64,
36}
37
38impl Detector {
39    /// Recovers the prevalence-independent behaviour from a paper's
40    /// reported `precision` and `recall` at that paper's corpus
41    /// `prevalence`.
42    ///
43    /// # Errors
44    ///
45    /// Inputs outside `0.0..=1.0`, a zero precision or recall (nothing to
46    /// recover), or a corpus prevalence of 1.0 (no negatives to have
47    /// produced the reported false positives).
48    pub fn from_reported(precision: f64, recall: f64, prevalence: f64) -> Result<Self, String> {
49        for (name, v) in [
50            ("precision", precision),
51            ("recall", recall),
52            ("prevalence", prevalence),
53        ] {
54            if !(0.0..=1.0).contains(&v) || v.is_nan() {
55                return Err(format!("{name} must be in 0.0..=1.0, got {v}"));
56            }
57        }
58        if precision <= 0.0 || recall <= 0.0 {
59            return Err("precision and recall must be positive to recover a rate".to_string());
60        }
61        if prevalence >= 1.0 {
62            return Err(
63                "a corpus with no negatives cannot yield a false-positive rate".to_string(),
64            );
65        }
66        // true positives per merge, then false positives per merge from
67        // precision = TP / (TP + FP).
68        let tp = prevalence * recall;
69        let fp = tp * (1.0 - precision) / precision;
70        Ok(Self {
71            recall,
72            false_positive_rate: fp / (1.0 - prevalence),
73        })
74    }
75
76    /// Precision this detector reaches at `prevalence`.
77    ///
78    /// Returns 0.0 when it would flag nothing at all, which is the honest
79    /// reading: a detector that never fires has no precision to speak of.
80    #[must_use]
81    pub fn precision_at(&self, prevalence: f64) -> f64 {
82        let tp = prevalence * self.recall;
83        let fp = (1.0 - prevalence) * self.false_positive_rate;
84        if tp + fp <= 0.0 {
85            return 0.0;
86        }
87        tp / (tp + fp)
88    }
89
90    /// False alarms per true alarm at `prevalence` — the number an
91    /// on-call human actually experiences. `None` when it never fires on
92    /// a true positive, so the ratio is undefined rather than infinite.
93    #[must_use]
94    pub fn false_alarms_per_hit(&self, prevalence: f64) -> Option<f64> {
95        let tp = prevalence * self.recall;
96        if tp <= 0.0 {
97            return None;
98        }
99        Some((1.0 - prevalence) * self.false_positive_rate / tp)
100    }
101}
102
103/// The false-positive rate a detector must not exceed to reach
104/// `target_precision` at `prevalence` with `recall`.
105///
106/// This is the build-or-don't-build number: compare it against what the
107/// literature actually achieves, or against a test suite's flake rate.
108///
109/// # Errors
110///
111/// Inputs outside `0.0..=1.0`, or a target precision of 1.0 (which
112/// demands a zero false-positive rate and is not a useful target).
113pub fn required_false_positive_rate(
114    prevalence: f64,
115    recall: f64,
116    target_precision: f64,
117) -> Result<f64, String> {
118    for (name, v) in [
119        ("prevalence", prevalence),
120        ("recall", recall),
121        ("target_precision", target_precision),
122    ] {
123        if !(0.0..=1.0).contains(&v) || v.is_nan() {
124            return Err(format!("{name} must be in 0.0..=1.0, got {v}"));
125        }
126    }
127    if target_precision >= 1.0 {
128        return Err("a target precision of 1.0 demands zero false positives".to_string());
129    }
130    if prevalence >= 1.0 {
131        return Err("prevalence must leave some negatives".to_string());
132    }
133    // precision = TP / (TP + FP) = target  =>  FP = TP * (1 - target) / target
134    let tp = prevalence * recall;
135    let fp = tp * (1.0 - target_precision) / target_precision;
136    Ok(fp / (1.0 - prevalence))
137}