pub fn percentile_rank(cohort: &[f64], v: f64) -> f64 {
if !v.is_finite() {
return 0.0;
}
let mut n = 0usize;
let mut below = 0usize;
let mut equal = 0usize;
for &c in cohort {
if !c.is_finite() {
continue;
}
n += 1;
if c < v {
below += 1;
} else if c == v {
equal += 1;
}
}
if n == 0 {
return 0.0;
}
(below as f64 + 0.5 * equal as f64) / n as f64
}
pub const MIN_COHORT: usize = 5;
pub fn pe_industry_pctile(pe: f64, cohort: &[f64]) -> Option<f64> {
if !pe.is_finite() || pe <= 0.0 {
return None;
}
let finite = cohort.iter().filter(|c| c.is_finite()).count();
if finite < MIN_COHORT {
return None;
}
Some(percentile_rank(cohort, pe) * 100.0)
}
pub fn analyst_upside_pct(target: f64, close: f64) -> Option<f64> {
if !target.is_finite() || !close.is_finite() || close <= 0.0 {
return None;
}
Some((target - close) / close * 100.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn percentile_rank_midrank_and_edges() {
let cohort = [10.0, 20.0, 30.0, 40.0, 50.0];
assert!((percentile_rank(&cohort, 30.0) - 0.5).abs() < 1e-9);
assert_eq!(percentile_rank(&[], 1.0), 0.0);
assert_eq!(percentile_rank(&cohort, f64::NAN), 0.0);
}
#[test]
fn pe_pctile_scale_and_thin_cohort() {
let cohort = [10.0, 20.0, 30.0, 40.0, 50.0];
assert_eq!(pe_industry_pctile(30.0, &cohort), Some(50.0));
assert_eq!(pe_industry_pctile(30.0, &[10.0, 20.0]), None);
assert_eq!(pe_industry_pctile(-1.0, &cohort), None);
}
#[test]
fn upside() {
assert_eq!(analyst_upside_pct(120.0, 100.0), Some(20.0));
assert!(analyst_upside_pct(1.0, 0.0).is_none());
assert!(analyst_upside_pct(f64::NAN, 100.0).is_none());
}
}