UNPKG

autotel

Version:
87 lines (85 loc) 3.29 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); //#region src/analysis.ts const DEFAULT_MAX_VALUES_PER_FIELD = 50; const DEFAULT_MAX_UNIQUE_RATIO = .5; const DEFAULT_MIN_DIFFERENCE = .1; const DEFAULT_LIMIT = 20; /** * Render a value as a grouping key. * * Objects and arrays return undefined: a nested structure does not name a * cohort, and stringifying it produces noise rather than a testable split. */ function valueKey(value) { if (value === null || value === void 0) return; const kind = typeof value; if (kind === "string" || kind === "number" || kind === "boolean" || kind === "bigint") return String(value); } /** * Fold one group's events into the shared tally. * * Both groups accumulate into the same structure, so the caller never builds a * combined array or revisits an event once per candidate field. */ function accumulate(tally, events, group, includes) { for (const event of events) for (const [field, raw] of Object.entries(event)) { if (!includes(field)) continue; const value = valueKey(raw); if (value === void 0) continue; let values = tally.get(field); if (values === void 0) { values = /* @__PURE__ */ new Map(); tally.set(field, values); } const counts = values.get(value) ?? { outlier: 0, baseline: 0 }; counts[group]++; values.set(value, counts); } } /** * Rank the field/value pairs that separate an outlier group from a baseline. * * Returns an empty array when either group is empty, because a fraction over * zero events carries no information. * * @param options - The two populations and the scan limits * @returns Differences sorted by absolute strength, strongest first */ function compareCohorts(options) { const { outlier, baseline, fields, ignoreFields, maxValuesPerField = DEFAULT_MAX_VALUES_PER_FIELD, maxUniqueRatio = DEFAULT_MAX_UNIQUE_RATIO, minDifference = DEFAULT_MIN_DIFFERENCE, limit = DEFAULT_LIMIT } = options; if (outlier.length === 0 || baseline.length === 0) return []; const ignored = new Set(ignoreFields); const selected = fields === void 0 ? void 0 : new Set(fields); const includes = (field) => !ignored.has(field) && (selected === void 0 || selected.has(field)); const tally = /* @__PURE__ */ new Map(); accumulate(tally, outlier, "outlier", includes); accumulate(tally, baseline, "baseline", includes); const results = []; const total = outlier.length + baseline.length; for (const [field, values] of tally) { if (values.size > maxValuesPerField || values.size > total * maxUniqueRatio) continue; for (const [value, counts] of values) { const outlierFraction = counts.outlier / outlier.length; const baselineFraction = counts.baseline / baseline.length; const difference = outlierFraction - baselineFraction; if (Math.abs(difference) < minDifference) continue; results.push({ field, value, outlierFraction, baselineFraction, difference, outlierCount: counts.outlier, baselineCount: counts.baseline }); } } results.sort((a, b) => Math.abs(b.difference) - Math.abs(a.difference) || b.difference - a.difference || a.field.localeCompare(b.field) || a.value.localeCompare(b.value)); return results.slice(0, limit); } //#endregion exports.compareCohorts = compareCohorts; //# sourceMappingURL=analysis.cjs.map