herta
Version:
Advanced mathematics framework for scientific, engineering, and financial applications
541 lines (459 loc) • 15.1 kB
JavaScript
/**
* Probability Theory module for herta.js
* Provides probability distributions, stochastic processes, and statistical functions
*/
const matrix = require('../core/matrix');
const arithmetic = require('../core/arithmetic');
const probabilityTheory = {};
/**
* Calculate factorial
* @param {number} n - Non-negative integer
* @returns {number} - Factorial of n
*/
probabilityTheory.factorial = function (n) {
if (n < 0 || !Number.isInteger(n)) {
throw new Error('Factorial requires a non-negative integer');
}
if (n === 0 || n === 1) return 1;
let result = 1;
for (let i = 2; i <= n; i++) {
result *= i;
}
return result;
};
/**
* Calculate binomial coefficient (n choose k)
* @param {number} n - Total items
* @param {number} k - Items to choose
* @returns {number} - Binomial coefficient
*/
probabilityTheory.binomialCoefficient = function (n, k) {
if (k < 0 || n < k || !Number.isInteger(n) || !Number.isInteger(k)) {
throw new Error('Binomial coefficient requires integers with 0 ≤ k ≤ n');
}
// Optimize calculation for large values
if (k > n - k) {
k = n - k;
}
let result = 1;
for (let i = 0; i < k; i++) {
result *= (n - i) / (i + 1);
}
return Math.round(result);
};
/**
* Probability mass function for Binomial distribution
* @param {number} k - Number of successes
* @param {number} n - Number of trials
* @param {number} p - Probability of success in a single trial
* @returns {number} - Probability of exactly k successes
*/
probabilityTheory.binomialPMF = function (k, n, p) {
if (k < 0 || k > n || !Number.isInteger(k) || !Number.isInteger(n)) {
throw new Error('Binomial PMF requires integers with 0 ≤ k ≤ n');
}
if (p < 0 || p > 1) {
throw new Error('Probability p must be between 0 and 1');
}
return this.binomialCoefficient(n, k) * p ** k * (1 - p) ** (n - k);
};
/**
* Cumulative distribution function for Binomial distribution
* @param {number} k - Number of successes
* @param {number} n - Number of trials
* @param {number} p - Probability of success in a single trial
* @returns {number} - Probability of at most k successes
*/
probabilityTheory.binomialCDF = function (k, n, p) {
let sum = 0;
for (let i = 0; i <= k; i++) {
sum += this.binomialPMF(i, n, p);
}
return sum;
};
/**
* Probability mass function for Poisson distribution
* @param {number} k - Number of events
* @param {number} lambda - Expected number of events
* @returns {number} - Probability of exactly k events
*/
probabilityTheory.poissonPMF = function (k, lambda) {
if (k < 0 || !Number.isInteger(k)) {
throw new Error('Poisson PMF requires non-negative integer k');
}
if (lambda <= 0) {
throw new Error('Lambda must be positive');
}
return Math.exp(-lambda) * lambda ** k / this.factorial(k);
};
/**
* Cumulative distribution function for Poisson distribution
* @param {number} k - Number of events
* @param {number} lambda - Expected number of events
* @returns {number} - Probability of at most k events
*/
probabilityTheory.poissonCDF = function (k, lambda) {
let sum = 0;
for (let i = 0; i <= k; i++) {
sum += this.poissonPMF(i, lambda);
}
return sum;
};
/**
* Probability density function for Normal distribution
* @param {number} x - Value
* @param {number} mean - Mean of the distribution
* @param {number} stdDev - Standard deviation of the distribution
* @returns {number} - PDF at x
*/
probabilityTheory.normalPDF = function (x, mean = 0, stdDev = 1) {
if (stdDev <= 0) {
throw new Error('Standard deviation must be positive');
}
const z = (x - mean) / stdDev;
return Math.exp(-0.5 * z * z) / (stdDev * Math.sqrt(2 * Math.PI));
};
/**
* Cumulative distribution function for Normal distribution
* @param {number} x - Value
* @param {number} mean - Mean of the distribution
* @param {number} stdDev - Standard deviation of the distribution
* @returns {number} - CDF at x
*/
probabilityTheory.normalCDF = function (x, mean = 0, stdDev = 1) {
if (stdDev <= 0) {
throw new Error('Standard deviation must be positive');
}
const z = (x - mean) / stdDev;
return 0.5 * (1 + this.erf(z / Math.sqrt(2)));
};
/**
* Error function implementation
* @param {number} x - Input value
* @returns {number} - Error function value
*/
probabilityTheory.erf = function (x) {
// Abramowitz and Stegun approximation (accuracy ~1.5×10−7)
const sign = x >= 0 ? 1 : -1;
const t = 1 / (1 + 0.3275911 * Math.abs(x));
const y = 1 - ((((1.061405429 * t + -1.453152027) * t + 1.421413741) * t + -0.284496736) * t + 0.254829592) * t * Math.exp(-x * x);
return sign * y;
};
/**
* Probability density function for Exponential distribution
* @param {number} x - Value
* @param {number} lambda - Rate parameter
* @returns {number} - PDF at x
*/
probabilityTheory.exponentialPDF = function (x, lambda) {
if (lambda <= 0) {
throw new Error('Rate parameter must be positive');
}
if (x < 0) {
return 0;
}
return lambda * Math.exp(-lambda * x);
};
/**
* Cumulative distribution function for Exponential distribution
* @param {number} x - Value
* @param {number} lambda - Rate parameter
* @returns {number} - CDF at x
*/
probabilityTheory.exponentialCDF = function (x, lambda) {
if (lambda <= 0) {
throw new Error('Rate parameter must be positive');
}
if (x < 0) {
return 0;
}
return 1 - Math.exp(-lambda * x);
};
/**
* Quantile function (inverse CDF) for Normal distribution
* @param {number} p - Probability (0 to 1)
* @param {number} mean - Mean of the distribution
* @param {number} stdDev - Standard deviation of the distribution
* @returns {number} - Value x such that P(X ≤ x) = p
*/
probabilityTheory.normalQuantile = function (p, mean = 0, stdDev = 1) {
if (p < 0 || p > 1) {
throw new Error('Probability p must be between 0 and 1');
}
if (stdDev <= 0) {
throw new Error('Standard deviation must be positive');
}
// Approximation of the standard normal quantile function
if (p === 0) return -Infinity;
if (p === 1) return Infinity;
let q; let
r;
if (p < 0.5) {
q = p;
r = -1;
} else {
q = 1 - p;
r = 1;
}
const y = Math.sqrt(-2 * Math.log(q));
const a = 2.515517 + (0.802853 * y) + (0.010328 * y * y);
const b = 1 + (1.432788 * y) + (0.189269 * y * y) + (0.001308 * y * y * y);
return mean + stdDev * r * (y - a / b);
};
/**
* Generate n random samples from Normal distribution
* @param {number} n - Number of samples
* @param {number} mean - Mean of the distribution
* @param {number} stdDev - Standard deviation of the distribution
* @returns {Array} - Array of random samples
*/
probabilityTheory.normalRandom = function (n, mean = 0, stdDev = 1) {
if (n <= 0 || !Number.isInteger(n)) {
throw new Error('Number of samples must be a positive integer');
}
if (stdDev <= 0) {
throw new Error('Standard deviation must be positive');
}
const samples = [];
// Box-Muller transform
for (let i = 0; i < n; i += 2) {
const u1 = Math.random();
const u2 = Math.random();
const z1 = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
samples.push(mean + stdDev * z1);
// Only add the second sample if we need it
if (i + 1 < n) {
const z2 = Math.sqrt(-2 * Math.log(u1)) * Math.sin(2 * Math.PI * u2);
samples.push(mean + stdDev * z2);
}
}
return samples;
};
/**
* Generate n random samples from Exponential distribution
* @param {number} n - Number of samples
* @param {number} lambda - Rate parameter
* @returns {Array} - Array of random samples
*/
probabilityTheory.exponentialRandom = function (n, lambda) {
if (n <= 0 || !Number.isInteger(n)) {
throw new Error('Number of samples must be a positive integer');
}
if (lambda <= 0) {
throw new Error('Rate parameter must be positive');
}
const samples = [];
// Inverse transform sampling
for (let i = 0; i < n; i++) {
const u = Math.random();
samples.push(-Math.log(1 - u) / lambda);
}
return samples;
};
/**
* Perform Monte Carlo integration
* @param {Function} func - Function to integrate
* @param {Array} lowerBounds - Lower bounds of integration
* @param {Array} upperBounds - Upper bounds of integration
* @param {number} samples - Number of samples
* @returns {Object} - Result containing estimate and error
*/
probabilityTheory.monteCarloIntegration = function (func, lowerBounds, upperBounds, samples = 10000) {
if (lowerBounds.length !== upperBounds.length) {
throw new Error('Lower and upper bounds must have the same dimension');
}
if (samples <= 0 || !Number.isInteger(samples)) {
throw new Error('Number of samples must be a positive integer');
}
const dimension = lowerBounds.length;
let sum = 0;
let sumSquared = 0;
// Calculate volume of integration region
let volume = 1;
for (let i = 0; i < dimension; i++) {
volume *= (upperBounds[i] - lowerBounds[i]);
}
// Perform Monte Carlo integration
for (let i = 0; i < samples; i++) {
// Generate random point in integration region
const point = [];
for (let j = 0; j < dimension; j++) {
point.push(lowerBounds[j] + Math.random() * (upperBounds[j] - lowerBounds[j]));
}
// Evaluate function at random point
const value = func(...point);
sum += value;
sumSquared += value * value;
}
// Calculate estimate and standard error
const estimate = (sum / samples) * volume;
const variance = (sumSquared / samples - (sum / samples) ** 2) * volume * volume / samples;
const error = Math.sqrt(variance);
return { estimate, error };
};
/**
* Calculate sample statistics
* @param {Array} data - Array of numerical data
* @returns {Object} - Statistics including mean, variance, skewness, etc.
*/
probabilityTheory.sampleStatistics = function (data) {
if (!Array.isArray(data) || data.length === 0) {
throw new Error('Data must be a non-empty array');
}
const n = data.length;
// Calculate mean
const mean = data.reduce((sum, x) => sum + x, 0) / n;
// Calculate variance and standard deviation
let variance = 0;
let sumCubed = 0;
let sumFourth = 0;
for (const x of data) {
const dev = x - mean;
const devSquared = dev * dev;
variance += devSquared;
sumCubed += devSquared * dev;
sumFourth += devSquared * devSquared;
}
variance /= (n - 1); // Unbiased estimator
const stdDev = Math.sqrt(variance);
// Calculate skewness
const skewness = (sumCubed / n) / stdDev ** 3;
// Calculate kurtosis
const kurtosis = (sumFourth / n) / variance ** 2;
// Calculate median
const sortedData = [...data].sort((a, b) => a - b);
let median;
if (n % 2 === 0) {
median = (sortedData[n / 2 - 1] + sortedData[n / 2]) / 2;
} else {
median = sortedData[Math.floor(n / 2)];
}
// Calculate quantiles
const q1 = sortedData[Math.floor(n / 4)];
const q3 = sortedData[Math.floor(3 * n / 4)];
return {
n,
mean,
variance,
stdDev,
skewness,
kurtosis,
median,
q1,
q3,
min: sortedData[0],
max: sortedData[n - 1]
};
};
/**
* Calculate correlation coefficient between two variables
* @param {Array} x - First variable
* @param {Array} y - Second variable
* @returns {number} - Pearson correlation coefficient
*/
probabilityTheory.correlation = function (x, y) {
if (!Array.isArray(x) || !Array.isArray(y) || x.length !== y.length || x.length === 0) {
throw new Error('Inputs must be arrays of the same non-zero length');
}
const n = x.length;
// Calculate means
const meanX = x.reduce((sum, val) => sum + val, 0) / n;
const meanY = y.reduce((sum, val) => sum + val, 0) / n;
// Calculate covariance and variances
let covariance = 0;
let varianceX = 0;
let varianceY = 0;
for (let i = 0; i < n; i++) {
const devX = x[i] - meanX;
const devY = y[i] - meanY;
covariance += devX * devY;
varianceX += devX * devX;
varianceY += devY * devY;
}
// Calculate correlation coefficient
return covariance / Math.sqrt(varianceX * varianceY);
};
/**
* Perform hypothesis test for the mean of a Normal distribution
* @param {Array} data - Sample data
* @param {number} mu0 - Null hypothesis mean
* @param {Object} options - Test options
* @returns {Object} - Test result
*/
probabilityTheory.tTest = function (data, mu0, options = {}) {
const { alternative = 'two-sided', alpha = 0.05 } = options;
if (!Array.isArray(data) || data.length < 2) {
throw new Error('Data must be an array with at least two elements');
}
if (!['two-sided', 'less', 'greater'].includes(alternative)) {
throw new Error('Alternative must be "two-sided", "less", or "greater"');
}
const stats = this.sampleStatistics(data);
const { n } = stats;
const se = stats.stdDev / Math.sqrt(n);
const tStat = (stats.mean - mu0) / se;
// Calculate p-value based on t-distribution
// This is an approximation using the normal distribution
let pValue;
if (alternative === 'two-sided') {
pValue = 2 * (1 - this.normalCDF(Math.abs(tStat)));
} else if (alternative === 'greater') {
pValue = 1 - this.normalCDF(tStat);
} else { // alternative === 'less'
pValue = this.normalCDF(tStat);
}
// Calculate confidence interval
const criticalValue = this.normalQuantile(1 - alpha / 2);
const margin = criticalValue * se;
const ciLower = stats.mean - margin;
const ciUpper = stats.mean + margin;
return {
statistic: tStat,
pValue,
rejectNull: pValue < alpha,
confidenceInterval: [ciLower, ciUpper],
sampleMean: stats.mean,
sampleStdDev: stats.stdDev,
standardError: se
};
};
/**
* Simple linear regression
* @param {Array} x - Independent variable values
* @param {Array} y - Dependent variable values
* @returns {Object} - Regression results
*/
probabilityTheory.linearRegression = function (x, y) {
if (!Array.isArray(x) || !Array.isArray(y) || x.length !== y.length || x.length < 2) {
throw new Error('Inputs must be arrays of the same length (at least 2)');
}
const n = x.length;
// Calculate means
const meanX = x.reduce((sum, val) => sum + val, 0) / n;
const meanY = y.reduce((sum, val) => sum + val, 0) / n;
// Calculate slope and intercept
let numerator = 0;
let denominator = 0;
for (let i = 0; i < n; i++) {
const devX = x[i] - meanX;
numerator += devX * (y[i] - meanY);
denominator += devX * devX;
}
const slope = numerator / denominator;
const intercept = meanY - slope * meanX;
// Calculate predictions and residuals
const predictions = x.map((xi) => intercept + slope * xi);
const residuals = y.map((yi, i) => yi - predictions[i]);
// Calculate R-squared
const ssTotal = y.reduce((sum, yi) => sum + (yi - meanY) ** 2, 0);
const ssResidual = residuals.reduce((sum, r) => sum + r * r, 0);
const rSquared = 1 - ssResidual / ssTotal;
return {
slope,
intercept,
rSquared,
predictions,
residuals
};
};
module.exports = probabilityTheory;