diff --git a/backend/pricing-engine/src/analysis/regression.ts b/backend/pricing-engine/src/analysis/regression.ts index b23629f9..2b22e023 100644 --- a/backend/pricing-engine/src/analysis/regression.ts +++ b/backend/pricing-engine/src/analysis/regression.ts @@ -2,6 +2,7 @@ import { PricingTier, RideSample, RegressionResult } from './types'; import { twoStageRegression, robustMultipleLinearRegression, + simpleDistanceModel, calcRMSE, calcRSquared, detectMinimumFare, @@ -78,17 +79,29 @@ export function analyzeTier(tier: PricingTier): PricingTier { price: s.price, })); - // Run both models - const modelA = twoStageRegression(input); // Stage 1: flag fall | Stage 2: km + min - const modelB = robustMultipleLinearRegression(input); // Current robust approach + // Run all three models + const modelA = twoStageRegression(input); // Stage 1: flag fall | Stage 2: km + min + const modelB = robustMultipleLinearRegression(input); // Iterative outlier removal + const modelC = simpleDistanceModel(input); // distance-only: price = k × dist const best = selectBestModel(modelA, modelB, samples); - if (!best) { + + // Distance-only fallback: if it's within 10% of the best model, prefer it + let finalModel = best; + if (finalModel && modelC) { + const rmseBest = evalRMSE(finalModel.model, samples); + const rmseDist = evalRMSE(modelC, samples); + if (rmseDist <= rmseBest * 1.10) { + finalModel = { model: modelC, name: 'distance-only' }; + } + } + + if (!finalModel) { tier.regression = null; return tier; } - const { model: mlrResult, name: modelName } = best; + const { model: mlrResult, name: modelName } = finalModel; // Compute final metrics using the winning model const actualPrices = samples.map(s => s.price); diff --git a/backend/pricing-engine/src/utils/math.ts b/backend/pricing-engine/src/utils/math.ts index 3fd1cfb9..a2c55625 100644 --- a/backend/pricing-engine/src/utils/math.ts +++ b/backend/pricing-engine/src/utils/math.ts @@ -209,6 +209,30 @@ export function twoStageRegression( return { baseFare, kmRate: rates.kmRate, minRate: rates.minRate }; } +/** + * Simple distance-only model: price = kmRate × dist + * Returns null if data is degenerate. + */ +export function simpleDistanceModel( + samples: Array<{ distance_km: number; duration_min: number; price: number }> +): { baseFare: number; kmRate: number; minRate: number } | null { + const dists = samples.map(s => s.distance_km); + const prices = samples.map(s => s.price); + + // Mean of price/distance ratios, weighted by distance + let sumRatio = 0, count = 0; + for (let i = 0; i < dists.length; i++) { + if (dists[i] > 0 && prices[i] > 0) { + sumRatio += prices[i] / dists[i]; + count++; + } + } + if (count < 3) return null; + + const kmRate = Math.round((sumRatio / count) * 1000) / 1000; + return { baseFare: 0, kmRate, minRate: 0 }; +} + // ───────────────────────────────────────────── // Robust regression (iterative outlier removal) // ─────────────────────────────────────────────