fine
This commit is contained in:
parent
6df2812a4e
commit
9e4e65fa2a
35 changed files with 1172 additions and 70 deletions
|
|
@ -4,7 +4,7 @@ use metrics::counter;
|
|||
use rustc_hash::FxHashMap;
|
||||
use tracing::error;
|
||||
|
||||
use crate::consts::PRICE_HISTORY_POINTS_LIMIT;
|
||||
use crate::consts::{AREA_PRICE_HISTORY_POINTS_LIMIT, PRICE_HISTORY_POINTS_LIMIT};
|
||||
use crate::data::area_crime_averages::AreaCrimeAverages;
|
||||
use crate::data::crime_by_year::CrimeByYearData;
|
||||
use crate::data::{FeatureStats, PostcodePoiMetrics, PropertyData};
|
||||
|
|
@ -15,47 +15,122 @@ use super::hexagon_stats::{
|
|||
NumericFeatureStats, PricePoint,
|
||||
};
|
||||
|
||||
/// Extract price history (year, price) pairs from matching rows, downsampled if needed.
|
||||
/// Feature indexes needed to build price-history points, resolved once.
|
||||
struct PriceHistoryIndexes {
|
||||
year: usize,
|
||||
/// "Price per sqm" (last sale price / EPC floor area), when the feature
|
||||
/// exists. Populates each point's optional `price_per_sqm`.
|
||||
price_per_sqm: Option<usize>,
|
||||
}
|
||||
|
||||
impl PriceHistoryIndexes {
|
||||
fn resolve(feature_name_to_index: &FxHashMap<String, usize>) -> Option<Self> {
|
||||
Some(Self {
|
||||
year: feature_name_to_index
|
||||
.get("Date of last transaction")
|
||||
.copied()?,
|
||||
price_per_sqm: feature_name_to_index.get("Price per sqm").copied(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Build (year, price, price_per_sqm) points from an iterator of rows, dropping
|
||||
/// rows without a finite year or price, then stride-downsampling to `limit`.
|
||||
fn build_price_points(
|
||||
rows: impl Iterator<Item = usize>,
|
||||
data: &PropertyData,
|
||||
idx: &PriceHistoryIndexes,
|
||||
limit: usize,
|
||||
) -> Vec<PricePoint> {
|
||||
let mut points: Vec<PricePoint> = rows
|
||||
.filter_map(|row| {
|
||||
let year = data.get_feature(row, idx.year);
|
||||
let price = data.last_known_price_raw(row);
|
||||
if !(year.is_finite() && price.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
let price_per_sqm = idx
|
||||
.price_per_sqm
|
||||
.map(|pi| data.get_feature(row, pi))
|
||||
.filter(|v| v.is_finite() && *v > 0.0);
|
||||
Some(PricePoint {
|
||||
year,
|
||||
price,
|
||||
price_per_sqm,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if points.len() > limit {
|
||||
let step = points.len() as f64 / limit as f64;
|
||||
points = (0..limit)
|
||||
.map(|i| {
|
||||
let src = &points[(i as f64 * step) as usize];
|
||||
PricePoint {
|
||||
year: src.year,
|
||||
price: src.price,
|
||||
price_per_sqm: src.price_per_sqm,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
points
|
||||
}
|
||||
|
||||
/// Extract price history (year, price, price_per_sqm) pairs from matching rows,
|
||||
/// downsampled if needed.
|
||||
pub fn extract_price_history(
|
||||
matching_rows: &[usize],
|
||||
data: &PropertyData,
|
||||
feature_name_to_index: &FxHashMap<String, usize>,
|
||||
) -> Vec<PricePoint> {
|
||||
let year_idx = feature_name_to_index
|
||||
.get("Date of last transaction")
|
||||
.copied();
|
||||
match year_idx {
|
||||
Some(yi) => {
|
||||
let mut points: Vec<PricePoint> = matching_rows
|
||||
.iter()
|
||||
.filter_map(|&row| {
|
||||
let year = data.get_feature(row, yi);
|
||||
let price = data.last_known_price_raw(row);
|
||||
if year.is_finite() && price.is_finite() {
|
||||
Some(PricePoint { year, price })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if points.len() > PRICE_HISTORY_POINTS_LIMIT {
|
||||
let step = points.len() as f64 / PRICE_HISTORY_POINTS_LIMIT as f64;
|
||||
points = (0..PRICE_HISTORY_POINTS_LIMIT)
|
||||
.map(|i| {
|
||||
let idx = (i as f64 * step) as usize;
|
||||
PricePoint {
|
||||
year: points[idx].year,
|
||||
price: points[idx].price,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
points
|
||||
}
|
||||
match PriceHistoryIndexes::resolve(feature_name_to_index) {
|
||||
Some(idx) => build_price_points(
|
||||
matching_rows.iter().copied(),
|
||||
data,
|
||||
&idx,
|
||||
PRICE_HISTORY_POINTS_LIMIT,
|
||||
),
|
||||
None => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Price histories for the selection's postcode sector and outward code, taken
|
||||
/// over *every* sale in those areas (filter-independent), as wider-area context
|
||||
/// for the selection's own chart. Returns `(sector_history, outcode_history)`.
|
||||
pub fn extract_area_price_histories(
|
||||
central_postcode: Option<&str>,
|
||||
data: &PropertyData,
|
||||
feature_name_to_index: &FxHashMap<String, usize>,
|
||||
) -> (Vec<PricePoint>, Vec<PricePoint>) {
|
||||
let (Some(postcode), Some(idx)) = (
|
||||
central_postcode,
|
||||
PriceHistoryIndexes::resolve(feature_name_to_index),
|
||||
) else {
|
||||
return (Vec::new(), Vec::new());
|
||||
};
|
||||
let sector = postcode_sector(postcode)
|
||||
.map(|s| {
|
||||
build_price_points(
|
||||
data.rows_for_sector(s).into_iter().map(|r| r as usize),
|
||||
data,
|
||||
&idx,
|
||||
AREA_PRICE_HISTORY_POINTS_LIMIT,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let outcode = postcode_outcode(postcode)
|
||||
.map(|o| {
|
||||
build_price_points(
|
||||
data.rows_for_outcode(o).into_iter().map(|r| r as usize),
|
||||
data,
|
||||
&idx,
|
||||
AREA_PRICE_HISTORY_POINTS_LIMIT,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
(sector, outcode)
|
||||
}
|
||||
|
||||
/// Per-feature accumulator kind, determined once before the row loop.
|
||||
enum FeatureAccum {
|
||||
/// Numeric: track count, min, max, sum, histogram bins.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue