Rust things
This commit is contained in:
parent
fc10381692
commit
3debacab4f
30 changed files with 3257 additions and 647 deletions
|
|
@ -1,6 +1,7 @@
|
|||
//! Static feature configuration. Every numeric and enum column in wide.parquet
|
||||
//! must be declared here. Unknown columns cause a startup panic.
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Bounds {
|
||||
/// Fixed min/max values for the slider
|
||||
Fixed { min: f32, max: f32 },
|
||||
|
|
@ -61,6 +62,26 @@ pub struct FeatureGroup {
|
|||
}
|
||||
|
||||
pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
||||
FeatureGroup {
|
||||
name: "Transport",
|
||||
features: &[
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Distance to nearest train or tube station (km)",
|
||||
bounds: Bounds::Percentile {
|
||||
low: 2.0,
|
||||
high: 98.0,
|
||||
},
|
||||
step: 0.1,
|
||||
description: "Distance to the closest train or tube station",
|
||||
detail: "Straight-line distance in kilometres from the postcode to the nearest rail station or Tube/metro/tram stop.",
|
||||
source: "naptan",
|
||||
prefix: "",
|
||||
suffix: " km",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
FeatureGroup {
|
||||
name: "Properties",
|
||||
features: &[
|
||||
|
|
@ -78,6 +99,21 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
detail: "From HM Land Registry Price Paid data. Freehold means you own the building and the land it stands on. Leasehold means you own the building but not the land: you have a lease from the freeholder for a set number of years.",
|
||||
source: "price-paid",
|
||||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Estimated current price",
|
||||
bounds: Bounds::Fixed {
|
||||
min: 0.0,
|
||||
max: 2_500_000.0,
|
||||
},
|
||||
step: 10000.0,
|
||||
description: "Modelled estimate of the current property value",
|
||||
detail: "Based on the last sale price, local repeat-sales price movement, and nearby recently sold properties. The repeat-sales index is tracked by postcode sector and property type, with smoothing and neighbour blending where data is sparse. Recent sales stay close to the recorded price; older sales depend more on the model.",
|
||||
source: "price-paid",
|
||||
prefix: "£",
|
||||
suffix: "",
|
||||
raw: false,
|
||||
absolute: true,
|
||||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Last known price",
|
||||
bounds: Bounds::Fixed {
|
||||
|
|
@ -94,19 +130,19 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
absolute: true,
|
||||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Estimated current price",
|
||||
bounds: Bounds::Fixed {
|
||||
min: 0.0,
|
||||
max: 2_500_000.0,
|
||||
name: "Est. price per sqm",
|
||||
bounds: Bounds::Percentile {
|
||||
low: 0.0,
|
||||
high: 98.0,
|
||||
},
|
||||
step: 10000.0,
|
||||
description: "Inflation-adjusted estimate of the current property value",
|
||||
detail: "Based on the last sale price, adjusted for local price changes over time using a repeat-sales index (tracked per postcode sector and property type). If post-sale improvements are detected from EPC records, a renovation premium is added. Recent sales will be close to the original price; older sales are adjusted more.",
|
||||
step: 100.0,
|
||||
description: "Estimated current price divided by total floor area",
|
||||
detail: "Calculated by dividing the modelled estimated current price by the total floor area from the EPC certificate. Provides a more up-to-date price-per-area comparison than the historical sale price per sqm.",
|
||||
source: "price-paid",
|
||||
prefix: "£",
|
||||
suffix: "",
|
||||
raw: false,
|
||||
absolute: true,
|
||||
absolute: false,
|
||||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Price per sqm",
|
||||
|
|
@ -123,21 +159,6 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
raw: false,
|
||||
absolute: false,
|
||||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Est. price per sqm",
|
||||
bounds: Bounds::Percentile {
|
||||
low: 0.0,
|
||||
high: 98.0,
|
||||
},
|
||||
step: 100.0,
|
||||
description: "Estimated current price divided by total floor area",
|
||||
detail: "Calculated by dividing the inflation-adjusted estimated current price (including any renovation premium) by the total floor area from the EPC certificate. Provides a more up-to-date price-per-area comparison than the historical sale price per sqm.",
|
||||
source: "price-paid",
|
||||
prefix: "£",
|
||||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Estimated monthly rent",
|
||||
bounds: Bounds::Percentile { low: 2.0, high: 98.0 },
|
||||
|
|
@ -248,26 +269,6 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
}),
|
||||
],
|
||||
},
|
||||
FeatureGroup {
|
||||
name: "Transport",
|
||||
features: &[
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Distance to nearest train or tube station (km)",
|
||||
bounds: Bounds::Percentile {
|
||||
low: 2.0,
|
||||
high: 98.0,
|
||||
},
|
||||
step: 0.1,
|
||||
description: "Distance to the closest train or tube station",
|
||||
detail: "Straight-line distance in kilometres from the postcode to the nearest rail station or Tube/metro/tram stop.",
|
||||
source: "naptan",
|
||||
prefix: "",
|
||||
suffix: " km",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
FeatureGroup {
|
||||
name: "Education",
|
||||
features: &[
|
||||
|
|
@ -393,18 +394,18 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Education, Skills and Training Score",
|
||||
bounds: Bounds::Percentile {
|
||||
low: 2.0,
|
||||
high: 98.0,
|
||||
bounds: Bounds::Fixed {
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
},
|
||||
step: 0.1,
|
||||
description: "Education quality score for the local area (higher = better)",
|
||||
detail: "From the English Indices of Deprivation (inverted so higher = better). Covers school attainment, entry to higher education, adult qualifications, and English language proficiency. Higher scores indicate less deprivation.",
|
||||
step: 1.0,
|
||||
description: "Education and skills deprivation percentile (higher = less deprived)",
|
||||
detail: "From the English Indices of Deprivation, converted to a national percentile where 0% is most deprived and 100% is least deprived. Covers school attainment, entry to higher education, adult qualifications, and English language proficiency.",
|
||||
source: "iod",
|
||||
prefix: "",
|
||||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
suffix: "%",
|
||||
raw: true,
|
||||
absolute: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
|
@ -413,72 +414,78 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
features: &[
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Income Score",
|
||||
bounds: Bounds::Fixed { min: 0.0, max: 1.0 },
|
||||
step: 0.01,
|
||||
description: "Income deprivation rate, inverted (higher = less deprived)",
|
||||
detail: "From the English Indices of Deprivation (inverted so higher = better). Higher values indicate less income deprivation. Based on Income Support, income-based Jobseeker's Allowance, income-based Employment and Support Allowance, Pension Credit, Working Tax Credit and Child Tax Credit, Universal Credit, and asylum seekers.",
|
||||
bounds: Bounds::Fixed {
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
},
|
||||
step: 1.0,
|
||||
description: "Income deprivation percentile (higher = less deprived)",
|
||||
detail: "From the English Indices of Deprivation, converted to a national percentile where 0% is most income deprived and 100% is least income deprived. Based on Income Support, income-based Jobseeker's Allowance, income-based Employment and Support Allowance, Pension Credit, Working Tax Credit and Child Tax Credit, Universal Credit, and asylum seekers.",
|
||||
source: "iod",
|
||||
prefix: "",
|
||||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
suffix: "%",
|
||||
raw: true,
|
||||
absolute: true,
|
||||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Employment Score",
|
||||
bounds: Bounds::Fixed { min: 0.0, max: 1.0 },
|
||||
step: 0.01,
|
||||
description: "Employment deprivation rate, inverted (higher = less deprived)",
|
||||
detail: "From the English Indices of Deprivation (inverted so higher = better). Higher values indicate less employment deprivation. Based on claimants of Jobseeker's Allowance, Employment and Support Allowance, Incapacity Benefit, Severe Disablement Allowance, Carer's Allowance, and relevant Universal Credit claimants.",
|
||||
bounds: Bounds::Fixed {
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
},
|
||||
step: 1.0,
|
||||
description: "Employment deprivation percentile (higher = less deprived)",
|
||||
detail: "From the English Indices of Deprivation, converted to a national percentile where 0% is most employment deprived and 100% is least employment deprived. Based on claimants of Jobseeker's Allowance, Employment and Support Allowance, Incapacity Benefit, Severe Disablement Allowance, Carer's Allowance, and relevant Universal Credit claimants.",
|
||||
source: "iod",
|
||||
prefix: "",
|
||||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
suffix: "%",
|
||||
raw: true,
|
||||
absolute: true,
|
||||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Health Deprivation and Disability Score",
|
||||
bounds: Bounds::Percentile {
|
||||
low: 2.0,
|
||||
high: 98.0,
|
||||
bounds: Bounds::Fixed {
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
},
|
||||
step: 0.1,
|
||||
description: "Health and disability score (higher = better health outcomes)",
|
||||
detail: "From the English Indices of Deprivation (inverted so higher = better). Higher scores indicate lower risk of premature death and better quality of life. Derived from years of potential life lost, comparative illness and disability ratio, acute morbidity, and mood and anxiety disorders.",
|
||||
step: 1.0,
|
||||
description: "Health and disability deprivation percentile (higher = better outcomes)",
|
||||
detail: "From the English Indices of Deprivation, converted to a national percentile where 0% is most health deprived and 100% is least health deprived. Derived from years of potential life lost, comparative illness and disability ratio, acute morbidity, and mood and anxiety disorders.",
|
||||
source: "iod",
|
||||
prefix: "",
|
||||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
suffix: "%",
|
||||
raw: true,
|
||||
absolute: true,
|
||||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Housing Conditions Score",
|
||||
bounds: Bounds::Percentile {
|
||||
low: 2.0,
|
||||
high: 98.0,
|
||||
bounds: Bounds::Fixed {
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
},
|
||||
step: 0.1,
|
||||
description: "Housing quality and conditions (higher = better)",
|
||||
detail: "From the English Indices of Deprivation, Living Environment domain (inverted so higher = better). Measures the quality of housing stock: central heating availability, housing condition, and Decent Homes standards. Higher scores indicate better housing conditions.",
|
||||
step: 1.0,
|
||||
description: "Housing conditions percentile (higher = better conditions)",
|
||||
detail: "From the English Indices of Deprivation, Living Environment domain, converted to a national percentile where 0% is most deprived and 100% is least deprived. Measures the quality of housing stock: central heating availability, housing condition, and Decent Homes standards.",
|
||||
source: "iod",
|
||||
prefix: "",
|
||||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
suffix: "%",
|
||||
raw: true,
|
||||
absolute: true,
|
||||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Air Quality and Road Safety Score",
|
||||
bounds: Bounds::Percentile {
|
||||
low: 2.0,
|
||||
high: 98.0,
|
||||
bounds: Bounds::Fixed {
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
},
|
||||
step: 0.1,
|
||||
description: "Air quality and road safety (higher = better)",
|
||||
detail: "From the English Indices of Deprivation, Living Environment domain (inverted so higher = better). Measures the outdoor living environment quality through air quality indicators and road traffic accident casualties involving pedestrians and cyclists. Higher scores indicate better outdoor environments.",
|
||||
step: 1.0,
|
||||
description: "Air quality and road safety percentile (higher = better conditions)",
|
||||
detail: "From the English Indices of Deprivation, Living Environment domain, converted to a national percentile where 0% is most deprived and 100% is least deprived. Measures the outdoor living environment through air quality indicators and road traffic accident casualties involving pedestrians and cyclists.",
|
||||
source: "iod",
|
||||
prefix: "",
|
||||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
suffix: "%",
|
||||
raw: true,
|
||||
absolute: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
|
@ -996,6 +1003,126 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
raw: false,
|
||||
absolute: false,
|
||||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Distance to nearest grocery store (km)",
|
||||
bounds: Bounds::Percentile {
|
||||
low: 2.0,
|
||||
high: 98.0,
|
||||
},
|
||||
step: 0.1,
|
||||
description: "Distance to the closest grocery shop or supermarket",
|
||||
detail: "Straight-line distance in kilometres from the postcode to the nearest grocery shop, supermarket, or convenience store. Uses OpenStreetMap POIs, with Waitrose and Tesco coverage from GEOLYTIX retail points.",
|
||||
source: "osm-pois",
|
||||
prefix: "",
|
||||
suffix: " km",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Distance to nearest tube station (km)",
|
||||
bounds: Bounds::Percentile {
|
||||
low: 2.0,
|
||||
high: 98.0,
|
||||
},
|
||||
step: 0.1,
|
||||
description: "Distance to the closest Tube, metro, tram, or DLR stop",
|
||||
detail: "Straight-line distance in kilometres from the postcode to the nearest NaPTAN station classified as Tube, metro, tram, or DLR.",
|
||||
source: "naptan",
|
||||
prefix: "",
|
||||
suffix: " km",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Distance to nearest rail station (km)",
|
||||
bounds: Bounds::Percentile {
|
||||
low: 2.0,
|
||||
high: 98.0,
|
||||
},
|
||||
step: 0.1,
|
||||
description: "Distance to the closest National Rail station",
|
||||
detail: "Straight-line distance in kilometres from the postcode to the nearest NaPTAN railway station.",
|
||||
source: "naptan",
|
||||
prefix: "",
|
||||
suffix: " km",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Distance to nearest Waitrose (km)",
|
||||
bounds: Bounds::Percentile {
|
||||
low: 2.0,
|
||||
high: 98.0,
|
||||
},
|
||||
step: 0.1,
|
||||
description: "Distance to the closest Waitrose store",
|
||||
detail: "Straight-line distance in kilometres from the postcode to the nearest Waitrose or Little Waitrose store in the GEOLYTIX Grocery Retail Points dataset.",
|
||||
source: "geolytix-retail-points",
|
||||
prefix: "",
|
||||
suffix: " km",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Distance to nearest Tesco (km)",
|
||||
bounds: Bounds::Percentile {
|
||||
low: 2.0,
|
||||
high: 98.0,
|
||||
},
|
||||
step: 0.1,
|
||||
description: "Distance to the closest Tesco store",
|
||||
detail: "Straight-line distance in kilometres from the postcode to the nearest Tesco store in the GEOLYTIX Grocery Retail Points dataset.",
|
||||
source: "geolytix-retail-points",
|
||||
prefix: "",
|
||||
suffix: " km",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Distance to nearest cafe (km)",
|
||||
bounds: Bounds::Percentile {
|
||||
low: 2.0,
|
||||
high: 98.0,
|
||||
},
|
||||
step: 0.1,
|
||||
description: "Distance to the closest cafe",
|
||||
detail: "Straight-line distance in kilometres from the postcode to the nearest cafe, ice-cream shop, or internet cafe mapped in OpenStreetMap.",
|
||||
source: "osm-pois",
|
||||
prefix: "",
|
||||
suffix: " km",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Distance to nearest pub (km)",
|
||||
bounds: Bounds::Percentile {
|
||||
low: 2.0,
|
||||
high: 98.0,
|
||||
},
|
||||
step: 0.1,
|
||||
description: "Distance to the closest pub",
|
||||
detail: "Straight-line distance in kilometres from the postcode to the nearest pub, social club, brewery, distillery, or winery mapped in OpenStreetMap.",
|
||||
source: "osm-pois",
|
||||
prefix: "",
|
||||
suffix: " km",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Distance to nearest restaurant (km)",
|
||||
bounds: Bounds::Percentile {
|
||||
low: 2.0,
|
||||
high: 98.0,
|
||||
},
|
||||
step: 0.1,
|
||||
description: "Distance to the closest restaurant",
|
||||
detail: "Straight-line distance in kilometres from the postcode to the nearest restaurant or food court mapped in OpenStreetMap.",
|
||||
source: "osm-pois",
|
||||
prefix: "",
|
||||
suffix: " km",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
}),
|
||||
Feature::Numeric(FeatureConfig {
|
||||
name: "Number of parks within 1km",
|
||||
bounds: Bounds::Percentile {
|
||||
|
|
@ -1105,20 +1232,76 @@ pub fn order_for(name: &str) -> Option<&'static [&'static str]> {
|
|||
|
||||
/// Whether this feature should use integer-width histogram bins.
|
||||
pub fn has_integer_bins(name: &str) -> bool {
|
||||
INTEGER_BIN_FEATURES.contains(&name)
|
||||
INTEGER_BIN_FEATURES.contains(&name) || dynamic_poi_count_radius(name).is_some()
|
||||
}
|
||||
|
||||
/// Look up the Bounds config for a numeric feature by name.
|
||||
pub fn bounds_for(name: &str) -> Option<&'static Bounds> {
|
||||
pub fn bounds_for(name: &str) -> Option<Bounds> {
|
||||
if dynamic_poi_distance_category(name).is_some() {
|
||||
return Some(Bounds::Percentile {
|
||||
low: 2.0,
|
||||
high: 98.0,
|
||||
});
|
||||
}
|
||||
if dynamic_poi_count_radius(name).is_some() {
|
||||
return Some(Bounds::Percentile {
|
||||
low: 5.0,
|
||||
high: 95.0,
|
||||
});
|
||||
}
|
||||
|
||||
FEATURE_GROUPS
|
||||
.iter()
|
||||
.flat_map(|group| group.features.iter())
|
||||
.find_map(|feature| match feature {
|
||||
Feature::Numeric(c) if c.name == name => Some(&c.bounds),
|
||||
Feature::Numeric(c) if c.name == name => Some(c.bounds),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn dynamic_poi_distance_category(name: &str) -> Option<&str> {
|
||||
name.strip_prefix("Distance to nearest ")
|
||||
.and_then(|rest| rest.strip_suffix(" POI (km)"))
|
||||
.filter(|category| !category.is_empty())
|
||||
}
|
||||
|
||||
pub fn dynamic_poi_count_radius(name: &str) -> Option<u8> {
|
||||
let rest = name.strip_prefix("Number of ")?;
|
||||
let (_category, suffix) = rest.rsplit_once(" POIs within ")?;
|
||||
match suffix {
|
||||
"2km" => Some(2),
|
||||
"5km" => Some(5),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dynamic_poi_count_category(name: &str) -> Option<&str> {
|
||||
let rest = name.strip_prefix("Number of ")?;
|
||||
let (category, suffix) = rest.rsplit_once(" POIs within ")?;
|
||||
matches!(suffix, "2km" | "5km")
|
||||
.then_some(category)
|
||||
.filter(|category| !category.is_empty())
|
||||
}
|
||||
|
||||
pub fn is_dynamic_poi_feature(name: &str) -> bool {
|
||||
dynamic_poi_distance_category(name).is_some() || dynamic_poi_count_category(name).is_some()
|
||||
}
|
||||
|
||||
pub fn dynamic_poi_feature_sort_key(name: &str) -> (u8, String) {
|
||||
if let Some(category) = dynamic_poi_distance_category(name) {
|
||||
return (0, category.to_ascii_lowercase());
|
||||
}
|
||||
if let Some(category) = dynamic_poi_count_category(name) {
|
||||
let metric_order = match dynamic_poi_count_radius(name) {
|
||||
Some(2) => 1,
|
||||
Some(5) => 2,
|
||||
_ => 3,
|
||||
};
|
||||
return (metric_order, category.to_ascii_lowercase());
|
||||
}
|
||||
(9, name.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
/// Canonical display order for POI category groups.
|
||||
/// The server will panic at startup if the data contains groups not in this list or vice versa.
|
||||
pub const POI_GROUP_ORDER: &[&str] = &[
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue