@qe-libs/rena-wasm 0.1.7 → 0.1.8
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/README.md +20 -0
- package/package.json +1 -1
- package/src/data.js +15 -4
- package/src/index.js +14 -9
- package/src/tensor.js +4 -1
package/README.md
CHANGED
|
@@ -91,6 +91,26 @@ const model = ena.fit(rows, { codes, units, conversations, window: 4, weightMode
|
|
|
91
91
|
|
|
92
92
|
---
|
|
93
93
|
|
|
94
|
+
## Modeling a Subset of Units
|
|
95
|
+
|
|
96
|
+
`unitsUsed` (= R's `units.used`) lists the units to model, as unit keys (the
|
|
97
|
+
unit column values joined with `__`, as in `model.unitLabels`). Every row
|
|
98
|
+
stays in the data: rows of other units still count as context in the modeled
|
|
99
|
+
units' windows, but those units get no network and never reach sphere
|
|
100
|
+
normalization, centering or rotation. Omit it to model every unit.
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
// Students' connections to what teachers said are kept; teachers aren't units.
|
|
104
|
+
const model = ena.fit(rows, { codes, units: ['Role', 'Name'], conversations,
|
|
105
|
+
window: 4, unitsUsed: studentKeys });
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`unitsUsed` works in `fit()`, `accumulate()`, `tuneWindowSize()` and `pria()`.
|
|
109
|
+
This differs from dropping the other units' rows, which also removes them from
|
|
110
|
+
everyone's windows.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
94
114
|
## Reduced-Code Search (PRIA)
|
|
95
115
|
|
|
96
116
|
`pria()` finds the largest set of codes (up to `removeNum`, never leaving
|
package/package.json
CHANGED
package/src/data.js
CHANGED
|
@@ -24,6 +24,10 @@ export function rowKey(row, cols) {
|
|
|
24
24
|
* @param {string[]} codes - Code column names
|
|
25
25
|
* @param {string[]} unitCols - Column(s) that identify a unit (e.g. ['UserName', 'Condition'])
|
|
26
26
|
* @param {string[]} convoCols - Column(s) that identify a conversation (e.g. ['Condition', 'GroupName'])
|
|
27
|
+
* @param {string[]|null} [unitsUsed=null] - Unit keys (rowKey format) to model
|
|
28
|
+
* (= R's `units.used`). Rows of other units stay in their conversations, so
|
|
29
|
+
* they still count as context in the included units' windows, but they get
|
|
30
|
+
* no unit (unitOf = -1) and so produce no network. null = every unit.
|
|
27
31
|
*
|
|
28
32
|
* @returns {{
|
|
29
33
|
* codeMatrix: Float64Array, // n_rows × n_codes, row-major
|
|
@@ -31,14 +35,14 @@ export function rowKey(row, cols) {
|
|
|
31
35
|
* nCodes: number,
|
|
32
36
|
* nUnits: number,
|
|
33
37
|
* unitLabels: string[], // unit key for each unit index
|
|
34
|
-
* unitOf: Int32Array, // unit index for each row
|
|
38
|
+
* unitOf: Int32Array, // unit index for each row (-1 = not a modeled unit)
|
|
35
39
|
* convoOf: Int32Array, // conversation index for each row
|
|
36
40
|
* convoGroups: Map<number, number[]>, // convoIdx → [rowIdx, ...]
|
|
37
41
|
* metaData: Object[], // one metadata object per unit (first-row representative)
|
|
38
42
|
* metaCols: string[], // names of metadata columns
|
|
39
43
|
* }}
|
|
40
44
|
*/
|
|
41
|
-
export function parseData(rows, codes, unitCols, convoCols) {
|
|
45
|
+
export function parseData(rows, codes, unitCols, convoCols, unitsUsed = null) {
|
|
42
46
|
const nRows = rows.length;
|
|
43
47
|
const nCodes = codes.length;
|
|
44
48
|
|
|
@@ -58,6 +62,8 @@ export function parseData(rows, codes, unitCols, convoCols) {
|
|
|
58
62
|
// Per-unit metadata rows — populated on first occurrence of each unit.
|
|
59
63
|
const metaData = [];
|
|
60
64
|
|
|
65
|
+
const used = unitsUsed ? new Set(unitsUsed.map(String)) : null;
|
|
66
|
+
|
|
61
67
|
for (let i = 0; i < nRows; i++) {
|
|
62
68
|
const row = rows[i];
|
|
63
69
|
|
|
@@ -68,7 +74,9 @@ export function parseData(rows, codes, unitCols, convoCols) {
|
|
|
68
74
|
|
|
69
75
|
// Unit index
|
|
70
76
|
const uKey = rowKey(row, unitCols);
|
|
71
|
-
if (!
|
|
77
|
+
if (used && !used.has(uKey)) {
|
|
78
|
+
unitOf[i] = -1;
|
|
79
|
+
} else if (!unitIndex.has(uKey)) {
|
|
72
80
|
const uIdx = unitIndex.size;
|
|
73
81
|
unitIndex.set(uKey, uIdx);
|
|
74
82
|
// First row for this unit → representative metadata
|
|
@@ -76,7 +84,7 @@ export function parseData(rows, codes, unitCols, convoCols) {
|
|
|
76
84
|
for (const col of metaCols) meta[col] = row[col];
|
|
77
85
|
metaData[uIdx] = meta;
|
|
78
86
|
}
|
|
79
|
-
unitOf[i] = unitIndex.get(uKey);
|
|
87
|
+
if (unitOf[i] !== -1) unitOf[i] = unitIndex.get(uKey);
|
|
80
88
|
|
|
81
89
|
// Conversation index
|
|
82
90
|
const cKey = rowKey(row, convoCols);
|
|
@@ -90,6 +98,9 @@ export function parseData(rows, codes, unitCols, convoCols) {
|
|
|
90
98
|
|
|
91
99
|
const unitLabels = Array.from(unitIndex.keys());
|
|
92
100
|
const nUnits = unitLabels.length;
|
|
101
|
+
if (used && nUnits === 0) {
|
|
102
|
+
throw new Error('opts.unitsUsed matched no units in the data');
|
|
103
|
+
}
|
|
93
104
|
|
|
94
105
|
return { codeMatrix, nRows, nCodes, nUnits, unitLabels, unitOf, convoOf, convoGroups,
|
|
95
106
|
metaData, metaCols };
|
package/src/index.js
CHANGED
|
@@ -407,6 +407,8 @@ export default async function loadENA() {
|
|
|
407
407
|
* @param {number[]} [opts.groupB] - Unit indices for means rotation group B
|
|
408
408
|
* @param {object} [opts.gParams] - Pre-built GMR parameters for 'generalized' rotation
|
|
409
409
|
* @param {number} [opts.dims=2] - Number of dimensions to return
|
|
410
|
+
* @param {string[]} [opts.unitsUsed] - Unit keys to model (= R's units.used);
|
|
411
|
+
* other units' rows remain as context
|
|
410
412
|
*
|
|
411
413
|
* @returns {ENAModel}
|
|
412
414
|
*/
|
|
@@ -426,6 +428,7 @@ export default async function loadENA() {
|
|
|
426
428
|
dims = 2,
|
|
427
429
|
codeMask,
|
|
428
430
|
weightModel,
|
|
431
|
+
unitsUsed,
|
|
429
432
|
} = opts;
|
|
430
433
|
|
|
431
434
|
if (!codes?.length) throw new Error('opts.codes is required');
|
|
@@ -442,7 +445,7 @@ export default async function loadENA() {
|
|
|
442
445
|
|
|
443
446
|
const { codeMatrix, nRows, nCodes, nUnits, unitLabels,
|
|
444
447
|
unitOf, convoGroups, metaData } =
|
|
445
|
-
parseData(rows, codes, units, conversations);
|
|
448
|
+
parseData(rows, codes, units, conversations, unitsUsed);
|
|
446
449
|
|
|
447
450
|
// Every model accumulates through the tensor path; a plain moving
|
|
448
451
|
// window is expressed as defaultTensor(window).
|
|
@@ -486,11 +489,12 @@ export default async function loadENA() {
|
|
|
486
489
|
tensor: tensorDef,
|
|
487
490
|
codeMask,
|
|
488
491
|
weightModel,
|
|
492
|
+
unitsUsed,
|
|
489
493
|
} = opts;
|
|
490
494
|
|
|
491
495
|
const { codeMatrix, nRows, nCodes, nUnits, unitLabels,
|
|
492
496
|
unitOf, convoGroups, metaData } =
|
|
493
|
-
parseData(rows, codes, units, conversations);
|
|
497
|
+
parseData(rows, codes, units, conversations, unitsUsed);
|
|
494
498
|
|
|
495
499
|
const { networks, rowConnectionCounts } = accumulateTensor(
|
|
496
500
|
qe, rows, codeMatrix, nRows, nCodes, nUnits,
|
|
@@ -509,7 +513,7 @@ export default async function loadENA() {
|
|
|
509
513
|
// (= R's ENAAccumulation$`_function.call`).
|
|
510
514
|
_call: { rows, codes, units, conversations,
|
|
511
515
|
window: windowSize, binary, ordered, tensor: tensorDef,
|
|
512
|
-
weightModel },
|
|
516
|
+
weightModel, unitsUsed },
|
|
513
517
|
};
|
|
514
518
|
},
|
|
515
519
|
|
|
@@ -547,12 +551,12 @@ export default async function loadENA() {
|
|
|
547
551
|
'maxSize must be greater than minSize to compare windows.'
|
|
548
552
|
);
|
|
549
553
|
|
|
550
|
-
const { rows, codes, units, conversations, binary, ordered, weightModel } = call;
|
|
554
|
+
const { rows, codes, units, conversations, binary, ordered, weightModel, unitsUsed } = call;
|
|
551
555
|
|
|
552
556
|
// Parse once; only the window size changes between iterations.
|
|
553
557
|
const { codeMatrix, nRows, nCodes, nUnits, unitLabels,
|
|
554
558
|
unitOf, convoGroups, metaData } =
|
|
555
|
-
parseData(rows, codes, units, conversations);
|
|
559
|
+
parseData(rows, codes, units, conversations, unitsUsed);
|
|
556
560
|
const nConnections = ordered ? nCodes * nCodes : qe.choose_two(nCodes);
|
|
557
561
|
|
|
558
562
|
// 1. Rebuild + fit at each window, collecting unit points.
|
|
@@ -590,7 +594,7 @@ export default async function loadENA() {
|
|
|
590
594
|
// 4. Rebuild the accumulation at the selected window size.
|
|
591
595
|
return api.accumulate(rows, {
|
|
592
596
|
codes, units, conversations, window: bestWindow, binary, ordered,
|
|
593
|
-
weightModel,
|
|
597
|
+
weightModel, unitsUsed,
|
|
594
598
|
});
|
|
595
599
|
},
|
|
596
600
|
|
|
@@ -722,7 +726,8 @@ export default async function loadENA() {
|
|
|
722
726
|
* @param {Object[]} rows
|
|
723
727
|
* @param {object} opts - fit() options (codes, units, conversations,
|
|
724
728
|
* window, binary, ordered, tensor, weightModel,
|
|
725
|
-
* codeMask, rotation, groupA, groupB, gParams
|
|
729
|
+
* codeMask, rotation, groupA, groupB, gParams,
|
|
730
|
+
* unitsUsed)
|
|
726
731
|
* plus removeNum (default 3) and threshold
|
|
727
732
|
* (default 0.95). `dims` is ignored: scoring
|
|
728
733
|
* uses dims 1:2 and the variance uses all dims.
|
|
@@ -735,7 +740,7 @@ export default async function loadENA() {
|
|
|
735
740
|
window: windowSize = 4, binary = true, ordered = false,
|
|
736
741
|
tensor: tensorDef, weightModel, codeMask,
|
|
737
742
|
rotation = 'svd', gParams, groupA, groupB,
|
|
738
|
-
removeNum = 3, threshold = 0.95,
|
|
743
|
+
removeNum = 3, threshold = 0.95, unitsUsed,
|
|
739
744
|
} = opts;
|
|
740
745
|
if (!codes?.length) throw new Error('opts.codes is required');
|
|
741
746
|
if (!units?.length) throw new Error('opts.units is required');
|
|
@@ -749,7 +754,7 @@ export default async function loadENA() {
|
|
|
749
754
|
// Accumulate once, exactly as fit() would for these options.
|
|
750
755
|
const { codeMatrix, nRows, nCodes, nUnits, unitLabels,
|
|
751
756
|
unitOf, convoGroups, metaData } =
|
|
752
|
-
parseData(rows, codes, units, conversations);
|
|
757
|
+
parseData(rows, codes, units, conversations, unitsUsed);
|
|
753
758
|
const { networks: raw } = accumulateTensor(
|
|
754
759
|
qe, rows, codeMatrix, nRows, nCodes, nUnits,
|
|
755
760
|
unitOf, convoGroups, tensorDef ?? defaultTensor(windowSize),
|
package/src/tensor.js
CHANGED
|
@@ -197,10 +197,13 @@ export function accumulateTensor(qe, rows, codeMatrix, nRows, nCodes, nUnits,
|
|
|
197
197
|
? buildContextLookupWithTimes(rows, rowIndices, factors, factorLevels, timesCol)
|
|
198
198
|
: buildContextLookup(rows, rowIndices, factors, factorLevels);
|
|
199
199
|
|
|
200
|
-
// Group response rows by unit (within this conversation)
|
|
200
|
+
// Group response rows by unit (within this conversation). Rows with
|
|
201
|
+
// no unit (unitOf = -1, excluded via unitsUsed) are never responses,
|
|
202
|
+
// but stay in convoCodes as context for the other units' windows.
|
|
201
203
|
const unitConvoRows = new Map();
|
|
202
204
|
for (let r = 0; r < nConvo; r++) {
|
|
203
205
|
const unit = unitOf[rowIndices[r]];
|
|
206
|
+
if (unit < 0) continue;
|
|
204
207
|
if (!unitConvoRows.has(unit)) unitConvoRows.set(unit, []);
|
|
205
208
|
unitConvoRows.get(unit).push(r); // local (conversation-relative) index
|
|
206
209
|
}
|