Update: 2026-07-06 17:00:43
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import { PricingTier, RegressionResult, RideSample } from './types';
|
||||
import {
|
||||
robustMultipleLinearRegression,
|
||||
calcRMSE,
|
||||
calcRSquared,
|
||||
detectMinimumFare,
|
||||
} from '../utils/math';
|
||||
import { mean } from 'simple-statistics';
|
||||
|
||||
/**
|
||||
* Run multiple linear regression on each pricing tier.
|
||||
* Detects minimum fare and computes RMSE/R².
|
||||
*/
|
||||
export function analyzeTier(tier: PricingTier): PricingTier {
|
||||
const samples = tier.samples;
|
||||
if (samples.length < 5) {
|
||||
tier.regression = null;
|
||||
return tier;
|
||||
}
|
||||
|
||||
// Primary model: price = baseFare + kmRate * dist + minRate * dur
|
||||
// We use robust regression to strip out surge outliers and find the floor price
|
||||
const mlrResult = robustMultipleLinearRegression(
|
||||
samples.map(s => ({
|
||||
distance_km: s.distance_km,
|
||||
duration_min: s.duration_min,
|
||||
price: s.price,
|
||||
}))
|
||||
);
|
||||
|
||||
if (!mlrResult) {
|
||||
tier.regression = null;
|
||||
return tier;
|
||||
}
|
||||
|
||||
// Predict and compute RMSE/R²
|
||||
const actualPrices = samples.map(s => s.price);
|
||||
const predictedPrices = samples.map(s =>
|
||||
mlrResult.baseFare +
|
||||
mlrResult.kmRate * s.distance_km +
|
||||
mlrResult.minRate * s.duration_min
|
||||
);
|
||||
|
||||
const rmse = calcRMSE(actualPrices, predictedPrices);
|
||||
const rSquared = calcRSquared(actualPrices, predictedPrices);
|
||||
|
||||
// Detect minimum fare
|
||||
const minFare = detectMinimumFare(
|
||||
samples.map(s => s.distance_km),
|
||||
samples.map(s => s.price),
|
||||
mlrResult.kmRate
|
||||
);
|
||||
|
||||
// If minFare is detected and the short-ride residuals improve,
|
||||
// apply minFare-adjusted model
|
||||
let hasMinFare = false;
|
||||
let adjustedRMSE = rmse;
|
||||
let adjustedRSquared = rSquared;
|
||||
|
||||
if (minFare && minFare > 0) {
|
||||
const adjustedPredicted = samples.map(s => {
|
||||
const raw = mlrResult.baseFare + mlrResult.kmRate * s.distance_km + mlrResult.minRate * s.duration_min;
|
||||
return Math.max(raw, minFare);
|
||||
});
|
||||
const adjRmse = calcRMSE(actualPrices, adjustedPredicted);
|
||||
const adjRsq = calcRSquared(actualPrices, adjustedPredicted);
|
||||
|
||||
// If minimum fare improves the fit, use it
|
||||
if (adjRmse < rmse) {
|
||||
hasMinFare = true;
|
||||
adjustedRMSE = adjRmse;
|
||||
adjustedRSquared = adjRsq;
|
||||
}
|
||||
}
|
||||
|
||||
tier.regression = {
|
||||
baseFare: mlrResult.baseFare,
|
||||
kmRate: mlrResult.kmRate,
|
||||
minRate: mlrResult.minRate,
|
||||
minFare: minFare || 0,
|
||||
rmse: adjustedRMSE,
|
||||
rSquared: adjustedRSquared,
|
||||
sampleCount: samples.length,
|
||||
hasMinFare,
|
||||
};
|
||||
|
||||
return tier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run regression on all tiers.
|
||||
*/
|
||||
export function analyzeAllTiers(tiers: PricingTier[]): PricingTier[] {
|
||||
return tiers.map(tier => analyzeTier(tier));
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple distance-only regression for comparison.
|
||||
* price = kmRate * dist
|
||||
*/
|
||||
export function distanceOnlyRegression(
|
||||
samples: RideSample[]
|
||||
): { kmRate: number; rmse: number } | null {
|
||||
if (samples.length < 3) return null;
|
||||
|
||||
const distances = samples.map(s => s.distance_km);
|
||||
const prices = samples.map(s => s.price);
|
||||
|
||||
// Simple average of price/km
|
||||
const ratios = distances.map((d, i) => d > 0 ? prices[i] / d : 0)
|
||||
.filter(r => r > 0 && isFinite(r));
|
||||
|
||||
if (ratios.length < 3) return null;
|
||||
|
||||
const kmRate = mean(ratios);
|
||||
const predicted = distances.map(d => kmRate * d);
|
||||
const rmse = calcRMSE(prices, predicted);
|
||||
|
||||
return { kmRate, rmse };
|
||||
}
|
||||
Reference in New Issue
Block a user