lgtm
This commit is contained in:
parent
f7e0814a38
commit
fd2860070a
55 changed files with 4084 additions and 186 deletions
|
|
@ -26,7 +26,9 @@ use rustc_hash::FxHashMap;
|
|||
use serde::Serialize;
|
||||
|
||||
use crate::consts::NAN_U16;
|
||||
use crate::data::area_crime_averages::{AreaCrimeAverages, AVG_YR_SUFFIX};
|
||||
use crate::data::spill::SpillVec;
|
||||
use crate::utils::{postcode_outcode, postcode_sector};
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct RenovationEvent {
|
||||
|
|
@ -224,6 +226,109 @@ impl PropertyData {
|
|||
num_numeric: self.num_numeric,
|
||||
}
|
||||
}
|
||||
|
||||
/// Precompute mean headline crime rates nationally and per outcode / postcode
|
||||
/// sector.
|
||||
///
|
||||
/// Crime values are identical for every property in a postcode (the pipeline
|
||||
/// merges them on the postcode key), so each postcode is sampled once from
|
||||
/// its first row and property-weighted by its row count. All three scopes use
|
||||
/// the same exact property-weighted estimator over the same row universe as
|
||||
/// the per-selection mean, so the four numbers shown in a crime row (this
|
||||
/// selection / sector / outcode / nation) are directly comparable — without
|
||||
/// the upward bias of the histogram-bin national average.
|
||||
pub fn compute_area_crime_averages(&self) -> AreaCrimeAverages {
|
||||
// Crime headline columns are exactly the " (avg/yr)" features.
|
||||
let crime_indices: Vec<usize> = self
|
||||
.feature_names
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, name)| name.ends_with(AVG_YR_SUFFIX))
|
||||
.map(|(idx, _)| idx)
|
||||
.collect();
|
||||
if crime_indices.is_empty() {
|
||||
return AreaCrimeAverages::empty();
|
||||
}
|
||||
let crime_types: Vec<String> = crime_indices
|
||||
.iter()
|
||||
.map(|&idx| {
|
||||
self.feature_names[idx]
|
||||
.strip_suffix(AVG_YR_SUFFIX)
|
||||
.unwrap_or(&self.feature_names[idx])
|
||||
.to_string()
|
||||
})
|
||||
.collect();
|
||||
let n = crime_indices.len();
|
||||
|
||||
// (weighted value sum, weight) accumulators per crime type.
|
||||
let mut nat_sums = vec![0.0f64; n];
|
||||
let mut nat_weights = vec![0u64; n];
|
||||
let mut out_acc: FxHashMap<String, (Vec<f64>, Vec<u64>)> = FxHashMap::default();
|
||||
let mut sec_acc: FxHashMap<String, (Vec<f64>, Vec<u64>)> = FxHashMap::default();
|
||||
|
||||
for (key, rows) in &self.postcode_row_index {
|
||||
let Some(&first) = rows.first() else { continue };
|
||||
let count = rows.len() as u64;
|
||||
let postcode = self.postcode_interner.resolve(key);
|
||||
let outcode = postcode_outcode(postcode);
|
||||
let sector = postcode_sector(postcode);
|
||||
|
||||
for (j, &fi) in crime_indices.iter().enumerate() {
|
||||
// A NaN value is "no crime data for this postcode" — skip it so
|
||||
// it dilutes neither the sum nor the weight (a genuine gap, not
|
||||
// a zero), exactly as the global histogram excludes it.
|
||||
let value = self.get_feature(first as usize, fi);
|
||||
if !value.is_finite() {
|
||||
continue;
|
||||
}
|
||||
let weighted = value as f64 * count as f64;
|
||||
// National counts every postcode (the population the global mean
|
||||
// is built over); outcode/sector only when the postcode parses.
|
||||
nat_sums[j] += weighted;
|
||||
nat_weights[j] += count;
|
||||
if let Some(outcode) = outcode {
|
||||
let acc = out_acc
|
||||
.entry(outcode.to_string())
|
||||
.or_insert_with(|| (vec![0.0; n], vec![0; n]));
|
||||
acc.0[j] += weighted;
|
||||
acc.1[j] += count;
|
||||
}
|
||||
if let Some(sector) = sector {
|
||||
let acc = sec_acc
|
||||
.entry(sector.to_string())
|
||||
.or_insert_with(|| (vec![0.0; n], vec![0; n]));
|
||||
acc.0[j] += weighted;
|
||||
acc.1[j] += count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let means_of = |sums: &[f64], weights: &[u64]| -> Vec<f32> {
|
||||
sums.iter()
|
||||
.zip(weights.iter())
|
||||
.map(|(&sum, &weight)| {
|
||||
if weight == 0 {
|
||||
f32::NAN
|
||||
} else {
|
||||
(sum / weight as f64) as f32
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let finalize =
|
||||
|acc: FxHashMap<String, (Vec<f64>, Vec<u64>)>| -> FxHashMap<String, Vec<f32>> {
|
||||
acc.into_iter()
|
||||
.map(|(area, (sums, weights))| (area, means_of(&sums, &weights)))
|
||||
.collect()
|
||||
};
|
||||
|
||||
AreaCrimeAverages {
|
||||
crime_types,
|
||||
national: means_of(&nat_sums, &nat_weights),
|
||||
by_outcode: finalize(out_acc),
|
||||
by_sector: finalize(sec_acc),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue