oracular 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#58](https://github.com/Yuribenjamin/oracular/pull/58) [`d6d1e87`](https://github.com/Yuribenjamin/oracular/commit/d6d1e87e176fc0112384e47fb4782e12d6fa1d1a) Thanks [@Yuribenjamin](https://github.com/Yuribenjamin)! - **Learning no longer drifts without outcomes.** The outcome rates a signal is judged by, with it and without it, were Laplace-smoothed, which centres both on 0.5 with one pseudo-hit each: two groups with no outcome at all then differ only by their sizes, so with zero outcomes a common signal drifted negative and a rare one positive, and a weight could change sign on no evidence (a weight of 2 seen in 90 of 100 outcome-free episodes ended at −0.59). Both rates are now shrunk toward the intent's own base rate by two pseudo-episodes, `(hits + 2·q₀) / (n + 2)` with `q₀ = outcomes / episodes`: a signal whose outcome rate equals the base rate learns exactly zero whatever the sample sizes, with no outcome every rate is 0 and no weight moves away from the developer's, and a weight never changes sign without outcomes on at least one side. The association-rule baseline becomes the base rate itself. `tune` fits with the same smoothing, so its weights, intervals and lifts move a little on small samples (a signal seen once with the outcome over a low base rate is worth a little less than before); on large samples the change is in the third decimal. Every learned number in the documentation's generated tables was regenerated. The state format is unchanged, and existing learned counts are kept: only the weight read from them moves.
8
+
3
9
  ## 0.10.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -193,8 +193,9 @@ A signal's weight is then a blend of your prior and the learned estimate:
193
193
  ```
194
194
  nWith = hits + misses episodes with the signal
195
195
  nWithout = episodes − nWith episodes without it
196
- qWith = (hits + 1) / (nWith + 2) outcome rate with the signal
197
- qWithout = (outcomes − hits + 1) / (nWithout + 2) outcome rate without it
196
+ q₀ = outcomes / episodes the intent's own base rate
197
+ qWith = (hits + 2·q₀) / (nWith + 2) outcome rate with the signal
198
+ qWithout = (outcomes − hits + 2·q₀) / (nWithout + 2) outcome rate without it
198
199
  lift = (qWith − qWithout) / (1 − qWithout) if qWith ≥ qWithout, else (qWith − qWithout) / qWithout → in [−1, 1]
199
200
  learned = gain × |w₀| × lift 0 until the signal has history
200
201
  nEff = (nWith × nWithout) / (nWith + nWithout) 0 if either side is 0
package/dist/cli.js CHANGED
@@ -437,6 +437,9 @@ import {
437
437
  function laplace(successes, total) {
438
438
  return (successes + 1) / (total + 2);
439
439
  }
440
+ function smoothRate(successes, total, rate) {
441
+ return (successes + 2 * rate) / (total + 2);
442
+ }
440
443
  function lift(q, b, type = "normalized-difference") {
441
444
  if (type === "absolute-difference") return q - b;
442
445
  if (type !== "normalized-difference") {
@@ -528,9 +531,10 @@ function fitSignal(signal, current, tally, strength, priorEpisodes, liftType) {
528
531
  const nWith = hits + misses;
529
532
  const without = Math.max(0, tally.episodes - nWith);
530
533
  const outcomesWithout = Math.min(without, Math.max(0, tally.outcomes - hits));
531
- const qWith = laplace(hits, nWith);
532
- const qWithout = laplace(outcomesWithout, without);
533
- const baseline = liftType === "association-rule" ? (hits + outcomesWithout + 2) / (nWith + without + 4) : qWithout;
534
+ const base = tally.episodes === 0 ? 0 : tally.outcomes / tally.episodes;
535
+ const qWith = smoothRate(hits, nWith, base);
536
+ const qWithout = smoothRate(outcomesWithout, without, base);
537
+ const baseline = liftType === "association-rule" ? smoothRate(hits + outcomesWithout, nWith + without, base) : qWithout;
534
538
  const noContrast = without === 0;
535
539
  const rawLift = noContrast ? null : nWith === 0 ? 0 : lift(qWith, baseline, liftType);
536
540
  const score = rawLift === null ? null : nWith === 0 ? 0 : learningScore(rawLift, liftType);
package/dist/index.cjs CHANGED
@@ -79,8 +79,8 @@ function lowerBound(alpha, beta, z, bound) {
79
79
  function repeatFactor(n) {
80
80
  return Math.log1p(1 / n) / Math.LN2;
81
81
  }
82
- function laplace(successes, total) {
83
- return (successes + 1) / (total + 2);
82
+ function smoothRate(successes, total, rate) {
83
+ return (successes + 2 * rate) / (total + 2);
84
84
  }
85
85
  function lift(q, b, type = "normalized-difference") {
86
86
  if (type === "absolute-difference") return q - b;
@@ -154,12 +154,13 @@ function signalWeights(state, intent, learning, signal) {
154
154
  }
155
155
  const record2 = getOwn(state.learned, signal) ?? NONE;
156
156
  const nWith = record2.hits + record2.misses;
157
- const qWith = laplace(record2.hits, nWith);
158
157
  const totalEpisodes = state.episodes.hits + state.episodes.misses;
159
158
  const nWithout = Math.max(0, totalEpisodes - nWith);
160
159
  const outcomesWithout = Math.min(nWithout, Math.max(0, state.episodes.hits - record2.hits));
161
- const qWithout = laplace(outcomesWithout, nWithout);
162
- const baseline = learning.lift === "association-rule" ? (record2.hits + outcomesWithout + 2) / (nWith + nWithout + 4) : qWithout;
160
+ const base = totalEpisodes === 0 ? 0 : state.episodes.hits / totalEpisodes;
161
+ const qWith = smoothRate(record2.hits, nWith, base);
162
+ const qWithout = smoothRate(outcomesWithout, nWithout, base);
163
+ const baseline = learning.lift === "association-rule" ? smoothRate(record2.hits + outcomesWithout, nWith + nWithout, base) : qWithout;
163
164
  const score = learningScore(lift(qWith, baseline, learning.lift), learning.lift);
164
165
  const learned = nWith === 0 ? 0 : learning.gain * Math.abs(initial) * score;
165
166
  const nEff = nWith + nWithout === 0 ? 0 : nWith * nWithout / (nWith + nWithout);