@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qe-libs/rena-wasm",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "JavaScript/WebAssembly ENA pipeline — thin orchestration layer over @qe-libs/libqe-wasm",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
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 (!unitIndex.has(uKey)) {
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
  }