use crate::metrics::{self, Scope};
use crate::result::{AnalysisResult, FileResult};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq)]
pub struct Unit {
pub language: String,
pub metrics: BTreeMap<String, Option<f64>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UnitScope {
File,
Function,
}
impl UnitScope {
fn scope(self) -> Scope {
match self {
UnitScope::File => Scope::File,
UnitScope::Function => Scope::Function,
}
}
}
pub fn units(results: &[AnalysisResult], scope: UnitScope) -> Vec<Unit> {
let mut units = vec![];
for file in results.iter().flat_map(|r| &r.files) {
if let FileResult::Ok {
language,
metrics,
functions,
..
} = file
{
let unit = |m: &BTreeMap<String, Option<f64>>| Unit {
language: language.clone(),
metrics: m.clone(),
};
match scope {
UnitScope::File => units.push(unit(&metrics.metrics)),
UnitScope::Function => {
units.extend(functions.iter().map(|f| unit(&f.metrics.metrics)))
}
}
}
}
units
}
pub fn metric_ids(scope: UnitScope) -> Vec<&'static str> {
metrics::specs()
.into_iter()
.filter(|d| d.scopes.contains(&scope.scope()))
.map(|d| d.id)
.collect()
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Summary {
pub n: usize,
pub missing: usize,
pub mean: Option<f64>,
pub sd: Option<f64>,
pub min: Option<f64>,
pub q1: Option<f64>,
pub median: Option<f64>,
pub q3: Option<f64>,
pub max: Option<f64>,
}
impl Summary {
pub fn of(values: &[f64], missing: usize) -> Summary {
let mut sorted = values.to_vec();
sorted.sort_by(f64::total_cmp);
let n = sorted.len();
let mean = (n > 0).then(|| sorted.iter().sum::<f64>() / n as f64);
let sd = mean
.filter(|_| n > 1)
.map(|m| (sorted.iter().map(|v| (v - m).powi(2)).sum::<f64>() / (n - 1) as f64).sqrt());
let q = |p| quantile(&sorted, p);
Summary {
n,
missing,
mean,
sd,
min: sorted.first().copied(),
q1: q(0.25),
median: q(0.5),
q3: q(0.75),
max: sorted.last().copied(),
}
}
}
fn quantile(sorted: &[f64], p: f64) -> Option<f64> {
let last = sorted.len().checked_sub(1)?;
let h = p * last as f64;
let (lower, upper) = (h.floor() as usize, h.ceil() as usize);
Some(sorted[lower] + (h - lower as f64) * (sorted[upper] - sorted[lower]))
}
pub fn pearson(x: &[f64], y: &[f64]) -> Option<f64> {
let n = x.len();
if n < 3 {
return None;
}
let (mx, my) = (
x.iter().sum::<f64>() / n as f64,
y.iter().sum::<f64>() / n as f64,
);
let (mut sxy, mut sxx, mut syy) = (0.0, 0.0, 0.0);
for (a, b) in x.iter().zip(y) {
sxy += (a - mx) * (b - my);
sxx += (a - mx).powi(2);
syy += (b - my).powi(2);
}
(sxx > 0.0 && syy > 0.0).then(|| sxy / (sxx * syy).sqrt())
}
pub fn spearman(x: &[f64], y: &[f64]) -> Option<f64> {
pearson(&ranks(x), &ranks(y))
}
pub fn ranks(values: &[f64]) -> Vec<f64> {
let mut order: Vec<usize> = (0..values.len()).collect();
order.sort_by(|a, b| values[*a].total_cmp(&values[*b]));
let mut ranks = vec![0.0; values.len()];
let mut start = 0;
while start < order.len() {
let end = start
+ order[start..]
.iter()
.take_while(|i| values[**i] == values[order[start]])
.count();
let average = (start + end + 1) as f64 / 2.0;
for i in &order[start..end] {
ranks[*i] = average;
}
start = end;
}
ranks
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Correlation {
pub a: String,
pub b: String,
pub n: usize,
pub pearson: Option<f64>,
pub spearman: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Report {
pub units: usize,
pub metrics: BTreeMap<String, Summary>,
pub by_language: BTreeMap<String, BTreeMap<String, Summary>>,
pub correlations: Vec<Correlation>,
}
impl Report {
pub fn of(units: &[Unit], ids: &[&str]) -> Report {
let columns: Vec<Vec<Option<f64>>> = ids
.iter()
.map(|id| {
units
.iter()
.map(|u| u.metrics.get(*id).copied().flatten())
.collect()
})
.collect();
let summarize = |rows: &[usize]| -> BTreeMap<String, Summary> {
ids.iter()
.zip(&columns)
.map(|(id, column)| {
let values: Vec<f64> = rows.iter().filter_map(|r| column[*r]).collect();
(
id.to_string(),
Summary::of(&values, rows.len() - values.len()),
)
})
.collect()
};
let mut languages: BTreeMap<&str, Vec<usize>> = BTreeMap::new();
for (row, u) in units.iter().enumerate() {
languages.entry(&u.language).or_default().push(row);
}
let mut correlations = vec![];
for (i, a) in ids.iter().enumerate() {
for (j, b) in ids.iter().enumerate().skip(i + 1) {
let (x, y): (Vec<f64>, Vec<f64>) = columns[i]
.iter()
.zip(&columns[j])
.filter_map(|(x, y)| Some(((*x)?, (*y)?)))
.unzip();
correlations.push(Correlation {
a: a.to_string(),
b: b.to_string(),
n: x.len(),
pearson: pearson(&x, &y),
spearman: spearman(&x, &y),
});
}
}
Report {
units: units.len(),
metrics: summarize(&(0..units.len()).collect::<Vec<_>>()),
by_language: languages
.into_iter()
.map(|(l, rows)| (l.to_string(), summarize(&rows)))
.collect(),
correlations,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: Option<f64>, b: f64) {
let a = a.expect("value");
assert!((a - b).abs() < 1e-9, "{a} != {b}");
}
#[test]
fn describes_values_with_type7_quantiles() {
let s = Summary::of(&[4.0, 1.0, 3.0, 2.0], 1);
assert_eq!((s.n, s.missing), (4, 1));
close(s.mean, 2.5);
close(s.sd, (5.0f64 / 3.0).sqrt());
close(s.min, 1.0);
close(s.q1, 1.75);
close(s.median, 2.5);
close(s.q3, 3.25);
close(s.max, 4.0);
}
#[test]
fn small_samples_have_null_statistics() {
let empty = Summary::of(&[], 2);
assert_eq!((empty.n, empty.mean, empty.median), (0, None, None));
let one = Summary::of(&[5.0], 0);
assert_eq!((one.mean, one.sd), (Some(5.0), None));
}
#[test]
fn pearson_and_spearman() {
close(pearson(&[1.0, 2.0, 3.0, 4.0], &[2.0, 4.0, 6.0, 8.0]), 1.0);
close(pearson(&[1.0, 2.0, 3.0], &[3.0, 2.0, 1.0]), -1.0);
assert_eq!(pearson(&[1.0, 2.0, 3.0], &[5.0, 5.0, 5.0]), None);
assert_eq!(pearson(&[1.0, 2.0], &[1.0, 2.0]), None);
close(
spearman(&[1.0, 2.0, 3.0, 4.0], &[1.0, 4.0, 9.0, 100.0]),
1.0,
);
}
#[test]
fn ranks_average_ties() {
assert_eq!(ranks(&[10.0, 20.0, 10.0, 30.0]), vec![1.5, 3.0, 1.5, 4.0]);
}
fn unit(language: &str, values: &[(&str, Option<f64>)]) -> Unit {
Unit {
language: language.to_string(),
metrics: values.iter().map(|(k, v)| (k.to_string(), *v)).collect(),
}
}
#[test]
fn report_uses_pairwise_complete_observations_and_groups_by_language() {
let units = vec![
unit("c", &[("a", Some(1.0)), ("b", Some(2.0))]),
unit("c", &[("a", Some(2.0)), ("b", Some(4.0))]),
unit("python", &[("a", Some(3.0)), ("b", None)]),
unit("python", &[("a", Some(4.0)), ("b", Some(8.0))]),
];
let report = Report::of(&units, &["a", "b"]);
assert_eq!(report.units, 4);
assert_eq!((report.metrics["b"].n, report.metrics["b"].missing), (3, 1));
close(report.by_language["python"]["a"].mean, 3.5);
let ab = &report.correlations[0];
assert_eq!((ab.a.as_str(), ab.b.as_str(), ab.n), ("a", "b", 3));
close(ab.pearson, 1.0);
close(ab.spearman, 1.0);
}
}