Skip to content

Commit 2d67264

Browse files
committed
implement homogenity test
1 parent 76c19a0 commit 2d67264

File tree

4 files changed

+182
-1
lines changed

4 files changed

+182
-1
lines changed

src/homogenity/fFactors.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* F factors for homogenity calculation
3+
* Array contains F1 and F2 values for each sample size
4+
*/
5+
const fFactors = {
6+
20: [1.59, 0.57],
7+
19: [1.6, 0.59],
8+
18: [1.62, 0.62],
9+
17: [1.64, 0.64],
10+
16: [1.67, 0.68],
11+
15: [1.69, 0.71],
12+
14: [1.72, 0.75],
13+
13: [1.75, 0.8],
14+
12: [1.79, 0.86],
15+
11: [1.83, 0.93],
16+
10: [1.88, 1.01],
17+
9: [1.94, 1.11],
18+
8: [2.01, 1.25],
19+
7: [2.1, 1.43],
20+
};
21+
22+
export default fFactors;

src/homogenity/index.ts

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { round, sum } from 'lodash';
2+
import { average, sampleStandardDeviation } from 'simple-statistics';
3+
import fFactors from './fFactors';
4+
5+
export type HomogenityTestResult = {
6+
/**
7+
* Label of the sample used in the test
8+
*/
9+
label: string;
10+
11+
/**
12+
* The result(s) of the test
13+
*/
14+
values: number[];
15+
};
16+
17+
const R_DIVIDER = 2.8;
18+
19+
/**
20+
*
21+
* @param results An array of test results
22+
* @param R reproducibility constant
23+
*/
24+
export function homogenity(results: HomogenityTestResult[], r: number) {
25+
if (results.length < 1) {
26+
throw new Error('At least one test result is required');
27+
}
28+
29+
if (results[0].values.length !== 2) {
30+
throw new Error(
31+
'We currently only support two values per test. You provided ' +
32+
results[0].values.length +
33+
' values.'
34+
);
35+
}
36+
37+
const numTests = results.length;
38+
const numRepetitions = results[0].values.length;
39+
40+
const enrichedResults = results.map((result) => ({
41+
...result,
42+
avg: average(result.values),
43+
deltaPow: Math.pow(Math.abs(result.values[0] - result.values[1]), 2),
44+
}));
45+
46+
const xAvg = average(enrichedResults.map((r) => r.avg))
47+
48+
const sd = round(
49+
sampleStandardDeviation(enrichedResults.map((r) => r.avg)),
50+
3
51+
);
52+
53+
const sw = Math.sqrt(
54+
sum(enrichedResults.map((r) => r.deltaPow)) / (numTests * numRepetitions)
55+
);
56+
57+
const ss2 = Math.pow(sd, 2) - Math.pow(sw, 2) / 2;
58+
59+
const ss = ss2 < 0 ? 0 : Math.sqrt(ss2);
60+
61+
const fValues = fFactors[numTests as keyof typeof fFactors];
62+
63+
if (!fValues) {
64+
throw new Error(
65+
'No F values found for ' + numTests + ' tests. Supported range: 7 to 20.'
66+
);
67+
}
68+
69+
const sigmaAllow2 = Math.pow((r / R_DIVIDER) * 0.3, 2);
70+
const c = fValues[0] * sigmaAllow2 + fValues[1] * Math.pow(sw, 2);
71+
72+
const cSqrt = Math.sqrt(c);
73+
const homogenity = ss < cSqrt;
74+
75+
return {
76+
xAvg,
77+
sd,
78+
sw,
79+
ss2,
80+
ss,
81+
c,
82+
cSqrt,
83+
homogenity,
84+
};
85+
}

src/lib.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ export * from './algorithms/cochran';
1010

1111
export * from './algorithms/reference-value';
1212

13-
1413
export * from './grubbs';
14+
15+
export * from './homogenity';

tests/homogenity.test.ts

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { homogenity, HomogenityTestResult } from '../src/homogenity';
2+
3+
describe('homogenity', () => {
4+
it('should correctly calculate homogenity for given dataset', () => {
5+
// Arrange
6+
const testData: HomogenityTestResult[] = [
7+
{ label: 'X', values: [0.452, 0.438] },
8+
{ label: 'X', values: [0.436, 0.432] },
9+
{ label: 'X', values: [0.435, 0.434] },
10+
{ label: 'X', values: [0.456, 0.441] },
11+
{ label: 'X', values: [0.434, 0.433] },
12+
{ label: 'X', values: [0.439, 0.430] },
13+
{ label: 'X', values: [0.433, 0.430] },
14+
{ label: 'X', values: [0.434, 0.429] },
15+
{ label: 'X', values: [0.434, 0.436] }
16+
];
17+
const r = 0.07;
18+
19+
// Act
20+
const result = homogenity(testData, r);
21+
22+
// Assert
23+
expect(result.homogenity).toBe(true);
24+
expect(result.xAvg).toBeCloseTo(0.436, 3);
25+
expect(result.sw).toBeCloseTo(0.006, 3);
26+
expect(result.ss).toBeCloseTo(0.005, 3);
27+
expect(result.ss2).toBeCloseTo(0.00002, 5);
28+
expect(result.c).toBeCloseTo(0.0001, 4);
29+
expect(result.cSqrt).toBeCloseTo(0.012, 3);
30+
});
31+
32+
it('should correctly calculate homogenity for second dataset with 10 measurements', () => {
33+
// Arrange
34+
const testData: HomogenityTestResult[] = [
35+
{ label: 'X', values: [0.452, 0.438] },
36+
{ label: 'X', values: [0.436, 0.432] },
37+
{ label: 'X', values: [0.435, 0.434] },
38+
{ label: 'X', values: [0.456, 0.441] },
39+
{ label: 'X', values: [0.434, 0.433] },
40+
{ label: 'X', values: [0.439, 0.43] },
41+
{ label: 'X', values: [0.433, 0.430] },
42+
{ label: 'X', values: [0.434, 0.429] },
43+
{ label: 'X', values: [0.434, 0.436] },
44+
{ label: 'X', values: [0.47, 0.43] }
45+
];
46+
47+
const r = 0.07;
48+
49+
// Act
50+
const result = homogenity(testData, r);
51+
52+
// Assert
53+
expect(result.homogenity).toBe(true);
54+
expect(result.xAvg).toBeCloseTo(0.438, 3);
55+
expect(result.sw).toBeCloseTo(0.011, 2);
56+
expect(result.ss).toBeCloseTo(0.000, 3);
57+
expect(result.ss2).toBeCloseTo(-0.00001, 4);
58+
expect(result.c).toBeCloseTo(0.0002, 4);
59+
expect(result.cSqrt).toBeCloseTo(0.015, 3);
60+
});
61+
62+
it('should throw error when input array is empty', () => {
63+
expect(() => homogenity([], 0.07)).toThrow('At least one test result is required');
64+
});
65+
66+
it('should throw error when values array does not contain exactly 2 values', () => {
67+
const invalidData: HomogenityTestResult[] = [
68+
{ label: 'X', values: [0.452, 0.438, 0.434] }
69+
];
70+
71+
expect(() => homogenity(invalidData, 0.07)).toThrow('We currently only support two values per test');
72+
});
73+
});

0 commit comments

Comments
 (0)