Pragmastat / TypeScript

Copy-pastable implementation:

/**
 * Pragmastat estimators implementation
 */

import { median } from './utils';

/**
 * Calculate the Center - median of all pairwise averages (x[i] + x[j])/2
 * @param x Array of sample values
 * @returns The center estimate
 */
export function center(x: number[]): number {
  const n = x.length;
  if (n === 0) {
    throw new Error('Input array cannot be empty');
  }

  const pairwiseAverages: number[] = [];
  for (let i = 0; i < n; i++) {
    for (let j = i; j < n; j++) {
      pairwiseAverages.push((x[i] + x[j]) / 2);
    }
  }

  return median(pairwiseAverages);
}

/**
 * Calculate the Spread - median of all pairwise absolute differences |x[i] - x[j]|
 * @param x Array of sample values
 * @returns The spread estimate
 */
export function spread(x: number[]): number {
  const n = x.length;
  if (n === 0) {
    throw new Error('Input array cannot be empty');
  }
  if (n === 1) {
    return 0;
  }

  const pairwiseDifferences: number[] = [];
  for (let i = 0; i < n; i++) {
    for (let j = i + 1; j < n; j++) {
      pairwiseDifferences.push(Math.abs(x[j] - x[i]));
    }
  }

  return median(pairwiseDifferences);
}

/**
 * Calculate the Volatility - ratio of Spread to absolute Center
 * @param x Array of sample values
 * @returns The volatility estimate
 */
export function volatility(x: number[]): number {
  if (x.length === 0) {
    throw new Error('Input array cannot be empty');
  }

  const s = spread(x);
  const c = center(x);

  if (c === 0) {
    throw new Error('Volatility is undefined when Center equals zero');
  }

  return s / Math.abs(c);
}

/**
 * Calculate the Precision - 2 * Spread / sqrt(n)
 * @param x Array of sample values
 * @returns The precision estimate
 */
export function precision(x: number[]): number {
  const n = x.length;
  if (n === 0) {
    throw new Error('Input array cannot be empty');
  }

  return (2 * spread(x)) / Math.sqrt(n);
}

/**
 * Calculate the MedShift - median of all pairwise differences (x[i] - y[j])
 * @param x First sample
 * @param y Second sample
 * @returns The shift estimate
 */
export function medShift(x: number[], y: number[]): number {
  const nx = x.length;
  const ny = y.length;

  if (nx === 0 || ny === 0) {
    throw new Error('Input arrays cannot be empty');
  }

  const pairwiseDifferences: number[] = [];
  for (let i = 0; i < nx; i++) {
    for (let j = 0; j < ny; j++) {
      pairwiseDifferences.push(x[i] - y[j]);
    }
  }

  return median(pairwiseDifferences);
}

/**
 * Calculate the MedRatio - median of all pairwise ratios (x[i] / y[j])
 * @param x First sample
 * @param y Second sample
 * @returns The ratio estimate
 */
export function medRatio(x: number[], y: number[]): number {
  const nx = x.length;
  const ny = y.length;

  if (nx === 0 || ny === 0) {
    throw new Error('Input arrays cannot be empty');
  }

  // Check that all y values are strictly positive
  if (y.some((val) => val <= 0)) {
    throw new Error('All values in y must be strictly positive');
  }

  const pairwiseRatios: number[] = [];
  for (let i = 0; i < nx; i++) {
    for (let j = 0; j < ny; j++) {
      pairwiseRatios.push(x[i] / y[j]);
    }
  }

  return median(pairwiseRatios);
}

/**
 * Calculate the MedSpread - weighted average of spreads: (n*Spread(x) + m*Spread(y))/(n+m)
 * @param x First sample
 * @param y Second sample
 * @returns The combined spread estimate
 */
export function medSpread(x: number[], y: number[]): number {
  const nx = x.length;
  const ny = y.length;

  if (nx === 0 || ny === 0) {
    throw new Error('Input arrays cannot be empty');
  }

  // Calculate weighted average of individual spreads
  const spreadX = spread(x);
  const spreadY = spread(y);

  return (nx * spreadX + ny * spreadY) / (nx + ny);
}

/**
 * Calculate the MedDisparity - MedShift / MedSpread
 * @param x First sample
 * @param y Second sample
 * @returns The disparity estimate
 */
export function medDisparity(x: number[], y: number[]): number {
  if (x.length === 0 || y.length === 0) {
    throw new Error('Input arrays cannot be empty');
  }

  const shift = medShift(x, y);
  const combinedSpread = medSpread(x, y);

  if (combinedSpread === 0) {
    return Infinity;
  }

  return shift / combinedSpread;
}