180
crates/domain/tests/correlation/strategy_test.rs
Normal file
180
crates/domain/tests/correlation/strategy_test.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
use domain::activity::ActivityId;
|
||||
use domain::correlation::{CorrelationInput, CorrelationStrategy, Observation, SeriesShape};
|
||||
use domain::entry::{DayMood, Mood};
|
||||
use domain::metric::MetricKind;
|
||||
|
||||
fn day(mood: Mood) -> DayMood {
|
||||
DayMood::of(&[mood]).unwrap()
|
||||
}
|
||||
|
||||
fn observations(pairs: &[(f64, Mood)]) -> Vec<Observation> {
|
||||
pairs
|
||||
.iter()
|
||||
.map(|(value, mood)| Observation::new(*value, day(*mood)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn score(strategy: CorrelationStrategy, pairs: &[(f64, Mood)]) -> Option<f64> {
|
||||
strategy
|
||||
.score(&observations(pairs))
|
||||
.map(|coefficient| coefficient.value())
|
||||
}
|
||||
|
||||
const TIED: [(f64, Mood); 6] = [
|
||||
(1_000.0, Mood::Meh),
|
||||
(2_000.0, Mood::Meh),
|
||||
(3_000.0, Mood::Meh),
|
||||
(4_000.0, Mood::Good),
|
||||
(5_000.0, Mood::Good),
|
||||
(6_000.0, Mood::Rad),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn kendall_corrects_for_ties_rather_than_counting_pairs_alone() {
|
||||
let score = score(CorrelationStrategy::Kendall, &TIED).unwrap();
|
||||
|
||||
let tau_b = 0.856_348_838_6;
|
||||
let tau_a = 0.733_333_333_3;
|
||||
|
||||
assert!((score - tau_b).abs() < 1e-9, "expected tau-b, got {score}");
|
||||
assert!(
|
||||
(score - tau_a).abs() > 1e-9,
|
||||
"this is tau-a, which overstates disagreement when ranks are tied"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kendall_reaches_both_extremes() {
|
||||
let rising = score(
|
||||
CorrelationStrategy::Kendall,
|
||||
&[
|
||||
(1.0, Mood::Awful),
|
||||
(2.0, Mood::Bad),
|
||||
(3.0, Mood::Meh),
|
||||
(4.0, Mood::Good),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let falling = score(
|
||||
CorrelationStrategy::Kendall,
|
||||
&[
|
||||
(1.0, Mood::Good),
|
||||
(2.0, Mood::Meh),
|
||||
(3.0, Mood::Bad),
|
||||
(4.0, Mood::Awful),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!((rising - 1.0).abs() < 1e-12);
|
||||
assert!((falling + 1.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pearson_measures_the_line_where_the_rank_methods_measure_the_order() {
|
||||
let curved = [
|
||||
(1.0, Mood::Awful),
|
||||
(2.0, Mood::Bad),
|
||||
(3.0, Mood::Meh),
|
||||
(100.0, Mood::Good),
|
||||
];
|
||||
|
||||
let pearson = score(CorrelationStrategy::Pearson, &curved).unwrap();
|
||||
let spearman = score(CorrelationStrategy::Spearman, &curved).unwrap();
|
||||
|
||||
assert!((spearman - 1.0).abs() < 1e-12, "the order is perfect");
|
||||
assert!(
|
||||
(pearson - 0.785_026_421).abs() < 1e-9,
|
||||
"the line is not, got {pearson}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mean_difference_is_told_as_a_share_of_the_mood_scale() {
|
||||
let score = score(
|
||||
CorrelationStrategy::MeanDifference,
|
||||
&[
|
||||
(1.0, Mood::Good),
|
||||
(1.0, Mood::Rad),
|
||||
(1.0, Mood::Good),
|
||||
(1.0, Mood::Rad),
|
||||
(0.0, Mood::Bad),
|
||||
(0.0, Mood::Meh),
|
||||
(0.0, Mood::Bad),
|
||||
(0.0, Mood::Awful),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
(score - 0.625).abs() < 1e-12,
|
||||
"two and a half mood points out of four, got {score}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mean_difference_needs_days_on_both_sides() {
|
||||
let only_present = score(
|
||||
CorrelationStrategy::MeanDifference,
|
||||
&[(1.0, Mood::Good), (1.0, Mood::Rad)],
|
||||
);
|
||||
|
||||
let only_absent = score(
|
||||
CorrelationStrategy::MeanDifference,
|
||||
&[(0.0, Mood::Good), (0.0, Mood::Rad)],
|
||||
);
|
||||
|
||||
assert!(only_present.is_none());
|
||||
assert!(only_absent.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_strategy_scores_one_shape_of_series_and_an_input_has_one() {
|
||||
assert_eq!(
|
||||
CorrelationStrategy::MeanDifference.scores(),
|
||||
SeriesShape::Presence
|
||||
);
|
||||
assert_eq!(
|
||||
CorrelationInput::Activity(ActivityId::generate()).series(),
|
||||
SeriesShape::Presence
|
||||
);
|
||||
|
||||
for continuous in [
|
||||
CorrelationStrategy::Pearson,
|
||||
CorrelationStrategy::Spearman,
|
||||
CorrelationStrategy::Kendall,
|
||||
] {
|
||||
assert_eq!(continuous.scores(), SeriesShape::Continuous);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
CorrelationInput::Metric(MetricKind::Steps).series(),
|
||||
SeriesShape::Continuous
|
||||
);
|
||||
assert_eq!(
|
||||
CorrelationInput::MoonPhase.series(),
|
||||
SeriesShape::Continuous
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_matching_strategies_are_offered_for_an_input() {
|
||||
let steps = CorrelationInput::Metric(MetricKind::Steps);
|
||||
let exercise = CorrelationInput::Activity(ActivityId::generate());
|
||||
|
||||
let for_steps: Vec<&str> = CorrelationStrategy::ALL
|
||||
.into_iter()
|
||||
.filter(|strategy| strategy.can_score(&steps))
|
||||
.map(|strategy| strategy.name())
|
||||
.collect();
|
||||
|
||||
let for_exercise: Vec<&str> = CorrelationStrategy::ALL
|
||||
.into_iter()
|
||||
.filter(|strategy| strategy.can_score(&exercise))
|
||||
.map(|strategy| strategy.name())
|
||||
.collect();
|
||||
|
||||
assert_eq!(for_steps, ["pearson", "spearman", "kendall"]);
|
||||
assert_eq!(for_exercise, ["meanDifference"]);
|
||||
}
|
||||
Reference in New Issue
Block a user