Pragmastat / Kotlin

Copy-pastable implementation:

package com.pragmastat

import kotlin.math.abs
import kotlin.math.sqrt

/**
 * Calculates the median of a list of values
 */
fun median(values: List<Double>): Double {
    require(values.isNotEmpty()) { "Input list cannot be empty" }
    
    val sorted = values.sorted()
    val n = sorted.size
    
    return if (n % 2 == 0) {
        (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
    } else {
        sorted[n / 2]
    }
}

/**
 * Estimates the central value of the data (Center)
 * 
 * Calculates the median of all pairwise averages (x[i] + x[j])/2.
 * More robust than the mean and more efficient than the median.
 */
fun center(x: List<Double>): Double {
    require(x.isNotEmpty()) { "Input list cannot be empty" }
    
    val pairwiseAverages = mutableListOf<Double>()
    for (i in x.indices) {
        for (j in i until x.size) {
            pairwiseAverages.add((x[i] + x[j]) / 2.0)
        }
    }
    
    return median(pairwiseAverages)
}

/**
 * Estimates data dispersion (Spread)
 * 
 * Calculates the median of all pairwise absolute differences |x[i] - x[j]|.
 * More robust than standard deviation and more efficient than MAD.
 */
fun spread(x: List<Double>): Double {
    require(x.isNotEmpty()) { "Input list cannot be empty" }
    if (x.size == 1) return 0.0
    
    val pairwiseDiffs = mutableListOf<Double>()
    for (i in x.indices) {
        for (j in i + 1 until x.size) {
            pairwiseDiffs.add(abs(x[i] - x[j]))
        }
    }
    
    return median(pairwiseDiffs)
}

/**
 * Measures the relative dispersion of a sample (Volatility)
 * 
 * Calculates the ratio of Spread to absolute Center.
 * Robust alternative to the coefficient of variation.
 */
fun volatility(x: List<Double>): Double {
    val centerVal = center(x)
    require(centerVal != 0.0) { "Volatility is undefined when Center equals zero" }
    
    return spread(x) / abs(centerVal)
}

/**
 * Measures precision: the distance between two estimations of independent random samples (Precision)
 * 
 * Calculated as 2 * Spread / sqrt(n). The interval center ± precision forms a range
 * that probably contains the true center value.
 */
fun precision(x: List<Double>): Double {
    require(x.isNotEmpty()) { "Input list cannot be empty" }
    
    return 2.0 * spread(x) / sqrt(x.size.toDouble())
}

/**
 * Measures the typical difference between elements of x and y (MedShift)
 * 
 * Calculates the median of all pairwise differences (x[i] - y[j]).
 * Positive values mean x is typically larger, negative means y is typically larger.
 */
fun medShift(x: List<Double>, y: List<Double>): Double {
    require(x.isNotEmpty() && y.isNotEmpty()) { "Input lists cannot be empty" }
    
    val pairwiseShifts = mutableListOf<Double>()
    for (xi in x) {
        for (yj in y) {
            pairwiseShifts.add(xi - yj)
        }
    }
    
    return median(pairwiseShifts)
}

/**
 * Measures how many times larger x is compared to y (MedRatio)
 * 
 * Calculates the median of all pairwise ratios (x[i] / y[j]).
 * For example, medRatio = 1.2 means x is typically 20% larger than y.
 */
fun medRatio(x: List<Double>, y: List<Double>): Double {
    require(x.isNotEmpty() && y.isNotEmpty()) { "Input lists cannot be empty" }
    require(y.all { it > 0 }) { "All values in y must be strictly positive" }
    
    val pairwiseRatios = mutableListOf<Double>()
    for (xi in x) {
        for (yj in y) {
            pairwiseRatios.add(xi / yj)
        }
    }
    
    return median(pairwiseRatios)
}

/**
 * Measures the typical variability when considering both samples together (MedSpread)
 * 
 * Computes the weighted average of individual spreads: (n*Spread(x) + m*Spread(y))/(n+m).
 */
fun medSpread(x: List<Double>, y: List<Double>): Double {
    require(x.isNotEmpty() && y.isNotEmpty()) { "Input lists cannot be empty" }
    
    val n = x.size
    val m = y.size
    val spreadX = spread(x)
    val spreadY = spread(y)
    
    return (n * spreadX + m * spreadY) / (n + m).toDouble()
}

/**
 * Measures effect size: a normalized absolute difference between x and y (MedDisparity)
 * 
 * Calculated as MedShift / MedSpread. Robust alternative to Cohen's d.
 * Returns infinity if medSpread is zero.
 */
fun medDisparity(x: List<Double>, y: List<Double>): Double {
    val medSpreadVal = medSpread(x, y)
    if (medSpreadVal == 0.0) return Double.POSITIVE_INFINITY
    
    return medShift(x, y) / medSpreadVal
}