This commit is contained in:
Andras Schmelczer 2026-06-25 22:29:52 +01:00
parent 2efa4d9f47
commit 5e73287eaf
99 changed files with 6392 additions and 1462 deletions

View file

@ -11,8 +11,8 @@ use crate::data::{FeatureStats, PostcodePoiMetrics, PropertyData};
use crate::utils::{postcode_outcode, postcode_sector};
use super::hexagon_stats::{
CrimeAreaAverage, CrimeYearPoint, CrimeYearStats, EnumFeatureStats, HistogramStats,
NumericFeatureStats, PricePoint,
CrimeAreaAverage, CrimeYearPoint, CrimeYearStats, EnumFeatureStats,
HistogramStats, NumericFeatureStats, PricePoint,
};
/// Extract price history (year, price) pairs from matching rows, downsampled if needed.
@ -352,11 +352,14 @@ pub fn compute_crime_by_year(
let mut out = Vec::new();
for (type_idx, name) in crime_by_year.crime_types.iter().enumerate() {
// Crime types in the by-year side table are bare (e.g. "Burglary"), while
// the configured feature names carry an " (avg/yr)" suffix. Match either
// form so callers can pass the feature names they already know.
// the configured feature names carry a window suffix ("Burglary (/yr,
// 7y)"). Emit the bare-type trend if the bare name is requested directly or
// any of its windowed features is.
if fields_specified {
let with_suffix = format!("{name} (avg/yr)");
if !field_set.contains(name.as_str()) && !field_set.contains(with_suffix.as_str()) {
let prefix = format!("{name} (");
if !field_set.contains(name.as_str())
&& !field_set.iter().any(|f| f.starts_with(&prefix))
{
continue;
}
}
@ -395,6 +398,7 @@ pub fn compute_crime_by_year(
out
}
/// Latest year present anywhere in the by-year crime dataset. The client
/// compares each selection's last charted year against this to caption
/// force-level publication gaps (e.g. Greater Manchester ends mid-2019) as
@ -440,13 +444,10 @@ pub fn area_crime_averages_for(
let mut out = Vec::new();
for (idx, name) in averages.crime_types.iter().enumerate() {
// Crime types are bare here ("Burglary"); requested fields may carry the
// " (avg/yr)" suffix — accept either form, like compute_crime_by_year.
if fields_specified {
let with_suffix = format!("{name} (avg/yr)");
if !field_set.contains(name.as_str()) && !field_set.contains(with_suffix.as_str()) {
continue;
}
// `name` is the full crime-feature name here (e.g. "Burglary (/yr,
// 7y)"), matching exactly the feature fields the caller requests.
if fields_specified && !field_set.contains(name.as_str()) {
continue;
}
let national_val = finite_at(Some(&averages.national), idx);
let outcode_val = finite_at(outcode_means, idx);
@ -595,7 +596,10 @@ mod tests {
let mut by_sector = rustc_hash::FxHashMap::default();
by_sector.insert("E14 2".to_string(), vec![5.0, 7.0]);
AreaCrimeAverages {
crime_types: vec!["Burglary".to_string(), "Robbery".to_string()],
crime_types: vec![
"Burglary (/yr, 7y)".to_string(),
"Robbery (/yr, 7y)".to_string(),
],
national: vec![8.0, 6.0],
by_outcode,
by_sector,
@ -611,12 +615,18 @@ mod tests {
assert_eq!(sector.as_deref(), Some("E14 2"));
assert_eq!(out.len(), 2);
let burglary = out.iter().find(|c| c.name == "Burglary").unwrap();
let burglary = out
.iter()
.find(|c| c.name == "Burglary (/yr, 7y)")
.unwrap();
assert_eq!(burglary.national, Some(8.0));
assert_eq!(burglary.outcode, Some(10.0));
assert_eq!(burglary.sector, Some(5.0));
let robbery = out.iter().find(|c| c.name == "Robbery").unwrap();
let robbery = out
.iter()
.find(|c| c.name == "Robbery (/yr, 7y)")
.unwrap();
assert_eq!(robbery.national, Some(6.0));
// The outcode value was NaN — dropped to None; the sector value is finite.
assert_eq!(robbery.outcode, None);
@ -626,11 +636,13 @@ mod tests {
#[test]
fn area_crime_averages_respect_fields_filter() {
let avgs = sample_averages();
// The suffixed feature-name form is accepted, like compute_crime_by_year.
let fields: HashSet<String> = ["Burglary (avg/yr)".to_string()].into_iter().collect();
// Area averages are keyed by the full crime-feature name.
let fields: HashSet<String> = ["Burglary (/yr, 7y)".to_string()]
.into_iter()
.collect();
let (_, _, out) = area_crime_averages_for(Some("E14 2DG"), &avgs, true, &fields);
assert_eq!(out.len(), 1);
assert_eq!(out[0].name, "Burglary");
assert_eq!(out[0].name, "Burglary (/yr, 7y)");
}
#[test]