SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
1.3 KB · 39 lines typescript
Raw Blame History
1import { mad, median } from '@rareindex/shared';23export interface PricePoint {4  id: string;5  priceUsd: number;6  date: Date;7  quantity?: number;8  isBundle?: boolean;9}1011export interface OutlierFlag {12  id: string;13  reason: string;14  score: number;15}1617/**18 * Robust outlier detection on log prices within a variant (§116): modified z-score19 * 0.6745·(x − median)/MAD > threshold. Needs ≥ 5 points; with a MAD of 0 (identical prices) any20 * different price beyond 3× is flagged. Never deletes — returns flags for audit.21 */22export function detectOutliers(points: PricePoint[], threshold = 3.5): OutlierFlag[] {23  const valid = points.filter((p) => p.priceUsd > 0 && !p.isBundle && (p.quantity ?? 1) === 1);24  if (valid.length < 5) return [];25  const logs = valid.map((p) => Math.log(p.priceUsd));26  const med = median(logs)!;27  const m = mad(logs)!;28  const out: OutlierFlag[] = [];29  for (let i = 0; i < valid.length; i++) {30    const p = valid[i]!;31    const dev = logs[i]! - med;32    let score: number;33    if (m > 0) score = (0.6745 * dev) / m;34    else score = Math.abs(dev) > Math.log(3) ? Math.sign(dev) * threshold * 2 : 0;35    if (Math.abs(score) > threshold) out.push({ id: p.id, reason: score > 0 ? 'abnormally_high_price' : 'abnormally_low_price', score: Math.round(score * 100) / 100 });36  }37  return out;38}39