Files
LynkedUpPro_CRM/src/modules/estimateWizard/engine/hailProbability.js
T

105 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* hailProbability.js — conservative historical hail-risk model (spec §6).
*
* IMPORTANT framing (spec §6): the output is a HISTORICAL hazard estimate for the
* surrounding area, NOT a property-level forecast or guarantee. The model computes
* a RANGE (confidence band), not one overconfident number, and stores every
* assumption so the report can be defended.
*
* lambda (λ) = deduped hail events / years
* P(at least one hail event over N years) = 1 - e^(-λN)
*
* Pure function — no I/O. Fed by /api/storm-history (existing storm module, used
* read-only). Returns the hail_risk_snapshot shape (spec §12).
*/
function clamp(n, lo, hi) { return Math.max(lo, Math.min(hi, n)); }
function pct(n) { return Math.round(n * 100); }
/**
* @param {object} input
* eventCount {number} deduped hail events observed in the window
* windowYears {number} historical window analyzed
* radiusMiles {number} search radius around the property
* geocodeConfidence {'high'|'moderate'|'low'}
* maxHailInches {number|null}
* lastEventDate {string|null} ISO date of most recent hail event
* source {string} human-readable data source label
* generatedAt {string} ISO timestamp the snapshot was frozen
*/
export function computeHailRisk(input) {
const {
eventCount = 0,
windowYears = 5,
radiusMiles = 20,
geocodeConfidence = 'moderate',
maxHailInches = null,
lastEventDate = null,
source = 'NOAA / IEM Local Storm Reports',
generatedAt = new Date().toISOString(),
} = input || {};
const years = Math.max(1, windowYears);
const count = Math.max(0, eventCount);
// Annualized rate (events/year).
const lambda = count / years;
// Poisson confidence band using event-count standard error (√count).
const se = Math.sqrt(count);
const lambdaLow = Math.max(0, (count - se) / years);
const lambdaHigh = (count + se) / years;
const prob = (lam, n) => 1 - Math.exp(-lam * n);
const p10Base = prob(lambda, 10);
const p10Low = prob(lambdaLow, 10);
const p10High = prob(lambdaHigh, 10);
// Never present a 100% historical probability — that reads as a certainty/guarantee,
// which this model explicitly is not (spec §6). High-activity areas push p10High to
// ~1.0, so cap the DISPLAYED band just under certainty. Underlying p10* values still
// drive the exposure tier and scoring unchanged.
const MAX_DISPLAY_PROB = 0.95;
const dispLow = Math.min(p10Low, MAX_DISPLAY_PROB);
const dispBase = Math.min(p10Base, MAX_DISPLAY_PROB);
const dispHigh = Math.min(p10High, MAX_DISPLAY_PROB);
// Confidence score (0..1): sample size + window length + geocode precision.
const sampleScore = clamp(count / 8, 0, 1); // ~8+ events → strong
const windowScore = clamp(years / 10, 0, 1); // 10y window → full
const geoScore = geocodeConfidence === 'high' ? 1 : geocodeConfidence === 'moderate' ? 0.6 : 0.3;
const confidenceValue = +(0.4 * sampleScore + 0.3 * windowScore + 0.3 * geoScore).toFixed(2);
const confidenceLabel = confidenceValue >= 0.66 ? 'High' : confidenceValue >= 0.4 ? 'Moderate' : 'Low';
// Coarse exposure tier used downstream by the scoring engine (Phase 4).
const exposureTier = p10Base >= 0.66 ? 'high' : p10Base >= 0.33 ? 'moderate' : 'low';
return {
// hail_risk_snapshot fields (spec §12)
source,
windowYears: years,
radiusMiles,
eventCount: count,
annualRate: +lambda.toFixed(3),
maxHailInches,
lastEventDate,
probability10yr: { low: pct(dispLow), base: pct(dispBase), high: pct(dispHigh) },
confidence: confidenceLabel,
confidenceValue,
exposureTier,
generatedAt,
// Customer-facing wording (spec §6) — historical, never a forecast.
wording: {
annualRate: count === 0
? 'No qualifying hail events were found in the available reports for this area.'
: `This area has averaged about ${lambda.toFixed(1)} observed hail event(s) per year in the available dataset.`,
probability: count === 0
? 'There is not enough historical hail activity nearby to estimate a meaningful probability.'
: `Based on historical reports, the chance of at least one hail event in the next 10 years is estimated at ${pct(dispLow)}${pct(dispHigh)}%, assuming future patterns resemble the past.`,
confidence: `Confidence: ${confidenceLabel}. This is based on county-level and nearby reports within ~${radiusMiles} miles, not a roof-specific inspection.`,
lastUpdated: `Storm data reflects reports through ${new Date(generatedAt).toLocaleDateString()}.`,
},
};
}