use super::*;
use crate::analysis::data_quality::fixtures::measure;
use crate::analysis::data_quality::{ExpectedWindows, QualityCompute, UnsampledSegment};
use polars::prelude::{DataType, IntoLazy, LazyFrame, col, df};
fn at(text: &str) -> NaiveDateTime {
NaiveDateTime::parse_from_str(text, "%Y-%m-%d %H:%M").unwrap()
}
#[test]
fn window_labels_read_back_to_their_start() {
for (every, start, label) in [
("1h", "2024-03-05 13:00", "2024-03-05 13:00"),
("1d", "2024-03-05 00:00", "2024-03-05"),
("1w", "2024-03-04 00:00", "week of 2024-03-04"),
("1mo", "2024-03-01 00:00", "2024-03"),
] {
let start_text = at(start).format("%Y-%m-%d %H:%M:%S").to_string();
assert_eq!(
crate::analysis::data_quality::time_window_label("day", every, Some(&start_text)),
label
);
assert_eq!(window_start(label, every), Some(at(start)), "{label}");
assert_eq!(floor_window(at("2024-03-05 13:27"), every), at(start));
}
assert_eq!(window_start("day ∅", "1d"), None);
assert_eq!(
next_window(at("2024-01-01 00:00"), "1mo"),
Some(at("2024-02-01 00:00"))
);
assert_eq!(
window_count(at("2024-01-01 00:00"), at("2024-04-01 00:00"), "1mo"),
3
);
assert_eq!(
window_count(at("2024-01-01 00:00"), at("2024-04-02 00:00"), "1mo"),
4
);
assert_eq!(
window_count(at("2024-01-01 00:00"), at("2024-01-08 00:00"), "1d"),
7
);
assert_eq!(
calendar_span(at("2024-01-01 00:00"), at("2024-01-22 00:00"), "1w"),
"2024-01-01 to 2024-01-28"
);
assert_eq!(
calendar_span(at("2024-01-01 05:00"), at("2024-01-01 05:00"), "1h"),
"2024-01-01 05:00 to 2024-01-01 05:59"
);
}
#[test]
fn a_wilson_interval_stays_honest_at_the_edges() {
let (low, high) = wilson_interval(5.0, 100.0).unwrap();
assert!(
low < 0.05 && 0.05 < high && high - low < 0.12,
"{low} {high}"
);
let (low, high) = wilson_interval(0.0, 10.0).unwrap();
assert_eq!(low, 0.0);
assert!(high > 0.25, "ten rows say little: {high}");
let (low, high) = wilson_interval(10.0, 10.0).unwrap();
assert!((1.0 - high).abs() < 1e-12 && low < 0.75, "{low} {high}");
assert!(wilson_interval(0.0, 0.0).is_none());
}
fn weekdays() -> LazyFrame {
let monday = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
let days = (0..56)
.map(|day| monday + Duration::days(day))
.filter(|day| day.weekday().num_days_from_monday() < 5)
.filter(|day| !(7..14).contains(&(*day - monday).num_days()))
.collect::<Vec<_>>();
let rows = days
.iter()
.flat_map(|day| std::iter::repeat_n(*day, 40))
.collect::<Vec<_>>();
let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
let day = rows
.iter()
.map(|day| (*day - epoch).num_days() as i32)
.collect::<Vec<_>>();
let value = rows
.iter()
.enumerate()
.map(|(row, day)| ((*day - monday).num_days() < 35 || row % 2 == 0).then_some(row as i64))
.collect::<Vec<_>>();
df!("day" => day, "value" => value)
.unwrap()
.lazy()
.with_column(col("day").cast(DataType::Date))
}
fn daily(compute: QualityCompute, rows: usize) -> DataQualityPlan {
DataQualityPlan {
compute,
dataset_rows: rows,
grain: QualityGrain::TimeWindows {
column: "day".to_string(),
every: "1d".to_string(),
},
..DataQualityPlan::default()
}
}
#[test]
fn gaps_appear_only_with_stated_windows() {
let frame = weekdays();
let plan = daily(QualityCompute::Full, 0);
let results = measure(&frame, None, &plan);
assert_eq!(expected_gaps(&plan, &results), None);
let whole = DataQualityPlan {
grain: QualityGrain::Dataset,
expected: Some(ExpectedWindows::default()),
..plan.clone()
};
assert_eq!(expected_gaps(&whole, &results), None);
}
#[test]
fn an_exact_count_calls_a_window_empty() {
let frame = weekdays();
let mut plan = daily(QualityCompute::Full, 0);
plan.expected = Some(ExpectedWindows::default());
let results = measure(&frame, None, &plan);
let Some(Gaps::Checked(every_day)) = expected_gaps(&plan, &results) else {
panic!("checked");
};
assert_eq!(every_day.expected, 54, "first Monday to the last Friday");
assert_eq!(every_day.with_rows, 35);
assert_eq!(
every_day.empty, 19,
"seven days of week two and six weekends"
);
assert_eq!((every_day.unsampled, every_day.out_of_scope), (0, 0));
assert!(every_day.counted);
plan.expected = Some(ExpectedWindows {
weekdays: true,
..ExpectedWindows::default()
});
let Some(Gaps::Checked(weekdays)) = expected_gaps(&plan, &results) else {
panic!("checked");
};
assert_eq!(weekdays.weekend, 14);
assert_eq!(weekdays.empty, 5);
assert_eq!(
weekdays.runs,
[GapRun {
kind: GapKind::Empty,
first: at("2024-01-08 00:00"),
last: at("2024-01-12 00:00"),
windows: 5,
rows: None,
}]
);
plan.expected = Some(ExpectedWindows {
weekdays: true,
from: Some("2024-01-01".to_string()),
before: Some("2024-03-04".to_string()),
});
let Some(Gaps::Checked(longer)) = expected_gaps(&plan, &results) else {
panic!("checked");
};
assert_eq!(longer.empty, 5 + 5);
assert_eq!(longer.runs.len(), 2);
}
#[test]
fn a_window_the_sample_missed_is_not_empty() {
let frame = weekdays();
let mut plan = daily(QualityCompute::Sample, 30);
plan.expected = Some(ExpectedWindows {
weekdays: true,
..ExpectedWindows::default()
});
let results = measure(&frame, Some(35 * 40), &plan);
assert_eq!(results.precision, QualityPrecision::Sampled);
assert!(
!results.unsampled_segments.is_empty(),
"30 rows cannot reach 35 days"
);
assert!(
results
.unsampled_segments
.iter()
.all(|segment| segment.total_rows == 40)
);
let Some(Gaps::Checked(check)) = expected_gaps(&plan, &results) else {
panic!("checked");
};
assert!(check.counted);
assert_eq!(check.unsampled, results.unsampled_segments.len());
assert_eq!(check.empty, 5, "the missing week, from the count");
assert_eq!(check.with_rows + check.unsampled, 35);
let missed = check
.runs
.iter()
.filter(|run| run.kind == GapKind::Unsampled)
.map(|run| run.rows)
.collect::<Vec<_>>();
assert!(
missed
.iter()
.all(|rows| rows.is_some_and(|rows| rows % 40 == 0))
);
let view = trend_view(&results, QualityMetric::NullRate, 35);
assert_eq!(view.slots.len(), 35);
assert_eq!(view.per_bar, 1);
assert_eq!(view.coverage().0, results.unsampled_segments.len());
let (unsampled, thin) = view.coverage();
assert_eq!(segment_coverage(&results), (view.sampled, unsampled, thin));
assert_eq!(view.lines[0].names, ["rows"]);
assert_eq!(view.lines[1].names, ["sampled rows"]);
let missed = view.bars.iter().position(|bar| bar.unsampled == 1).unwrap();
assert_eq!(view.lines[0].bars[missed], Some(40.0), "counted");
assert_eq!(view.lines[1].bars[missed], Some(0.0), "drawn none");
if let Some(value) = view.lines.iter().find(|line| line.names == ["value"]) {
assert_eq!(value.bars[missed], None);
}
}
#[test]
fn windows_outside_the_scope_are_out_of_scope() {
let frame = weekdays();
let mut plan = daily(QualityCompute::Full, 0);
plan.scope = QualityScope::SourceTimeRange {
column: "day".to_string(),
start: "2024-01-15".to_string(),
end: "2024-02-01".to_string(),
};
plan.expected = Some(ExpectedWindows {
weekdays: true,
from: Some("2024-01-01".to_string()),
before: Some("2024-02-05".to_string()),
});
let scoped =
crate::analysis::data_quality::apply_quality_scope(frame, &plan.scope, None).unwrap();
let results = measure(&scoped, None, &plan);
let Some(Gaps::Checked(check)) = expected_gaps(&plan, &results) else {
panic!("checked");
};
assert_eq!(
check.out_of_scope,
10 + 2,
"two weeks before, two days after"
);
assert_eq!(check.empty, 0);
assert_eq!(check.with_rows, 13);
assert_eq!(check.runs[0].kind, GapKind::OutOfScope);
plan.grain = QualityGrain::TimeWindows {
column: "day".to_string(),
every: "1w".to_string(),
};
plan.scope = QualityScope::SourceTimeRange {
column: "day".to_string(),
start: "2024-01-17".to_string(),
end: "2024-02-07".to_string(),
};
let scoped =
crate::analysis::data_quality::apply_quality_scope(weekdays(), &plan.scope, None).unwrap();
let results = measure(&scoped, None, &plan);
let Some(Gaps::Checked(check)) = expected_gaps(&plan, &results) else {
panic!("checked");
};
assert_eq!(check.expected, 5, "January 1 to before February 5");
assert_eq!(
check.with_rows, 3,
"the weeks of January 15, 22 and 29: {check:?}"
);
assert_eq!(check.out_of_scope, 2, "the weeks of January 1 and 8");
}
#[test]
fn the_windows_checked_are_bounded() {
let frame = weekdays();
let mut plan = DataQualityPlan {
compute: QualityCompute::Full,
grain: QualityGrain::TimeWindows {
column: "day".to_string(),
every: "1h".to_string(),
},
..DataQualityPlan::default()
};
plan.expected = Some(ExpectedWindows {
from: Some("2020-01-01".to_string()),
before: Some("2024-01-01".to_string()),
..ExpectedWindows::default()
});
let results = measure(&frame, None, &plan);
assert_eq!(
expected_gaps(&plan, &results),
Some(Gaps::TooMany { windows: 35_064 })
);
}
#[test]
fn trend_lines_keep_their_order_at_any_width() {
let rows: i32 = 60 * 20;
let frame = df!(
"day" => (0..rows).map(|row| 19_723 + row / 20).collect::<Vec<_>>(),
"steady" => (0..rows).map(|row| (row % 10 != 0).then_some(1i64)).collect::<Vec<_>>(),
"late" => (0..rows).map(|row| (row < 900 || row % 2 == 0).then_some(1i64)).collect::<Vec<_>>(),
"spike" => (0..rows).map(|row| !(400..440).contains(&row)).map(|kept| kept.then_some(1i64)).collect::<Vec<_>>(),
)
.unwrap()
.lazy()
.with_column(col("day").cast(DataType::Date));
let results = measure(&frame, None, &daily(QualityCompute::Full, 0));
let order = |bars: usize| {
trend_view(&results, QualityMetric::NullRate, bars)
.lines
.into_iter()
.map(|line| line.names)
.collect::<Vec<_>>()
};
let first = order(8);
assert_eq!(first.len(), 4, "rows and three columns: {first:?}");
for bars in [1, 15, 23, 60, 200] {
assert_eq!(order(bars), first, "{bars} bars");
}
}
#[test]
fn missed_segments_keep_their_place() {
let frame = weekdays();
let plan = daily(QualityCompute::Full, 0);
let mut results = measure(&frame, None, &plan);
let moved = results.segments.remove(3);
results.unsampled_segments.push(UnsampledSegment {
label: moved.label.clone(),
total_rows: 40,
});
let slots = trend_slots(&results);
assert_eq!(slots[3].label, moved.label);
assert!(slots[3].profile.is_none());
}