new demo mode & tenure
Some checks failed
Build and publish Docker image / build-and-push (push) Successful in 8m43s
CI / Check (push) Failing after 8m49s

This commit is contained in:
Andras Schmelczer 2026-06-17 07:54:30 +01:00
parent 7656f24544
commit 4a0f00f2a4
64 changed files with 2875 additions and 338 deletions

View file

@ -255,6 +255,36 @@ pub fn compute_feature_stats(
(numeric_features, enum_features_out)
}
/// Count occurrences of each variant of a single enum feature across `rows`.
///
/// Unlike [`compute_feature_stats`], which is driven by the filter-matching
/// subset, this lets a caller compute a count that should reflect a whole area
/// regardless of the active filters (e.g. the council-house count, which is an
/// attribute of the postcode itself, not of the currently filtered properties).
/// Returns `None` if `feature_idx` is not an enum feature.
pub fn compute_enum_feature_counts(
rows: &[usize],
data: &PropertyData,
feature_idx: usize,
) -> Option<HashMap<String, u64>> {
let variants = data.enum_values.get(&feature_idx)?;
let mut value_counts = vec![0u64; variants.len()];
for &row in rows {
let value = data.get_feature(row, feature_idx);
if value.is_finite() && value >= 0.0 && (value as usize) < value_counts.len() {
value_counts[value as usize] += 1;
}
}
Some(
value_counts
.iter()
.enumerate()
.filter(|(_, &count)| count > 0)
.map(|(idx, &count)| (variants[idx].clone(), count))
.collect(),
)
}
/// Compute property-weighted per-year crime means across the selection.
///
/// Each matching property contributes its postcode's per-year counts (incidents
@ -447,3 +477,42 @@ pub fn compute_poi_feature_stats(
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::consts::NAN_U16;
fn enum_data(values: &[u16]) -> PropertyData {
let mut data = PropertyData::empty_for_tests();
data.num_features = 1;
data.num_numeric = 0; // single enum feature at index 0
data.feature_data = values.to_vec();
data.enum_values
.insert(0, vec!["Yes".to_string(), "No".to_string()]);
data
}
#[test]
fn enum_counts_tally_only_given_rows() {
// Rows: Yes, No, Yes, <missing>
let data = enum_data(&[0, 1, 0, NAN_U16]);
let all = compute_enum_feature_counts(&[0, 1, 2, 3], &data, 0).unwrap();
assert_eq!(all.get("Yes"), Some(&2));
assert_eq!(all.get("No"), Some(&1));
// A filter-matching subset would yield a different tally — confirming
// the count is driven purely by the rows passed in (so callers can pass
// the full area to make it filter-independent).
let subset = compute_enum_feature_counts(&[0, 3], &data, 0).unwrap();
assert_eq!(subset.get("Yes"), Some(&1));
assert_eq!(subset.get("No"), None);
}
#[test]
fn enum_counts_none_for_non_enum_feature() {
let data = enum_data(&[0, 1]);
assert!(compute_enum_feature_counts(&[0, 1], &data, 1).is_none());
}
}