Update: 2026-07-06 17:00:43

This commit is contained in:
Hamza-Ayed
2026-07-06 17:00:43 +03:00
parent 61cb615ae7
commit e42d700245
21 changed files with 3212 additions and 119 deletions
@@ -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 };
}