83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
import { RideSample, ZoneAnalysis } from './types';
|
|
import { assignZone, classifyZoneType } from './clustering';
|
|
|
|
/**
|
|
* Analyze pricing by geographical zone (2.5km grid).
|
|
* Groups samples into zones and computes per-zone statistics.
|
|
*/
|
|
export function analyzeByZone(samples: RideSample[]): ZoneAnalysis[] {
|
|
const zoneMap = new Map<string, RideSample[]>();
|
|
|
|
for (const s of samples) {
|
|
// Use start location for zone assignment
|
|
const zone = assignZone(s.startLat, s.startLng);
|
|
if (!zoneMap.has(zone)) zoneMap.set(zone, []);
|
|
zoneMap.get(zone)!.push(s);
|
|
}
|
|
|
|
const results: ZoneAnalysis[] = [];
|
|
|
|
for (const [zoneKey, zoneSamples] of zoneMap) {
|
|
if (zoneSamples.length < 3) continue;
|
|
|
|
const ppkValues = zoneSamples.map(s => s.ppk);
|
|
const avgPpk = Math.round(
|
|
(ppkValues.reduce((a, b) => a + b, 0) / ppkValues.length) * 1000
|
|
) / 1000;
|
|
|
|
// Count by tier — thresholds depend on currency scale
|
|
const tierCounts: Record<string, number> = {};
|
|
const sample = zoneSamples[0];
|
|
const isHighDenom = sample.countryCode === 'SY' || sample.countryCode === 'IQ';
|
|
const econThreshold = isHighDenom ? 15 : 0.35;
|
|
const stdThreshold = isHighDenom ? 40 : 0.55;
|
|
|
|
for (const s of zoneSamples) {
|
|
const tier =
|
|
s.ppk < econThreshold ? 'economy' :
|
|
s.ppk < stdThreshold ? 'standard' : 'premium';
|
|
tierCounts[tier] = (tierCounts[tier] || 0) + 1;
|
|
}
|
|
|
|
const [latStr, lngStr] = zoneKey.split(',');
|
|
results.push({
|
|
zoneKey,
|
|
centerLat: parseFloat(latStr),
|
|
centerLng: parseFloat(lngStr),
|
|
samples: zoneSamples,
|
|
avgPpk,
|
|
tierDistribution: tierCounts,
|
|
});
|
|
}
|
|
|
|
return results.sort((a, b) => a.avgPpk - b.avgPpk);
|
|
}
|
|
|
|
/**
|
|
* Analyze pricing by zone type (centre, mid, suburb, outskirts).
|
|
* Passes countryCode to classifyZoneType so the correct city center is used.
|
|
*/
|
|
export function analyzeByZoneType(
|
|
samples: RideSample[]
|
|
): Array<{ zoneType: string; avgPpk: number; sampleCount: number; avgPrice: number }> {
|
|
const typeMap = new Map<string, number[]>();
|
|
|
|
for (const s of samples) {
|
|
// Pass countryCode so we use the correct city center (not always Amman)
|
|
const zoneType = classifyZoneType(s.startLat, s.startLng, s.countryCode);
|
|
if (!typeMap.has(zoneType)) typeMap.set(zoneType, []);
|
|
typeMap.get(zoneType)!.push(s.ppk);
|
|
}
|
|
|
|
return Array.from(typeMap.entries())
|
|
.map(([zoneType, ppks]) => ({
|
|
zoneType,
|
|
avgPpk: Math.round(
|
|
(ppks.reduce((a, b) => a + b, 0) / ppks.length) * 1000
|
|
) / 1000,
|
|
sampleCount: ppks.length,
|
|
avgPrice: 0, // calculated below if needed
|
|
}))
|
|
.sort((a, b) => a.avgPpk - b.avgPpk);
|
|
}
|