taguchi
Version:
A TypeScript package for implementing Taguchi Method design of experiments
349 lines (347 loc) • 12.2 kB
JavaScript
// src/index.ts
var SNRatioType;
(function(SNRatioType2) {
SNRatioType2["LARGER_IS_BETTER"] = "LARGER_IS_BETTER";
SNRatioType2["SMALLER_IS_BETTER"] = "SMALLER_IS_BETTER";
SNRatioType2["NOMINAL_IS_BEST"] = "NOMINAL_IS_BEST";
})(SNRatioType || (SNRatioType = {}));
var STANDARD_ARRAYS = {
L4: [
[1, 1, 1],
[1, 2, 2],
[2, 1, 2],
[2, 2, 1]
],
L8: [
[1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 2, 2, 2, 2],
[1, 2, 2, 1, 1, 2, 2],
[1, 2, 2, 2, 2, 1, 1],
[2, 1, 2, 1, 2, 1, 2],
[2, 1, 2, 2, 1, 2, 1],
[2, 2, 1, 1, 2, 2, 1],
[2, 2, 1, 2, 1, 1, 2]
],
L9: [
[1, 1, 1, 1],
[1, 2, 2, 2],
[1, 3, 3, 3],
[2, 1, 2, 3],
[2, 2, 3, 1],
[2, 3, 1, 2],
[3, 1, 3, 2],
[3, 2, 1, 3],
[3, 3, 2, 1]
],
L16: [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
[1, 1, 2, 2, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 2],
[1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 1],
[1, 2, 1, 2, 1, 2, 1, 2, 2, 1, 2, 1, 2, 1, 2],
[1, 2, 1, 2, 2, 1, 2, 1, 1, 2, 1, 2, 1, 2, 1],
[1, 2, 2, 1, 1, 2, 2, 1, 1, 2, 2, 1, 1, 2, 1],
[1, 2, 2, 1, 2, 1, 1, 2, 2, 1, 1, 2, 2, 1, 2],
[2, 1, 1, 2, 1, 2, 2, 1, 2, 1, 1, 2, 2, 1, 1],
[2, 1, 1, 2, 2, 1, 1, 2, 1, 2, 2, 1, 1, 2, 2],
[2, 1, 2, 1, 1, 2, 1, 2, 1, 2, 2, 1, 2, 1, 1],
[2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 2, 1, 2, 2],
[2, 2, 1, 2, 1, 1, 2, 2, 1, 1, 2, 2, 1, 2, 1],
[2, 2, 1, 2, 2, 2, 1, 1, 2, 2, 1, 1, 2, 1, 2],
[2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 1, 2, 2, 1, 2],
[2, 2, 2, 1, 2, 2, 2, 1, 1, 1, 2, 1, 1, 2, 1]
],
L18: [
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 2, 2, 2, 2, 2, 2],
[1, 1, 3, 3, 3, 3, 3, 3],
[1, 2, 1, 1, 2, 2, 3, 3],
[1, 2, 2, 2, 3, 3, 1, 1],
[1, 2, 3, 3, 1, 1, 2, 2],
[1, 3, 1, 2, 1, 3, 2, 3],
[1, 3, 2, 3, 2, 1, 3, 1],
[1, 3, 3, 1, 3, 2, 1, 2],
[2, 1, 1, 3, 3, 2, 2, 1],
[2, 1, 2, 1, 1, 3, 3, 2],
[2, 1, 3, 2, 2, 1, 1, 3],
[2, 2, 1, 2, 3, 1, 3, 2],
[2, 2, 2, 3, 1, 2, 1, 3],
[2, 2, 3, 1, 2, 3, 2, 1],
[2, 3, 1, 3, 2, 3, 1, 2],
[2, 3, 2, 1, 3, 1, 2, 3],
[2, 3, 3, 2, 1, 2, 3, 1]
]
};
class Taguchi {
factors = [];
orthogonalArray = [];
snRatioType;
targetValue;
poolingThreshold;
error;
constructor(config) {
Object.entries(config.factors).forEach(([name, levels]) => {
if (levels.length < 2) {
throw new Error(`Factor ${name} must have at least 2 levels`);
}
this.factors.push({ name, levels });
});
this.validateArrayType(config.type);
this.orthogonalArray = STANDARD_ARRAYS[config.type];
this.snRatioType = config.snRatioType;
this.targetValue = config.targetValue;
this.poolingThreshold = config.poolingThreshold ?? 2;
if (this.snRatioType === SNRatioType.NOMINAL_IS_BEST && this.targetValue === undefined) {
throw new Error("Target value must be specified for nominal-is-best optimization");
}
}
validateArrayType(type) {
const array = STANDARD_ARRAYS[type];
const maxLevels = Math.max(...array.flat());
for (const factor of this.factors) {
if (factor.levels.length > maxLevels) {
throw new Error(`Factor ${factor.name} has ${factor.levels.length} levels, but ${type} can only accommodate ${maxLevels} levels`);
}
if (factor.levels.length < 2) {
throw new Error(`Factor ${factor.name} must have at least 2 levels`);
}
}
const factorCount = this.factors.length;
const maxFactors = array[0].length;
if (factorCount > maxFactors) {
throw new Error(`${type} can only accommodate ${maxFactors} factors, but ${factorCount} were provided`);
}
const uniqueLevels = new Set(array.flat());
for (const level of uniqueLevels) {
const factorsWithLevel = this.factors.filter((f) => f.levels.length >= level);
if (factorsWithLevel.length === 0) {
throw new Error(`${type} requires factors with at least ${level} levels, but none were provided`);
}
}
}
generateExperiments() {
if (this.factors.length === 0) {
throw new Error("At least one factor must be added before generating experiments");
}
return this.orthogonalArray.map((row) => {
const experiment = {};
this.factors.forEach((factor, index) => {
const levelIndex = row[index] - 1;
experiment[factor.name] = factor.levels[levelIndex];
});
return experiment;
});
}
calculateSNRatio(results) {
const n = results.length;
const EPSILON = 0.0000000001;
switch (this.snRatioType) {
case SNRatioType.LARGER_IS_BETTER: {
const sum = results.reduce((acc, y) => {
const value = Math.max(Math.abs(y), EPSILON);
return acc + 1 / (value * value);
}, 0);
return -10 * Math.log10(sum / n);
}
case SNRatioType.SMALLER_IS_BETTER: {
const sum = results.reduce((acc, y) => acc + y * y, 0);
return -10 * Math.log10(Math.max(sum / n, EPSILON));
}
case SNRatioType.NOMINAL_IS_BEST: {
const target = this.targetValue;
const deviations = results.map((y) => (y - target) ** 2);
const msd = Math.max(deviations.reduce((sum, d) => sum + d, 0) / n, EPSILON);
return -10 * Math.log10(msd);
}
default:
throw new Error("Invalid S/N ratio type");
}
}
calculateConfidenceInterval(ms, errorMS, n, alpha = 0.05) {
const tTable = {
1: 12.706,
2: 4.303,
3: 3.182,
4: 2.776,
5: 2.571,
6: 2.447,
7: 2.365,
8: 2.306,
9: 2.262,
10: 2.228,
15: 2.131,
20: 2.086,
30: 2.042,
60: 2,
120: 1.98,
Infinity: 1.96
};
const errorDf = this.error?.df ?? 4;
const keys = Object.keys(tTable).map(Number).sort((a, b) => a - b);
let tValue;
if (errorDf <= keys[0]) {
tValue = tTable[keys[0]];
} else if (errorDf >= keys[keys.length - 1]) {
tValue = tTable[keys[keys.length - 1]];
} else {
const lowerKey = keys.filter((k) => k <= errorDf).pop();
const upperKey = keys.find((k) => k > errorDf);
const ratio = (errorDf - lowerKey) / (upperKey - lowerKey);
tValue = tTable[lowerKey] + ratio * (tTable[upperKey] - tTable[lowerKey]);
}
const se = Math.sqrt(2 * errorMS / n);
const margin = tValue * se;
return [-margin, margin];
}
calculateMainEffects(results) {
const mainEffects = {};
this.factors.forEach((factor) => {
mainEffects[factor.name] = factor.levels.map((_, levelIndex) => {
const experimentsWithLevel = results.filter((result) => result.factors[factor.name] === factor.levels[levelIndex]);
return experimentsWithLevel.reduce((sum, exp) => sum + exp.result, 0) / experimentsWithLevel.length;
});
});
return mainEffects;
}
calculateANOVA(results) {
const MIN_ERROR_MS = 0.0000000001;
const grandMean = results.reduce((sum, r) => sum + r.result, 0) / results.length;
const totalSS = results.reduce((sum, r) => sum + (r.result - grandMean) ** 2, 0);
const anova = {};
this.factors.forEach((factor) => {
const levelMeans = factor.levels.map((_, levelIndex) => {
const experimentsWithLevel = results.filter((result) => result.factors[factor.name] === factor.levels[levelIndex]);
return {
mean: experimentsWithLevel.reduce((sum, exp) => sum + exp.result, 0) / experimentsWithLevel.length,
n: experimentsWithLevel.length
};
});
const ss = levelMeans.reduce((sum, { mean, n }) => sum + n * (mean - grandMean) ** 2, 0);
const df = factor.levels.length - 1;
anova[factor.name] = {
ss,
df,
ms: ss / df,
f: 0,
contribution: 0,
isPooled: false
};
});
let errorSS = totalSS - Object.values(anova).reduce((sum, { ss }) => sum + ss, 0);
let errorDf = results.length - 1 - Object.values(anova).reduce((sum, { df }) => sum + df, 0);
let errorMS = MIN_ERROR_MS;
const pooledFactors = [];
if (errorDf <= 0) {
const smallestFactor = Object.entries(anova).sort(([, a], [, b]) => a.ss - b.ss)[0];
if (smallestFactor) {
const [factorName, analysis] = smallestFactor;
analysis.isPooled = true;
analysis.f = 0;
errorSS = analysis.ss;
errorDf = analysis.df;
errorMS = errorSS / errorDf;
pooledFactors.push(factorName);
}
} else {
errorMS = Math.max(errorSS / errorDf, MIN_ERROR_MS);
}
Object.entries(anova).forEach(([factor, analysis]) => {
if (!analysis.isPooled) {
analysis.f = analysis.ms / errorMS;
}
});
if (errorDf <= 0 && pooledFactors.length > 0) {
const pooledFactor = pooledFactors[0];
anova[pooledFactor].f = 0;
}
const sortedFactors = Object.entries(anova).sort(([, a], [, b]) => b.f - a.f).map(([name]) => name);
let changed = true;
while (changed) {
changed = false;
Object.entries(anova).forEach(([factor, analysis]) => {
if (!analysis.isPooled) {
analysis.f = errorMS === 0 ? 1e6 : analysis.ms / errorMS;
}
});
let minF = Infinity;
let minFactor = "";
Object.entries(anova).forEach(([factor, analysis]) => {
if (!analysis.isPooled && analysis.f < minF) {
minF = analysis.f;
minFactor = factor;
}
});
if (minF < this.poolingThreshold) {
const analysis = anova[minFactor];
analysis.isPooled = true;
analysis.f = 0;
pooledFactors.push(minFactor);
errorSS += analysis.ss;
errorDf += analysis.df;
errorMS = errorSS / Math.max(errorDf, 1);
changed = true;
}
}
const nonPooledSS = Object.values(anova).filter((a) => !a.isPooled).reduce((sum, { ss }) => sum + ss, 0);
Object.entries(anova).forEach(([factorName, analysis]) => {
if (!analysis.isPooled) {
const factor = this.factors.find((f) => f.name === factorName);
analysis.confidenceInterval = this.calculateConfidenceInterval(analysis.ms, errorMS, results.length / factor.levels.length);
analysis.contribution = nonPooledSS > 0 ? analysis.ss / nonPooledSS * 100 : 0;
} else {
analysis.contribution = 0;
analysis.confidenceInterval = undefined;
}
});
const totalContribution = Object.values(anova).filter((a) => !a.isPooled).reduce((sum, { contribution }) => sum + contribution, 0);
if (totalContribution > 0) {
Object.values(anova).forEach((analysis) => {
if (!analysis.isPooled) {
analysis.contribution = analysis.contribution / totalContribution * 100;
}
});
}
return {
variance: anova,
error: {
ss: errorSS,
df: errorDf,
ms: errorMS,
pooledFactors
}
};
}
analyzeResults(results) {
const optimalLevels = {};
const snRatios = {};
const mainEffects = this.calculateMainEffects(results);
const { variance, error } = this.calculateANOVA(results);
this.factors.forEach((factor) => {
snRatios[factor.name] = factor.levels.map((_, levelIndex) => {
const experimentsWithLevel = results.filter((result) => result.factors[factor.name] === factor.levels[levelIndex]);
return this.calculateSNRatio(experimentsWithLevel.map((exp) => exp.result));
});
const values = mainEffects[factor.name];
const optimalValue = this.snRatioType === SNRatioType.SMALLER_IS_BETTER ? Math.min(...values) : Math.max(...values);
optimalLevels[factor.name] = values.findIndex((value, index) => {
if (value === optimalValue) {
return !values.slice(0, index).includes(value);
}
return false;
});
});
const contributions = Object.fromEntries(Object.entries(variance).filter(([_, analysis]) => !analysis.isPooled).map(([factor, analysis]) => [factor, analysis.contribution]));
return {
optimalLevels,
snRatios,
mainEffects,
contributions,
variance,
error
};
}
}
export {
Taguchi,
SNRatioType
};