@logg/signals 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -2
- package/dist/reco.d.mts +5 -2
- package/dist/reco.d.ts +5 -2
- package/dist/reco.js +31 -4
- package/dist/reco.mjs +30 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -522,9 +522,30 @@ Methods shared by both engines: `prime()`, `engage()`, `recommend()`,
|
|
|
522
522
|
`setOwned()`, `clearSeen()`, `reset()`, `seenCount()`. The dual engine adds
|
|
523
523
|
`setSampleSize(k)` to tune explore vs. exploit.
|
|
524
524
|
|
|
525
|
+
### Weighted scoring (dual engine) — `0.4.0`
|
|
526
|
+
|
|
527
|
+
By default `DualBucketRecommender` ranks candidates by the sum of raw slot
|
|
528
|
+
powers, which ignores each feature's schema `weight` and lets a
|
|
529
|
+
low-cardinality feature (e.g. one dominant `category`) outweigh a
|
|
530
|
+
high-cardinality one (e.g. `brand`) purely by accumulated mass. Pass
|
|
531
|
+
`weightedScoring: true` to instead weight each feature by its schema `weight`
|
|
532
|
+
and normalize a value's power to its share of the bucket (an ε-floored share,
|
|
533
|
+
the same scheme the v1 engine uses):
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
const reco = new DualBucketRecommender(catalog, { schema, weightedScoring: true });
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
This makes schema weights authoritative and removes the cardinality bias. It
|
|
540
|
+
is **off by default** to preserve existing v2 ranking. Note the scale change:
|
|
541
|
+
with the flag on, `net` / `diagnostics.bestNet` move from unbounded raw power
|
|
542
|
+
to roughly `[−Σweight, +Σweight]` (≈ `[−1, 1]` when weights sum to 1), so any
|
|
543
|
+
consumer comparing `bestNet` against a raw-power threshold must rescale.
|
|
544
|
+
|
|
525
545
|
Advanced primitives (slot tables, interest state, bucket admission, the
|
|
526
|
-
weighted sampler, and `seededRng` for
|
|
527
|
-
the same entry for callers building
|
|
546
|
+
weighted sampler, the dual-bucket `bucketContribution`, and `seededRng` for
|
|
547
|
+
deterministic tests) are exported from the same entry for callers building
|
|
548
|
+
custom pipelines or CLIs on top.
|
|
528
549
|
|
|
529
550
|
## License
|
|
530
551
|
|
package/dist/reco.d.mts
CHANGED
|
@@ -39,7 +39,7 @@ interface SlotTableState {
|
|
|
39
39
|
declare function emptyState(): SlotTableState;
|
|
40
40
|
declare function observe(state: SlotTableState, value: string, weight: number, config?: SlotTableConfig): void;
|
|
41
41
|
declare function decay(state: SlotTableState, factor: number): void;
|
|
42
|
-
declare function contribution(state: SlotTableState, value: string | null, config?: SlotTableConfig): number;
|
|
42
|
+
declare function contribution$1(state: SlotTableState, value: string | null, config?: SlotTableConfig): number;
|
|
43
43
|
declare function isEmpty$1(state: SlotTableState): boolean;
|
|
44
44
|
|
|
45
45
|
interface InterestConfig<T extends BaseItem> {
|
|
@@ -125,10 +125,12 @@ interface BucketConfig {
|
|
|
125
125
|
declare function emptyBucket(): BucketState;
|
|
126
126
|
declare function admit(state: BucketState, value: string, score: number, config?: BucketConfig): void;
|
|
127
127
|
declare function powerOf(state: BucketState, value: string | null): number;
|
|
128
|
+
declare function contribution(state: BucketState, value: string | null, epsilon: number): number;
|
|
128
129
|
declare function isEmpty(state: BucketState): boolean;
|
|
129
130
|
|
|
130
131
|
interface DualInterestConfig<T extends BaseItem> {
|
|
131
132
|
schema: Schema<T>;
|
|
133
|
+
weightedScoring?: boolean;
|
|
132
134
|
}
|
|
133
135
|
interface DualBucketState {
|
|
134
136
|
liked: Record<string, BucketState>;
|
|
@@ -160,6 +162,7 @@ interface DualRecommenderConfig<T extends BaseItem> {
|
|
|
160
162
|
schema: Schema<T>;
|
|
161
163
|
sampleSize?: number;
|
|
162
164
|
shuffleThreshold?: number;
|
|
165
|
+
weightedScoring?: boolean;
|
|
163
166
|
rng?: RNG;
|
|
164
167
|
}
|
|
165
168
|
interface ResolvedConfig<T extends BaseItem> {
|
|
@@ -213,4 +216,4 @@ declare class DualBucketRecommender<T extends BaseItem> {
|
|
|
213
216
|
declare function logDecadeBucket(value: number | null, multiplier?: number, prefix?: string): string | null;
|
|
214
217
|
declare function yearBucket(year: number | null, bandSize?: number): string | null;
|
|
215
218
|
|
|
216
|
-
export { type BaseItem, type BucketConfig, type BucketState, DEFAULT_CAPACITY, DEFAULT_DECREMENT_CAP, DEFAULT_EPSILON, DEFAULT_INTEREST_CONFIG, DEFAULT_SLOT_CONFIG, DualBucketRecommender, type DualBucketState, type DualInterestConfig, type ItemScore as DualItemScore, type DualRecommendDiagnostics, type DualRecommendResult, type DualRecommenderConfig, type FeatureConfig, type InterestConfig, type InterestState, type RNG, type RecommendDiagnostics, type RecommendResult, Recommender, type RecommenderConfig, SIGNALS, type Schema, type SignalName, type Slot, type SlotTableConfig, type SlotTableState, admit, applyEvent, contribution, decay, defaultRng, describe$1 as describe, describe as describeDual, emptyBucket, emptyDualState, emptyInterest, emptyState, engageNegative, engagePositive, isCold$1 as isCold, isCold as isColdDual, isEmpty$1 as isEmpty, isEmpty as isEmptyBucket, logDecadeBucket, observe, powerOf, scoreItem$1 as scoreItem, scoreItem as scoreItemDual, seededRng, weightedSampleWithoutReplacement, yearBucket };
|
|
219
|
+
export { type BaseItem, type BucketConfig, type BucketState, DEFAULT_CAPACITY, DEFAULT_DECREMENT_CAP, DEFAULT_EPSILON, DEFAULT_INTEREST_CONFIG, DEFAULT_SLOT_CONFIG, DualBucketRecommender, type DualBucketState, type DualInterestConfig, type ItemScore as DualItemScore, type DualRecommendDiagnostics, type DualRecommendResult, type DualRecommenderConfig, type FeatureConfig, type InterestConfig, type InterestState, type RNG, type RecommendDiagnostics, type RecommendResult, Recommender, type RecommenderConfig, SIGNALS, type Schema, type SignalName, type Slot, type SlotTableConfig, type SlotTableState, admit, applyEvent, contribution as bucketContribution, contribution$1 as contribution, decay, defaultRng, describe$1 as describe, describe as describeDual, emptyBucket, emptyDualState, emptyInterest, emptyState, engageNegative, engagePositive, isCold$1 as isCold, isCold as isColdDual, isEmpty$1 as isEmpty, isEmpty as isEmptyBucket, logDecadeBucket, observe, powerOf, scoreItem$1 as scoreItem, scoreItem as scoreItemDual, seededRng, weightedSampleWithoutReplacement, yearBucket };
|
package/dist/reco.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ interface SlotTableState {
|
|
|
39
39
|
declare function emptyState(): SlotTableState;
|
|
40
40
|
declare function observe(state: SlotTableState, value: string, weight: number, config?: SlotTableConfig): void;
|
|
41
41
|
declare function decay(state: SlotTableState, factor: number): void;
|
|
42
|
-
declare function contribution(state: SlotTableState, value: string | null, config?: SlotTableConfig): number;
|
|
42
|
+
declare function contribution$1(state: SlotTableState, value: string | null, config?: SlotTableConfig): number;
|
|
43
43
|
declare function isEmpty$1(state: SlotTableState): boolean;
|
|
44
44
|
|
|
45
45
|
interface InterestConfig<T extends BaseItem> {
|
|
@@ -125,10 +125,12 @@ interface BucketConfig {
|
|
|
125
125
|
declare function emptyBucket(): BucketState;
|
|
126
126
|
declare function admit(state: BucketState, value: string, score: number, config?: BucketConfig): void;
|
|
127
127
|
declare function powerOf(state: BucketState, value: string | null): number;
|
|
128
|
+
declare function contribution(state: BucketState, value: string | null, epsilon: number): number;
|
|
128
129
|
declare function isEmpty(state: BucketState): boolean;
|
|
129
130
|
|
|
130
131
|
interface DualInterestConfig<T extends BaseItem> {
|
|
131
132
|
schema: Schema<T>;
|
|
133
|
+
weightedScoring?: boolean;
|
|
132
134
|
}
|
|
133
135
|
interface DualBucketState {
|
|
134
136
|
liked: Record<string, BucketState>;
|
|
@@ -160,6 +162,7 @@ interface DualRecommenderConfig<T extends BaseItem> {
|
|
|
160
162
|
schema: Schema<T>;
|
|
161
163
|
sampleSize?: number;
|
|
162
164
|
shuffleThreshold?: number;
|
|
165
|
+
weightedScoring?: boolean;
|
|
163
166
|
rng?: RNG;
|
|
164
167
|
}
|
|
165
168
|
interface ResolvedConfig<T extends BaseItem> {
|
|
@@ -213,4 +216,4 @@ declare class DualBucketRecommender<T extends BaseItem> {
|
|
|
213
216
|
declare function logDecadeBucket(value: number | null, multiplier?: number, prefix?: string): string | null;
|
|
214
217
|
declare function yearBucket(year: number | null, bandSize?: number): string | null;
|
|
215
218
|
|
|
216
|
-
export { type BaseItem, type BucketConfig, type BucketState, DEFAULT_CAPACITY, DEFAULT_DECREMENT_CAP, DEFAULT_EPSILON, DEFAULT_INTEREST_CONFIG, DEFAULT_SLOT_CONFIG, DualBucketRecommender, type DualBucketState, type DualInterestConfig, type ItemScore as DualItemScore, type DualRecommendDiagnostics, type DualRecommendResult, type DualRecommenderConfig, type FeatureConfig, type InterestConfig, type InterestState, type RNG, type RecommendDiagnostics, type RecommendResult, Recommender, type RecommenderConfig, SIGNALS, type Schema, type SignalName, type Slot, type SlotTableConfig, type SlotTableState, admit, applyEvent, contribution, decay, defaultRng, describe$1 as describe, describe as describeDual, emptyBucket, emptyDualState, emptyInterest, emptyState, engageNegative, engagePositive, isCold$1 as isCold, isCold as isColdDual, isEmpty$1 as isEmpty, isEmpty as isEmptyBucket, logDecadeBucket, observe, powerOf, scoreItem$1 as scoreItem, scoreItem as scoreItemDual, seededRng, weightedSampleWithoutReplacement, yearBucket };
|
|
219
|
+
export { type BaseItem, type BucketConfig, type BucketState, DEFAULT_CAPACITY, DEFAULT_DECREMENT_CAP, DEFAULT_EPSILON, DEFAULT_INTEREST_CONFIG, DEFAULT_SLOT_CONFIG, DualBucketRecommender, type DualBucketState, type DualInterestConfig, type ItemScore as DualItemScore, type DualRecommendDiagnostics, type DualRecommendResult, type DualRecommenderConfig, type FeatureConfig, type InterestConfig, type InterestState, type RNG, type RecommendDiagnostics, type RecommendResult, Recommender, type RecommenderConfig, SIGNALS, type Schema, type SignalName, type Slot, type SlotTableConfig, type SlotTableState, admit, applyEvent, contribution as bucketContribution, contribution$1 as contribution, decay, defaultRng, describe$1 as describe, describe as describeDual, emptyBucket, emptyDualState, emptyInterest, emptyState, engageNegative, engagePositive, isCold$1 as isCold, isCold as isColdDual, isEmpty$1 as isEmpty, isEmpty as isEmptyBucket, logDecadeBucket, observe, powerOf, scoreItem$1 as scoreItem, scoreItem as scoreItemDual, seededRng, weightedSampleWithoutReplacement, yearBucket };
|
package/dist/reco.js
CHANGED
|
@@ -30,6 +30,7 @@ __export(reco_exports, {
|
|
|
30
30
|
SIGNALS: () => SIGNALS,
|
|
31
31
|
admit: () => admit,
|
|
32
32
|
applyEvent: () => applyEvent,
|
|
33
|
+
bucketContribution: () => contribution2,
|
|
33
34
|
contribution: () => contribution,
|
|
34
35
|
decay: () => decay,
|
|
35
36
|
defaultRng: () => defaultRng,
|
|
@@ -406,6 +407,14 @@ function powerOf(state, value) {
|
|
|
406
407
|
const slot = state.slots.find((s) => s.value === value);
|
|
407
408
|
return slot ? slot.power : 0;
|
|
408
409
|
}
|
|
410
|
+
function contribution2(state, value, epsilon) {
|
|
411
|
+
if (!value) return epsilon;
|
|
412
|
+
const slot = state.slots.find((s) => s.value === value);
|
|
413
|
+
if (!slot) return epsilon;
|
|
414
|
+
const total = state.slots.reduce((sum, s) => sum + s.power, 0);
|
|
415
|
+
if (total <= 0) return epsilon;
|
|
416
|
+
return epsilon + (1 - epsilon) * (slot.power / total);
|
|
417
|
+
}
|
|
409
418
|
function isEmpty2(state) {
|
|
410
419
|
return state.slots.length === 0;
|
|
411
420
|
}
|
|
@@ -445,14 +454,31 @@ function engageNegative(state, item, magnitude, config) {
|
|
|
445
454
|
if (!Number.isFinite(magnitude) || magnitude <= 0) return;
|
|
446
455
|
admitAcrossFeatures(state, "disliked", item, magnitude, config.schema);
|
|
447
456
|
}
|
|
457
|
+
function featureWeight2(fc, featureCount) {
|
|
458
|
+
if (typeof fc.weight === "number") return fc.weight;
|
|
459
|
+
return featureCount > 0 ? 1 / featureCount : 0;
|
|
460
|
+
}
|
|
461
|
+
function epsilonOf(fc) {
|
|
462
|
+
return fc.epsilon ?? DEFAULT_EPSILON;
|
|
463
|
+
}
|
|
448
464
|
function scoreItem2(state, item, config) {
|
|
449
465
|
let liked = 0;
|
|
450
466
|
let disliked = 0;
|
|
451
467
|
const byFeature = [];
|
|
452
|
-
|
|
468
|
+
const entries = Object.entries(config.schema);
|
|
469
|
+
for (const [key, fc] of entries) {
|
|
453
470
|
const value = fc.extract(item);
|
|
454
|
-
|
|
455
|
-
|
|
471
|
+
let l;
|
|
472
|
+
let d;
|
|
473
|
+
if (config.weightedScoring) {
|
|
474
|
+
const w = featureWeight2(fc, entries.length);
|
|
475
|
+
const eps = epsilonOf(fc);
|
|
476
|
+
l = w * contribution2(state.liked[key], value, eps);
|
|
477
|
+
d = w * contribution2(state.disliked[key], value, eps);
|
|
478
|
+
} else {
|
|
479
|
+
l = powerOf(state.liked[key], value);
|
|
480
|
+
d = powerOf(state.disliked[key], value);
|
|
481
|
+
}
|
|
456
482
|
liked += l;
|
|
457
483
|
disliked += d;
|
|
458
484
|
byFeature.push({ key, value, liked: l, disliked: d });
|
|
@@ -479,7 +505,7 @@ function describe2(state, top = 4) {
|
|
|
479
505
|
// src/reco/dual-bucket/recommender.ts
|
|
480
506
|
function resolve2(input) {
|
|
481
507
|
return {
|
|
482
|
-
interest: { schema: input.schema },
|
|
508
|
+
interest: { schema: input.schema, weightedScoring: input.weightedScoring ?? false },
|
|
483
509
|
// 100 is a heavy exploit setting; drop to 20–30 if exploration feels
|
|
484
510
|
// weak. Live-tunable via setSampleSize().
|
|
485
511
|
sampleSize: input.sampleSize ?? 100,
|
|
@@ -633,6 +659,7 @@ function yearBucket(year, bandSize = 5) {
|
|
|
633
659
|
SIGNALS,
|
|
634
660
|
admit,
|
|
635
661
|
applyEvent,
|
|
662
|
+
bucketContribution,
|
|
636
663
|
contribution,
|
|
637
664
|
decay,
|
|
638
665
|
defaultRng,
|
package/dist/reco.mjs
CHANGED
|
@@ -350,6 +350,14 @@ function powerOf(state, value) {
|
|
|
350
350
|
const slot = state.slots.find((s) => s.value === value);
|
|
351
351
|
return slot ? slot.power : 0;
|
|
352
352
|
}
|
|
353
|
+
function contribution2(state, value, epsilon) {
|
|
354
|
+
if (!value) return epsilon;
|
|
355
|
+
const slot = state.slots.find((s) => s.value === value);
|
|
356
|
+
if (!slot) return epsilon;
|
|
357
|
+
const total = state.slots.reduce((sum, s) => sum + s.power, 0);
|
|
358
|
+
if (total <= 0) return epsilon;
|
|
359
|
+
return epsilon + (1 - epsilon) * (slot.power / total);
|
|
360
|
+
}
|
|
353
361
|
function isEmpty2(state) {
|
|
354
362
|
return state.slots.length === 0;
|
|
355
363
|
}
|
|
@@ -389,14 +397,31 @@ function engageNegative(state, item, magnitude, config) {
|
|
|
389
397
|
if (!Number.isFinite(magnitude) || magnitude <= 0) return;
|
|
390
398
|
admitAcrossFeatures(state, "disliked", item, magnitude, config.schema);
|
|
391
399
|
}
|
|
400
|
+
function featureWeight2(fc, featureCount) {
|
|
401
|
+
if (typeof fc.weight === "number") return fc.weight;
|
|
402
|
+
return featureCount > 0 ? 1 / featureCount : 0;
|
|
403
|
+
}
|
|
404
|
+
function epsilonOf(fc) {
|
|
405
|
+
return fc.epsilon ?? DEFAULT_EPSILON;
|
|
406
|
+
}
|
|
392
407
|
function scoreItem2(state, item, config) {
|
|
393
408
|
let liked = 0;
|
|
394
409
|
let disliked = 0;
|
|
395
410
|
const byFeature = [];
|
|
396
|
-
|
|
411
|
+
const entries = Object.entries(config.schema);
|
|
412
|
+
for (const [key, fc] of entries) {
|
|
397
413
|
const value = fc.extract(item);
|
|
398
|
-
|
|
399
|
-
|
|
414
|
+
let l;
|
|
415
|
+
let d;
|
|
416
|
+
if (config.weightedScoring) {
|
|
417
|
+
const w = featureWeight2(fc, entries.length);
|
|
418
|
+
const eps = epsilonOf(fc);
|
|
419
|
+
l = w * contribution2(state.liked[key], value, eps);
|
|
420
|
+
d = w * contribution2(state.disliked[key], value, eps);
|
|
421
|
+
} else {
|
|
422
|
+
l = powerOf(state.liked[key], value);
|
|
423
|
+
d = powerOf(state.disliked[key], value);
|
|
424
|
+
}
|
|
400
425
|
liked += l;
|
|
401
426
|
disliked += d;
|
|
402
427
|
byFeature.push({ key, value, liked: l, disliked: d });
|
|
@@ -423,7 +448,7 @@ function describe2(state, top = 4) {
|
|
|
423
448
|
// src/reco/dual-bucket/recommender.ts
|
|
424
449
|
function resolve2(input) {
|
|
425
450
|
return {
|
|
426
|
-
interest: { schema: input.schema },
|
|
451
|
+
interest: { schema: input.schema, weightedScoring: input.weightedScoring ?? false },
|
|
427
452
|
// 100 is a heavy exploit setting; drop to 20–30 if exploration feels
|
|
428
453
|
// weak. Live-tunable via setSampleSize().
|
|
429
454
|
sampleSize: input.sampleSize ?? 100,
|
|
@@ -576,6 +601,7 @@ export {
|
|
|
576
601
|
SIGNALS,
|
|
577
602
|
admit,
|
|
578
603
|
applyEvent,
|
|
604
|
+
contribution2 as bucketContribution,
|
|
579
605
|
contribution,
|
|
580
606
|
decay,
|
|
581
607
|
defaultRng,
|