Aggregation.js

// src/Aggregation.js

import { convertToNumber } from './utils/conversions.js';

/**
 * This class is responsible for aggregating functions on a column.
 * It provides functions such as min, max, range, mean, median, count,
 * sample standard deviation, and population standard deviation.
 */
export class Aggregation {
  // Instance Variables
  column;

  /**
   * The class responsible for aggregating functions on a column
   * @param {Array} column The column to perform aggregation functions for
   */
  constructor(column) {
    // Check for if column is provided
    if (!column) {
      throw new TypeError('Column must be provided');
    }
    // Check for if column is an array of all numbers
    if (!Array.isArray(column)) {
      throw new TypeError('Column must be an array');
    }
    if (column.length == 0) {
      throw new TypeError('Column must not be empty');
    }
    column = column.map((value) => convertToNumber(value));

    this.column = column;
  }

  /**
   * The minimum value of the column
   * @function
   * @returns {number} The minimum value of the column
   */
  min() {
    return Math.min.apply(Math, this.column);
  }

  /**
   * The maximum value of the column
   * @function
   * @returns {number} The maximum value of the column
   */
  max() {
    return Math.max.apply(Math, this.column);
  }

  /**
   * The range of the column
   * @function
   * @returns {number} The range of the column
   */
  range() {
    return Math.abs(this.max() - this.min());
  }

  /**
   * The mean of the column
   * @function
   * @returns {number} The mean value of the column
   */
  mean() {
    let sum = 0;
    // Find the sum of the columns
    for (let i = 0; i < this.column.length; i++) {
      sum += this.column[i];
    }

    // Devide the sum by the count
    return sum / this.count();
  }

  /**
   * The median of the column
   * @function
   * @returns {number} The median value of the column
   */
  median() {
    // Sort the column from smallest to biggest
    const sorted = Array.from(this.column).sort((a, b) => a - b);
    // Find the halfway point's floor
    const middle = Math.floor(sorted.length / 2);

    // Check if the length is even. If it is, get the mean of the middle two
    // values
    if (sorted.length % 2 === 0) {
      return (sorted[middle - 1] + sorted[middle]) / 2;
    }

    // If odd, return the middle value
    return sorted[middle];
  }

  /**
   * The number of elements present in the column
   * @function
   * @returns {number} The count of the column
   */
  count() {
    return this.column.length;
  }

  /**
   * Sample Standard Deviation of the column. Use this to estimate the
   * variability for the population this sample is a subset of
   * @function
   * @returns {number} The sample standard deviation of the column
   */
  stds() {
    // Get the mean of the column
    const columnMean = this.mean();
    // Find (xi - xbar)^2 of every value and add it
    let deltaSquaredSum = 0;
    for (let i = 0; i < this.column.length; i++) {
      deltaSquaredSum += Math.pow(this.column[i] - columnMean, 2);
    }

    // Use the sample standard deviation formula
    return Math.sqrt(deltaSquaredSum / (this.count() - 1));
  }

  /**
   * Population Standard Deviation. Use this if the column is the population.
   * @function
   * @returns {number} The population standard deviation of the column
   */
  stdp() {
    const populationMean = this.mean();
    // Find (xi - populationMean)^2 of every value and add it
    let deltaSquaredSum = 0;
    for (let i = 0; i < this.column.length; i++) {
      deltaSquaredSum += Math.pow(this.column[i] - populationMean, 2);
    }

    // Use the population standard deviation formula
    return Math.sqrt(deltaSquaredSum / this.count());
  }

  /**
   * Get the number that is at the given percentile based on the given column
   * @param {number} percentile The percentile of the data to get
   * @returns {number} A number that is at the given percentile based on the
   * column
   */
  percentile(percentile) {
    // Error handling
    // if percentile isn't given
    if (!percentile) {
      throw new TypeError('Percentile must be given!');
    }
    // If given percentile is not a number
    if (typeof percentile != 'number') {
      throw new TypeError('Percentile must be a number!');
    }

    // Step 1: Make a copy of the array and sort it in ascending order
    const sorted = [...this.column].sort((a, b) => a - b);

    // Step 2: Calculate the index for the desired percentile using:
    // index = (percentile / 100) * (n - 1)
    const index = (percentile / 100) * (sorted.length - 1);

    // Step 3: Get the lower and upper indices around the index
    const lower = Math.floor(index);
    const upper = Math.ceil(index);

    // If index is an integer, return the value at that index
    if (lower === upper) return sorted[lower];

    // Otherwise, interpolate between the two surrounding values
    const weight = index - lower;
    return sorted[lower] * (1 - weight) + sorted[upper] * weight;
  }
}