@m2c2kit/assessment-color-shapes 0.8.33 → 0.8.34
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/dist/__tests__/data.d.ts +3 -0
- package/dist/__tests__/data.d.ts.map +1 -0
- package/dist/__tests__/python-code.d.ts +2 -0
- package/dist/__tests__/python-code.d.ts.map +1 -0
- package/dist/__tests__/scoring.test.d.ts +2 -0
- package/dist/__tests__/scoring.test.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1345 -47
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/package.json +12 -10
- package/schemas.json +247 -1
package/dist/index.js
CHANGED
|
@@ -11,6 +11,72 @@ class M2Error extends Error {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
const CHAIN_REGISTRY = /* @__PURE__ */ new Map();
|
|
15
|
+
let CHAIN_COUNTER = 0;
|
|
16
|
+
function getChainOps(id) {
|
|
17
|
+
return CHAIN_REGISTRY.get(id);
|
|
18
|
+
}
|
|
19
|
+
function clearChainOps(id) {
|
|
20
|
+
CHAIN_REGISTRY.delete(id);
|
|
21
|
+
}
|
|
22
|
+
function makeChainFn() {
|
|
23
|
+
const fn = ((dataCalc) => {
|
|
24
|
+
if (!dataCalc) return void 0;
|
|
25
|
+
let current = dataCalc;
|
|
26
|
+
for (const op of fn.ops) {
|
|
27
|
+
const method = current[op.name];
|
|
28
|
+
if (typeof method !== "function") {
|
|
29
|
+
throw new M2Error(
|
|
30
|
+
`chain: method ${op.name} does not exist on DataCalc`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
const res = method.apply(current, op.args);
|
|
34
|
+
if (res instanceof DataCalc) {
|
|
35
|
+
current = res;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
return res;
|
|
39
|
+
}
|
|
40
|
+
return void 0;
|
|
41
|
+
});
|
|
42
|
+
fn.ops = [];
|
|
43
|
+
const record = (name, args) => {
|
|
44
|
+
fn.ops.push({ name, args });
|
|
45
|
+
return fn;
|
|
46
|
+
};
|
|
47
|
+
fn.arrange = (...variables) => record("arrange", variables);
|
|
48
|
+
fn.slice = (start, end) => record("slice", [start, end]);
|
|
49
|
+
fn.pull = (variable) => record("pull", [variable]);
|
|
50
|
+
fn.filter = (predicate) => record("filter", [predicate]);
|
|
51
|
+
fn.mutate = (mutations) => record("mutate", [mutations]);
|
|
52
|
+
fn.select = (...variables) => record("select", variables);
|
|
53
|
+
fn.groupBy = (...groups) => record("groupBy", groups);
|
|
54
|
+
fn.ungroup = () => record("ungroup", []);
|
|
55
|
+
fn.rename = (renames) => record("rename", [renames]);
|
|
56
|
+
fn.distinct = () => record("distinct", []);
|
|
57
|
+
Object.defineProperty(fn, "length", {
|
|
58
|
+
configurable: true,
|
|
59
|
+
get: function() {
|
|
60
|
+
try {
|
|
61
|
+
const id = `c${++CHAIN_COUNTER}`;
|
|
62
|
+
CHAIN_REGISTRY.set(
|
|
63
|
+
id,
|
|
64
|
+
(fn.ops || []).map((o) => ({ ...o }))
|
|
65
|
+
);
|
|
66
|
+
return `__CHAIN_EXPR__[${id}]`;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return fn;
|
|
73
|
+
}
|
|
74
|
+
function arrange(...variables) {
|
|
75
|
+
return makeChainFn().arrange(...variables);
|
|
76
|
+
}
|
|
77
|
+
function filter(predicate) {
|
|
78
|
+
return makeChainFn().filter(predicate);
|
|
79
|
+
}
|
|
14
80
|
class DataCalc {
|
|
15
81
|
/**
|
|
16
82
|
* A class for transformation and calculation of m2c2kit data.
|
|
@@ -34,6 +100,7 @@ class DataCalc {
|
|
|
34
100
|
*/
|
|
35
101
|
constructor(data, options) {
|
|
36
102
|
this._groups = new Array();
|
|
103
|
+
this._warnings = false;
|
|
37
104
|
if (!Array.isArray(data)) {
|
|
38
105
|
throw new M2Error(
|
|
39
106
|
"DataCalc constructor expects an array of observations as first argument"
|
|
@@ -63,6 +130,9 @@ class DataCalc {
|
|
|
63
130
|
if (options?.groups) {
|
|
64
131
|
this._groups = Array.from(options.groups);
|
|
65
132
|
}
|
|
133
|
+
if (options?.warnings) {
|
|
134
|
+
this._warnings = true;
|
|
135
|
+
}
|
|
66
136
|
}
|
|
67
137
|
/**
|
|
68
138
|
* Returns the groups in the data.
|
|
@@ -106,9 +176,11 @@ class DataCalc {
|
|
|
106
176
|
*/
|
|
107
177
|
pull(variable) {
|
|
108
178
|
if (this._observations.length === 0) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
179
|
+
if (this._warnings) {
|
|
180
|
+
console.warn(
|
|
181
|
+
`DataCalc.pull(): No observations available to pull variable "${variable}" from. Returning null.`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
112
184
|
return null;
|
|
113
185
|
}
|
|
114
186
|
this.verifyObservationsContainVariable(variable);
|
|
@@ -168,7 +240,7 @@ class DataCalc {
|
|
|
168
240
|
this._observations.filter(
|
|
169
241
|
predicate
|
|
170
242
|
),
|
|
171
|
-
{ groups: this._groups }
|
|
243
|
+
{ groups: this._groups, warnings: this._warnings }
|
|
172
244
|
);
|
|
173
245
|
}
|
|
174
246
|
/**
|
|
@@ -177,7 +249,8 @@ class DataCalc {
|
|
|
177
249
|
* @remarks This is used with the `summarize()` method to calculate summaries
|
|
178
250
|
* by group.
|
|
179
251
|
*
|
|
180
|
-
* @param groups - variable names to group by
|
|
252
|
+
* @param groups - variable names to group by. Grouping variables must be
|
|
253
|
+
* primitive values (string, number, boolean).
|
|
181
254
|
* @returns A new `DataCalc` object with the observations grouped by one or
|
|
182
255
|
* more variables
|
|
183
256
|
*
|
|
@@ -198,6 +271,16 @@ class DataCalc {
|
|
|
198
271
|
groupBy(...groups) {
|
|
199
272
|
groups.forEach((group) => {
|
|
200
273
|
this.verifyObservationsContainVariable(group);
|
|
274
|
+
for (let i = 0; i < this._observations.length; i++) {
|
|
275
|
+
const val = this._observations[i][group];
|
|
276
|
+
if (val === null) continue;
|
|
277
|
+
const t = typeof val;
|
|
278
|
+
if (t !== "number" && t !== "string" && t !== "boolean") {
|
|
279
|
+
throw new M2Error(
|
|
280
|
+
`groupBy(): variable "${group}" contains non-primitive value at index ${i} (type=${t}). Only number, string, boolean, or null are allowed for grouping.`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
201
284
|
});
|
|
202
285
|
return new DataCalc(this._observations, { groups });
|
|
203
286
|
}
|
|
@@ -251,7 +334,10 @@ class DataCalc {
|
|
|
251
334
|
}
|
|
252
335
|
return newObservation;
|
|
253
336
|
});
|
|
254
|
-
return new DataCalc(newObservations, {
|
|
337
|
+
return new DataCalc(newObservations, {
|
|
338
|
+
groups: this._groups,
|
|
339
|
+
warnings: this._warnings
|
|
340
|
+
});
|
|
255
341
|
}
|
|
256
342
|
/**
|
|
257
343
|
* Calculates summaries of the data.
|
|
@@ -288,6 +374,31 @@ class DataCalc {
|
|
|
288
374
|
* );
|
|
289
375
|
* // [ { filteredTotalC: 10 } ]
|
|
290
376
|
* ```
|
|
377
|
+
*
|
|
378
|
+
* @remarks Within a `summarize()` call, regular arithmetic operations
|
|
379
|
+
* (+, -, *, /, etc.) must not be used directly on `DataCalc` objects
|
|
380
|
+
* or summary functions. This is not allowed:
|
|
381
|
+
*
|
|
382
|
+
* @example
|
|
383
|
+
* ```js
|
|
384
|
+
* dc.summarize({
|
|
385
|
+
* meanA: mean("a"),
|
|
386
|
+
* // The below will cause an error because it attempts to add a number
|
|
387
|
+
* // to a DataCalc object, which is not supported
|
|
388
|
+
* totalBPlus5: sum("b") + 5
|
|
389
|
+
* })
|
|
390
|
+
* ```
|
|
391
|
+
*
|
|
392
|
+
* Instead, use the `DataCalc` helper functions such as `add` to perform
|
|
393
|
+
* calculations within summary results. For example, to add 5 to the sum
|
|
394
|
+
* of "b", you can do:
|
|
395
|
+
* @example
|
|
396
|
+
* ```js
|
|
397
|
+
* dc.summarize({
|
|
398
|
+
* meanA: mean("a"),
|
|
399
|
+
* totalBPlus5: sum("b").add(5)
|
|
400
|
+
* })
|
|
401
|
+
* ```
|
|
291
402
|
*/
|
|
292
403
|
summarize(summarizations) {
|
|
293
404
|
if (this._groups.length === 0) {
|
|
@@ -300,20 +411,103 @@ class DataCalc {
|
|
|
300
411
|
summarizeOperation.parameters,
|
|
301
412
|
summarizeOperation.options
|
|
302
413
|
);
|
|
414
|
+
} else if (typeof value === "function") {
|
|
415
|
+
try {
|
|
416
|
+
const res = value(this);
|
|
417
|
+
if (typeof res === "function") {
|
|
418
|
+
throw new M2Error(
|
|
419
|
+
`summarize(): lazy callback for ${newVariable} returned a function; expected a value or array of values.`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
if (res instanceof DataCalc) {
|
|
423
|
+
throw new M2Error(
|
|
424
|
+
`summarize(): lazy callback for ${newVariable} returned a DataCalc; expected a value or array of values.`
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
obs[newVariable] = res;
|
|
428
|
+
} catch (err) {
|
|
429
|
+
throw new M2Error(
|
|
430
|
+
`summarize(): lazy callback for ${newVariable} threw an error: ${err && err.message ? err.message : String(err)}`
|
|
431
|
+
);
|
|
432
|
+
}
|
|
303
433
|
} else {
|
|
434
|
+
if (typeof value === "string") {
|
|
435
|
+
const re = /__CHAIN_EXPR__\[(.*?)\]/g;
|
|
436
|
+
const matches = Array.from(value.matchAll(re));
|
|
437
|
+
if (matches.length > 0) {
|
|
438
|
+
let sum2 = 0;
|
|
439
|
+
for (const m of matches) {
|
|
440
|
+
const payload = m[1];
|
|
441
|
+
let ops = getChainOps(payload);
|
|
442
|
+
if (!ops) {
|
|
443
|
+
try {
|
|
444
|
+
ops = JSON.parse(decodeURIComponent(payload));
|
|
445
|
+
} catch (err) {
|
|
446
|
+
throw new M2Error(
|
|
447
|
+
`summarize(): failed to parse chain payload for ${newVariable}`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
let current = this;
|
|
452
|
+
let evaluated = void 0;
|
|
453
|
+
if (!ops) {
|
|
454
|
+
throw new M2Error(
|
|
455
|
+
`summarize(): empty chain ops for ${newVariable}`
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
for (const op of ops) {
|
|
459
|
+
const method = current[op.name];
|
|
460
|
+
if (typeof method !== "function") {
|
|
461
|
+
throw new M2Error(
|
|
462
|
+
`summarize(): chain method ${op.name} does not exist on DataCalc`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
const res = method.apply(current, op.args);
|
|
466
|
+
if (res instanceof DataCalc) {
|
|
467
|
+
current = res;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (Array.isArray(res)) {
|
|
471
|
+
evaluated = res.length;
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
if (typeof res === "boolean") {
|
|
475
|
+
evaluated = res ? 1 : 0;
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
evaluated = typeof res === "number" ? res : Number(res);
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
if (evaluated === void 0) {
|
|
482
|
+
evaluated = current instanceof DataCalc ? current.length : 0;
|
|
483
|
+
}
|
|
484
|
+
sum2 += evaluated;
|
|
485
|
+
}
|
|
486
|
+
let leftover = value;
|
|
487
|
+
for (const m of matches) leftover = leftover.replace(m[0], "");
|
|
488
|
+
const leftoverNum = Number(leftover);
|
|
489
|
+
if (!Number.isNaN(leftoverNum)) sum2 += leftoverNum;
|
|
490
|
+
obs[newVariable] = sum2;
|
|
491
|
+
for (const m of matches) {
|
|
492
|
+
clearChainOps(m[1]);
|
|
493
|
+
}
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
304
497
|
obs[newVariable] = value;
|
|
305
498
|
}
|
|
306
499
|
}
|
|
307
|
-
return new DataCalc([obs], {
|
|
500
|
+
return new DataCalc([obs], {
|
|
501
|
+
groups: this._groups,
|
|
502
|
+
warnings: this._warnings
|
|
503
|
+
});
|
|
308
504
|
}
|
|
309
505
|
return this.summarizeByGroups(summarizations);
|
|
310
506
|
}
|
|
311
507
|
summarizeByGroups(summarizations) {
|
|
312
508
|
const groupMap = /* @__PURE__ */ new Map();
|
|
313
509
|
this._observations.forEach((obs) => {
|
|
314
|
-
const groupKey = this._groups.map(
|
|
315
|
-
(g) => typeof obs[g] === "object" ? JSON.stringify(obs[g]) : obs[g]
|
|
316
|
-
).join("|");
|
|
510
|
+
const groupKey = this._groups.map((g) => String(obs[g])).join("|");
|
|
317
511
|
if (!groupMap.has(groupKey)) {
|
|
318
512
|
groupMap.set(groupKey, []);
|
|
319
513
|
}
|
|
@@ -331,21 +525,17 @@ class DataCalc {
|
|
|
331
525
|
const summaryObj = {};
|
|
332
526
|
this._groups.forEach((group, i) => {
|
|
333
527
|
const valueStr = groupValues[i];
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
summaryObj[group] = Number(valueStr);
|
|
337
|
-
} else if (originalType === "boolean") {
|
|
338
|
-
summaryObj[group] = valueStr === "true";
|
|
339
|
-
} else if (valueStr.startsWith("{") || valueStr.startsWith("[")) {
|
|
340
|
-
try {
|
|
341
|
-
summaryObj[group] = JSON.parse(valueStr);
|
|
342
|
-
} catch {
|
|
343
|
-
throw new M2Error(
|
|
344
|
-
`Failed to parse group value ${valueStr} as JSON for group ${group}`
|
|
345
|
-
);
|
|
346
|
-
}
|
|
528
|
+
if (firstObs[group] === null) {
|
|
529
|
+
summaryObj[group] = null;
|
|
347
530
|
} else {
|
|
348
|
-
|
|
531
|
+
const originalType = typeof firstObs[group];
|
|
532
|
+
if (originalType === "number") {
|
|
533
|
+
summaryObj[group] = Number(valueStr);
|
|
534
|
+
} else if (originalType === "boolean") {
|
|
535
|
+
summaryObj[group] = valueStr === "true";
|
|
536
|
+
} else {
|
|
537
|
+
summaryObj[group] = valueStr;
|
|
538
|
+
}
|
|
349
539
|
}
|
|
350
540
|
});
|
|
351
541
|
const groupDataCalc = new DataCalc(groupObs);
|
|
@@ -357,13 +547,98 @@ class DataCalc {
|
|
|
357
547
|
summarizeOperation.parameters,
|
|
358
548
|
summarizeOperation.options
|
|
359
549
|
);
|
|
550
|
+
} else if (typeof value === "function") {
|
|
551
|
+
try {
|
|
552
|
+
const res = value(groupDataCalc);
|
|
553
|
+
if (typeof res === "function") {
|
|
554
|
+
throw new M2Error(
|
|
555
|
+
`summarize(): lazy callback for ${newVariable} returned a function; expected a value or array of values.`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
if (res instanceof DataCalc) {
|
|
559
|
+
throw new M2Error(
|
|
560
|
+
`summarize(): lazy callback for ${newVariable} returned a DataCalc; expected a value or array of values.`
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
summaryObj[newVariable] = res;
|
|
564
|
+
} catch (err) {
|
|
565
|
+
throw new M2Error(
|
|
566
|
+
`summarize(): lazy callback for ${newVariable} threw an error: ${err && err.message ? err.message : String(err)}`
|
|
567
|
+
);
|
|
568
|
+
}
|
|
360
569
|
} else {
|
|
570
|
+
if (typeof value === "string") {
|
|
571
|
+
const re = /__CHAIN_EXPR__\[(.*?)\]/g;
|
|
572
|
+
const matches = Array.from(value.matchAll(re));
|
|
573
|
+
if (matches.length > 0) {
|
|
574
|
+
let sum2 = 0;
|
|
575
|
+
for (const m of matches) {
|
|
576
|
+
const payload = m[1];
|
|
577
|
+
let ops = getChainOps(payload);
|
|
578
|
+
if (!ops) {
|
|
579
|
+
try {
|
|
580
|
+
ops = JSON.parse(decodeURIComponent(payload));
|
|
581
|
+
} catch (err) {
|
|
582
|
+
throw new M2Error(
|
|
583
|
+
`summarize(): failed to parse chain payload for ${newVariable}`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
let current = groupDataCalc;
|
|
588
|
+
let evaluated = void 0;
|
|
589
|
+
if (!ops) {
|
|
590
|
+
throw new M2Error(
|
|
591
|
+
`summarize(): empty chain ops for ${newVariable}`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
for (const op of ops) {
|
|
595
|
+
const method = current[op.name];
|
|
596
|
+
if (typeof method !== "function") {
|
|
597
|
+
throw new M2Error(
|
|
598
|
+
`summarize(): chain method ${op.name} does not exist on DataCalc`
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
const res = method.apply(current, op.args);
|
|
602
|
+
if (res instanceof DataCalc) {
|
|
603
|
+
current = res;
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (Array.isArray(res)) {
|
|
607
|
+
evaluated = res.length;
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
if (typeof res === "boolean") {
|
|
611
|
+
evaluated = res ? 1 : 0;
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
evaluated = typeof res === "number" ? res : Number(res);
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
if (evaluated === void 0) {
|
|
618
|
+
evaluated = current instanceof DataCalc ? current.length : 0;
|
|
619
|
+
}
|
|
620
|
+
sum2 += evaluated;
|
|
621
|
+
}
|
|
622
|
+
let leftover = value;
|
|
623
|
+
for (const m of matches) leftover = leftover.replace(m[0], "");
|
|
624
|
+
const leftoverNum = Number(leftover);
|
|
625
|
+
if (!Number.isNaN(leftoverNum)) sum2 += leftoverNum;
|
|
626
|
+
summaryObj[newVariable] = sum2;
|
|
627
|
+
for (const m of matches) {
|
|
628
|
+
clearChainOps(m[1]);
|
|
629
|
+
}
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
361
633
|
summaryObj[newVariable] = value;
|
|
362
634
|
}
|
|
363
635
|
}
|
|
364
636
|
summarizedObservations.push(summaryObj);
|
|
365
637
|
});
|
|
366
|
-
return new DataCalc(summarizedObservations, {
|
|
638
|
+
return new DataCalc(summarizedObservations, {
|
|
639
|
+
groups: this._groups,
|
|
640
|
+
warnings: this._warnings
|
|
641
|
+
});
|
|
367
642
|
}
|
|
368
643
|
/**
|
|
369
644
|
* Selects specific variables to keep in the dataset.
|
|
@@ -416,7 +691,10 @@ class DataCalc {
|
|
|
416
691
|
}
|
|
417
692
|
return newObservation;
|
|
418
693
|
});
|
|
419
|
-
return new DataCalc(newObservations, {
|
|
694
|
+
return new DataCalc(newObservations, {
|
|
695
|
+
groups: this._groups,
|
|
696
|
+
warnings: this._warnings
|
|
697
|
+
});
|
|
420
698
|
}
|
|
421
699
|
/**
|
|
422
700
|
* Arranges (sorts) the observations based on one or more variables.
|
|
@@ -468,7 +746,10 @@ class DataCalc {
|
|
|
468
746
|
}
|
|
469
747
|
return 0;
|
|
470
748
|
});
|
|
471
|
-
return new DataCalc(sortedObservations, {
|
|
749
|
+
return new DataCalc(sortedObservations, {
|
|
750
|
+
groups: this._groups,
|
|
751
|
+
warnings: this._warnings
|
|
752
|
+
});
|
|
472
753
|
}
|
|
473
754
|
/**
|
|
474
755
|
* Keeps only unique/distinct observations.
|
|
@@ -497,11 +778,19 @@ class DataCalc {
|
|
|
497
778
|
seen.add(key);
|
|
498
779
|
return true;
|
|
499
780
|
});
|
|
500
|
-
return new DataCalc(uniqueObs, {
|
|
781
|
+
return new DataCalc(uniqueObs, {
|
|
782
|
+
groups: this._groups,
|
|
783
|
+
warnings: this._warnings
|
|
784
|
+
});
|
|
501
785
|
}
|
|
502
786
|
/**
|
|
503
787
|
* Renames variables in the observations.
|
|
504
788
|
*
|
|
789
|
+
* @remarks If a target name in `renames` already exists in the dataset (and is not
|
|
790
|
+
* simply the source name being renamed), the rename will overwrite the existing
|
|
791
|
+
* variable. When warnings are enabled, a warning will be logged to make users aware
|
|
792
|
+
* of the potential overwrite.
|
|
793
|
+
*
|
|
505
794
|
* @param renames - Object mapping new variable names to old variable names
|
|
506
795
|
* @returns A new DataCalc object with renamed variables
|
|
507
796
|
*
|
|
@@ -523,21 +812,37 @@ class DataCalc {
|
|
|
523
812
|
Object.values(renames).forEach((oldName) => {
|
|
524
813
|
this.verifyObservationsContainVariable(oldName);
|
|
525
814
|
});
|
|
815
|
+
if (this._observations.length > 0) {
|
|
816
|
+
const existingKeys = new Set(Object.keys(this._observations[0]));
|
|
817
|
+
const oldNames = new Set(Object.values(renames));
|
|
818
|
+
const collisions = Object.keys(renames).filter(
|
|
819
|
+
(n2) => existingKeys.has(n2) && !oldNames.has(n2)
|
|
820
|
+
);
|
|
821
|
+
if (collisions.length > 0 && this._warnings) {
|
|
822
|
+
console.warn(
|
|
823
|
+
`DataCalc.rename(): renaming will overwrite existing variables: ${collisions.join(", ")}`
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
526
827
|
const newObservations = this._observations.map((observation) => {
|
|
527
828
|
const newObservation = {};
|
|
829
|
+
const newNames = new Set(Object.keys(renames));
|
|
528
830
|
for (const [key, value] of Object.entries(observation)) {
|
|
529
831
|
const newKey = Object.entries(renames).find(
|
|
530
832
|
([, old]) => old === key
|
|
531
833
|
)?.[0];
|
|
532
834
|
if (newKey) {
|
|
533
835
|
newObservation[newKey] = value;
|
|
534
|
-
} else if (!
|
|
836
|
+
} else if (!newNames.has(key)) {
|
|
535
837
|
newObservation[key] = value;
|
|
536
838
|
}
|
|
537
839
|
}
|
|
538
840
|
return newObservation;
|
|
539
841
|
});
|
|
540
|
-
return new DataCalc(newObservations, {
|
|
842
|
+
return new DataCalc(newObservations, {
|
|
843
|
+
groups: this._groups,
|
|
844
|
+
warnings: this._warnings
|
|
845
|
+
});
|
|
541
846
|
}
|
|
542
847
|
/**
|
|
543
848
|
* Performs an inner join with another DataCalc object.
|
|
@@ -877,7 +1182,10 @@ class DataCalc {
|
|
|
877
1182
|
}
|
|
878
1183
|
let sliced;
|
|
879
1184
|
if (start >= this._observations.length) {
|
|
880
|
-
return new DataCalc([], {
|
|
1185
|
+
return new DataCalc([], {
|
|
1186
|
+
groups: this._groups,
|
|
1187
|
+
warnings: this._warnings
|
|
1188
|
+
});
|
|
881
1189
|
}
|
|
882
1190
|
if (end === void 0) {
|
|
883
1191
|
const index = start < 0 ? this._observations.length + start : start;
|
|
@@ -885,7 +1193,10 @@ class DataCalc {
|
|
|
885
1193
|
} else {
|
|
886
1194
|
sliced = this._observations.slice(start, end);
|
|
887
1195
|
}
|
|
888
|
-
return new DataCalc(sliced, {
|
|
1196
|
+
return new DataCalc(sliced, {
|
|
1197
|
+
groups: this._groups,
|
|
1198
|
+
warnings: this._warnings
|
|
1199
|
+
});
|
|
889
1200
|
}
|
|
890
1201
|
/**
|
|
891
1202
|
* Combines observations from two DataCalc objects by rows.
|
|
@@ -1047,6 +1358,26 @@ class DataCalc {
|
|
|
1047
1358
|
* @returns a deep copy of the object
|
|
1048
1359
|
*/
|
|
1049
1360
|
deepCopy(source, map = /* @__PURE__ */ new WeakMap()) {
|
|
1361
|
+
const sClone = globalThis.structuredClone;
|
|
1362
|
+
if (typeof sClone === "function") {
|
|
1363
|
+
try {
|
|
1364
|
+
const proto = source && typeof source === "object" ? Object.getPrototypeOf(source) : null;
|
|
1365
|
+
const isPlain = Array.isArray(source) || proto === Object.prototype || proto === null;
|
|
1366
|
+
if (isPlain) {
|
|
1367
|
+
const descriptors = Object.getOwnPropertyDescriptors(
|
|
1368
|
+
source
|
|
1369
|
+
);
|
|
1370
|
+
const hasAccessors = Object.values(descriptors).some(
|
|
1371
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1372
|
+
(d) => typeof d.get === "function" || typeof d.set === "function"
|
|
1373
|
+
);
|
|
1374
|
+
if (!hasAccessors) {
|
|
1375
|
+
return sClone(source);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
} catch {
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1050
1381
|
if (source === null || typeof source !== "object") {
|
|
1051
1382
|
return source;
|
|
1052
1383
|
}
|
|
@@ -1065,8 +1396,9 @@ class DataCalc {
|
|
|
1065
1396
|
key
|
|
1066
1397
|
);
|
|
1067
1398
|
if (descriptor) {
|
|
1399
|
+
const { get, set, ...descWithoutAccessors } = descriptor;
|
|
1068
1400
|
Object.defineProperty(copy, key, {
|
|
1069
|
-
...
|
|
1401
|
+
...descWithoutAccessors,
|
|
1070
1402
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1071
1403
|
value: this.deepCopy(source[key], map)
|
|
1072
1404
|
});
|
|
@@ -1086,7 +1418,613 @@ class DataCalc {
|
|
|
1086
1418
|
return keys.some((key) => obs[key] === null || obs[key] === void 0);
|
|
1087
1419
|
}
|
|
1088
1420
|
}
|
|
1089
|
-
|
|
1421
|
+
const PRECEDENCE = {
|
|
1422
|
+
"+": 1,
|
|
1423
|
+
"-": 1,
|
|
1424
|
+
"*": 2,
|
|
1425
|
+
"/": 2,
|
|
1426
|
+
"^": 3
|
|
1427
|
+
};
|
|
1428
|
+
class SummarizeOperation {
|
|
1429
|
+
constructor(leafFn, parameters, options, tokens) {
|
|
1430
|
+
this.leafFn = leafFn;
|
|
1431
|
+
this.parameters = parameters;
|
|
1432
|
+
this.options = options;
|
|
1433
|
+
if (tokens && tokens.length > 0) {
|
|
1434
|
+
this.tokens = tokens.slice();
|
|
1435
|
+
} else if (leafFn) {
|
|
1436
|
+
this.tokens = [{ t: "operand", v: this }];
|
|
1437
|
+
} else {
|
|
1438
|
+
this.tokens = [];
|
|
1439
|
+
}
|
|
1440
|
+
this.summarizeFunction = (dc) => {
|
|
1441
|
+
return this.evaluateAsValue(dc);
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
// Factory for creating a leaf SummarizeOperation (use in helpers)
|
|
1445
|
+
static leaf(leafFn, parameters, options) {
|
|
1446
|
+
return new SummarizeOperation(leafFn, parameters, options);
|
|
1447
|
+
}
|
|
1448
|
+
// clone with new token stream (immutable-ish)
|
|
1449
|
+
cloneWithTokens(newTokens) {
|
|
1450
|
+
return new SummarizeOperation(void 0, void 0, void 0, newTokens);
|
|
1451
|
+
}
|
|
1452
|
+
// append operator + operand (operand can be number or SummarizeOperation)
|
|
1453
|
+
appendOp(op, operand) {
|
|
1454
|
+
const newTokens = this.tokens.slice();
|
|
1455
|
+
newTokens.push({ t: "op", v: op });
|
|
1456
|
+
newTokens.push({ t: "operand", v: operand });
|
|
1457
|
+
return this.cloneWithTokens(newTokens);
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Append addition to this expression.
|
|
1461
|
+
*
|
|
1462
|
+
* @param x - A numeric literal or another `SummarizeOperation` to add to this expression
|
|
1463
|
+
* @returns A new `SummarizeOperation` representing `(this + x)`
|
|
1464
|
+
*
|
|
1465
|
+
* @example
|
|
1466
|
+
* ```js
|
|
1467
|
+
* const d = [
|
|
1468
|
+
* { a: 1, b: 2, c: 3 },
|
|
1469
|
+
* { a: 0, b: 8, c: 3 },
|
|
1470
|
+
* { a: 9, b: 4, c: 7 },
|
|
1471
|
+
* ];
|
|
1472
|
+
* const dc = new DataCalc(d);
|
|
1473
|
+
* console.log(
|
|
1474
|
+
* dc.summarize({
|
|
1475
|
+
* result: mean("a").add(10),
|
|
1476
|
+
* }).observations
|
|
1477
|
+
* );
|
|
1478
|
+
* // [ { result: 13.33333 } ]
|
|
1479
|
+
* ```
|
|
1480
|
+
*/
|
|
1481
|
+
add(x) {
|
|
1482
|
+
return this.appendOp("+", x);
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Append subtraction to this expression.
|
|
1486
|
+
*
|
|
1487
|
+
* @param x - A numeric literal or another `SummarizeOperation` to subtract from this expression
|
|
1488
|
+
* @returns A new `SummarizeOperation` representing `(this - x)`
|
|
1489
|
+
*
|
|
1490
|
+
* @example
|
|
1491
|
+
* ```js
|
|
1492
|
+
* const d = [
|
|
1493
|
+
* { a: 1, b: 2, c: 3 },
|
|
1494
|
+
* { a: 0, b: 8, c: 3 },
|
|
1495
|
+
* { a: 9, b: 4, c: 7 },
|
|
1496
|
+
* ];
|
|
1497
|
+
* const dc = new DataCalc(d);
|
|
1498
|
+
* console.log(
|
|
1499
|
+
* dc.summarize({
|
|
1500
|
+
* result: mean("a").sub(10),
|
|
1501
|
+
* }).observations
|
|
1502
|
+
* );
|
|
1503
|
+
* // [ { result: -6.6667 } ]
|
|
1504
|
+
* ```
|
|
1505
|
+
*/
|
|
1506
|
+
sub(x) {
|
|
1507
|
+
return this.appendOp("-", x);
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Append multiplication to this expression. Multiplication has higher
|
|
1511
|
+
* precedence than addition/subtraction.
|
|
1512
|
+
*
|
|
1513
|
+
* @param x - A numeric literal or another `SummarizeOperation` to multiply with this expression
|
|
1514
|
+
* @returns A new `SummarizeOperation` representing `(this * x)`
|
|
1515
|
+
*
|
|
1516
|
+
* @example
|
|
1517
|
+
* ```js
|
|
1518
|
+
* const d = [
|
|
1519
|
+
* { a: 1, b: 2, c: 3 },
|
|
1520
|
+
* { a: 0, b: 8, c: 3 },
|
|
1521
|
+
* { a: 9, b: 4, c: 7 },
|
|
1522
|
+
* ];
|
|
1523
|
+
* const dc = new DataCalc(d);
|
|
1524
|
+
* console.log(
|
|
1525
|
+
* dc.summarize({
|
|
1526
|
+
* result: mean("a").mul(10),
|
|
1527
|
+
* }).observations
|
|
1528
|
+
* );
|
|
1529
|
+
* // [ { result: 33.3333 } ]
|
|
1530
|
+
* ```
|
|
1531
|
+
*/
|
|
1532
|
+
mul(x) {
|
|
1533
|
+
return this.appendOp("*", x);
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Append division to this expression.
|
|
1537
|
+
*
|
|
1538
|
+
* @param x - A numeric literal or another `SummarizeOperation` to divide this expression by
|
|
1539
|
+
* @returns A new `SummarizeOperation` representing `(this / x)`
|
|
1540
|
+
*
|
|
1541
|
+
* @example
|
|
1542
|
+
* ```js
|
|
1543
|
+
* const d = [
|
|
1544
|
+
* { a: 1, b: 2, c: 3 },
|
|
1545
|
+
* { a: 0, b: 8, c: 3 },
|
|
1546
|
+
* { a: 9, b: 4, c: 7 },
|
|
1547
|
+
* ];
|
|
1548
|
+
* const dc = new DataCalc(d);
|
|
1549
|
+
* console.log(
|
|
1550
|
+
* dc.summarize({
|
|
1551
|
+
* result: mean("a").div(10),
|
|
1552
|
+
* }).observations
|
|
1553
|
+
* );
|
|
1554
|
+
* // [ { result: .3333 } ]
|
|
1555
|
+
* ```
|
|
1556
|
+
*/
|
|
1557
|
+
div(x) {
|
|
1558
|
+
return this.appendOp("/", x);
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Append exponentiation (power) to this expression.
|
|
1562
|
+
*
|
|
1563
|
+
* Note: exponentiation uses right-associative semantics (a ^ b ^ c -> a ^ (b ^ c)).
|
|
1564
|
+
*
|
|
1565
|
+
* @param x - A numeric literal or another `SummarizeOperation` used as the exponent
|
|
1566
|
+
* @returns A new `SummarizeOperation` representing `(this ^ x)`
|
|
1567
|
+
*
|
|
1568
|
+
* @example
|
|
1569
|
+
* ```js
|
|
1570
|
+
* const d = [
|
|
1571
|
+
* { a: 1, b: 2, c: 3 },
|
|
1572
|
+
* { a: 0, b: 8, c: 3 },
|
|
1573
|
+
* { a: 9, b: 4, c: 7 },
|
|
1574
|
+
* ];
|
|
1575
|
+
* const dc = new DataCalc(d);
|
|
1576
|
+
* console.log(
|
|
1577
|
+
* dc.summarize({
|
|
1578
|
+
* result: mean("a").pow(2),
|
|
1579
|
+
* }).observations
|
|
1580
|
+
* );
|
|
1581
|
+
* // [ { result: 11.1111 } ]
|
|
1582
|
+
* ```
|
|
1583
|
+
*/
|
|
1584
|
+
pow(x) {
|
|
1585
|
+
return this.appendOp("^", x);
|
|
1586
|
+
}
|
|
1587
|
+
// Evaluate an operand token to a number (returns NaN for non-numeric)
|
|
1588
|
+
evaluateOperandToNumber(opd, dc) {
|
|
1589
|
+
if (!this._usedChainIds) this._usedChainIds = /* @__PURE__ */ new Set();
|
|
1590
|
+
if (typeof opd === "number") return opd;
|
|
1591
|
+
if (typeof opd === "string") {
|
|
1592
|
+
const re = /^__CHAIN_EXPR__\[(.*?)\]$/;
|
|
1593
|
+
const m = re.exec(opd);
|
|
1594
|
+
if (m) {
|
|
1595
|
+
const payload = m[1];
|
|
1596
|
+
let ops = getChainOps(payload);
|
|
1597
|
+
if (ops) this._usedChainIds?.add(payload);
|
|
1598
|
+
if (!ops) {
|
|
1599
|
+
try {
|
|
1600
|
+
ops = JSON.parse(decodeURIComponent(payload));
|
|
1601
|
+
} catch {
|
|
1602
|
+
ops = void 0;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
if (!ops) return NaN;
|
|
1606
|
+
let current = dc;
|
|
1607
|
+
let evaluated = void 0;
|
|
1608
|
+
for (const op of ops) {
|
|
1609
|
+
const method = current[op.name];
|
|
1610
|
+
if (typeof method !== "function") return NaN;
|
|
1611
|
+
const res = method.apply(current, op.args);
|
|
1612
|
+
if (res instanceof DataCalc) {
|
|
1613
|
+
current = res;
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
if (Array.isArray(res)) {
|
|
1617
|
+
evaluated = res.length;
|
|
1618
|
+
break;
|
|
1619
|
+
}
|
|
1620
|
+
if (typeof res === "boolean") {
|
|
1621
|
+
const coerce = this.options && typeof this.options.coerceBooleans === "boolean" ? this.options.coerceBooleans : true;
|
|
1622
|
+
if (coerce) {
|
|
1623
|
+
evaluated = res ? 1 : 0;
|
|
1624
|
+
break;
|
|
1625
|
+
} else {
|
|
1626
|
+
return NaN;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
evaluated = typeof res === "number" ? res : Number(res);
|
|
1630
|
+
break;
|
|
1631
|
+
}
|
|
1632
|
+
if (evaluated === void 0)
|
|
1633
|
+
evaluated = current instanceof DataCalc ? current.length : NaN;
|
|
1634
|
+
return typeof evaluated === "number" && !isNaN(evaluated) ? evaluated : NaN;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
const params = Array.isArray(opd.parameters) ? opd.parameters : opd.parameters === void 0 ? void 0 : [opd.parameters];
|
|
1638
|
+
const raw = opd.leafFn ? opd.leafFn(dc, params, opd.options) : opd.evaluateAsValue(dc);
|
|
1639
|
+
if (raw === null || raw === void 0) return NaN;
|
|
1640
|
+
const num = Number(raw);
|
|
1641
|
+
return typeof num === "number" && !isNaN(num) ? num : NaN;
|
|
1642
|
+
}
|
|
1643
|
+
// Evaluate a flat token stream (no nested handling required here; nested SummarizeOperation
|
|
1644
|
+
// operands will evaluate themselves recursively)
|
|
1645
|
+
evaluateFlatTokens(tokens, dc) {
|
|
1646
|
+
const operands = [];
|
|
1647
|
+
const ops = [];
|
|
1648
|
+
for (const tk of tokens) {
|
|
1649
|
+
if (tk.t === "operand") operands.push(tk.v);
|
|
1650
|
+
else ops.push(tk.v);
|
|
1651
|
+
}
|
|
1652
|
+
if (operands.length === 0) return NaN;
|
|
1653
|
+
while (ops.length > 0) {
|
|
1654
|
+
let bestIdx = 0;
|
|
1655
|
+
let bestPrec = PRECEDENCE[ops[0]] ?? 0;
|
|
1656
|
+
for (let i = 1; i < ops.length; i++) {
|
|
1657
|
+
const p = PRECEDENCE[ops[i]] ?? 0;
|
|
1658
|
+
if (p > bestPrec || p === bestPrec && ops[i] === "^") {
|
|
1659
|
+
bestPrec = p;
|
|
1660
|
+
bestIdx = i;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
const op = ops.splice(bestIdx, 1)[0];
|
|
1664
|
+
const left = operands.splice(bestIdx, 1)[0];
|
|
1665
|
+
const right = operands.splice(bestIdx, 1)[0];
|
|
1666
|
+
const a = this.evaluateOperandToNumber(left, dc);
|
|
1667
|
+
const b = this.evaluateOperandToNumber(right, dc);
|
|
1668
|
+
let res2 = NaN;
|
|
1669
|
+
if (op === "+") res2 = a + b;
|
|
1670
|
+
else if (op === "-") res2 = a - b;
|
|
1671
|
+
else if (op === "*") res2 = a * b;
|
|
1672
|
+
else if (op === "/") res2 = b === 0 ? NaN : a / b;
|
|
1673
|
+
else if (op === "^") res2 = Math.pow(a, b);
|
|
1674
|
+
operands.splice(bestIdx, 0, res2);
|
|
1675
|
+
}
|
|
1676
|
+
const final = operands[0];
|
|
1677
|
+
if (typeof final === "number") {
|
|
1678
|
+
if (this._usedChainIds) {
|
|
1679
|
+
for (const id of this._usedChainIds) clearChainOps(id);
|
|
1680
|
+
this._usedChainIds = void 0;
|
|
1681
|
+
}
|
|
1682
|
+
return final;
|
|
1683
|
+
}
|
|
1684
|
+
const res = this.evaluateOperandToNumber(final, dc);
|
|
1685
|
+
if (this._usedChainIds) {
|
|
1686
|
+
for (const id of this._usedChainIds) clearChainOps(id);
|
|
1687
|
+
this._usedChainIds = void 0;
|
|
1688
|
+
}
|
|
1689
|
+
return res;
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Instance helper: return a grouped version of this operation so it becomes
|
|
1693
|
+
* a single operand in outer expressions. Equivalent to parens(this).
|
|
1694
|
+
*/
|
|
1695
|
+
parens() {
|
|
1696
|
+
return parens(this);
|
|
1697
|
+
}
|
|
1698
|
+
// Top-level evaluation: handles the case where this instance is a leaf (leafFn present)
|
|
1699
|
+
// or an expression token stream.
|
|
1700
|
+
evaluateAsValue(dc) {
|
|
1701
|
+
if (this.leafFn) {
|
|
1702
|
+
const params = Array.isArray(this.parameters) ? this.parameters : this.parameters === void 0 ? void 0 : [this.parameters];
|
|
1703
|
+
const res = this.leafFn(dc, params, this.options);
|
|
1704
|
+
if (typeof res === "number" && Number.isNaN(res)) return null;
|
|
1705
|
+
return res;
|
|
1706
|
+
}
|
|
1707
|
+
if (!this.tokens || this.tokens.length === 0) return NaN;
|
|
1708
|
+
const val = this.evaluateFlatTokens(this.tokens, dc);
|
|
1709
|
+
if (typeof val === "number" && Number.isNaN(val)) return null;
|
|
1710
|
+
return val;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
function parens(op) {
|
|
1714
|
+
const tokens = [{ t: "operand", v: op }];
|
|
1715
|
+
return new SummarizeOperation(void 0, void 0, void 0, tokens);
|
|
1716
|
+
}
|
|
1717
|
+
const DEFAULT_SUMMARIZE_OPTIONS = {
|
|
1718
|
+
coerceBooleans: true,
|
|
1719
|
+
skipMissing: false
|
|
1720
|
+
};
|
|
1721
|
+
function applyDefaultOptions(options) {
|
|
1722
|
+
return { ...DEFAULT_SUMMARIZE_OPTIONS, ...options };
|
|
1723
|
+
}
|
|
1724
|
+
function resolveLazy(param, dataCalc, opName) {
|
|
1725
|
+
if (typeof param !== "function") {
|
|
1726
|
+
return param;
|
|
1727
|
+
}
|
|
1728
|
+
try {
|
|
1729
|
+
const res = param(dataCalc);
|
|
1730
|
+
if (typeof res === "function") {
|
|
1731
|
+
throw new M2Error(
|
|
1732
|
+
`${opName || "summarize()"}: lazy callback returned a function; expected a value or array of values.`
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
if (res instanceof DataCalc) {
|
|
1736
|
+
throw new M2Error(
|
|
1737
|
+
`${opName || "summarize()"}: lazy callback returned a DataCalc instance; expected a value or array of values.`
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
return res;
|
|
1741
|
+
} catch (err) {
|
|
1742
|
+
throw new M2Error(
|
|
1743
|
+
`${"summarize()"}: lazy callback threw an error: ${err && err.message ? err.message : String(err)}`
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
function processNumericValues(dataCalc, variable, options, collector, errorPrefix, initialState) {
|
|
1748
|
+
const mergedOptions = applyDefaultOptions(options);
|
|
1749
|
+
dataCalc.verifyObservationsContainVariable(variable);
|
|
1750
|
+
let count = 0;
|
|
1751
|
+
let state = initialState;
|
|
1752
|
+
let containsMissing = false;
|
|
1753
|
+
dataCalc.observations.forEach((o) => {
|
|
1754
|
+
if (dataCalc.isNonMissingNumeric(o[variable])) {
|
|
1755
|
+
state = collector(o[variable], state);
|
|
1756
|
+
count++;
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
if (typeof o[variable] === "boolean" && mergedOptions.coerceBooleans) {
|
|
1760
|
+
state = collector(o[variable] ? 1 : 0, state);
|
|
1761
|
+
count++;
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
if (dataCalc.isMissingNumeric(o[variable])) {
|
|
1765
|
+
containsMissing = true;
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
throw new M2Error(
|
|
1769
|
+
`${errorPrefix}: variable ${variable} has non-numeric value ${o[variable]} in this observation: ${JSON.stringify(o)}`
|
|
1770
|
+
);
|
|
1771
|
+
});
|
|
1772
|
+
return { state, count, containsMissing };
|
|
1773
|
+
}
|
|
1774
|
+
function processSingleValue(value, options, errorPrefix) {
|
|
1775
|
+
const mergedOptions = applyDefaultOptions(options);
|
|
1776
|
+
if (typeof value === "number" && !isNaN(value) && isFinite(value)) {
|
|
1777
|
+
return { value, isMissing: false };
|
|
1778
|
+
} else if (typeof value === "boolean" && mergedOptions.coerceBooleans) {
|
|
1779
|
+
return { value: value ? 1 : 0, isMissing: false };
|
|
1780
|
+
} else if (value === null || value === void 0 || typeof value === "number" && (isNaN(value) || !isFinite(value))) {
|
|
1781
|
+
return { value: 0, isMissing: true };
|
|
1782
|
+
} else {
|
|
1783
|
+
throw new M2Error(`${errorPrefix}: has non-numeric value ${value}`);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
const varianceInternal = (dataCalc, params, options) => {
|
|
1787
|
+
let variableOrValues = params ? params[0] : void 0;
|
|
1788
|
+
variableOrValues = resolveLazy(variableOrValues, dataCalc);
|
|
1789
|
+
const mergedOptions = applyDefaultOptions(options);
|
|
1790
|
+
if (typeof variableOrValues === "string") {
|
|
1791
|
+
if (!dataCalc.variableExists(variableOrValues)) {
|
|
1792
|
+
return null;
|
|
1793
|
+
}
|
|
1794
|
+
const variable = variableOrValues;
|
|
1795
|
+
const meanResult = processNumericValues(
|
|
1796
|
+
dataCalc,
|
|
1797
|
+
variable,
|
|
1798
|
+
options,
|
|
1799
|
+
(value, sum2) => sum2 + value,
|
|
1800
|
+
"variance()",
|
|
1801
|
+
0
|
|
1802
|
+
);
|
|
1803
|
+
if (meanResult.containsMissing && !mergedOptions.skipMissing) {
|
|
1804
|
+
return null;
|
|
1805
|
+
}
|
|
1806
|
+
if (meanResult.count <= 1) {
|
|
1807
|
+
return null;
|
|
1808
|
+
}
|
|
1809
|
+
const meanValue = meanResult.state / meanResult.count;
|
|
1810
|
+
const varianceResult = processNumericValues(
|
|
1811
|
+
dataCalc,
|
|
1812
|
+
variable,
|
|
1813
|
+
options,
|
|
1814
|
+
(value, sum2) => {
|
|
1815
|
+
const actualValue = typeof value === "boolean" && mergedOptions.coerceBooleans ? value ? 1 : 0 : value;
|
|
1816
|
+
return sum2 + Math.pow(actualValue - meanValue, 2);
|
|
1817
|
+
},
|
|
1818
|
+
"variance()",
|
|
1819
|
+
0
|
|
1820
|
+
);
|
|
1821
|
+
return varianceResult.state / (meanResult.count - 1);
|
|
1822
|
+
} else if (Array.isArray(variableOrValues)) {
|
|
1823
|
+
const validValues = [];
|
|
1824
|
+
let containsMissing = false;
|
|
1825
|
+
for (const value of variableOrValues) {
|
|
1826
|
+
if (typeof value === "number" && !isNaN(value) && isFinite(value)) {
|
|
1827
|
+
validValues.push(value);
|
|
1828
|
+
} else if (typeof value === "boolean" && mergedOptions.coerceBooleans) {
|
|
1829
|
+
validValues.push(value ? 1 : 0);
|
|
1830
|
+
} else if (value === null || value === void 0 || typeof value === "number" && (isNaN(value) || !isFinite(value))) {
|
|
1831
|
+
containsMissing = true;
|
|
1832
|
+
} else {
|
|
1833
|
+
throw new M2Error(`variance(): has non-numeric value ${value}`);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
if (containsMissing && !mergedOptions.skipMissing) {
|
|
1837
|
+
return null;
|
|
1838
|
+
}
|
|
1839
|
+
if (validValues.length <= 1) {
|
|
1840
|
+
return null;
|
|
1841
|
+
}
|
|
1842
|
+
const sum2 = validValues.reduce((acc, val) => acc + val, 0);
|
|
1843
|
+
const mean2 = sum2 / validValues.length;
|
|
1844
|
+
const sumSquaredDiffs = validValues.reduce(
|
|
1845
|
+
(acc, val) => acc + Math.pow(val - mean2, 2),
|
|
1846
|
+
0
|
|
1847
|
+
);
|
|
1848
|
+
return sumSquaredDiffs / (validValues.length - 1);
|
|
1849
|
+
} else {
|
|
1850
|
+
return null;
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
const medianInternal = (dataCalc, params, options) => {
|
|
1854
|
+
let variableOrValues = params ? params[0] : void 0;
|
|
1855
|
+
variableOrValues = resolveLazy(variableOrValues, dataCalc);
|
|
1856
|
+
const mergedOptions = applyDefaultOptions(options);
|
|
1857
|
+
if (typeof variableOrValues === "string") {
|
|
1858
|
+
if (!dataCalc.variableExists(variableOrValues)) {
|
|
1859
|
+
return null;
|
|
1860
|
+
}
|
|
1861
|
+
const variable = variableOrValues;
|
|
1862
|
+
dataCalc.verifyObservationsContainVariable(variable);
|
|
1863
|
+
const values = [];
|
|
1864
|
+
let containsMissing = false;
|
|
1865
|
+
dataCalc.observations.forEach((o) => {
|
|
1866
|
+
if (dataCalc.isNonMissingNumeric(o[variable])) {
|
|
1867
|
+
values.push(o[variable]);
|
|
1868
|
+
} else if (typeof o[variable] === "boolean" && mergedOptions.coerceBooleans) {
|
|
1869
|
+
values.push(o[variable] ? 1 : 0);
|
|
1870
|
+
} else if (dataCalc.isMissingNumeric(o[variable])) {
|
|
1871
|
+
containsMissing = true;
|
|
1872
|
+
} else {
|
|
1873
|
+
throw new M2Error(
|
|
1874
|
+
`median(): variable ${variable} has non-numeric value ${o[variable]} in this observation: ${JSON.stringify(o)}`
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
});
|
|
1878
|
+
if (containsMissing && !mergedOptions.skipMissing) {
|
|
1879
|
+
return null;
|
|
1880
|
+
}
|
|
1881
|
+
if (values.length === 0) {
|
|
1882
|
+
return null;
|
|
1883
|
+
}
|
|
1884
|
+
values.sort((a, b) => a - b);
|
|
1885
|
+
const mid = Math.floor(values.length / 2);
|
|
1886
|
+
if (values.length % 2 === 0) {
|
|
1887
|
+
return (values[mid - 1] + values[mid]) / 2;
|
|
1888
|
+
} else {
|
|
1889
|
+
return values[mid];
|
|
1890
|
+
}
|
|
1891
|
+
} else if (Array.isArray(variableOrValues)) {
|
|
1892
|
+
const values = [];
|
|
1893
|
+
let containsMissing = false;
|
|
1894
|
+
for (const value of variableOrValues) {
|
|
1895
|
+
if (typeof value === "number" && !isNaN(value) && isFinite(value)) {
|
|
1896
|
+
values.push(value);
|
|
1897
|
+
} else if (typeof value === "boolean" && mergedOptions.coerceBooleans) {
|
|
1898
|
+
values.push(value ? 1 : 0);
|
|
1899
|
+
} else if (value === null || value === void 0 || typeof value === "number" && (isNaN(value) || !isFinite(value))) {
|
|
1900
|
+
containsMissing = true;
|
|
1901
|
+
} else {
|
|
1902
|
+
throw new M2Error(`median(): has non-numeric value ${value}`);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
if (containsMissing && !mergedOptions.skipMissing) {
|
|
1906
|
+
return null;
|
|
1907
|
+
}
|
|
1908
|
+
if (values.length === 0) {
|
|
1909
|
+
return null;
|
|
1910
|
+
}
|
|
1911
|
+
values.sort((a, b) => a - b);
|
|
1912
|
+
const mid = Math.floor(values.length / 2);
|
|
1913
|
+
if (values.length % 2 === 0) {
|
|
1914
|
+
return (values[mid - 1] + values[mid]) / 2;
|
|
1915
|
+
} else {
|
|
1916
|
+
return values[mid];
|
|
1917
|
+
}
|
|
1918
|
+
} else {
|
|
1919
|
+
const result = processSingleValue(variableOrValues, options, "median()");
|
|
1920
|
+
if (result.isMissing && !mergedOptions.skipMissing) {
|
|
1921
|
+
return null;
|
|
1922
|
+
}
|
|
1923
|
+
return result.isMissing ? null : result.value;
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
function median(variableOrValues, options) {
|
|
1927
|
+
return SummarizeOperation.leaf(medianInternal, [variableOrValues], options);
|
|
1928
|
+
}
|
|
1929
|
+
const sdInternal = (dataCalc, params, options) => {
|
|
1930
|
+
let variableOrValues = params ? params[0] : void 0;
|
|
1931
|
+
variableOrValues = resolveLazy(variableOrValues, dataCalc);
|
|
1932
|
+
if (typeof variableOrValues === "string") {
|
|
1933
|
+
if (!dataCalc.variableExists(variableOrValues)) {
|
|
1934
|
+
return null;
|
|
1935
|
+
}
|
|
1936
|
+
const varianceValue = varianceInternal(dataCalc, params, options);
|
|
1937
|
+
if (varianceValue === null) {
|
|
1938
|
+
return null;
|
|
1939
|
+
}
|
|
1940
|
+
return Math.sqrt(varianceValue);
|
|
1941
|
+
} else if (Array.isArray(variableOrValues)) {
|
|
1942
|
+
const newParams = params ? [...params] : [variableOrValues];
|
|
1943
|
+
const varianceValue = varianceInternal(dataCalc, newParams, options);
|
|
1944
|
+
if (varianceValue === null) {
|
|
1945
|
+
return null;
|
|
1946
|
+
}
|
|
1947
|
+
return Math.sqrt(varianceValue);
|
|
1948
|
+
} else {
|
|
1949
|
+
return null;
|
|
1950
|
+
}
|
|
1951
|
+
};
|
|
1952
|
+
function sd(variableOrValues, options) {
|
|
1953
|
+
return SummarizeOperation.leaf(sdInternal, [variableOrValues], options);
|
|
1954
|
+
}
|
|
1955
|
+
const scalarInternal = (_dataCalc, params, options) => {
|
|
1956
|
+
let v = params ? params[0] : void 0;
|
|
1957
|
+
if (typeof v === "function") v = v(_dataCalc);
|
|
1958
|
+
if (typeof v === "string") {
|
|
1959
|
+
const re = /^__CHAIN_EXPR__\[(.*?)\]$/;
|
|
1960
|
+
const m = re.exec(v);
|
|
1961
|
+
if (m) {
|
|
1962
|
+
const payload = m[1];
|
|
1963
|
+
let ops = getChainOps(payload);
|
|
1964
|
+
if (!ops) {
|
|
1965
|
+
try {
|
|
1966
|
+
ops = JSON.parse(decodeURIComponent(payload));
|
|
1967
|
+
} catch {
|
|
1968
|
+
ops = void 0;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
if (ops) {
|
|
1972
|
+
let current = _dataCalc;
|
|
1973
|
+
let evaluated = void 0;
|
|
1974
|
+
for (const op of ops) {
|
|
1975
|
+
const method = current[op.name];
|
|
1976
|
+
if (typeof method !== "function") {
|
|
1977
|
+
evaluated = NaN;
|
|
1978
|
+
break;
|
|
1979
|
+
}
|
|
1980
|
+
const res = method.apply(current, op.args);
|
|
1981
|
+
if (res instanceof DataCalc) {
|
|
1982
|
+
current = res;
|
|
1983
|
+
continue;
|
|
1984
|
+
}
|
|
1985
|
+
if (Array.isArray(res)) {
|
|
1986
|
+
evaluated = res.length;
|
|
1987
|
+
break;
|
|
1988
|
+
}
|
|
1989
|
+
if (typeof res === "boolean") {
|
|
1990
|
+
evaluated = res;
|
|
1991
|
+
break;
|
|
1992
|
+
}
|
|
1993
|
+
evaluated = typeof res === "number" ? res : Number(res);
|
|
1994
|
+
break;
|
|
1995
|
+
}
|
|
1996
|
+
if (evaluated === void 0)
|
|
1997
|
+
evaluated = current instanceof DataCalc ? current.length : NaN;
|
|
1998
|
+
if (!Number.isNaN(evaluated)) v = evaluated;
|
|
1999
|
+
else v = null;
|
|
2000
|
+
try {
|
|
2001
|
+
clearChainOps(payload);
|
|
2002
|
+
} catch {
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
if (typeof v === "object" && v !== null && "summarizeFunction" in v) {
|
|
2008
|
+
try {
|
|
2009
|
+
const op = v;
|
|
2010
|
+
const paramsForOp = Array.isArray(op.parameters) ? op.parameters : op.parameters === void 0 ? void 0 : [op.parameters];
|
|
2011
|
+
const raw = op.summarizeFunction(
|
|
2012
|
+
_dataCalc,
|
|
2013
|
+
paramsForOp,
|
|
2014
|
+
op.options
|
|
2015
|
+
);
|
|
2016
|
+
v = raw;
|
|
2017
|
+
} catch {
|
|
2018
|
+
v = null;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
const result = processSingleValue(v, options, "scalar()");
|
|
2022
|
+
return result.isMissing ? null : result.value;
|
|
2023
|
+
};
|
|
2024
|
+
function scalar(value) {
|
|
2025
|
+
return SummarizeOperation.leaf(scalarInternal, [value], void 0);
|
|
2026
|
+
}
|
|
2027
|
+
console.log("\u26AA @m2c2kit/data-calc version 0.8.7 (38a5862e)");
|
|
1090
2028
|
|
|
1091
2029
|
class ColorShapes extends Game {
|
|
1092
2030
|
constructor() {
|
|
@@ -1199,6 +2137,14 @@ class ColorShapes extends Game {
|
|
|
1199
2137
|
type: "boolean",
|
|
1200
2138
|
default: false,
|
|
1201
2139
|
description: "Should scoring data be generated? Default is false."
|
|
2140
|
+
},
|
|
2141
|
+
scoring_filter_response_time_duration_ms: {
|
|
2142
|
+
type: "array",
|
|
2143
|
+
items: {
|
|
2144
|
+
type: "number"
|
|
2145
|
+
},
|
|
2146
|
+
default: [100, 1e4],
|
|
2147
|
+
description: "When scoring, values of response_time_duration_ms less than the lower bound or greater than the upper bound are discarded. This array contains two numbers, the lower and upper bounds."
|
|
1202
2148
|
}
|
|
1203
2149
|
};
|
|
1204
2150
|
const colorShapesTrialSchema = {
|
|
@@ -1331,6 +2277,14 @@ class ColorShapes extends Game {
|
|
|
1331
2277
|
format: "date-time",
|
|
1332
2278
|
description: "ISO 8601 timestamp at the end of the last trial. Null if no trials were completed."
|
|
1333
2279
|
},
|
|
2280
|
+
response_time_filter_lower_bound: {
|
|
2281
|
+
type: "number",
|
|
2282
|
+
description: "Response times less than this lower bound were discarded when calculating filtered response times."
|
|
2283
|
+
},
|
|
2284
|
+
response_time_filter_upper_bound: {
|
|
2285
|
+
type: "number",
|
|
2286
|
+
description: "Response times greater than this upper bound were discarded when calculating filtered response times."
|
|
2287
|
+
},
|
|
1334
2288
|
n_trials: {
|
|
1335
2289
|
type: "integer",
|
|
1336
2290
|
description: "Number of trials completed."
|
|
@@ -1350,6 +2304,158 @@ class ColorShapes extends Game {
|
|
|
1350
2304
|
participant_score: {
|
|
1351
2305
|
type: ["number", "null"],
|
|
1352
2306
|
description: "Participant-facing score, calculated as (number of correct trials / number of trials attempted) * 100. This is a simple metric to provide feedback to the participant. Null if no trials attempted."
|
|
2307
|
+
},
|
|
2308
|
+
flag_trials_lt_expected: {
|
|
2309
|
+
type: "number",
|
|
2310
|
+
description: "Is the number of completed trials fewer than expected? 1 = true, 0 = false."
|
|
2311
|
+
},
|
|
2312
|
+
flag_trials_gt_expected: {
|
|
2313
|
+
type: "number",
|
|
2314
|
+
description: "Is the number of completed trials greater than expected? 1 = true, 0 = false."
|
|
2315
|
+
},
|
|
2316
|
+
n_trials_HIT: {
|
|
2317
|
+
type: "number",
|
|
2318
|
+
description: "Number of HIT trials (correctly identified a 'different' trial)"
|
|
2319
|
+
},
|
|
2320
|
+
n_trials_MISS: {
|
|
2321
|
+
type: "number",
|
|
2322
|
+
description: "Number of MISS trials (failed to detect a 'different' trial, responded 'same')"
|
|
2323
|
+
},
|
|
2324
|
+
n_trials_FA: {
|
|
2325
|
+
type: "number",
|
|
2326
|
+
description: "Number of False Alarm trials (incorrectly responded 'different' on a 'same' trial)"
|
|
2327
|
+
},
|
|
2328
|
+
n_trials_CR: {
|
|
2329
|
+
type: "number",
|
|
2330
|
+
description: "Number of Correct Rejection trials (correctly responded 'same' on a 'same' trial)"
|
|
2331
|
+
},
|
|
2332
|
+
n_trials_type_same: {
|
|
2333
|
+
type: "number",
|
|
2334
|
+
description: "Number of trials where the true signal type was 'SAME' (presented and response shapes were identical)"
|
|
2335
|
+
},
|
|
2336
|
+
n_trials_type_different: {
|
|
2337
|
+
type: "number",
|
|
2338
|
+
description: "Number of trials where the true signal type was 'DIFFERENT' (response shapes differed from presented)"
|
|
2339
|
+
},
|
|
2340
|
+
HIT_rate: {
|
|
2341
|
+
type: ["number", "null"],
|
|
2342
|
+
description: "Proportion of 'different' trials correctly identified as different. Calculated as n_trials_HIT / n_trials_type_different"
|
|
2343
|
+
},
|
|
2344
|
+
MISS_rate: {
|
|
2345
|
+
type: ["number", "null"],
|
|
2346
|
+
description: "Proportion of 'different' trials incorrectly identified as same. Calculated as 1 - HIT_rate"
|
|
2347
|
+
},
|
|
2348
|
+
FA_rate: {
|
|
2349
|
+
type: ["number", "null"],
|
|
2350
|
+
description: "Proportion of 'same' trials incorrectly identified as different. Calculated as n_trials_FA / n_trials_type_same"
|
|
2351
|
+
},
|
|
2352
|
+
CR_rate: {
|
|
2353
|
+
type: ["number", "null"],
|
|
2354
|
+
description: "Proportion of 'same' trials correctly identified as same. Calculated as 1 - FA_rate"
|
|
2355
|
+
},
|
|
2356
|
+
n_trials_rt_invalid: {
|
|
2357
|
+
type: "number",
|
|
2358
|
+
description: "Number of trials with null or non-positive response times (excluded from RT calculations)"
|
|
2359
|
+
},
|
|
2360
|
+
median_rt_overall_valid: {
|
|
2361
|
+
type: ["number", "null"],
|
|
2362
|
+
description: "Median response time (ms) across all trials with valid (non-null, positive) response times. No outlier filtering applied"
|
|
2363
|
+
},
|
|
2364
|
+
sd_rt_overall_valid: {
|
|
2365
|
+
type: ["number", "null"],
|
|
2366
|
+
description: "Standard deviation of response time (ms) across all trials with valid response times. No outlier filtering applied"
|
|
2367
|
+
},
|
|
2368
|
+
median_rt_HIT_valid: {
|
|
2369
|
+
type: ["number", "null"],
|
|
2370
|
+
description: "Median response time (ms) for HIT trials with valid response times. No outlier filtering"
|
|
2371
|
+
},
|
|
2372
|
+
sd_rt_HIT_valid: {
|
|
2373
|
+
type: ["number", "null"],
|
|
2374
|
+
description: "Standard deviation of response time (ms) for HIT trials with valid response times. No outlier filtering"
|
|
2375
|
+
},
|
|
2376
|
+
median_rt_MISS_valid: {
|
|
2377
|
+
type: ["number", "null"],
|
|
2378
|
+
description: "Median response time (ms) for MISS trials with valid response times. No outlier filtering"
|
|
2379
|
+
},
|
|
2380
|
+
sd_rt_MISS_valid: {
|
|
2381
|
+
type: ["number", "null"],
|
|
2382
|
+
description: "Standard deviation of response time (ms) for MISS trials with valid response times. No outlier filtering"
|
|
2383
|
+
},
|
|
2384
|
+
median_rt_FA_valid: {
|
|
2385
|
+
type: ["number", "null"],
|
|
2386
|
+
description: "Median response time (ms) for False Alarm trials with valid response times. No outlier filtering"
|
|
2387
|
+
},
|
|
2388
|
+
sd_rt_FA_valid: {
|
|
2389
|
+
type: ["number", "null"],
|
|
2390
|
+
description: "Standard deviation of response time (ms) for False Alarm trials with valid response times. No outlier filtering"
|
|
2391
|
+
},
|
|
2392
|
+
median_rt_CR_valid: {
|
|
2393
|
+
type: ["number", "null"],
|
|
2394
|
+
description: "Median response time (ms) for Correct Rejection trials with valid response times. No outlier filtering"
|
|
2395
|
+
},
|
|
2396
|
+
sd_rt_CR_valid: {
|
|
2397
|
+
type: ["number", "null"],
|
|
2398
|
+
description: "Standard deviation of response time (ms) for Correct Rejection trials with valid response times. No outlier filtering"
|
|
2399
|
+
},
|
|
2400
|
+
median_rt_overall_valid_filtered: {
|
|
2401
|
+
type: ["number", "null"],
|
|
2402
|
+
description: "Median response time (ms) across all valid trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2403
|
+
},
|
|
2404
|
+
sd_rt_overall_valid_filtered: {
|
|
2405
|
+
type: ["number", "null"],
|
|
2406
|
+
description: "Standard deviation of response time (ms) across all valid trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2407
|
+
},
|
|
2408
|
+
n_outliers_rt_overall_valid: {
|
|
2409
|
+
type: "number",
|
|
2410
|
+
description: "Number of valid trials removed as RT outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2411
|
+
},
|
|
2412
|
+
median_rt_HIT_valid_filtered: {
|
|
2413
|
+
type: ["number", "null"],
|
|
2414
|
+
description: "Median response time (ms) for HIT trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2415
|
+
},
|
|
2416
|
+
sd_rt_HIT_valid_filtered: {
|
|
2417
|
+
type: ["number", "null"],
|
|
2418
|
+
description: "Standard deviation of response time (ms) for HIT trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2419
|
+
},
|
|
2420
|
+
n_outliers_rt_HIT_valid: {
|
|
2421
|
+
type: "number",
|
|
2422
|
+
description: "Number of HIT trials removed as RT outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2423
|
+
},
|
|
2424
|
+
median_rt_MISS_valid_filtered: {
|
|
2425
|
+
type: ["number", "null"],
|
|
2426
|
+
description: "Median response time (ms) for MISS trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2427
|
+
},
|
|
2428
|
+
sd_rt_MISS_valid_filtered: {
|
|
2429
|
+
type: ["number", "null"],
|
|
2430
|
+
description: "Standard deviation of response time (ms) for MISS trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2431
|
+
},
|
|
2432
|
+
n_outliers_rt_MISS_valid: {
|
|
2433
|
+
type: "number",
|
|
2434
|
+
description: "Number of MISS trials removed as RT outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2435
|
+
},
|
|
2436
|
+
median_rt_FA_valid_filtered: {
|
|
2437
|
+
type: ["number", "null"],
|
|
2438
|
+
description: "Median response time (ms) for False Alarm trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2439
|
+
},
|
|
2440
|
+
sd_rt_FA_valid_filtered: {
|
|
2441
|
+
type: ["number", "null"],
|
|
2442
|
+
description: "Standard deviation of response time (ms) for False Alarm trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2443
|
+
},
|
|
2444
|
+
n_outliers_rt_FA_valid: {
|
|
2445
|
+
type: "number",
|
|
2446
|
+
description: "Number of False Alarm trials removed as RT outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2447
|
+
},
|
|
2448
|
+
median_rt_CR_valid_filtered: {
|
|
2449
|
+
type: ["number", "null"],
|
|
2450
|
+
description: "Median response time (ms) for Correct Rejection trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2451
|
+
},
|
|
2452
|
+
sd_rt_CR_valid_filtered: {
|
|
2453
|
+
type: ["number", "null"],
|
|
2454
|
+
description: "Standard deviation of response time (ms) for Correct Rejection trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2455
|
+
},
|
|
2456
|
+
n_outliers_rt_CR_valid: {
|
|
2457
|
+
type: ["number", "null"],
|
|
2458
|
+
description: "Number of Correct Rejection trials removed as RT outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
1353
2459
|
}
|
|
1354
2460
|
};
|
|
1355
2461
|
const translation = {
|
|
@@ -1416,8 +2522,8 @@ class ColorShapes extends Game {
|
|
|
1416
2522
|
*/
|
|
1417
2523
|
id: "color-shapes",
|
|
1418
2524
|
publishUuid: "394cb010-2ccf-4a87-9d23-cda7fb07a960",
|
|
1419
|
-
version: "0.8.
|
|
1420
|
-
moduleMetadata: { "name": "@m2c2kit/assessment-color-shapes", "version": "0.8.
|
|
2525
|
+
version: "0.8.34 (38a5862e)",
|
|
2526
|
+
moduleMetadata: { "name": "@m2c2kit/assessment-color-shapes", "version": "0.8.34", "dependencies": { "@m2c2kit/addons": "0.3.35", "@m2c2kit/core": "0.3.36", "@m2c2kit/data-calc": "0.8.7" } },
|
|
1421
2527
|
translation,
|
|
1422
2528
|
shortDescription: "Color Shapes is a visual array change detection task, measuring intra-item feature binding, where participants determine if shapes change color across two sequential presentations of shape stimuli.",
|
|
1423
2529
|
longDescription: `Color Shapes is a change detection paradigm used to measure visual short-term memory binding (Parra et al., 2009). Participants are asked to memorize the shapes and colors of three different polygons for 3 seconds. The three polygons are then removed from the screen and re-displayed at different locations, either having the same or different colors. Participants are then asked to decide whether the combination of colors and shapes are the "Same" or "Different" between the study and test phases.`,
|
|
@@ -1495,6 +2601,12 @@ class ColorShapes extends Game {
|
|
|
1495
2601
|
game.trialComplete();
|
|
1496
2602
|
if (game.getParameter("scoring")) {
|
|
1497
2603
|
const scores = game.calculateScores([], {
|
|
2604
|
+
rtLowerBound: game.getParameter(
|
|
2605
|
+
"scoring_filter_response_time_duration_ms"
|
|
2606
|
+
)[0],
|
|
2607
|
+
rtUpperBound: game.getParameter(
|
|
2608
|
+
"scoring_filter_response_time_duration_ms"
|
|
2609
|
+
)[1],
|
|
1498
2610
|
numberOfTrials: game.getParameter("number_of_trials")
|
|
1499
2611
|
});
|
|
1500
2612
|
game.addScoringData(scores);
|
|
@@ -1917,6 +3029,12 @@ class ColorShapes extends Game {
|
|
|
1917
3029
|
} else {
|
|
1918
3030
|
if (game.getParameter("scoring")) {
|
|
1919
3031
|
const scores = game.calculateScores(game.data.trials, {
|
|
3032
|
+
rtLowerBound: game.getParameter(
|
|
3033
|
+
"scoring_filter_response_time_duration_ms"
|
|
3034
|
+
)[0],
|
|
3035
|
+
rtUpperBound: game.getParameter(
|
|
3036
|
+
"scoring_filter_response_time_duration_ms"
|
|
3037
|
+
)[1],
|
|
1920
3038
|
numberOfTrials: game.getParameter("number_of_trials")
|
|
1921
3039
|
});
|
|
1922
3040
|
game.addScoringData(scores);
|
|
@@ -1960,18 +3078,198 @@ class ColorShapes extends Game {
|
|
|
1960
3078
|
}
|
|
1961
3079
|
calculateScores(data, extras) {
|
|
1962
3080
|
const dc = new DataCalc(data);
|
|
1963
|
-
const
|
|
3081
|
+
const n_trials = dc.filter((o) => o.quit_button_pressed === false).length;
|
|
3082
|
+
const scores = dc.mutate({
|
|
3083
|
+
metric_accuracy: (obs) => {
|
|
3084
|
+
const ur = obs.user_response;
|
|
3085
|
+
const uc = obs.user_response_correct;
|
|
3086
|
+
if (ur === "same" && uc === true) return "CR";
|
|
3087
|
+
if (ur === "same" && uc === false) return "MISS";
|
|
3088
|
+
if (ur === "different" && uc === true) return "HIT";
|
|
3089
|
+
if (ur === "different" && uc === false) return "FA";
|
|
3090
|
+
return null;
|
|
3091
|
+
},
|
|
3092
|
+
metric_trial_type: (obs) => {
|
|
3093
|
+
const ur = obs.user_response;
|
|
3094
|
+
const uc = obs.user_response_correct;
|
|
3095
|
+
if (ur === "same" && uc === true) return "SAME";
|
|
3096
|
+
if (ur === "same" && uc === false) return "DIFFERENT";
|
|
3097
|
+
if (ur === "different" && uc === true) return "DIFFERENT";
|
|
3098
|
+
if (ur === "different" && uc === false) return "SAME";
|
|
3099
|
+
return null;
|
|
3100
|
+
},
|
|
3101
|
+
is_valid: (obs) => typeof obs.response_time_duration_ms === "number" && obs.response_time_duration_ms > 0
|
|
3102
|
+
}).summarize({
|
|
1964
3103
|
activity_begin_iso8601_timestamp: this.beginIso8601Timestamp,
|
|
1965
|
-
first_trial_begin_iso8601_timestamp:
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
3104
|
+
first_trial_begin_iso8601_timestamp: arrange(
|
|
3105
|
+
"trial_begin_iso8601_timestamp"
|
|
3106
|
+
).slice(0).pull("trial_begin_iso8601_timestamp"),
|
|
3107
|
+
last_trial_end_iso8601_timestamp: arrange(
|
|
3108
|
+
"-trial_end_iso8601_timestamp"
|
|
3109
|
+
).slice(0).pull("trial_end_iso8601_timestamp"),
|
|
3110
|
+
n_trials,
|
|
3111
|
+
flag_trials_match_expected: n_trials === extras.numberOfTrials ? 1 : 0,
|
|
3112
|
+
flag_trials_lt_expected: n_trials < extras.numberOfTrials ? 1 : 0,
|
|
3113
|
+
flag_trials_gt_expected: n_trials > extras.numberOfTrials ? 1 : 0,
|
|
3114
|
+
// counts by accuracy
|
|
3115
|
+
n_trials_HIT: filter((o) => o.metric_accuracy === "HIT").length,
|
|
3116
|
+
n_trials_MISS: filter((o) => o.metric_accuracy === "MISS").length,
|
|
3117
|
+
n_trials_FA: filter((o) => o.metric_accuracy === "FA").length,
|
|
3118
|
+
n_trials_CR: filter((o) => o.metric_accuracy === "CR").length,
|
|
3119
|
+
// trial types
|
|
3120
|
+
n_trials_type_same: filter((o) => o.metric_trial_type === "SAME").length,
|
|
3121
|
+
n_trials_type_different: filter(
|
|
3122
|
+
(o) => o.metric_trial_type === "DIFFERENT"
|
|
3123
|
+
).length,
|
|
3124
|
+
// Note: When the summarize operation is a custom function,
|
|
3125
|
+
// () => value, rather than a built in function like sum or mean,
|
|
3126
|
+
// we must provide the DataCalc object as an argument to the
|
|
3127
|
+
// function, and do the filtering and pulling of values on that
|
|
3128
|
+
// object within the function. Specifically below, we must do:
|
|
3129
|
+
// const denom = d.filter(...).length
|
|
3130
|
+
// NOT
|
|
3131
|
+
// const denom = filter(...).length
|
|
3132
|
+
// rates
|
|
3133
|
+
HIT_rate: (d) => {
|
|
3134
|
+
const denom = d.filter(
|
|
3135
|
+
(o) => o.metric_trial_type === "DIFFERENT"
|
|
3136
|
+
).length;
|
|
3137
|
+
return denom > 0 ? d.filter((o) => o.metric_accuracy === "HIT").length / denom : null;
|
|
3138
|
+
},
|
|
3139
|
+
MISS_rate: (d) => {
|
|
3140
|
+
const denom = d.filter(
|
|
3141
|
+
(o) => o.metric_trial_type === "DIFFERENT"
|
|
3142
|
+
).length;
|
|
3143
|
+
return denom > 0 ? 1 - d.filter((o) => o.metric_accuracy === "HIT").length / denom : null;
|
|
3144
|
+
},
|
|
3145
|
+
FA_rate: (d) => {
|
|
3146
|
+
const denom = d.filter((o) => o.metric_trial_type === "SAME").length;
|
|
3147
|
+
return denom > 0 ? d.filter((o) => o.metric_accuracy === "FA").length / denom : null;
|
|
3148
|
+
},
|
|
3149
|
+
CR_rate: (d) => {
|
|
3150
|
+
const denom = d.filter((o) => o.metric_trial_type === "SAME").length;
|
|
3151
|
+
return denom > 0 ? 1 - d.filter((o) => o.metric_accuracy === "FA").length / denom : null;
|
|
3152
|
+
},
|
|
3153
|
+
// response time summaries
|
|
3154
|
+
// overall valid RTs (>0)
|
|
3155
|
+
median_rt_overall_valid: median(
|
|
3156
|
+
filter((o) => o.is_valid).pull("response_time_duration_ms")
|
|
3157
|
+
),
|
|
3158
|
+
sd_rt_overall_valid: sd(
|
|
3159
|
+
filter((o) => o.is_valid).pull("response_time_duration_ms")
|
|
3160
|
+
),
|
|
3161
|
+
// filtered overall RTs
|
|
3162
|
+
median_rt_overall_valid_filtered: median(
|
|
3163
|
+
filter((o) => o.is_valid).filter(
|
|
3164
|
+
(o) => o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3165
|
+
).pull("response_time_duration_ms")
|
|
3166
|
+
),
|
|
3167
|
+
sd_rt_overall_valid_filtered: sd(
|
|
3168
|
+
filter((o) => o.is_valid).filter(
|
|
3169
|
+
(o) => o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3170
|
+
).pull("response_time_duration_ms")
|
|
3171
|
+
),
|
|
3172
|
+
// counts of invalid/outliers
|
|
3173
|
+
n_trials_rt_invalid: filter((o) => !o.is_valid).length,
|
|
3174
|
+
n_outliers_rt_overall_valid: filter((o) => o.is_valid).filter(
|
|
3175
|
+
(o) => o.response_time_duration_ms < extras.rtLowerBound || o.response_time_duration_ms > extras.rtUpperBound
|
|
3176
|
+
).length,
|
|
3177
|
+
response_time_filter_lower_bound: extras.rtLowerBound,
|
|
3178
|
+
response_time_filter_upper_bound: extras.rtUpperBound,
|
|
3179
|
+
// per-metric RT summaries
|
|
3180
|
+
median_rt_HIT_valid: median(
|
|
3181
|
+
filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "HIT").pull("response_time_duration_ms")
|
|
3182
|
+
),
|
|
3183
|
+
sd_rt_HIT_valid: sd(
|
|
3184
|
+
filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "HIT").pull("response_time_duration_ms")
|
|
3185
|
+
),
|
|
3186
|
+
median_rt_HIT_valid_filtered: median(
|
|
3187
|
+
filter((o) => o.is_valid).filter(
|
|
3188
|
+
(o) => o.metric_accuracy === "HIT" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3189
|
+
).pull("response_time_duration_ms")
|
|
3190
|
+
),
|
|
3191
|
+
sd_rt_HIT_valid_filtered: sd(
|
|
3192
|
+
filter((o) => o.is_valid).filter(
|
|
3193
|
+
(o) => o.metric_accuracy === "HIT" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3194
|
+
).pull("response_time_duration_ms")
|
|
3195
|
+
),
|
|
3196
|
+
n_outliers_rt_HIT_valid: filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "HIT").filter(
|
|
3197
|
+
(o) => o.response_time_duration_ms < extras.rtLowerBound || o.response_time_duration_ms > extras.rtUpperBound
|
|
3198
|
+
).length,
|
|
3199
|
+
median_rt_MISS_valid: median(
|
|
3200
|
+
filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "MISS").pull("response_time_duration_ms")
|
|
3201
|
+
),
|
|
3202
|
+
sd_rt_MISS_valid: sd(
|
|
3203
|
+
filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "MISS").pull("response_time_duration_ms")
|
|
3204
|
+
),
|
|
3205
|
+
median_rt_MISS_valid_filtered: median(
|
|
3206
|
+
filter((o) => o.is_valid).filter(
|
|
3207
|
+
(o) => o.metric_accuracy === "MISS" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3208
|
+
).pull("response_time_duration_ms")
|
|
3209
|
+
),
|
|
3210
|
+
sd_rt_MISS_valid_filtered: sd(
|
|
3211
|
+
filter((o) => o.is_valid).filter(
|
|
3212
|
+
(o) => o.metric_accuracy === "MISS" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3213
|
+
).pull("response_time_duration_ms")
|
|
3214
|
+
),
|
|
3215
|
+
n_outliers_rt_MISS_valid: filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "MISS").filter(
|
|
3216
|
+
(o) => o.response_time_duration_ms < extras.rtLowerBound || o.response_time_duration_ms > extras.rtUpperBound
|
|
3217
|
+
).length,
|
|
3218
|
+
median_rt_FA_valid: median(
|
|
3219
|
+
filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "FA").pull("response_time_duration_ms")
|
|
3220
|
+
),
|
|
3221
|
+
sd_rt_FA_valid: sd(
|
|
3222
|
+
filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "FA").pull("response_time_duration_ms")
|
|
3223
|
+
),
|
|
3224
|
+
median_rt_FA_valid_filtered: median(
|
|
3225
|
+
filter((o) => o.is_valid).filter(
|
|
3226
|
+
(o) => o.metric_accuracy === "FA" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3227
|
+
).pull("response_time_duration_ms")
|
|
3228
|
+
),
|
|
3229
|
+
sd_rt_FA_valid_filtered: sd(
|
|
3230
|
+
filter((o) => o.is_valid).filter(
|
|
3231
|
+
(o) => o.metric_accuracy === "FA" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3232
|
+
).pull("response_time_duration_ms")
|
|
3233
|
+
),
|
|
3234
|
+
n_outliers_rt_FA_valid: filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "FA").filter(
|
|
3235
|
+
(o) => o.response_time_duration_ms < extras.rtLowerBound || o.response_time_duration_ms > extras.rtUpperBound
|
|
3236
|
+
).length,
|
|
3237
|
+
median_rt_CR_valid: median(
|
|
3238
|
+
filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "CR").pull("response_time_duration_ms")
|
|
3239
|
+
),
|
|
3240
|
+
sd_rt_CR_valid: sd(
|
|
3241
|
+
filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "CR").pull("response_time_duration_ms")
|
|
3242
|
+
),
|
|
3243
|
+
median_rt_CR_valid_filtered: median(
|
|
3244
|
+
filter((o) => o.is_valid).filter(
|
|
3245
|
+
(o) => o.metric_accuracy === "CR" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3246
|
+
).pull("response_time_duration_ms")
|
|
3247
|
+
),
|
|
3248
|
+
sd_rt_CR_valid_filtered: sd(
|
|
3249
|
+
filter((o) => o.is_valid).filter(
|
|
3250
|
+
(o) => o.metric_accuracy === "CR" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3251
|
+
).pull("response_time_duration_ms")
|
|
3252
|
+
),
|
|
3253
|
+
n_outliers_rt_CR_valid: filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "CR").filter(
|
|
3254
|
+
(o) => o.response_time_duration_ms < extras.rtLowerBound || o.response_time_duration_ms > extras.rtUpperBound
|
|
3255
|
+
).length,
|
|
3256
|
+
n_trials_correct: filter(
|
|
3257
|
+
(o) => o.metric_accuracy === "HIT" || o.metric_accuracy === "CR"
|
|
3258
|
+
).length,
|
|
3259
|
+
n_trials_incorrect: filter(
|
|
3260
|
+
(o) => o.metric_accuracy === "MISS" || o.metric_accuracy === "FA"
|
|
3261
|
+
).length,
|
|
3262
|
+
participant_score: (d) => {
|
|
3263
|
+
const total = d.length;
|
|
3264
|
+
if (total === 0) return null;
|
|
3265
|
+
return d.summarize({
|
|
3266
|
+
correct: parens(
|
|
3267
|
+
scalar(d.filter((o) => o.metric_accuracy === "HIT").length).add(
|
|
3268
|
+
scalar(d.filter((o) => o.metric_accuracy === "CR").length)
|
|
3269
|
+
)
|
|
3270
|
+
).div(scalar(total)).mul(scalar(100))
|
|
3271
|
+
}).pull("correct");
|
|
3272
|
+
}
|
|
1975
3273
|
});
|
|
1976
3274
|
return scores.observations;
|
|
1977
3275
|
}
|