@datagrok/proteomics 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -3
- package/CLAUDE.md +178 -0
- package/README.md +195 -3
- package/detectors.js +152 -9
- package/dist/package-test.js +1 -1744
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +1 -146
- package/dist/package.js.map +1 -1
- package/docs/personas-and-capabilities.md +165 -0
- package/files/demo/README.md +264 -0
- package/files/demo/cptac-spike-in.txt +1571 -0
- package/files/demo/enrichment-demo.csv +120 -0
- package/files/demo/fragpipe-smoke-test.tsv +11 -0
- package/files/demo/proteinGroups.txt +28 -0
- package/files/demo/spectronaut-hye-candidates.tsv +94 -0
- package/files/demo/spectronaut-hye-demo.tsv +8761 -0
- package/files/demo/spectronaut-hye-mix.tsv +8761 -0
- package/files/demo/spectronaut-hye-precursor-golden.json +938 -0
- package/files/demo/spectronaut-hye-precursor-golden.tsv +235 -0
- package/files/demo/spectronaut-hye-precursor.tsv +493 -0
- package/images/enrichment-crosslink.png +0 -0
- package/images/enrichment-term-selected.png +0 -0
- package/images/hero.png +0 -0
- package/images/pipeline.svg +80 -0
- package/package.json +87 -63
- package/scripts/deqms_de.R +60 -0
- package/scripts/limma_de.R +42 -0
- package/scripts/vsn_normalize.R +19 -0
- package/src/analysis/differential-expression.ts +450 -0
- package/src/analysis/enrichment-export.ts +101 -0
- package/src/analysis/enrichment.ts +602 -0
- package/src/analysis/experiment-setup.ts +199 -0
- package/src/analysis/imputation.ts +407 -0
- package/src/analysis/log2-scale.ts +139 -0
- package/src/analysis/normalization.ts +255 -0
- package/src/analysis/pca.ts +254 -0
- package/src/analysis/spc-storage.ts +515 -0
- package/src/analysis/spc.ts +544 -0
- package/src/analysis/subcellular-location.ts +431 -0
- package/src/demo/enrichment-demo.ts +94 -0
- package/src/demo/proteomics-demo.ts +123 -0
- package/src/global.d.ts +15 -0
- package/src/menu.ts +133 -0
- package/src/package-api.ts +136 -14
- package/src/package-test.ts +45 -20
- package/src/package.g.ts +161 -0
- package/src/package.ts +1029 -17
- package/src/panels/protein-focus.ts +63 -0
- package/src/panels/published-analysis-panel.ts +151 -0
- package/src/panels/uniprot-panel.ts +349 -0
- package/src/parsers/fragpipe-parser.ts +200 -0
- package/src/parsers/generic-parser.ts +197 -0
- package/src/parsers/maxquant-parser.ts +162 -0
- package/src/parsers/shared-utils.ts +163 -0
- package/src/parsers/spectronaut-candidates-parser.ts +307 -0
- package/src/parsers/spectronaut-parser.ts +604 -0
- package/src/publishing/assert-published-shape.ts +260 -0
- package/src/publishing/post-open-recovery.ts +104 -0
- package/src/publishing/publish-project.ts +515 -0
- package/src/publishing/publish-settings.ts +59 -0
- package/src/publishing/publish-state.ts +316 -0
- package/src/publishing/share-dialog.ts +171 -0
- package/src/publishing/trim-dataframe.ts +247 -0
- package/src/tests/analysis.ts +658 -0
- package/src/tests/enrichment-export.ts +61 -0
- package/src/tests/enrichment-visualization.ts +340 -0
- package/src/tests/enrichment.ts +224 -0
- package/src/tests/fragpipe-e2e.ts +74 -0
- package/src/tests/fragpipe-parser.ts +147 -0
- package/src/tests/gene-label-resolver.ts +387 -0
- package/src/tests/generic-parser.ts +152 -0
- package/src/tests/group-mean-correlation.ts +139 -0
- package/src/tests/log2-scale.ts +93 -0
- package/src/tests/organisms.ts +56 -0
- package/src/tests/parsers.ts +182 -0
- package/src/tests/publish-roundtrip.ts +584 -0
- package/src/tests/publish-spike.ts +377 -0
- package/src/tests/qc-dashboard.ts +210 -0
- package/src/tests/smart-pathway-filter.ts +193 -0
- package/src/tests/spc-formula-lines-spike.ts +129 -0
- package/src/tests/spc.ts +640 -0
- package/src/tests/spectronaut-candidates-e2e.ts +140 -0
- package/src/tests/spectronaut-candidates-parser.ts +398 -0
- package/src/tests/spectronaut-parser.ts +668 -0
- package/src/tests/subcellular-location.ts +361 -0
- package/src/tests/uniprot-panel.ts +202 -0
- package/src/tests/volcano.ts +603 -0
- package/src/utils/column-detection.ts +28 -0
- package/src/utils/gene-label-resolver.ts +443 -0
- package/src/utils/organisms.ts +82 -0
- package/src/utils/proteomics-types.ts +30 -0
- package/src/viewers/enrichment-viewers.ts +274 -0
- package/src/viewers/group-mean-correlation.ts +218 -0
- package/src/viewers/heatmap.ts +168 -0
- package/src/viewers/pca-plot.ts +169 -0
- package/src/viewers/qc-computations.ts +266 -0
- package/src/viewers/qc-dashboard.ts +176 -0
- package/src/viewers/spc-dashboard.ts +755 -0
- package/src/viewers/volcano.ts +690 -0
- package/test-console-output-1.log +2055 -0
- package/test-record-1.mp4 +0 -0
- package/tools/derive-precursor-golden-sidecar.mjs +81 -0
- package/tools/generate-enrichment-fixture.sh +160 -0
- package/tools/generate-spectronaut-candidates-fixture.mjs +212 -0
- package/tools/generate-spectronaut-precursor-fixture.mjs +128 -0
- package/tools/spectronaut-aggregate.sh +46 -0
- package/tools/spectronaut-aggregate.sql +80 -0
- package/tsconfig.json +18 -71
- package/webpack.config.js +86 -45
- package/.eslintignore +0 -1
- package/.eslintrc.json +0 -56
- package/LICENSE +0 -674
- package/package.png +0 -0
- package/scripts/number_antibody.py +0 -190
- package/scripts/number_antibody_abnumber.py +0 -177
- package/scripts/number_antibody_anarci.py +0 -200
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import * as DG from 'datagrok-api/dg';
|
|
2
|
+
import {category, test, expect} from '@datagrok-libraries/test/src/test';
|
|
3
|
+
import {setGroups, getGroups, GroupAssignment, seedAnnotationDialogInputs,
|
|
4
|
+
setOrganism, getOrganism, applyAnnotation} from '../analysis/experiment-setup';
|
|
5
|
+
import {medianNormalize, quantileNormalize, vsnNormalize} from '../analysis/normalization';
|
|
6
|
+
import {imputeMinProb, imputeKnn, imputeZero, imputeMean, imputeMedian} from '../analysis/imputation';
|
|
7
|
+
import {runDifferentialExpression, copyDEResultsToFrame, getDefaultComparison}
|
|
8
|
+
from '../analysis/differential-expression';
|
|
9
|
+
import {SEMTYPE} from '../utils/proteomics-types';
|
|
10
|
+
|
|
11
|
+
/** Build a test DataFrame with protein IDs and intensity columns for two groups. */
|
|
12
|
+
function makeTestDf(
|
|
13
|
+
nProteins: number,
|
|
14
|
+
group1Names: string[],
|
|
15
|
+
group2Names: string[],
|
|
16
|
+
group1Values?: number[][],
|
|
17
|
+
group2Values?: number[][],
|
|
18
|
+
): DG.DataFrame {
|
|
19
|
+
const cols: DG.Column[] = [];
|
|
20
|
+
cols.push(DG.Column.fromStrings('Protein ID',
|
|
21
|
+
Array.from({length: nProteins}, (_, i) => `P${i}`)));
|
|
22
|
+
|
|
23
|
+
const allNames = [...group1Names, ...group2Names];
|
|
24
|
+
const allValues = group1Values && group2Values ?
|
|
25
|
+
[...group1Values, ...group2Values] : undefined;
|
|
26
|
+
|
|
27
|
+
for (let c = 0; c < allNames.length; c++) {
|
|
28
|
+
const values = new Float32Array(nProteins);
|
|
29
|
+
if (allValues && allValues[c]) {
|
|
30
|
+
for (let r = 0; r < nProteins; r++)
|
|
31
|
+
values[r] = allValues[c][r];
|
|
32
|
+
}
|
|
33
|
+
const col = DG.Column.fromFloat32Array(allNames[c], values);
|
|
34
|
+
col.semType = SEMTYPE.INTENSITY;
|
|
35
|
+
cols.push(col);
|
|
36
|
+
}
|
|
37
|
+
return DG.DataFrame.fromColumns(cols);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Experiment Setup ────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
category('Experiment Setup', () => {
|
|
43
|
+
test('setGroups persists group assignments as tag', async () => {
|
|
44
|
+
const df = DG.DataFrame.fromColumns([
|
|
45
|
+
DG.Column.fromStrings('id', ['P0']),
|
|
46
|
+
DG.Column.fromFloat32Array('s1', new Float32Array([1])),
|
|
47
|
+
]);
|
|
48
|
+
const groups: GroupAssignment = {
|
|
49
|
+
group1: {name: 'Control', columns: ['s1']},
|
|
50
|
+
group2: {name: 'Treatment', columns: ['s2']},
|
|
51
|
+
};
|
|
52
|
+
setGroups(df, groups);
|
|
53
|
+
const raw = df.getTag('proteomics.groups');
|
|
54
|
+
expect(raw !== null && raw !== '', true);
|
|
55
|
+
const parsed = JSON.parse(raw!);
|
|
56
|
+
expect(parsed.group1.name, 'Control');
|
|
57
|
+
expect(parsed.group2.name, 'Treatment');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('getGroups returns null for untagged DataFrame', async () => {
|
|
61
|
+
const df = DG.DataFrame.fromColumns([
|
|
62
|
+
DG.Column.fromStrings('id', ['P0']),
|
|
63
|
+
]);
|
|
64
|
+
const result = getGroups(df);
|
|
65
|
+
expect(result, null);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('getOrganism returns undefined until set, then round-trips the code', async () => {
|
|
69
|
+
const df = DG.DataFrame.fromColumns([DG.Column.fromStrings('id', ['P0'])]);
|
|
70
|
+
expect(getOrganism(df) === undefined, true);
|
|
71
|
+
setOrganism(df, 'rnorvegicus');
|
|
72
|
+
expect(getOrganism(df), 'rnorvegicus');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('applyAnnotation persists the organism alongside the groups', async () => {
|
|
76
|
+
const df = DG.DataFrame.fromColumns([DG.Column.fromStrings('id', ['P0'])]);
|
|
77
|
+
applyAnnotation(df, {
|
|
78
|
+
group1: {name: 'Control', columns: ['s1', 's2']},
|
|
79
|
+
group2: {name: 'Treatment', columns: ['s3', 's4']},
|
|
80
|
+
organism: 'rnorvegicus',
|
|
81
|
+
});
|
|
82
|
+
expect(getOrganism(df), 'rnorvegicus');
|
|
83
|
+
expect(getGroups(df)!.group1.name, 'Control');
|
|
84
|
+
// Organism is optional — omitting it leaves the tag untouched.
|
|
85
|
+
const df2 = DG.DataFrame.fromColumns([DG.Column.fromStrings('id', ['P0'])]);
|
|
86
|
+
applyAnnotation(df2, {
|
|
87
|
+
group1: {name: 'A', columns: ['s1']},
|
|
88
|
+
group2: {name: 'B', columns: ['s2']},
|
|
89
|
+
});
|
|
90
|
+
expect(getOrganism(df2) === undefined, true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('getGroups round-trips GroupAssignment correctly', async () => {
|
|
94
|
+
const df = DG.DataFrame.fromColumns([
|
|
95
|
+
DG.Column.fromStrings('id', ['P0']),
|
|
96
|
+
]);
|
|
97
|
+
const groups: GroupAssignment = {
|
|
98
|
+
group1: {name: 'Control', columns: ['s1', 's2', 's3']},
|
|
99
|
+
group2: {name: 'Treatment', columns: ['s4', 's5', 's6']},
|
|
100
|
+
};
|
|
101
|
+
setGroups(df, groups);
|
|
102
|
+
const retrieved = getGroups(df);
|
|
103
|
+
expect(retrieved !== null, true);
|
|
104
|
+
expect(retrieved!.group1.name, 'Control');
|
|
105
|
+
expect(retrieved!.group2.name, 'Treatment');
|
|
106
|
+
expect(retrieved!.group1.columns.length, 3);
|
|
107
|
+
expect(retrieved!.group2.columns.length, 3);
|
|
108
|
+
expect(retrieved!.group1.columns[0], 's1');
|
|
109
|
+
expect(retrieved!.group2.columns[2], 's6');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('seedAnnotationDialogInputs returns Control/Treatment defaults when no tag', async () => {
|
|
113
|
+
const df = makeTestDf(3, ['s1', 's2'], ['s3', 's4']);
|
|
114
|
+
const seed = seedAnnotationDialogInputs(df, ['s1', 's2', 's3', 's4']);
|
|
115
|
+
expect(seed.group1Name, 'Control');
|
|
116
|
+
expect(seed.group2Name, 'Treatment');
|
|
117
|
+
expect(seed.group1Cols.length, 0);
|
|
118
|
+
expect(seed.group2Cols.length, 0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('seedAnnotationDialogInputs reads names + columns from existing groups', async () => {
|
|
122
|
+
const df = makeTestDf(3, ['s1', 's2'], ['s3', 's4']);
|
|
123
|
+
setGroups(df, {
|
|
124
|
+
group1: {name: 'DMD', columns: ['s1', 's2']},
|
|
125
|
+
group2: {name: 'WT', columns: ['s3', 's4']},
|
|
126
|
+
});
|
|
127
|
+
const seed = seedAnnotationDialogInputs(df, ['s1', 's2', 's3', 's4']);
|
|
128
|
+
expect(seed.group1Name, 'DMD');
|
|
129
|
+
expect(seed.group2Name, 'WT');
|
|
130
|
+
expect(seed.group1Cols.length, 2);
|
|
131
|
+
expect(seed.group2Cols.length, 2);
|
|
132
|
+
expect(seed.group1Cols[0].name, 's1');
|
|
133
|
+
expect(seed.group2Cols[1].name, 's4');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('seedAnnotationDialogInputs drops column refs not in the DataFrame', async () => {
|
|
137
|
+
const df = makeTestDf(3, ['s1', 's2'], ['s3', 's4']);
|
|
138
|
+
setGroups(df, {
|
|
139
|
+
group1: {name: 'DMD', columns: ['s1', 'gone', 's2']},
|
|
140
|
+
group2: {name: 'WT', columns: ['also-gone', 's3', 's4']},
|
|
141
|
+
});
|
|
142
|
+
const seed = seedAnnotationDialogInputs(df, ['s1', 's2', 's3', 's4']);
|
|
143
|
+
expect(seed.group1Cols.length, 2);
|
|
144
|
+
expect(seed.group2Cols.length, 2);
|
|
145
|
+
expect(seed.group1Cols.map((c) => c.name).join(','), 's1,s2');
|
|
146
|
+
expect(seed.group2Cols.map((c) => c.name).join(','), 's3,s4');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('seedAnnotationDialogInputs drops column refs not in available list', async () => {
|
|
150
|
+
const df = makeTestDf(3, ['s1', 's2'], ['s3', 's4']);
|
|
151
|
+
setGroups(df, {
|
|
152
|
+
group1: {name: 'A', columns: ['s1', 's2']},
|
|
153
|
+
group2: {name: 'B', columns: ['s3', 's4']},
|
|
154
|
+
});
|
|
155
|
+
const seed = seedAnnotationDialogInputs(df, ['s1', 's3']);
|
|
156
|
+
expect(seed.group1Cols.length, 1);
|
|
157
|
+
expect(seed.group2Cols.length, 1);
|
|
158
|
+
expect(seed.group1Cols[0].name, 's1');
|
|
159
|
+
expect(seed.group2Cols[0].name, 's3');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('seedAnnotationDialogInputs falls back to defaults when stored names are empty', async () => {
|
|
163
|
+
const df = makeTestDf(3, ['s1', 's2'], ['s3', 's4']);
|
|
164
|
+
setGroups(df, {
|
|
165
|
+
group1: {name: '', columns: ['s1', 's2']},
|
|
166
|
+
group2: {name: '', columns: ['s3', 's4']},
|
|
167
|
+
});
|
|
168
|
+
const seed = seedAnnotationDialogInputs(df, ['s1', 's2', 's3', 's4']);
|
|
169
|
+
expect(seed.group1Name, 'Control');
|
|
170
|
+
expect(seed.group2Name, 'Treatment');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ── Normalization ───────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
category('Normalization', () => {
|
|
177
|
+
test('median normalization shifts column medians to zero', async () => {
|
|
178
|
+
const col = DG.Column.fromFloat32Array('intensity', new Float32Array([2, 4, 6, 8, 10]));
|
|
179
|
+
col.semType = SEMTYPE.INTENSITY;
|
|
180
|
+
const df = DG.DataFrame.fromColumns([
|
|
181
|
+
DG.Column.fromStrings('id', ['P0', 'P1', 'P2', 'P3', 'P4']),
|
|
182
|
+
col,
|
|
183
|
+
]);
|
|
184
|
+
// Median of [2,4,6,8,10] is 6
|
|
185
|
+
medianNormalize(df, ['intensity']);
|
|
186
|
+
const c = df.col('intensity')!;
|
|
187
|
+
// Values should be shifted by -6: [-4, -2, 0, 2, 4]
|
|
188
|
+
expect(Math.abs(c.get(0) - (-4)) < 0.01, true);
|
|
189
|
+
expect(Math.abs(c.get(1) - (-2)) < 0.01, true);
|
|
190
|
+
expect(Math.abs(c.get(2) - 0) < 0.01, true);
|
|
191
|
+
expect(Math.abs(c.get(3) - 2) < 0.01, true);
|
|
192
|
+
expect(Math.abs(c.get(4) - 4) < 0.01, true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('median normalization skips null values', async () => {
|
|
196
|
+
// 5 values: [2, null, 6, 8, null] -- non-null median of [2,6,8] = 6
|
|
197
|
+
const col = DG.Column.fromFloat32Array('intensity',
|
|
198
|
+
new Float32Array([2, DG.FLOAT_NULL, 6, 8, DG.FLOAT_NULL]));
|
|
199
|
+
col.semType = SEMTYPE.INTENSITY;
|
|
200
|
+
const df = DG.DataFrame.fromColumns([
|
|
201
|
+
DG.Column.fromStrings('id', ['P0', 'P1', 'P2', 'P3', 'P4']),
|
|
202
|
+
col,
|
|
203
|
+
]);
|
|
204
|
+
medianNormalize(df, ['intensity']);
|
|
205
|
+
const c = df.col('intensity')!;
|
|
206
|
+
// Nulls should remain null
|
|
207
|
+
expect(c.isNone(1), true);
|
|
208
|
+
expect(c.isNone(4), true);
|
|
209
|
+
// Non-null values shifted by -median
|
|
210
|
+
expect(!c.isNone(0), true);
|
|
211
|
+
expect(!c.isNone(2), true);
|
|
212
|
+
expect(!c.isNone(3), true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('normalization sets proteomics.normalized tag', async () => {
|
|
216
|
+
const col = DG.Column.fromFloat32Array('intensity', new Float32Array([1, 2, 3]));
|
|
217
|
+
col.semType = SEMTYPE.INTENSITY;
|
|
218
|
+
const df = DG.DataFrame.fromColumns([
|
|
219
|
+
DG.Column.fromStrings('id', ['P0', 'P1', 'P2']),
|
|
220
|
+
col,
|
|
221
|
+
]);
|
|
222
|
+
medianNormalize(df, ['intensity']);
|
|
223
|
+
expect(df.getTag('proteomics.normalized'), 'true');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('quantile normalization aligns column distributions', async () => {
|
|
227
|
+
// Three columns with different distributions -- after quantile norm, sorted non-null values should match
|
|
228
|
+
const c1 = DG.Column.fromFloat32Array('s1', new Float32Array([5, 2, 3, 6]));
|
|
229
|
+
const c2 = DG.Column.fromFloat32Array('s2', new Float32Array([4, 14, 8, 2]));
|
|
230
|
+
const c3 = DG.Column.fromFloat32Array('s3', new Float32Array([3, 1, 9, 7]));
|
|
231
|
+
c1.semType = SEMTYPE.INTENSITY;
|
|
232
|
+
c2.semType = SEMTYPE.INTENSITY;
|
|
233
|
+
c3.semType = SEMTYPE.INTENSITY;
|
|
234
|
+
const df = DG.DataFrame.fromColumns([
|
|
235
|
+
DG.Column.fromStrings('id', ['P0', 'P1', 'P2', 'P3']),
|
|
236
|
+
c1, c2, c3,
|
|
237
|
+
]);
|
|
238
|
+
quantileNormalize(df, ['s1', 's2', 's3']);
|
|
239
|
+
// After quantile normalization, sorted values across columns should be the same
|
|
240
|
+
const vals1 = [df.col('s1')!.get(0), df.col('s1')!.get(1), df.col('s1')!.get(2), df.col('s1')!.get(3)];
|
|
241
|
+
const vals2 = [df.col('s2')!.get(0), df.col('s2')!.get(1), df.col('s2')!.get(2), df.col('s2')!.get(3)];
|
|
242
|
+
const vals3 = [df.col('s3')!.get(0), df.col('s3')!.get(1), df.col('s3')!.get(2), df.col('s3')!.get(3)];
|
|
243
|
+
vals1.sort((a, b) => a - b);
|
|
244
|
+
vals2.sort((a, b) => a - b);
|
|
245
|
+
vals3.sort((a, b) => a - b);
|
|
246
|
+
for (let i = 0; i < 4; i++) {
|
|
247
|
+
expect(Math.abs(vals1[i] - vals2[i]) < 0.01, true);
|
|
248
|
+
expect(Math.abs(vals1[i] - vals3[i]) < 0.01, true);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('quantile normalization preserves null values', async () => {
|
|
253
|
+
const c1 = DG.Column.fromFloat32Array('s1', new Float32Array([5, DG.FLOAT_NULL, 3, 6]));
|
|
254
|
+
const c2 = DG.Column.fromFloat32Array('s2', new Float32Array([4, 14, 8, DG.FLOAT_NULL]));
|
|
255
|
+
c1.semType = SEMTYPE.INTENSITY;
|
|
256
|
+
c2.semType = SEMTYPE.INTENSITY;
|
|
257
|
+
const df = DG.DataFrame.fromColumns([
|
|
258
|
+
DG.Column.fromStrings('id', ['P0', 'P1', 'P2', 'P3']),
|
|
259
|
+
c1, c2,
|
|
260
|
+
]);
|
|
261
|
+
quantileNormalize(df, ['s1', 's2']);
|
|
262
|
+
expect(df.col('s1')!.isNone(1), true);
|
|
263
|
+
expect(df.col('s2')!.isNone(3), true);
|
|
264
|
+
// Non-null values should still be present
|
|
265
|
+
expect(!df.col('s1')!.isNone(0), true);
|
|
266
|
+
expect(!df.col('s2')!.isNone(0), true);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('quantile normalization sets proteomics.normalized tag', async () => {
|
|
270
|
+
const c1 = DG.Column.fromFloat32Array('s1', new Float32Array([1, 2, 3]));
|
|
271
|
+
const c2 = DG.Column.fromFloat32Array('s2', new Float32Array([4, 5, 6]));
|
|
272
|
+
c1.semType = SEMTYPE.INTENSITY;
|
|
273
|
+
c2.semType = SEMTYPE.INTENSITY;
|
|
274
|
+
const df = DG.DataFrame.fromColumns([
|
|
275
|
+
DG.Column.fromStrings('id', ['P0', 'P1', 'P2']),
|
|
276
|
+
c1, c2,
|
|
277
|
+
]);
|
|
278
|
+
quantileNormalize(df, ['s1', 's2']);
|
|
279
|
+
expect(df.getTag('proteomics.normalized'), 'true');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('quantile normalization with < 2 columns returns without error', async () => {
|
|
283
|
+
const c1 = DG.Column.fromFloat32Array('s1', new Float32Array([1, 2, 3]));
|
|
284
|
+
c1.semType = SEMTYPE.INTENSITY;
|
|
285
|
+
const df = DG.DataFrame.fromColumns([
|
|
286
|
+
DG.Column.fromStrings('id', ['P0', 'P1', 'P2']),
|
|
287
|
+
c1,
|
|
288
|
+
]);
|
|
289
|
+
// Should not throw
|
|
290
|
+
quantileNormalize(df, ['s1']);
|
|
291
|
+
// Values should be unchanged
|
|
292
|
+
expect(Math.abs(df.col('s1')!.get(0) - 1) < 0.01, true);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('vsnNormalize falls back to quantile on R failure', async () => {
|
|
296
|
+
const c1 = DG.Column.fromFloat32Array('log2(s1)', new Float32Array([5, 2, 3, 6]));
|
|
297
|
+
const c2 = DG.Column.fromFloat32Array('log2(s2)', new Float32Array([4, 14, 8, 2]));
|
|
298
|
+
c1.semType = SEMTYPE.INTENSITY;
|
|
299
|
+
c2.semType = SEMTYPE.INTENSITY;
|
|
300
|
+
// Also add raw columns so VSN can find them
|
|
301
|
+
const r1 = DG.Column.fromFloat32Array('s1', new Float32Array([32, 4, 8, 64]));
|
|
302
|
+
const r2 = DG.Column.fromFloat32Array('s2', new Float32Array([16, 16384, 256, 4]));
|
|
303
|
+
const df = DG.DataFrame.fromColumns([
|
|
304
|
+
DG.Column.fromStrings('id', ['P0', 'P1', 'P2', 'P3']),
|
|
305
|
+
r1, r2, c1, c2,
|
|
306
|
+
]);
|
|
307
|
+
// In test environment, R is unavailable, so vsnNormalize should fall back to quantile
|
|
308
|
+
await vsnNormalize(df, ['log2(s1)', 'log2(s2)']);
|
|
309
|
+
// Should still set the tag even via fallback
|
|
310
|
+
expect(df.getTag('proteomics.normalized'), 'true');
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// ── Imputation ──────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
category('Imputation', () => {
|
|
317
|
+
test('MinProb imputation fills all NaN values', async () => {
|
|
318
|
+
const values = new Float32Array([10, DG.FLOAT_NULL, 14, DG.FLOAT_NULL, 12]);
|
|
319
|
+
const col = DG.Column.fromFloat32Array('intensity', values);
|
|
320
|
+
col.semType = SEMTYPE.INTENSITY;
|
|
321
|
+
const df = DG.DataFrame.fromColumns([
|
|
322
|
+
DG.Column.fromStrings('id', ['P0', 'P1', 'P2', 'P3', 'P4']),
|
|
323
|
+
col,
|
|
324
|
+
]);
|
|
325
|
+
imputeMinProb(df, ['intensity']);
|
|
326
|
+
const c = df.col('intensity')!;
|
|
327
|
+
for (let i = 0; i < c.length; i++)
|
|
328
|
+
expect(c.isNone(i), false);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('imputed values are below the observed mean', async () => {
|
|
332
|
+
// Known values: [10, 12, 14, 16] mean=13, with nulls to impute
|
|
333
|
+
const values = new Float32Array([10, 12, 14, 16, DG.FLOAT_NULL, DG.FLOAT_NULL]);
|
|
334
|
+
const col = DG.Column.fromFloat32Array('intensity', values);
|
|
335
|
+
col.semType = SEMTYPE.INTENSITY;
|
|
336
|
+
const df = DG.DataFrame.fromColumns([
|
|
337
|
+
DG.Column.fromStrings('id', ['P0', 'P1', 'P2', 'P3', 'P4', 'P5']),
|
|
338
|
+
col,
|
|
339
|
+
]);
|
|
340
|
+
const observedMean = 13; // (10+12+14+16)/4
|
|
341
|
+
imputeMinProb(df, ['intensity'], 1.8, 0.3);
|
|
342
|
+
const c = df.col('intensity')!;
|
|
343
|
+
// Imputed values (indices 4 and 5) should be well below the observed mean
|
|
344
|
+
expect(c.get(4) < observedMean, true);
|
|
345
|
+
expect(c.get(5) < observedMean, true);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('imputation sets proteomics.imputed tag', async () => {
|
|
349
|
+
const col = DG.Column.fromFloat32Array('intensity',
|
|
350
|
+
new Float32Array([1, DG.FLOAT_NULL, 3]));
|
|
351
|
+
col.semType = SEMTYPE.INTENSITY;
|
|
352
|
+
const df = DG.DataFrame.fromColumns([
|
|
353
|
+
DG.Column.fromStrings('id', ['P0', 'P1', 'P2']),
|
|
354
|
+
col,
|
|
355
|
+
]);
|
|
356
|
+
imputeMinProb(df, ['intensity']);
|
|
357
|
+
expect(df.getTag('proteomics.imputed'), 'true');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('kNN imputation fills missing values using nearest neighbors', async () => {
|
|
361
|
+
// 5 rows, 3 columns. Row 2 (P2) has missing col s2.
|
|
362
|
+
// P0: [10, 20, 30], P1: [11, 21, 31], P3: [12, 22, 32], P4: [100, 200, 300]
|
|
363
|
+
// P2: [10.5, null, 30.5] -- nearest neighbors are P0, P1, P3 (not P4)
|
|
364
|
+
// Expected imputed s2 for P2: average of neighbors' s2 values ~ (20+21+22)/3 = 21
|
|
365
|
+
const c1 = DG.Column.fromFloat32Array('s1', new Float32Array([10, 11, 10.5, 12, 100]));
|
|
366
|
+
const c2 = DG.Column.fromFloat32Array('s2', new Float32Array([20, 21, DG.FLOAT_NULL, 22, 200]));
|
|
367
|
+
const c3 = DG.Column.fromFloat32Array('s3', new Float32Array([30, 31, 30.5, 32, 300]));
|
|
368
|
+
c1.semType = SEMTYPE.INTENSITY;
|
|
369
|
+
c2.semType = SEMTYPE.INTENSITY;
|
|
370
|
+
c3.semType = SEMTYPE.INTENSITY;
|
|
371
|
+
const df = DG.DataFrame.fromColumns([
|
|
372
|
+
DG.Column.fromStrings('id', ['P0', 'P1', 'P2', 'P3', 'P4']),
|
|
373
|
+
c1, c2, c3,
|
|
374
|
+
]);
|
|
375
|
+
const count = imputeKnn(df, ['s1', 's2', 's3'], 3);
|
|
376
|
+
expect(count, 1);
|
|
377
|
+
// The imputed value should be close to 21 (average of 3 nearest neighbors' s2 values)
|
|
378
|
+
const imputed = df.col('s2')!.get(2);
|
|
379
|
+
expect(Math.abs(imputed - 21) < 1, true);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test('kNN imputation falls back to column mean when no neighbors have values', async () => {
|
|
383
|
+
// All rows missing in s2 except row 0
|
|
384
|
+
// Row 1 and 2 are missing s2, and row 0 has it
|
|
385
|
+
const c1 = DG.Column.fromFloat32Array('s1', new Float32Array([10, DG.FLOAT_NULL, DG.FLOAT_NULL]));
|
|
386
|
+
const c2 = DG.Column.fromFloat32Array('s2', new Float32Array([20, DG.FLOAT_NULL, DG.FLOAT_NULL]));
|
|
387
|
+
c1.semType = SEMTYPE.INTENSITY;
|
|
388
|
+
c2.semType = SEMTYPE.INTENSITY;
|
|
389
|
+
const df = DG.DataFrame.fromColumns([
|
|
390
|
+
DG.Column.fromStrings('id', ['P0', 'P1', 'P2']),
|
|
391
|
+
c1, c2,
|
|
392
|
+
]);
|
|
393
|
+
const count = imputeKnn(df, ['s1', 's2'], 10);
|
|
394
|
+
// Should have imputed 4 values (rows 1 and 2, both columns)
|
|
395
|
+
// Row 1 and 2 are all-missing, so they should get column means
|
|
396
|
+
expect(count >= 2, true);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test('kNN imputation returns correct count and sets tag', async () => {
|
|
400
|
+
const c1 = DG.Column.fromFloat32Array('s1', new Float32Array([1, 2, DG.FLOAT_NULL, 4]));
|
|
401
|
+
const c2 = DG.Column.fromFloat32Array('s2', new Float32Array([5, DG.FLOAT_NULL, 7, 8]));
|
|
402
|
+
c1.semType = SEMTYPE.INTENSITY;
|
|
403
|
+
c2.semType = SEMTYPE.INTENSITY;
|
|
404
|
+
const df = DG.DataFrame.fromColumns([
|
|
405
|
+
DG.Column.fromStrings('id', ['P0', 'P1', 'P2', 'P3']),
|
|
406
|
+
c1, c2,
|
|
407
|
+
]);
|
|
408
|
+
const count = imputeKnn(df, ['s1', 's2'], 3);
|
|
409
|
+
expect(count, 2);
|
|
410
|
+
expect(df.getTag('proteomics.imputed'), 'true');
|
|
411
|
+
// All values should be filled
|
|
412
|
+
for (let i = 0; i < 4; i++) {
|
|
413
|
+
expect(df.col('s1')!.isNone(i), false);
|
|
414
|
+
expect(df.col('s2')!.isNone(i), false);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test('imputeZero replaces all nulls with 0', async () => {
|
|
419
|
+
const c1 = DG.Column.fromFloat32Array('s1', new Float32Array([1, DG.FLOAT_NULL, 3, DG.FLOAT_NULL]));
|
|
420
|
+
c1.semType = SEMTYPE.INTENSITY;
|
|
421
|
+
const df = DG.DataFrame.fromColumns([
|
|
422
|
+
DG.Column.fromStrings('id', ['P0', 'P1', 'P2', 'P3']),
|
|
423
|
+
c1,
|
|
424
|
+
]);
|
|
425
|
+
const count = imputeZero(df, ['s1']);
|
|
426
|
+
expect(count, 2);
|
|
427
|
+
expect(Math.abs(df.col('s1')!.get(1) - 0) < 0.01, true);
|
|
428
|
+
expect(Math.abs(df.col('s1')!.get(3) - 0) < 0.01, true);
|
|
429
|
+
expect(df.getTag('proteomics.imputed'), 'true');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test('imputeMean replaces all nulls with column mean', async () => {
|
|
433
|
+
// Values: [2, null, 6, null] -- mean of non-null = (2+6)/2 = 4
|
|
434
|
+
const c1 = DG.Column.fromFloat32Array('s1', new Float32Array([2, DG.FLOAT_NULL, 6, DG.FLOAT_NULL]));
|
|
435
|
+
c1.semType = SEMTYPE.INTENSITY;
|
|
436
|
+
const df = DG.DataFrame.fromColumns([
|
|
437
|
+
DG.Column.fromStrings('id', ['P0', 'P1', 'P2', 'P3']),
|
|
438
|
+
c1,
|
|
439
|
+
]);
|
|
440
|
+
const count = imputeMean(df, ['s1']);
|
|
441
|
+
expect(count, 2);
|
|
442
|
+
expect(Math.abs(df.col('s1')!.get(1) - 4) < 0.01, true);
|
|
443
|
+
expect(Math.abs(df.col('s1')!.get(3) - 4) < 0.01, true);
|
|
444
|
+
expect(df.getTag('proteomics.imputed'), 'true');
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test('imputeMedian replaces all nulls with column median', async () => {
|
|
448
|
+
// Values: [2, null, 6, 10, null] -- median of [2,6,10] = 6
|
|
449
|
+
const c1 = DG.Column.fromFloat32Array('s1', new Float32Array([2, DG.FLOAT_NULL, 6, 10, DG.FLOAT_NULL]));
|
|
450
|
+
c1.semType = SEMTYPE.INTENSITY;
|
|
451
|
+
const df = DG.DataFrame.fromColumns([
|
|
452
|
+
DG.Column.fromStrings('id', ['P0', 'P1', 'P2', 'P3', 'P4']),
|
|
453
|
+
c1,
|
|
454
|
+
]);
|
|
455
|
+
const count = imputeMedian(df, ['s1']);
|
|
456
|
+
expect(count, 2);
|
|
457
|
+
expect(Math.abs(df.col('s1')!.get(1) - 6) < 0.01, true);
|
|
458
|
+
expect(Math.abs(df.col('s1')!.get(4) - 6) < 0.01, true);
|
|
459
|
+
expect(df.getTag('proteomics.imputed'), 'true');
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// ── Differential Expression ─────────────────────────────────────────
|
|
464
|
+
|
|
465
|
+
category('Differential Expression', () => {
|
|
466
|
+
test('DE produces correct log2FC sign for known groups', async () => {
|
|
467
|
+
// Group 1 (control) values ~5, Group 2 (treatment) values ~10
|
|
468
|
+
const g1Names = ['ctrl1', 'ctrl2', 'ctrl3'];
|
|
469
|
+
const g2Names = ['treat1', 'treat2', 'treat3'];
|
|
470
|
+
const nProteins = 5;
|
|
471
|
+
const g1Vals = g1Names.map(() => Array.from({length: nProteins}, () => 5.0));
|
|
472
|
+
const g2Vals = g2Names.map(() => Array.from({length: nProteins}, () => 10.0));
|
|
473
|
+
const df = makeTestDf(nProteins, g1Names, g2Names, g1Vals, g2Vals);
|
|
474
|
+
runDifferentialExpression(df, g1Names, g2Names, 'Control', 'Treatment');
|
|
475
|
+
const fcCol = df.col('log2FC')!;
|
|
476
|
+
// log2FC = mean(group2) - mean(group1) = 10 - 5 = 5 (positive)
|
|
477
|
+
for (let i = 0; i < nProteins; i++)
|
|
478
|
+
expect(fcCol.get(i) > 0, true);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test('DE produces valid p-values between 0 and 1', async () => {
|
|
482
|
+
// Use slightly varied values so t-test produces real p-values
|
|
483
|
+
const g1Names = ['ctrl1', 'ctrl2', 'ctrl3'];
|
|
484
|
+
const g2Names = ['treat1', 'treat2', 'treat3'];
|
|
485
|
+
const nProteins = 3;
|
|
486
|
+
const g1Vals = [
|
|
487
|
+
[4.8, 5.1, 5.3],
|
|
488
|
+
[4.9, 5.0, 5.2],
|
|
489
|
+
[5.1, 4.8, 5.0],
|
|
490
|
+
];
|
|
491
|
+
const g2Vals = [
|
|
492
|
+
[9.8, 10.1, 10.3],
|
|
493
|
+
[9.9, 10.0, 10.2],
|
|
494
|
+
[10.1, 9.8, 10.0],
|
|
495
|
+
];
|
|
496
|
+
const df = makeTestDf(nProteins, g1Names, g2Names, g1Vals, g2Vals);
|
|
497
|
+
runDifferentialExpression(df, g1Names, g2Names, 'Control', 'Treatment');
|
|
498
|
+
const pCol = df.col('p-value')!;
|
|
499
|
+
const adjCol = df.col('adj.p-value')!;
|
|
500
|
+
for (let i = 0; i < nProteins; i++) {
|
|
501
|
+
expect(pCol.get(i) >= 0 && pCol.get(i) <= 1, true);
|
|
502
|
+
expect(adjCol.get(i) >= 0 && adjCol.get(i) <= 1, true);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test('proteins with <2 replicates get null p-values', async () => {
|
|
507
|
+
// Protein 0: only 1 non-null value in group1
|
|
508
|
+
const g1Names = ['ctrl1', 'ctrl2', 'ctrl3'];
|
|
509
|
+
const g2Names = ['treat1', 'treat2', 'treat3'];
|
|
510
|
+
const df = DG.DataFrame.fromColumns([
|
|
511
|
+
DG.Column.fromStrings('Protein ID', ['P0']),
|
|
512
|
+
DG.Column.fromFloat32Array('ctrl1', new Float32Array([5.0])),
|
|
513
|
+
DG.Column.fromFloat32Array('ctrl2', new Float32Array([DG.FLOAT_NULL])),
|
|
514
|
+
DG.Column.fromFloat32Array('ctrl3', new Float32Array([DG.FLOAT_NULL])),
|
|
515
|
+
DG.Column.fromFloat32Array('treat1', new Float32Array([10.0])),
|
|
516
|
+
DG.Column.fromFloat32Array('treat2', new Float32Array([10.5])),
|
|
517
|
+
DG.Column.fromFloat32Array('treat3', new Float32Array([9.5])),
|
|
518
|
+
]);
|
|
519
|
+
runDifferentialExpression(df, g1Names, g2Names, 'Control', 'Treatment');
|
|
520
|
+
const pCol = df.col('p-value')!;
|
|
521
|
+
expect(pCol.isNone(0), true);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test('significant column marks proteins passing both thresholds', async () => {
|
|
525
|
+
// Protein 0: large FC, small p (should be significant)
|
|
526
|
+
// Protein 1: small FC, large p (should not be significant)
|
|
527
|
+
const g1Names = ['ctrl1', 'ctrl2', 'ctrl3'];
|
|
528
|
+
const g2Names = ['treat1', 'treat2', 'treat3'];
|
|
529
|
+
const df = DG.DataFrame.fromColumns([
|
|
530
|
+
DG.Column.fromStrings('Protein ID', ['P_significant', 'P_not_significant']),
|
|
531
|
+
// Group 1 control values
|
|
532
|
+
DG.Column.fromFloat32Array('ctrl1', new Float32Array([5.0, 5.0])),
|
|
533
|
+
DG.Column.fromFloat32Array('ctrl2', new Float32Array([5.1, 5.1])),
|
|
534
|
+
DG.Column.fromFloat32Array('ctrl3', new Float32Array([4.9, 4.9])),
|
|
535
|
+
// Group 2: Protein 0 has large difference, Protein 1 is the same
|
|
536
|
+
DG.Column.fromFloat32Array('treat1', new Float32Array([12.0, 5.2])),
|
|
537
|
+
DG.Column.fromFloat32Array('treat2', new Float32Array([12.1, 4.8])),
|
|
538
|
+
DG.Column.fromFloat32Array('treat3', new Float32Array([11.9, 5.0])),
|
|
539
|
+
]);
|
|
540
|
+
runDifferentialExpression(df, g1Names, g2Names, 'Control', 'Treatment', 1.0, 0.05);
|
|
541
|
+
const sigCol = df.col('significant')!;
|
|
542
|
+
// Protein 0: FC ~ 7, very significant
|
|
543
|
+
expect(sigCol.get(0), true);
|
|
544
|
+
// Protein 1: FC ~ 0, not significant
|
|
545
|
+
expect(sigCol.get(1), false);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test('DE sets proteomics.de_complete tag', async () => {
|
|
549
|
+
const g1Names = ['ctrl1', 'ctrl2', 'ctrl3'];
|
|
550
|
+
const g2Names = ['treat1', 'treat2', 'treat3'];
|
|
551
|
+
const nProteins = 2;
|
|
552
|
+
const g1Vals = g1Names.map(() => Array.from({length: nProteins}, () => 5.0));
|
|
553
|
+
const g2Vals = g2Names.map(() => Array.from({length: nProteins}, () => 10.0));
|
|
554
|
+
const df = makeTestDf(nProteins, g1Names, g2Names, g1Vals, g2Vals);
|
|
555
|
+
runDifferentialExpression(df, g1Names, g2Names, 'Control', 'Treatment');
|
|
556
|
+
expect(df.getTag('proteomics.de_complete'), 'true');
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// R3/D-09: report-import DE direction default (13-05)
|
|
560
|
+
|
|
561
|
+
test('D-09: DE dialog defaults to the declared (group1 = numerator) contrast', async () => {
|
|
562
|
+
// Spectronaut report order: group1 = the parser's first condition = the
|
|
563
|
+
// declared Numerator (e.g. DMD), group2 = WT. The dialog default must be
|
|
564
|
+
// `${g1} vs ${g2}` (which the OK-handler maps to numerator = g1), NOT the
|
|
565
|
+
// alphabetical `${g2} vs ${g1}` that previously caused the mirror defect.
|
|
566
|
+
expect(getDefaultComparison('DMD', 'WT'), 'DMD vs WT');
|
|
567
|
+
expect(getDefaultComparison('WT', 'DMD'), 'WT vs DMD');
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test('D-09: contrast is direction-only — reversing flips sign, not |log2FC|', async () => {
|
|
571
|
+
const g1 = ['dmd1', 'dmd2', 'dmd3'];
|
|
572
|
+
const g2 = ['wt1', 'wt2', 'wt3'];
|
|
573
|
+
const n = 3;
|
|
574
|
+
const g1v = g1.map(() => Array.from({length: n}, () => 9.0)); // group1 high
|
|
575
|
+
const g2v = g2.map(() => Array.from({length: n}, () => 4.0)); // group2 low
|
|
576
|
+
|
|
577
|
+
// Declared default (numerator = group1 = DMD): OK-handler calls
|
|
578
|
+
// runDifferentialExpression(df, denominatorCols, numeratorCols, ...) and
|
|
579
|
+
// log2FC = mean(numerator) - mean(denominator).
|
|
580
|
+
const dfDeclared = makeTestDf(n, g1, g2, g1v, g2v);
|
|
581
|
+
runDifferentialExpression(dfDeclared, g2, g1, 'WT', 'DMD');
|
|
582
|
+
const declared = dfDeclared.col('log2FC')!.get(0) as number;
|
|
583
|
+
|
|
584
|
+
// Reversed selection (manual override): numerator = group2 = WT.
|
|
585
|
+
const dfReversed = makeTestDf(n, g1, g2, g1v, g2v);
|
|
586
|
+
runDifferentialExpression(dfReversed, g1, g2, 'DMD', 'WT');
|
|
587
|
+
const reversed = dfReversed.col('log2FC')!.get(0) as number;
|
|
588
|
+
|
|
589
|
+
expect(declared > 0, true); // DMD higher → positive in declared orientation
|
|
590
|
+
expect(reversed < 0, true); // sign flips under reversal
|
|
591
|
+
expect(Math.abs(Math.abs(declared) - Math.abs(reversed)) < 1e-6, true); // |unchanged|
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test('DE assigns correct semantic types', async () => {
|
|
595
|
+
const g1Names = ['ctrl1', 'ctrl2', 'ctrl3'];
|
|
596
|
+
const g2Names = ['treat1', 'treat2', 'treat3'];
|
|
597
|
+
const nProteins = 2;
|
|
598
|
+
const g1Vals = g1Names.map(() => Array.from({length: nProteins}, () => 5.0));
|
|
599
|
+
const g2Vals = g2Names.map(() => Array.from({length: nProteins}, () => 10.0));
|
|
600
|
+
const df = makeTestDf(nProteins, g1Names, g2Names, g1Vals, g2Vals);
|
|
601
|
+
runDifferentialExpression(df, g1Names, g2Names, 'Control', 'Treatment');
|
|
602
|
+
expect(df.col('log2FC')!.semType, SEMTYPE.LOG2FC);
|
|
603
|
+
expect(df.col('p-value')!.semType, SEMTYPE.P_VALUE);
|
|
604
|
+
expect(df.col('adj.p-value')!.semType, SEMTYPE.P_VALUE);
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// ── R-result alignment (DEqMS scramble regression) ──────────────────
|
|
609
|
+
|
|
610
|
+
category('DE result alignment', () => {
|
|
611
|
+
test('copyDEResultsToFrame realigns by row key, not position', async () => {
|
|
612
|
+
// Simulates an R result returned SORTED by significance (the DEqMS bug):
|
|
613
|
+
// result row j belongs to df protein index `row[j]-1`, NOT j.
|
|
614
|
+
const n = 4;
|
|
615
|
+
const df = DG.DataFrame.fromColumns([
|
|
616
|
+
DG.Column.fromStrings('Protein ID', ['P0', 'P1', 'P2', 'P3']),
|
|
617
|
+
]);
|
|
618
|
+
const rowKey = [3, 1, 4, 2]; // result rows map to df idx 2,0,3,1
|
|
619
|
+
// Encode each row's intended df index as its value, so a correct realign
|
|
620
|
+
// makes df row i hold value i. A positional copy would fail this.
|
|
621
|
+
const result = DG.DataFrame.fromColumns([
|
|
622
|
+
DG.Column.fromList('int', 'row', rowKey),
|
|
623
|
+
DG.Column.fromList('double', 'log2FC', rowKey.map((r) => r - 1)),
|
|
624
|
+
DG.Column.fromList('double', 'p.value', rowKey.map((r) => r - 1)),
|
|
625
|
+
DG.Column.fromList('double', 'adj.p.value', rowKey.map((r) => r - 1)),
|
|
626
|
+
DG.Column.fromList('bool', 'significant', rowKey.map((r) => r - 1 === 0)),
|
|
627
|
+
]);
|
|
628
|
+
|
|
629
|
+
const sigCount = copyDEResultsToFrame(df, result);
|
|
630
|
+
|
|
631
|
+
for (let i = 0; i < n; i++) {
|
|
632
|
+
expect(df.col('log2FC')!.get(i), i);
|
|
633
|
+
expect(df.col('p-value')!.get(i), i);
|
|
634
|
+
expect(df.col('adj.p-value')!.get(i), i);
|
|
635
|
+
expect(df.col('significant')!.get(i), i === 0);
|
|
636
|
+
}
|
|
637
|
+
expect(sigCount, 1);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test('copyDEResultsToFrame falls back to positional without row key', async () => {
|
|
641
|
+
const df = DG.DataFrame.fromColumns([
|
|
642
|
+
DG.Column.fromStrings('Protein ID', ['P0', 'P1', 'P2']),
|
|
643
|
+
]);
|
|
644
|
+
const result = DG.DataFrame.fromColumns([
|
|
645
|
+
DG.Column.fromList('double', 'log2FC', [10, 20, 30]),
|
|
646
|
+
DG.Column.fromList('double', 'p.value', [1, 2, 3]),
|
|
647
|
+
DG.Column.fromList('double', 'adj.p.value', [4, 5, 6]),
|
|
648
|
+
DG.Column.fromList('bool', 'significant', [false, true, false]),
|
|
649
|
+
]);
|
|
650
|
+
|
|
651
|
+
copyDEResultsToFrame(df, result);
|
|
652
|
+
|
|
653
|
+
expect(df.col('log2FC')!.get(1), 20);
|
|
654
|
+
expect(df.col('adj.p-value')!.get(2), 6);
|
|
655
|
+
expect(df.col('significant')!.get(1), true);
|
|
656
|
+
expect(df.col('significant')!.get(0), false);
|
|
657
|
+
});
|
|
658
|
+
});
|