diff --git a/src/homogenity/fFactors.ts b/src/homogenity/fFactors.ts new file mode 100644 index 0000000..d9882d4 --- /dev/null +++ b/src/homogenity/fFactors.ts @@ -0,0 +1,22 @@ +/** + * F factors for homogenity calculation + * Array contains F1 and F2 values for each sample size + */ +const fFactors = { + 20: [1.59, 0.57], + 19: [1.6, 0.59], + 18: [1.62, 0.62], + 17: [1.64, 0.64], + 16: [1.67, 0.68], + 15: [1.69, 0.71], + 14: [1.72, 0.75], + 13: [1.75, 0.8], + 12: [1.79, 0.86], + 11: [1.83, 0.93], + 10: [1.88, 1.01], + 9: [1.94, 1.11], + 8: [2.01, 1.25], + 7: [2.1, 1.43], +}; + +export default fFactors; diff --git a/src/homogenity/index.ts b/src/homogenity/index.ts new file mode 100644 index 0000000..5c1389e --- /dev/null +++ b/src/homogenity/index.ts @@ -0,0 +1,85 @@ +import { round, sum } from 'lodash'; +import { average, sampleStandardDeviation } from 'simple-statistics'; +import fFactors from './fFactors'; + +export type HomogenityTestResult = { + /** + * Label of the sample used in the test + */ + label: string; + + /** + * The result(s) of the test + */ + values: number[]; +}; + +const R_DIVIDER = 2.8; + +/** + * + * @param results An array of test results + * @param R reproducibility constant + */ +export function homogenity(results: HomogenityTestResult[], r: number) { + if (results.length < 1) { + throw new Error('At least one test result is required'); + } + + if (results[0].values.length !== 2) { + throw new Error( + 'We currently only support two values per test. You provided ' + + results[0].values.length + + ' values.' + ); + } + + const numTests = results.length; + const numRepetitions = results[0].values.length; + + const enrichedResults = results.map((result) => ({ + ...result, + avg: average(result.values), + deltaPow: Math.pow(Math.abs(result.values[0] - result.values[1]), 2), + })); + + const xAvg = average(enrichedResults.map((r) => r.avg)) + + const sd = round( + sampleStandardDeviation(enrichedResults.map((r) => r.avg)), + 3 + ); + + const sw = Math.sqrt( + sum(enrichedResults.map((r) => r.deltaPow)) / (numTests * numRepetitions) + ); + + const ss2 = Math.pow(sd, 2) - Math.pow(sw, 2) / 2; + + const ss = ss2 < 0 ? 0 : Math.sqrt(ss2); + + const fValues = fFactors[numTests as keyof typeof fFactors]; + + if (!fValues) { + throw new Error( + 'No F values found for ' + numTests + ' tests. Supported range: 7 to 20.' + ); + } + + const sigmaAllow2 = Math.pow((r / R_DIVIDER) * 0.3, 2); + const c = fValues[0] * sigmaAllow2 + fValues[1] * Math.pow(sw, 2); + + const cSqrt = Math.sqrt(c); + const homogenity = ss < cSqrt; + + return { + xAvg, + sd, + sw, + ss2, + ss, + c, + cSqrt, + homogenity, + }; +} diff --git a/src/lib.ts b/src/lib.ts index f58871d..e5203e5 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -10,5 +10,6 @@ export * from './algorithms/cochran'; export * from './algorithms/reference-value'; - export * from './grubbs'; + +export * from './homogenity'; diff --git a/tests/homogenity.test.ts b/tests/homogenity.test.ts new file mode 100644 index 0000000..9ca4973 --- /dev/null +++ b/tests/homogenity.test.ts @@ -0,0 +1,73 @@ +import { homogenity, HomogenityTestResult } from '../src/homogenity'; + +describe('homogenity', () => { + it('should correctly calculate homogenity for given dataset', () => { + // Arrange + const testData: HomogenityTestResult[] = [ + { label: 'X', values: [0.452, 0.438] }, + { label: 'X', values: [0.436, 0.432] }, + { label: 'X', values: [0.435, 0.434] }, + { label: 'X', values: [0.456, 0.441] }, + { label: 'X', values: [0.434, 0.433] }, + { label: 'X', values: [0.439, 0.430] }, + { label: 'X', values: [0.433, 0.430] }, + { label: 'X', values: [0.434, 0.429] }, + { label: 'X', values: [0.434, 0.436] } + ]; + const r = 0.07; + + // Act + const result = homogenity(testData, r); + + // Assert + expect(result.homogenity).toBe(true); + expect(result.xAvg).toBeCloseTo(0.436, 3); + expect(result.sw).toBeCloseTo(0.006, 3); + expect(result.ss).toBeCloseTo(0.005, 3); + expect(result.ss2).toBeCloseTo(0.00002, 5); + expect(result.c).toBeCloseTo(0.0001, 4); + expect(result.cSqrt).toBeCloseTo(0.012, 3); + }); + + it('should correctly calculate homogenity for second dataset with 10 measurements', () => { + // Arrange + const testData: HomogenityTestResult[] = [ + { label: 'X', values: [0.452, 0.438] }, + { label: 'X', values: [0.436, 0.432] }, + { label: 'X', values: [0.435, 0.434] }, + { label: 'X', values: [0.456, 0.441] }, + { label: 'X', values: [0.434, 0.433] }, + { label: 'X', values: [0.439, 0.43] }, + { label: 'X', values: [0.433, 0.430] }, + { label: 'X', values: [0.434, 0.429] }, + { label: 'X', values: [0.434, 0.436] }, + { label: 'X', values: [0.47, 0.43] } + ]; + + const r = 0.07; + + // Act + const result = homogenity(testData, r); + + // Assert + expect(result.homogenity).toBe(true); + expect(result.xAvg).toBeCloseTo(0.438, 3); + expect(result.sw).toBeCloseTo(0.011, 2); + expect(result.ss).toBeCloseTo(0.000, 3); + expect(result.ss2).toBeCloseTo(-0.00001, 4); + expect(result.c).toBeCloseTo(0.0002, 4); + expect(result.cSqrt).toBeCloseTo(0.015, 3); + }); + + it('should throw error when input array is empty', () => { + expect(() => homogenity([], 0.07)).toThrow('At least one test result is required'); + }); + + it('should throw error when values array does not contain exactly 2 values', () => { + const invalidData: HomogenityTestResult[] = [ + { label: 'X', values: [0.452, 0.438, 0.434] } + ]; + + expect(() => homogenity(invalidData, 0.07)).toThrow('We currently only support two values per test'); + }); +});