Skip to content

Commit

Permalink
Homogenity test (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
aozisik authored Nov 21, 2024
1 parent 76c19a0 commit ee126f7
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 1 deletion.
22 changes: 22 additions & 0 deletions src/homogenity/fFactors.ts
Original file line number Diff line number Diff line change
@@ -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;
85 changes: 85 additions & 0 deletions src/homogenity/index.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
3 changes: 2 additions & 1 deletion src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export * from './algorithms/cochran';

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


export * from './grubbs';

export * from './homogenity';
73 changes: 73 additions & 0 deletions tests/homogenity.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});

0 comments on commit ee126f7

Please sign in to comment.