datapilot-cli
Version:
Enterprise-grade streaming multi-format data analysis with comprehensive statistical insights and intelligent relationship detection - supports CSV, JSON, Excel, TSV, Parquet - memory-efficient, cross-platform
814 lines (806 loc) • 34.4 kB
JavaScript
;
/**
* Hypothesis Testing Module
* Implements proper statistical tests for DataPilot analysis
*
* Features:
* - ANOVA F-test for comparing group means
* - Kruskal-Wallis test for non-parametric group comparison
* - Comprehensive result interpretation
* - Numerical stability and edge case handling
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.anovaFTest = anovaFTest;
exports.kruskalWallisTest = kruskalWallisTest;
exports.welchsTTest = welchsTTest;
exports.mannWhitneyUTest = mannWhitneyUTest;
exports.andersonDarlingTest = andersonDarlingTest;
const distributions_1 = require("./distributions");
/**
* ANOVA (Analysis of Variance) F-test
* Tests H₀: μ₁ = μ₂ = ... = μₖ (all group means are equal)
* vs H₁: At least one mean differs
*
* Algorithm:
* 1. Calculate between-group variance (MSB)
* 2. Calculate within-group variance (MSW)
* 3. F = MSB/MSW ~ F(k-1, N-k)
* 4. p-value from F-distribution
*/
function anovaFTest(groups) {
(0, distributions_1.validateTestInputs)(groups);
const k = groups.length; // Number of groups
const N = groups.reduce((sum, group) => sum + group.count, 0); // Total observations
if (N <= k) {
throw new Error('ANOVA requires more total observations than groups');
}
// Calculate overall mean (weighted by group sizes)
const overallMean = groups.reduce((sum, group) => sum + group.mean * group.count, 0) / N;
// Calculate Sum of Squares Between groups (SSB)
const ssb = groups.reduce((sum, group) => sum + group.count * Math.pow(group.mean - overallMean, 2), 0);
// Calculate Sum of Squares Within groups (SSW)
const ssw = groups.reduce((sum, group) => sum + (group.count - 1) * group.variance, 0);
// Degrees of freedom
const dfBetween = k - 1;
const dfWithin = N - k;
// Mean squares
const msb = ssb / dfBetween;
const msw = ssw / dfWithin;
// Handle edge case: no within-group variance
if (msw === 0) {
if (ssb === 0) {
// All observations are identical
return {
testName: 'ANOVA F-test',
statistic: NaN,
pValue: 1.0,
degreesOfFreedom: [dfBetween, dfWithin],
effectSize: 0,
interpretation: 'All observations are identical. No variance to test.',
assumptions: getAnovaAssumptions(),
recommendations: ['Verify data collection and measurement procedures'],
};
}
else {
// Perfect separation (infinite F-statistic)
return {
testName: 'ANOVA F-test',
statistic: Infinity,
pValue: 0.0,
degreesOfFreedom: [dfBetween, dfWithin],
effectSize: 1,
interpretation: 'Perfect group separation detected. Groups have identical within-group values but different means.',
assumptions: getAnovaAssumptions(),
recommendations: [
'Extremely strong evidence against null hypothesis',
'Verify this is not due to data preprocessing artifacts',
],
};
}
}
// Calculate F-statistic
const fStatistic = (0, distributions_1.checkNumericalStability)(msb / msw, 'F-statistic calculation');
// Calculate p-value (survival function for upper tail)
const pValue = (0, distributions_1.fccdf)(fStatistic, dfBetween, dfWithin);
// Effect size (eta-squared)
const etaSquared = ssb / (ssb + ssw);
// Interpretation
const interpretation = generateAnovaInterpretation(fStatistic, pValue, dfBetween, dfWithin, etaSquared);
// Recommendations based on results
const recommendations = generateAnovaRecommendations(pValue, etaSquared, groups);
return {
testName: 'ANOVA F-test',
statistic: Number(fStatistic.toFixed(6)),
pValue: Number(pValue.toFixed(6)),
degreesOfFreedom: [dfBetween, dfWithin],
effectSize: Number(etaSquared.toFixed(4)),
interpretation,
assumptions: getAnovaAssumptions(),
recommendations,
};
}
/**
* Kruskal-Wallis test (non-parametric alternative to ANOVA)
* Tests H₀: All groups have the same distribution
* vs H₁: At least one group has a different distribution
*
* Algorithm:
* 1. Pool all observations and rank them
* 2. Calculate sum of ranks for each group
* 3. H = (12/[N(N+1)]) * Σ(Rᵢ²/nᵢ) - 3(N+1)
* 4. H ~ χ²(k-1) approximately
*/
function kruskalWallisTest(groups) {
(0, distributions_1.validateTestInputs)(groups);
// Check if we have raw values for all groups
const hasValues = groups.every((group) => group.values && group.values.length === group.count);
if (!hasValues) {
// Fallback: Use approximation based on summary statistics
return kruskalWallisApproximation(groups);
}
const k = groups.length;
// Pool all observations with group identifiers
const pooledData = [];
groups.forEach((group, groupIndex) => {
group.values.forEach((value) => {
pooledData.push({ value, group: groupIndex });
});
});
const N = pooledData.length;
// Sort by value and assign ranks
pooledData.sort((a, b) => a.value - b.value);
// Handle ties by assigning average ranks
const ranks = assignRanksWithTies(pooledData.map((d) => d.value));
// Calculate sum of ranks for each group
const rankSums = new Array(k).fill(0);
pooledData.forEach((item, index) => {
rankSums[item.group] += ranks[index];
});
// Calculate H statistic
let hStatistic = 0;
for (let i = 0; i < k; i++) {
const ni = groups[i].count;
const Ri = rankSums[i];
hStatistic += (Ri * Ri) / ni;
}
hStatistic = (12 / (N * (N + 1))) * hStatistic - 3 * (N + 1);
// Adjustment for ties
const tieAdjustment = calculateTieAdjustment(pooledData.map((d) => d.value));
if (tieAdjustment > 0) {
hStatistic = hStatistic / (1 - tieAdjustment / (N * N * N - N));
}
const df = k - 1;
const pValue = (0, distributions_1.chisqccdf)(hStatistic, df);
// Effect size (epsilon-squared)
const epsilonSquared = (hStatistic - df) / (N - 1);
const interpretation = generateKruskalWallisInterpretation(hStatistic, pValue, df, epsilonSquared);
const recommendations = generateKruskalWallisRecommendations(pValue, epsilonSquared, groups);
return {
testName: 'Kruskal-Wallis test',
statistic: Number(hStatistic.toFixed(6)),
pValue: Number(pValue.toFixed(6)),
degreesOfFreedom: df,
effectSize: Number(Math.max(0, epsilonSquared).toFixed(4)),
interpretation,
assumptions: getKruskalWallisAssumptions(),
recommendations,
};
}
/**
* Kruskal-Wallis approximation using summary statistics
* When raw values are not available, use statistical approximation
*/
function kruskalWallisApproximation(groups) {
// This is a simplified approximation based on means and variances
// Not as accurate as the rank-based test, but better than nothing
const k = groups.length;
const N = groups.reduce((sum, group) => sum + group.count, 0);
// Use a variance-weighted statistic as approximation
const overallMean = groups.reduce((sum, group) => sum + group.mean * group.count, 0) / N;
// Calculate a pseudo H-statistic based on standardized differences
let hApprox = 0;
for (const group of groups) {
const standardizedDiff = group.variance > 0 ? Math.pow(group.mean - overallMean, 2) / group.variance : 0;
hApprox += group.count * standardizedDiff;
}
const df = k - 1;
const pValue = (0, distributions_1.chisqccdf)(hApprox, df);
return {
testName: 'Kruskal-Wallis test (approximation)',
statistic: Number(hApprox.toFixed(6)),
pValue: Number(pValue.toFixed(6)),
degreesOfFreedom: df,
effectSize: 0, // Cannot calculate without raw values
interpretation: `**Approximate Kruskal-Wallis Test:**
- **Note:** This is an approximation based on summary statistics
- **Limitation:** True Kruskal-Wallis requires raw values for ranking
- **H-statistic (approx):** ${hApprox.toFixed(4)}
- **p-value:** ${pValue.toFixed(6)}
- **Interpretation:** ${pValue < 0.05 ? 'Suggests' : 'Does not suggest'} significant differences between groups`,
assumptions: [
...getKruskalWallisAssumptions(),
'Approximation assumes normally distributed residuals',
],
recommendations: [
'Use with caution - approximation only',
'Consider collecting raw values for exact Kruskal-Wallis test',
'Cross-validate with ANOVA F-test results',
],
};
}
/**
* Assign ranks with proper tie handling
*/
function assignRanksWithTies(values) {
const ranks = new Array(values.length);
let i = 0;
while (i < values.length) {
let j = i;
// Find all values equal to values[i]
while (j < values.length && values[j] === values[i]) {
j++;
}
// Assign average rank to tied values
const avgRank = (i + j + 1) / 2; // +1 because ranks are 1-based
for (let k = i; k < j; k++) {
ranks[k] = avgRank;
}
i = j;
}
return ranks;
}
/**
* Calculate tie adjustment factor for Kruskal-Wallis
*/
function calculateTieAdjustment(values) {
const tieGroups = new Map();
for (const value of values) {
tieGroups.set(value, (tieGroups.get(value) || 0) + 1);
}
let tieAdjustment = 0;
for (const count of tieGroups.values()) {
if (count > 1) {
tieAdjustment += count * count * count - count;
}
}
return tieAdjustment;
}
/**
* Generate detailed ANOVA interpretation
*/
function generateAnovaInterpretation(fStat, pValue, df1, df2, etaSquared) {
const significance = pValue < 0.001
? 'highly significant (p < 0.001)'
: pValue < 0.01
? 'very significant (p < 0.01)'
: pValue < 0.05
? 'significant (p < 0.05)'
: pValue < 0.1
? 'marginally significant (p < 0.1)'
: 'not significant (p ≥ 0.1)';
const effectInterpretation = etaSquared < 0.01
? 'negligible'
: etaSquared < 0.06
? 'small'
: etaSquared < 0.14
? 'medium'
: 'large';
return `**ANOVA F-test Results:**
- **Null Hypothesis (H₀):** All group means are equal
- **Alternative Hypothesis (H₁):** At least one group mean differs
- **F-statistic:** F(${df1}, ${df2}) = ${fStat.toFixed(4)}
- **p-value:** ${pValue.toFixed(6)} (${significance})
- **Effect Size (η²):** ${etaSquared.toFixed(4)} (${effectInterpretation} effect)
**Statistical Decision:**
${pValue < 0.05
? `Reject H₀. There is ${significance.replace('significant', 'evidence')} that at least one group mean differs from the others.`
: `Fail to reject H₀. There is insufficient evidence to conclude that group means differ significantly.`}
**Practical Interpretation:**
- **Variance Explained:** ${(etaSquared * 100).toFixed(1)}% of the total variance is explained by group differences
- **Residual Variance:** ${((1 - etaSquared) * 100).toFixed(1)}% remains unexplained (within-group variation)`;
}
/**
* Generate detailed Kruskal-Wallis interpretation
*/
function generateKruskalWallisInterpretation(hStat, pValue, df, epsilonSquared) {
const significance = pValue < 0.001
? 'highly significant (p < 0.001)'
: pValue < 0.01
? 'very significant (p < 0.01)'
: pValue < 0.05
? 'significant (p < 0.05)'
: pValue < 0.1
? 'marginally significant (p < 0.1)'
: 'not significant (p ≥ 0.1)';
const effectInterpretation = epsilonSquared < 0.01
? 'negligible'
: epsilonSquared < 0.06
? 'small'
: epsilonSquared < 0.14
? 'medium'
: 'large';
return `**Kruskal-Wallis Test Results:**
- **Null Hypothesis (H₀):** All groups have the same distribution
- **Alternative Hypothesis (H₁):** At least one group has a different distribution
- **H-statistic:** H = ${hStat.toFixed(4)} ~ χ²(${df})
- **p-value:** ${pValue.toFixed(6)} (${significance})
- **Effect Size (ε²):** ${epsilonSquared.toFixed(4)} (${effectInterpretation} effect)
**Statistical Decision:**
${pValue < 0.05
? `Reject H₀. There is ${significance.replace('significant', 'evidence')} that at least one group has a different distribution.`
: `Fail to reject H₀. There is insufficient evidence to conclude that group distributions differ significantly.`}
**Advantages over ANOVA:**
- **Non-parametric:** No assumption of normality required
- **Robust to outliers:** Uses ranks instead of raw values
- **Distribution-free:** Tests for any difference in distributions, not just means`;
}
/**
* ANOVA assumptions
*/
function getAnovaAssumptions() {
return [
'Independence: Observations within and between groups are independent',
'Normality: Residuals are approximately normally distributed',
'Homoscedasticity: Equal variances across all groups (homogeneity of variance)',
'Interval/ratio data: Dependent variable is measured at interval or ratio level',
];
}
/**
* Kruskal-Wallis assumptions
*/
function getKruskalWallisAssumptions() {
return [
'Independence: Observations within and between groups are independent',
'Ordinal data: Dependent variable is at least ordinal (rankable)',
'Similar distributions: Groups have similar distribution shapes (for location comparison)',
'Adequate sample size: Each group should have at least 5 observations for χ² approximation',
];
}
/**
* Generate ANOVA recommendations
*/
function generateAnovaRecommendations(pValue, etaSquared, groups) {
const recommendations = [];
if (pValue < 0.05) {
recommendations.push('Significant result detected - consider post-hoc tests to identify which groups differ');
if (groups.length > 2) {
recommendations.push('Use Tukey HSD, Bonferroni, or Scheffé tests for pairwise comparisons');
}
}
else {
recommendations.push('No significant differences detected - groups appear to have similar means');
}
if (etaSquared < 0.01) {
recommendations.push('Very small effect size - differences may not be practically meaningful');
}
else if (etaSquared > 0.14) {
recommendations.push('Large effect size - differences are likely practically significant');
}
// Check for potential assumption violations
const minGroupSize = Math.min(...groups.map((g) => g.count));
if (minGroupSize < 10) {
recommendations.push('Small group sizes detected - verify normality assumptions');
}
const variances = groups.map((g) => g.variance);
const maxVar = Math.max(...variances);
const minVar = Math.min(...variances);
if (maxVar / minVar > 4) {
recommendations.push('Unequal variances detected - consider Welch ANOVA or transformation');
}
return recommendations;
}
/**
* Generate Kruskal-Wallis recommendations
*/
function generateKruskalWallisRecommendations(pValue, epsilonSquared, groups) {
const recommendations = [];
if (pValue < 0.05) {
recommendations.push('Significant result detected - consider post-hoc tests (Dunn test) for pairwise comparisons');
}
else {
recommendations.push('No significant differences detected - group distributions appear similar');
}
if (epsilonSquared > 0 && epsilonSquared < 0.01) {
recommendations.push('Very small effect size - differences may not be practically meaningful');
}
else if (epsilonSquared > 0.14) {
recommendations.push('Large effect size - differences are likely practically significant');
}
const minGroupSize = Math.min(...groups.map((g) => g.count));
if (minGroupSize < 5) {
recommendations.push('Small group sizes - χ² approximation may be inaccurate, consider exact test');
}
recommendations.push('Non-parametric test - robust to outliers and non-normal distributions');
return recommendations;
}
/**
* Welch's t-test (unequal variances t-test)
* Tests H₀: μ₁ = μ₂ (means are equal) vs H₁: μ₁ ≠ μ₂ (means differ)
* Does not assume equal variances (unlike Student's t-test)
*
* Algorithm:
* 1. Calculate t-statistic: t = (x̄₁ - x̄₂) / √(s₁²/n₁ + s₂²/n₂)
* 2. Calculate Welch-Satterthwaite degrees of freedom
* 3. p-value from t-distribution
*/
function welchsTTest(group1, group2) {
if (!group1 || !group2) {
throw new Error("Welch's t-test requires exactly two groups");
}
const n1 = group1.count;
const n2 = group2.count;
const mean1 = group1.mean;
const mean2 = group2.mean;
const var1 = group1.variance;
const var2 = group2.variance;
if (n1 < 2 || n2 < 2) {
throw new Error("Each group must have at least 2 observations for Welch's t-test");
}
// Standard errors
const se1 = var1 / n1;
const se2 = var2 / n2;
const pooledSE = Math.sqrt(se1 + se2);
if (pooledSE === 0) {
// No variance in either group
return {
testName: "Welch's t-test",
statistic: mean1 === mean2 ? 0 : Infinity,
pValue: mean1 === mean2 ? 1.0 : 0.0,
degreesOfFreedom: n1 + n2 - 2,
effectSize: mean1 === mean2 ? 0 : Infinity,
interpretation: mean1 === mean2
? 'Groups have identical means and no variance'
: 'Perfect separation - groups have different means with no variance',
assumptions: getWelchsAssumptions(),
recommendations: ['Verify data collection procedures'],
};
}
// Calculate t-statistic
const tStatistic = (mean1 - mean2) / pooledSE;
// Welch-Satterthwaite degrees of freedom approximation
const df = Math.pow(se1 + se2, 2) / (Math.pow(se1, 2) / (n1 - 1) + Math.pow(se2, 2) / (n2 - 1));
// Two-tailed p-value (using normal approximation for large df, t-distribution for small df)
let pValue;
if (df > 100) {
// Normal approximation for large df
pValue = 2 * (1 - standardNormalCdf(Math.abs(tStatistic)));
}
else {
// Use chi-squared approximation for t-distribution (simplified)
pValue = 2 * (1 - standardNormalCdf(Math.abs(tStatistic))); // Simplified - full t-distribution would be better
}
// Cohen's d effect size
const pooledSD = Math.sqrt(((n1 - 1) * var1 + (n2 - 1) * var2) / (n1 + n2 - 2));
const cohensD = pooledSD > 0 ? Math.abs(mean1 - mean2) / pooledSD : 0;
const interpretation = generateWelchsInterpretation(tStatistic, pValue, df, cohensD, group1.name, group2.name);
const recommendations = generateWelchsRecommendations(pValue, cohensD, var1, var2, n1, n2);
return {
testName: "Welch's t-test",
statistic: Number(tStatistic.toFixed(6)),
pValue: Number(pValue.toFixed(6)),
degreesOfFreedom: Number(df.toFixed(2)),
effectSize: Number(cohensD.toFixed(4)),
interpretation,
assumptions: getWelchsAssumptions(),
recommendations,
};
}
/**
* Mann-Whitney U test (Wilcoxon rank-sum test)
* Non-parametric alternative to independent samples t-test
* Tests H₀: P(X > Y) = 0.5 vs H₁: P(X > Y) ≠ 0.5
*/
function mannWhitneyUTest(group1, group2) {
if (!group1 || !group2) {
throw new Error('Mann-Whitney U test requires exactly two groups');
}
if (!group1.values || !group2.values) {
return mannWhitneyApproximation(group1, group2);
}
const n1 = group1.count;
const n2 = group2.count;
const values1 = group1.values;
const values2 = group2.values;
// Combine all values and rank them
const combined = [
...values1.map((v) => ({ value: v, group: 1 })),
...values2.map((v) => ({ value: v, group: 2 })),
];
combined.sort((a, b) => a.value - b.value);
const ranks = assignRanksWithTies(combined.map((item) => item.value));
// Calculate rank sums
let R1 = 0; // Sum of ranks for group 1
combined.forEach((item, index) => {
if (item.group === 1) {
R1 += ranks[index];
}
});
// Calculate U statistics
const U1 = R1 - (n1 * (n1 + 1)) / 2;
const U2 = n1 * n2 - U1;
const U = Math.min(U1, U2); // Test statistic is the smaller U
// For large samples, use normal approximation
const N = n1 + n2;
if (n1 >= 8 && n2 >= 8) {
const meanU = (n1 * n2) / 2;
const varU = (n1 * n2 * (N + 1)) / 12;
// Tie correction
const tieCorrection = calculateMannWhitneyTieCorrection(combined.map((item) => item.value));
const adjustedVarU = varU - tieCorrection;
const zStatistic = (U - meanU) / Math.sqrt(adjustedVarU);
const pValue = 2 * (1 - standardNormalCdf(Math.abs(zStatistic)));
// Effect size (r = Z / √N)
const effectSize = Math.abs(zStatistic) / Math.sqrt(N);
const interpretation = generateMannWhitneyInterpretation(U, zStatistic, pValue, effectSize, group1.name, group2.name);
const recommendations = generateMannWhitneyRecommendations(pValue, effectSize, n1, n2);
return {
testName: 'Mann-Whitney U test',
statistic: Number(U.toFixed(6)),
pValue: Number(pValue.toFixed(6)),
degreesOfFreedom: 0, // No degrees of freedom for this test
effectSize: Number(effectSize.toFixed(4)),
interpretation,
assumptions: getMannWhitneyAssumptions(),
recommendations,
};
}
else {
// Small samples - return approximation
return mannWhitneyApproximation(group1, group2);
}
}
/**
* Anderson-Darling normality test
* Tests H₀: Data follows normal distribution vs H₁: Data does not follow normal distribution
* More sensitive to deviations in the tails than Kolmogorov-Smirnov
*/
function andersonDarlingTest(values) {
if (!values || values.length < 5) {
throw new Error('Anderson-Darling test requires at least 5 observations');
}
const n = values.length;
const sortedValues = [...values].sort((a, b) => a - b);
// Calculate sample mean and standard deviation
const mean = sortedValues.reduce((sum, val) => sum + val, 0) / n;
const variance = sortedValues.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / (n - 1);
const sd = Math.sqrt(variance);
if (sd === 0) {
return {
testName: 'Anderson-Darling normality test',
statistic: 0,
pValue: 1.0,
degreesOfFreedom: 0,
effectSize: 0,
interpretation: 'All values are identical - perfect "normality" but no variance',
assumptions: getAndersonDarlingAssumptions(),
recommendations: ['Verify data collection - constant values unusual in real data'],
};
}
// Standardize values
const standardized = sortedValues.map((val) => (val - mean) / sd);
// Calculate Anderson-Darling statistic
let adStatistic = 0;
for (let i = 0; i < n; i++) {
const zi = standardized[i];
const phiZi = standardNormalCdf(zi);
const phiZnMinusI = standardNormalCdf(standardized[n - 1 - i]);
// Avoid log(0) and log(1)
const term1 = phiZi > 0 && phiZi < 1 ? Math.log(phiZi) : 0;
const term2 = phiZnMinusI > 0 && phiZnMinusI < 1 ? Math.log(1 - phiZnMinusI) : 0;
adStatistic += (2 * i + 1) * (term1 + term2);
}
adStatistic = -n - adStatistic / n;
// Adjust for sample size
const adjustedAD = adStatistic * (1 + 0.75 / n + 2.25 / (n * n));
// Approximate p-value (simplified)
let pValue;
if (adjustedAD < 0.2) {
pValue = 1 - Math.exp(-1.2337 * Math.pow(adjustedAD, 5));
}
else if (adjustedAD < 0.34) {
pValue = 1 - Math.exp(-0.9177 * Math.pow(adjustedAD, 4.8));
}
else if (adjustedAD < 0.6) {
pValue = Math.exp(0.731 - 3.009 * adjustedAD + 1.78 * adjustedAD * adjustedAD);
}
else if (adjustedAD < 13) {
pValue = Math.exp(1.092 - 3.09 * adjustedAD + 0.177 * adjustedAD * adjustedAD);
}
else {
pValue = 0.0001; // Very small
}
pValue = Math.max(0.0001, Math.min(0.9999, pValue)); // Clamp to reasonable range
const interpretation = generateAndersonDarlingInterpretation(adjustedAD, pValue);
const recommendations = generateAndersonDarlingRecommendations(pValue, adjustedAD);
return {
testName: 'Anderson-Darling normality test',
statistic: Number(adjustedAD.toFixed(6)),
pValue: Number(pValue.toFixed(6)),
degreesOfFreedom: 0,
effectSize: Number((adjustedAD / n).toFixed(4)), // Normalized statistic as effect size
interpretation,
assumptions: getAndersonDarlingAssumptions(),
recommendations,
};
}
// Helper functions for new tests
function standardNormalCdf(z) {
// Standard normal CDF approximation
return 0.5 * (1 + erf(z / Math.sqrt(2)));
}
function erf(x) {
// Error function approximation
const a1 = 0.254829592;
const a2 = -0.284496736;
const a3 = 1.421413741;
const a4 = -1.453152027;
const a5 = 1.061405429;
const p = 0.3275911;
const sign = x >= 0 ? 1 : -1;
x = Math.abs(x);
const t = 1.0 / (1.0 + p * x);
const y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
return sign * y;
}
function mannWhitneyApproximation(group1, group2) {
// Simplified approximation when raw values not available
const meanDiff = Math.abs(group1.mean - group2.mean);
const pooledSD = Math.sqrt((group1.variance + group2.variance) / 2);
const effectSize = pooledSD > 0 ? meanDiff / pooledSD : 0;
return {
testName: 'Mann-Whitney U test (approximation)',
statistic: effectSize,
pValue: effectSize > 1 ? 0.05 : 0.5, // Very rough approximation
degreesOfFreedom: 0,
effectSize: Number(effectSize.toFixed(4)),
interpretation: 'Approximation based on means and variances (raw values needed for exact test)',
assumptions: [
...getMannWhitneyAssumptions(),
'Approximation only - collect raw values for exact test',
],
recommendations: [
'Use with extreme caution - approximation only',
'Collect raw values for proper Mann-Whitney test',
],
};
}
function calculateMannWhitneyTieCorrection(values) {
const tieGroups = new Map();
for (const value of values) {
tieGroups.set(value, (tieGroups.get(value) || 0) + 1);
}
let correction = 0;
for (const count of tieGroups.values()) {
if (count > 1) {
correction += (count * count * count - count) / 12;
}
}
return correction;
}
// Assumption and interpretation functions for new tests
function getWelchsAssumptions() {
return [
'Independence: Observations are independent within and between groups',
'Normality: Data in each group approximately normally distributed',
"No assumption of equal variances (major advantage over Student's t-test)",
'Interval/ratio data: Dependent variable measured at interval or ratio level',
];
}
function getMannWhitneyAssumptions() {
return [
'Independence: Observations are independent within and between groups',
'Ordinal data: Data can be meaningfully ranked',
'Similar distribution shapes for location comparison',
'Random sampling from populations',
];
}
function getAndersonDarlingAssumptions() {
return [
'Independence: Observations are independent',
'Data should be univariate and continuous',
'Sample size should be at least 5 (preferably > 20 for reliable results)',
'Tests specifically for normal distribution',
];
}
function generateWelchsInterpretation(tStat, pValue, df, cohensD, group1Name, group2Name) {
const significance = pValue < 0.001
? 'highly significant (p < 0.001)'
: pValue < 0.01
? 'very significant (p < 0.01)'
: pValue < 0.05
? 'significant (p < 0.05)'
: 'not significant (p ≥ 0.05)';
const effectInterpretation = cohensD < 0.2 ? 'negligible' : cohensD < 0.5 ? 'small' : cohensD < 0.8 ? 'medium' : 'large';
return `**Welch's t-test Results:**
- **Null Hypothesis (H₀):** Group means are equal
- **Alternative Hypothesis (H₁):** Group means differ
- **t-statistic:** t(${df.toFixed(1)}) = ${tStat.toFixed(4)}
- **p-value:** ${pValue.toFixed(6)} (${significance})
- **Effect Size (Cohen's d):** ${cohensD.toFixed(4)} (${effectInterpretation} effect)
**Statistical Decision:**
${pValue < 0.05
? `Reject H₀. There is ${significance.replace('significant', 'evidence')} that ${group1Name} and ${group2Name} have different means.`
: `Fail to reject H₀. No significant difference detected between ${group1Name} and ${group2Name} means.`}`;
}
function generateMannWhitneyInterpretation(U, zStat, pValue, effectSize, group1Name, group2Name) {
const significance = pValue < 0.001
? 'highly significant (p < 0.001)'
: pValue < 0.01
? 'very significant (p < 0.01)'
: pValue < 0.05
? 'significant (p < 0.05)'
: 'not significant (p ≥ 0.05)';
const effectInterpretation = effectSize < 0.1
? 'negligible'
: effectSize < 0.3
? 'small'
: effectSize < 0.5
? 'medium'
: 'large';
return `**Mann-Whitney U Test Results:**
- **Null Hypothesis (H₀):** Groups have same distribution
- **Alternative Hypothesis (H₁):** Groups have different distributions
- **U-statistic:** U = ${U.toFixed(4)}
- **Z-statistic:** Z = ${zStat.toFixed(4)}
- **p-value:** ${pValue.toFixed(6)} (${significance})
- **Effect Size (r):** ${effectSize.toFixed(4)} (${effectInterpretation} effect)
**Statistical Decision:**
${pValue < 0.05
? `Reject H₀. There is ${significance.replace('significant', 'evidence')} that ${group1Name} and ${group2Name} have different distributions.`
: `Fail to reject H₀. No significant difference detected between ${group1Name} and ${group2Name} distributions.`}`;
}
function generateAndersonDarlingInterpretation(adStat, pValue) {
const significance = pValue < 0.001
? 'highly significant (p < 0.001)'
: pValue < 0.01
? 'very significant (p < 0.01)'
: pValue < 0.05
? 'significant (p < 0.05)'
: 'not significant (p ≥ 0.05)';
return `**Anderson-Darling Normality Test Results:**
- **Null Hypothesis (H₀):** Data follows normal distribution
- **Alternative Hypothesis (H₁):** Data does not follow normal distribution
- **A²-statistic:** A² = ${adStat.toFixed(4)}
- **p-value:** ${pValue.toFixed(6)} (${significance})
**Statistical Decision:**
${pValue < 0.05
? `Reject H₀. There is ${significance.replace('significant', 'evidence')} that the data does not follow a normal distribution.`
: `Fail to reject H₀. The data appears to be consistent with a normal distribution.`}
**Interpretation:**
- **Sensitive to tails:** More sensitive than Kolmogorov-Smirnov to deviations in distribution tails
- **Distribution shape:** ${pValue < 0.05 ? 'Consider data transformation or non-parametric methods' : 'Normal distribution assumption reasonable'}`;
}
function generateWelchsRecommendations(pValue, cohensD, var1, var2, n1, n2) {
const recommendations = [];
if (pValue < 0.05) {
recommendations.push('Significant difference detected between group means');
if (cohensD > 0.8) {
recommendations.push('Large effect size - difference is likely practically significant');
}
}
else {
recommendations.push('No significant difference - groups have similar means');
}
const varianceRatio = Math.max(var1, var2) / Math.min(var1, var2);
if (varianceRatio > 2) {
recommendations.push("Unequal variances detected - Welch's t-test appropriately handles this");
}
if (Math.min(n1, n2) < 30) {
recommendations.push('Small sample size - verify normality assumption');
}
return recommendations;
}
function generateMannWhitneyRecommendations(pValue, effectSize, n1, n2) {
const recommendations = [];
if (pValue < 0.05) {
recommendations.push('Significant difference detected between group distributions');
}
else {
recommendations.push('No significant difference - groups have similar distributions');
}
if (effectSize > 0.5) {
recommendations.push('Large effect size - difference is likely practically significant');
}
if (Math.min(n1, n2) < 8) {
recommendations.push('Small sample size - consider exact test or bootstrap methods');
}
recommendations.push('Non-parametric test - robust to outliers and non-normal distributions');
return recommendations;
}
function generateAndersonDarlingRecommendations(pValue, adStat) {
const recommendations = [];
if (pValue < 0.05) {
recommendations.push('Normality assumption violated - consider data transformation');
recommendations.push('Alternative: Use non-parametric statistical methods');
if (adStat > 1) {
recommendations.push('Strong evidence against normality - transformation highly recommended');
}
}
else {
recommendations.push('Normality assumption satisfied - parametric methods appropriate');
}
recommendations.push('More powerful than Kolmogorov-Smirnov for detecting departures from normality');
recommendations.push('Particularly sensitive to differences in distribution tails');
return recommendations;
}
//# sourceMappingURL=hypothesis-tests.js.map