@cogitator-ai/evals 0.12.0 → 0.13.1
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 +76 -27
- package/dist/comparison-stats.d.ts +104 -0
- package/dist/comparison-stats.d.ts.map +1 -0
- package/dist/comparison-stats.js +179 -0
- package/dist/comparison-stats.js.map +1 -0
- package/dist/eval-comparison.d.ts +2 -51
- package/dist/eval-comparison.d.ts.map +1 -1
- package/dist/eval-comparison.js +12 -149
- package/dist/eval-comparison.js.map +1 -1
- package/dist/eval-suite.d.ts +33 -0
- package/dist/eval-suite.d.ts.map +1 -1
- package/dist/eval-suite.js +81 -2
- package/dist/eval-suite.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/metrics/types.d.ts +26 -1
- package/dist/metrics/types.d.ts.map +1 -1
- package/dist/metrics/types.js.map +1 -1
- package/dist/reporters/comparison.d.ts +24 -0
- package/dist/reporters/comparison.d.ts.map +1 -0
- package/dist/reporters/comparison.js +290 -0
- package/dist/reporters/comparison.js.map +1 -0
- package/dist/reporters/format.d.ts +21 -0
- package/dist/reporters/format.d.ts.map +1 -1
- package/dist/reporters/format.js +31 -0
- package/dist/reporters/format.js.map +1 -1
- package/dist/reporters/github.d.ts.map +1 -1
- package/dist/reporters/github.js +1 -16
- package/dist/reporters/github.js.map +1 -1
- package/dist/reporters/html-format.d.ts +11 -0
- package/dist/reporters/html-format.d.ts.map +1 -0
- package/dist/reporters/html-format.js +111 -0
- package/dist/reporters/html-format.js.map +1 -0
- package/dist/reporters/html.d.ts +12 -0
- package/dist/reporters/html.d.ts.map +1 -0
- package/dist/reporters/html.js +119 -0
- package/dist/reporters/html.js.map +1 -0
- package/dist/reporters/index.d.ts +21 -7
- package/dist/reporters/index.d.ts.map +1 -1
- package/dist/reporters/index.js +15 -4
- package/dist/reporters/index.js.map +1 -1
- package/dist/reporters/markdown.d.ts +15 -0
- package/dist/reporters/markdown.d.ts.map +1 -0
- package/dist/reporters/markdown.js +67 -0
- package/dist/reporters/markdown.js.map +1 -0
- package/dist/runs/compare.d.ts +103 -0
- package/dist/runs/compare.d.ts.map +1 -0
- package/dist/runs/compare.js +208 -0
- package/dist/runs/compare.js.map +1 -0
- package/dist/runs/index.d.ts +9 -0
- package/dist/runs/index.d.ts.map +1 -0
- package/dist/runs/index.js +5 -0
- package/dist/runs/index.js.map +1 -0
- package/dist/runs/json.d.ts +11 -0
- package/dist/runs/json.d.ts.map +1 -0
- package/dist/runs/json.js +58 -0
- package/dist/runs/json.js.map +1 -0
- package/dist/runs/run.d.ts +44 -0
- package/dist/runs/run.d.ts.map +1 -0
- package/dist/runs/run.js +136 -0
- package/dist/runs/run.js.map +1 -0
- package/dist/runs/schema.d.ts +504 -0
- package/dist/runs/schema.d.ts.map +1 -0
- package/dist/runs/schema.js +171 -0
- package/dist/runs/schema.js.map +1 -0
- package/dist/runs/store.d.ts +57 -0
- package/dist/runs/store.d.ts.map +1 -0
- package/dist/runs/store.js +146 -0
- package/dist/runs/store.js.map +1 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ pnpm add @cogitator-ai/core # agent targets and LLM-as-judge metrics
|
|
|
13
13
|
pnpm add -D vitest # describeEval from @cogitator-ai/evals/vitest
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
Website docs: [Evals](https://cogitator.app/docs/evals), [Datasets](https://cogitator.app/docs/evals/datasets), [Metrics](https://cogitator.app/docs/evals/metrics), [Assertions](https://cogitator.app/docs/evals/assertions), [Agents](https://cogitator.app/docs/evals/agents), [Comparison](https://cogitator.app/docs/evals/comparison), [Reporters](https://cogitator.app/docs/evals/reporters).
|
|
16
|
+
Website docs: [Evals](https://cogitator.app/docs/evals), [Datasets](https://cogitator.app/docs/evals/datasets), [Metrics](https://cogitator.app/docs/evals/metrics), [Assertions](https://cogitator.app/docs/evals/assertions), [Agents](https://cogitator.app/docs/evals/agents), [Comparison](https://cogitator.app/docs/evals/comparison), [Experiments](https://cogitator.app/docs/evals/experiments), [Reporters](https://cogitator.app/docs/evals/reporters).
|
|
17
17
|
|
|
18
18
|
## Features
|
|
19
19
|
|
|
@@ -26,7 +26,8 @@ Website docs: [Evals](https://cogitator.app/docs/evals), [Datasets](https://cogi
|
|
|
26
26
|
- **Trials and pass@k** - run each case several times and score how often it passes with `passAtK()`
|
|
27
27
|
- **Assertions** — threshold, noRegression, custom assertion with auto-detection of lower-is-better metrics
|
|
28
28
|
- **A/B Testing** - EvalComparison with McNemar's test (exact for few discordant pairs), the paired t-test or Wilcoxon signed-rank, Holm's correction across metrics, bootstrap confidence intervals, effect sizes, latency and cost comparisons and pairwise judging
|
|
29
|
-
- **
|
|
29
|
+
- **Experiment Tracking** - named runs with metadata saved as versioned run files, a `RunStore` directory of runs, `compareRuns()` with the A/B statistics, per-case regressions and improvements, and warnings when datasets, prompt versions, metrics or judges differ. Agent cases keep the `traceId` and versioned `prompt` of their runs
|
|
30
|
+
- **8 Reporters** - console (colored table), JSON, CSV, JUnit XML, Markdown (pull request comments), self-contained HTML, GitHub Actions annotations and job summary, CI (exit code on failure), plus `reportComparison()` for comparisons
|
|
30
31
|
- **Vitest** - `describeEval` from `@cogitator-ai/evals/vitest` runs a suite as a test file, one test per case plus one for the assertions, and `evalMatchers` from `@cogitator-ai/evals/matchers` (Vitest 5) adds `toHaveCalledTool`, `toFollowTrajectory`, `toHaveRetrieved` and `toScoreAtLeast`
|
|
31
32
|
- **Builder API** — Fluent `EvalBuilder` for composable eval pipelines
|
|
32
33
|
- **Baseline Workflow** — Save baselines, compare against them, catch regressions in CI
|
|
@@ -86,7 +87,7 @@ const suite = new EvalSuite({
|
|
|
86
87
|
|
|
87
88
|
`suite.run(options?: EvalRunOptions)` takes `{ maxCases?: number, signal?: AbortSignal }`: with `maxCases` only the first N cases of the dataset run, and the results, aggregates, assertions and `stats.total` cover just those cases. Aborting `signal` cancels the attempts in flight, starts no further case, and `run()` rejects with the signal's reason.
|
|
88
89
|
|
|
89
|
-
A case whose every attempt failed is not scored: its empty output costs no judge calls and counts as no 0, the aggregates cover the cases that answered (`aggregated.<metric>.count` says how many), and `maxErrorRate(rate)` fails a run where too many cases errored. An agent target's case result carries `truncated`, `iterationLimitReached` and `paused` when the run ended that way.
|
|
90
|
+
A case whose every attempt failed is not scored: its empty output costs no judge calls and counts as no 0, the aggregates cover the cases that answered (`aggregated.<metric>.count` says how many), and `maxErrorRate(rate)` fails a run where too many cases errored. An agent target's case result carries `truncated`, `iterationLimitReached` and `paused` when the run ended that way (`paused` for a run waiting on tool approvals or on a question one of its tools asked with `context.interrupt()`).
|
|
90
91
|
|
|
91
92
|
A failure never drops a case. A metric that throws or returns a score that is not a finite number (such as NaN) scores 0 for that case with the reason in `score.error`, and an `onProgress` callback that throws is reported as a process warning.
|
|
92
93
|
|
|
@@ -574,14 +575,16 @@ Access full suite results via `result.baseline` and `result.challenger`. `compar
|
|
|
574
575
|
|
|
575
576
|
Call `result.report()` after a suite run to output results.
|
|
576
577
|
|
|
577
|
-
| Reporter
|
|
578
|
-
|
|
|
579
|
-
| `console`
|
|
580
|
-
| `json`
|
|
581
|
-
| `csv`
|
|
582
|
-
| `junit`
|
|
583
|
-
| `github`
|
|
584
|
-
| `ci`
|
|
578
|
+
| Reporter | Output |
|
|
579
|
+
| ---------- | --------------------------------------------------------------------- |
|
|
580
|
+
| `console` | Colored table with metrics, assertions, summary |
|
|
581
|
+
| `json` | Writes `eval-report.json` (configurable path) |
|
|
582
|
+
| `csv` | Writes `eval-report.csv` (configurable path) |
|
|
583
|
+
| `junit` | Writes `eval-report.xml`, a JUnit report (configurable path) |
|
|
584
|
+
| `github` | GitHub Actions annotations, plus a job summary in Actions |
|
|
585
|
+
| `ci` | Compact verdict, sets `process.exitCode = 1` when an assertion fails |
|
|
586
|
+
| `markdown` | Writes `eval-report.md`, a compact summary for a pull request comment |
|
|
587
|
+
| `html` | Writes `eval-report.html`, a self-contained page for light and dark |
|
|
585
588
|
|
|
586
589
|
```typescript
|
|
587
590
|
const result = await suite.run();
|
|
@@ -592,9 +595,11 @@ result.report(['console', 'json', 'csv', 'junit'], { path: './reports/eval' });
|
|
|
592
595
|
result.report(['github', 'ci']);
|
|
593
596
|
```
|
|
594
597
|
|
|
595
|
-
The `path` is a base path: each file reporter adds its own extension (`.json`, `.csv`, `.xml`). `ci` runs last and does not end the process, so output flushes and later hooks still run before Node exits with code 1.
|
|
598
|
+
The `path` is a base path: each file reporter adds its own extension (`.json`, `.csv`, `.xml`, `.md`, `.html`). `ci` runs last and does not end the process, so output flushes and later hooks still run before Node exits with code 1.
|
|
596
599
|
|
|
597
600
|
- **junit** writes one `<testsuite name="eval">` with a `<testcase>` per case (a `<failure>` when it errored, its scores in `<system-out>`) and a `<testcase>` per assertion (a `<failure>` when it did not pass). Everything is XML-escaped, and characters XML 1.0 does not allow are replaced.
|
|
601
|
+
- **markdown** writes the totals (with the trials per case when there are several), each metric's mean and cases, the assertions with pass or fail, and the failures of every case and trial. Every value is escaped, so no answer breaks the tables.
|
|
602
|
+
- **html** writes one page with inline CSS and no scripts or external assets: totals, metrics, assertions and every case with its scores, status, turns, trials, trace id and prompt version. Every value is HTML-escaped.
|
|
598
603
|
- **github** prints `::error` for each failed assertion and errored case and `::warning` for each failed metric score. When `GITHUB_STEP_SUMMARY` is set it also appends a Markdown summary with the totals, the aggregated metrics (mean, p50, p95, cases scored) and the assertions.
|
|
599
604
|
|
|
600
605
|
Agent run flags (`truncated`, `iterationLimitReached`, `paused`) show up per case in the json, csv and junit reports, and each aggregated metric's `count` (cases it was scored on) in the console, json and github output. The json report keeps each case's `toolCalls`, `toolResults`, `retrieved`, `turns` and `trials`, and the console and github reports show the trials per case.
|
|
@@ -636,6 +641,41 @@ it('books a table', async () => {
|
|
|
636
641
|
|
|
637
642
|
---
|
|
638
643
|
|
|
644
|
+
## Experiment Tracking
|
|
645
|
+
|
|
646
|
+
Give a suite a `name` and JSON `metadata`, save each run as a run file, and compare runs later, across commits or prompt versions.
|
|
647
|
+
|
|
648
|
+
```typescript
|
|
649
|
+
import { EvalSuite, RunStore, compareRuns, reportComparison } from '@cogitator-ai/evals';
|
|
650
|
+
|
|
651
|
+
const suite = new EvalSuite({
|
|
652
|
+
dataset,
|
|
653
|
+
target: { agent, cogitator },
|
|
654
|
+
metrics: [exactMatch(), faithfulness()],
|
|
655
|
+
judge: { model: 'google/gemini-3.5-flash-lite', cogitator },
|
|
656
|
+
name: 'support-agent',
|
|
657
|
+
metadata: { branch: process.env.GITHUB_HEAD_REF, commit: process.env.GITHUB_SHA },
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
const result = await suite.run();
|
|
661
|
+
const store = new RunStore('./eval-runs');
|
|
662
|
+
const base = store.latest({ name: 'support-agent' });
|
|
663
|
+
store.save(result); // ./eval-runs/<runId>.json, as result.saveRun('./eval-runs') writes it
|
|
664
|
+
|
|
665
|
+
if (base) {
|
|
666
|
+
const comparison = compareRuns(base, result, { seed: 42 });
|
|
667
|
+
reportComparison(comparison, ['console', 'markdown'], { path: './reports/comparison' });
|
|
668
|
+
}
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
- **Run files.** `result.saveRun(target)` writes JSON with `format: 'cogitator.eval-run'` and `version: 1`: `runId`, `startedAt` and `finishedAt`, `name`, `metadata`, the dataset fingerprint (SHA-256 of the canonical JSON of the cases run), the config (metric names, judge model, temperature and samples, trials, concurrency, target kind, agent name and model), the prompt versions used, every case result with its `caseKey`, the aggregates, assertions and stats. A directory target (or one ending in `/`) gets `<runId>.json`. Functions, Cogitator instances and keys are never written. `result.toRun()` returns the same object, and `loadRun(path)` reads one back, validated with Zod.
|
|
672
|
+
- **Traces and prompts.** Agent cases keep the `traceId` of their run (`RunResult.trace.traceId`) and the versioned instructions in `prompt` (`{ key, versionId?, version?, abTest? }`, set when the Cogitator versions instructions with `cogitator.prompts`). Each turn of a multi-turn case keeps its own and the case keeps the last turn's, each trial its own.
|
|
673
|
+
- **RunStore.** `new RunStore(dir, { onSkip? })` keeps run files in a directory: `save(result | run)`, `list(filter?)` newest first with id, name, metadata, times and metric means, `load(id)`, and `latest(filter?)`, where a filter is `{ name }` or a predicate. `list()` and `latest()` skip `.json` entries they cannot use (a directory, a file that is not JSON, a run file of another version or edited by hand) and report each to `onSkip`, a process warning by default, while `load(id)` throws for them. A run is validated before it is written, so a hand-made `runId` cannot lead outside the directory.
|
|
674
|
+
- **compareRuns(base, head, { alpha?, test?, seed?, metrics? }).** Cases are matched by `caseKey` (SHA-256 of `{ input, turns, expected }`), the others listed in `added` and `removed`. Each metric both runs have gets the statistics of `EvalComparison`: means, `difference`, `ci`, `effectSize`, `pValue`, `adjustedPValue` (Holm across the compared metrics), `significant` and `winner` (`head`, `base` or `tie`), plus the `improved` and `regressed` cases. An errored side counts as 0, a case both runs errored on is dropped. `warnings` say when the datasets, the prompt versions, the metrics or the judge differ, and name a metric left out because no matched case was scored on it.
|
|
675
|
+
- **reportComparison(comparison, 'markdown' | 'html' | 'console', { path? }).** Renders a `compareRuns()` or `EvalComparison` result: the winner, a metric table with difference, CI, p and adjusted p and significance, the regressions and improvements, and the warnings. The default path is `eval-comparison`.
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
639
679
|
## Builder API
|
|
640
680
|
|
|
641
681
|
`EvalBuilder` provides a fluent interface for constructing eval suites.
|
|
@@ -720,6 +760,10 @@ result.report('ci');
|
|
|
720
760
|
| `EvalComparison` | A/B testing runner with statistical significance |
|
|
721
761
|
| `calibrateJudge` | Agreement of a judge metric with labelled examples |
|
|
722
762
|
| `EvalBuilder` | Fluent builder for EvalSuite |
|
|
763
|
+
| `compareRuns` | Compares two saved runs or suite results case by case |
|
|
764
|
+
| `RunStore` | Directory of run files: save, list, load, latest |
|
|
765
|
+
| `loadRun` | Reads and validates a run file |
|
|
766
|
+
| `caseKey` | The key a case is matched by across runs |
|
|
723
767
|
| `Dataset` | Immutable dataset with from/fromJsonl/fromCsv/filter/sample/shuffle |
|
|
724
768
|
| `loadJsonl` | Low-level JSONL file loader |
|
|
725
769
|
| `loadCsv` | Low-level CSV file loader |
|
|
@@ -767,25 +811,30 @@ result.report('ci');
|
|
|
767
811
|
|
|
768
812
|
### Reporters
|
|
769
813
|
|
|
770
|
-
| Export
|
|
771
|
-
|
|
|
772
|
-
| `report`
|
|
773
|
-
| `
|
|
774
|
-
| `
|
|
814
|
+
| Export | Description |
|
|
815
|
+
| -------------------------------------- | ------------------------------------------------ |
|
|
816
|
+
| `report` | Dispatch to one or more reporter types |
|
|
817
|
+
| `reportComparison` | Render a comparison as console, Markdown or HTML |
|
|
818
|
+
| `comparisonMarkdown`, `comparisonHtml` | The Markdown or HTML of a comparison as a string |
|
|
819
|
+
| `describeEval` (`/vitest` entry) | Run a suite as a Vitest describe block |
|
|
820
|
+
| `evalMatchers` (`/matchers` entry) | Matchers for `expect.extend()`, Vitest 5 |
|
|
775
821
|
|
|
776
|
-
`report` takes `console`, `json`, `csv`, `junit`, `github` and `
|
|
822
|
+
`report` takes `console`, `json`, `csv`, `junit`, `github`, `ci`, `markdown` and `html`. `describeEval` takes `DescribeEvalOptions` and returns a `DescribeEvalHandle`.
|
|
777
823
|
|
|
778
824
|
### Types
|
|
779
825
|
|
|
780
|
-
| Export | Description
|
|
781
|
-
| ------------------ |
|
|
782
|
-
| `JudgeConfigInput` | The `judge` option: model, settings, `cogitator`, `cache`, `concurrency`
|
|
783
|
-
| `JudgeCache` | Where verdicts are kept across runs: `get(key)` and `set(key, verdict)`
|
|
784
|
-
| `AggregatedMetric` | Aggregate of a metric, with `count` for the cases it covers
|
|
785
|
-
| `MetricComparison` | A metric of a comparison, with `n` for the cases it was compared on
|
|
786
|
-
| `ExpectedToolCall` | A tool call a case expects: `name` and optional `arguments`
|
|
787
|
-
| `Passage` | A retrieved passage: `id?`, `content`, `score?`, `source?`
|
|
788
|
-
| `EvalTurnResult` | A turn of a multi-turn case: `input`, `output`, `toolCalls?`, `error?`
|
|
826
|
+
| Export | Description |
|
|
827
|
+
| ------------------ | --------------------------------------------------------------------------------------------- |
|
|
828
|
+
| `JudgeConfigInput` | The `judge` option: model, settings, `cogitator`, `cache`, `concurrency` |
|
|
829
|
+
| `JudgeCache` | Where verdicts are kept across runs: `get(key)` and `set(key, verdict)` |
|
|
830
|
+
| `AggregatedMetric` | Aggregate of a metric, with `count` for the cases it covers |
|
|
831
|
+
| `MetricComparison` | A metric of a comparison, with `n` for the cases it was compared on |
|
|
832
|
+
| `ExpectedToolCall` | A tool call a case expects: `name` and optional `arguments` |
|
|
833
|
+
| `Passage` | A retrieved passage: `id?`, `content`, `score?`, `source?` |
|
|
834
|
+
| `EvalTurnResult` | A turn of a multi-turn case: `input`, `output`, `toolCalls?`, `traceId?`, `prompt?`, `error?` |
|
|
835
|
+
| `EvalPrompt` | Versioned instructions of an agent run: `key`, `versionId?`, `version?`, `abTest?` |
|
|
836
|
+
| `EvalRun` | A saved run, as `loadRun()` returns it |
|
|
837
|
+
| `RunComparison` | What `compareRuns()` returns |
|
|
789
838
|
|
|
790
839
|
### Statistics
|
|
791
840
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { EvalComparisonConfig } from './schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* The test a comparison ran: McNemar's (exact below 25 discordant pairs, chi-square with
|
|
4
|
+
* continuity correction from 25), the paired t-test, Wilcoxon signed-rank, or none with fewer
|
|
5
|
+
* than 2 cases
|
|
6
|
+
*/
|
|
7
|
+
export type ComparisonTest = 'mcnemar' | 'mcnemar-exact' | 't' | 'wilcoxon' | 'none';
|
|
8
|
+
export type ComparisonWinner = 'baseline' | 'challenger' | 'tie';
|
|
9
|
+
export interface MetricComparison {
|
|
10
|
+
/** Mean score over the compared cases, NaN when `n` is 0 */
|
|
11
|
+
baseline: number;
|
|
12
|
+
challenger: number;
|
|
13
|
+
/** Cases compared: those at least one side scored, an errored side counting as a 0 */
|
|
14
|
+
n: number;
|
|
15
|
+
/** Mean of the paired differences, challenger minus baseline, NaN when `n` is 0 */
|
|
16
|
+
difference: number;
|
|
17
|
+
/** Percentile bootstrap confidence interval of `difference` at `1 - alpha` */
|
|
18
|
+
ci: {
|
|
19
|
+
low: number;
|
|
20
|
+
high: number;
|
|
21
|
+
};
|
|
22
|
+
/** Cohen's d of the paired differences (dz), NaN with fewer than 2 cases */
|
|
23
|
+
effectSize: number;
|
|
24
|
+
test: ComparisonTest;
|
|
25
|
+
/** The test's own p-value */
|
|
26
|
+
pValue: number;
|
|
27
|
+
/** `pValue` after Holm's correction across the compared metrics */
|
|
28
|
+
adjustedPValue: number;
|
|
29
|
+
/** `adjustedPValue` is below `alpha` */
|
|
30
|
+
significant: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The higher side when significant, by the test's direction: the sign of `difference`, except
|
|
33
|
+
* for Wilcoxon's test, where it is the side with the larger rank sum and may differ from it
|
|
34
|
+
*/
|
|
35
|
+
winner: ComparisonWinner;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Latency or cost of the two sides, paired over the cases both answered, where lower is better.
|
|
39
|
+
* Not corrected for multiple comparisons and not part of the metrics' vote on the winner
|
|
40
|
+
*/
|
|
41
|
+
export interface ResourceComparison {
|
|
42
|
+
/** Mean per case, NaN when no case pairs up */
|
|
43
|
+
baseline: number;
|
|
44
|
+
challenger: number;
|
|
45
|
+
n: number;
|
|
46
|
+
/** Challenger minus baseline: below 0 when the challenger is cheaper or faster */
|
|
47
|
+
difference: number;
|
|
48
|
+
test: ComparisonTest;
|
|
49
|
+
pValue: number;
|
|
50
|
+
significant: boolean;
|
|
51
|
+
/** The lower side when significant, by the test's direction as for `MetricComparison.winner` */
|
|
52
|
+
winner: ComparisonWinner;
|
|
53
|
+
}
|
|
54
|
+
/** The statistical settings of a comparison */
|
|
55
|
+
export type ComparisonStatsConfig = Pick<EvalComparisonConfig, 'alpha' | 'test' | 'seed'>;
|
|
56
|
+
/** A case result as the comparison reads it: its error, its scores and its trials */
|
|
57
|
+
export interface ScoredTrial {
|
|
58
|
+
error?: string;
|
|
59
|
+
scores: ReadonlyArray<{
|
|
60
|
+
name: string;
|
|
61
|
+
score: number;
|
|
62
|
+
}>;
|
|
63
|
+
trials?: readonly ScoredTrial[];
|
|
64
|
+
}
|
|
65
|
+
/** The two sides of one case, either missing when that side has no such case */
|
|
66
|
+
export type CasePair<T extends ScoredTrial = ScoredTrial> = readonly [T | undefined, T | undefined];
|
|
67
|
+
/** How the score of a case moved from one side to the other */
|
|
68
|
+
export interface CaseChange {
|
|
69
|
+
/** Zero based position of the case in the pairs compared */
|
|
70
|
+
index: number;
|
|
71
|
+
/** The case input on one line, shortened */
|
|
72
|
+
input: string;
|
|
73
|
+
/** Score of the first side (the baseline, or the base run) */
|
|
74
|
+
before: number;
|
|
75
|
+
/** Score of the second side (the challenger, or the head run) */
|
|
76
|
+
after: number;
|
|
77
|
+
/** `after` minus `before` */
|
|
78
|
+
delta: number;
|
|
79
|
+
}
|
|
80
|
+
/** A score moves when it changes by more than this */
|
|
81
|
+
export declare const SCORE_EPSILON = 1e-9;
|
|
82
|
+
/**
|
|
83
|
+
* Compares each metric on its paired scores, then adjusts the p-values with Holm's correction
|
|
84
|
+
* across the metrics and decides each metric's winner
|
|
85
|
+
*/
|
|
86
|
+
export declare function compareMetrics(paired: ReadonlyMap<string, readonly [number[], number[]]>, config: ComparisonStatsConfig): Record<string, MetricComparison>;
|
|
87
|
+
export declare function compareResource(baseline: number[], challenger: number[], config: ComparisonStatsConfig): ResourceComparison;
|
|
88
|
+
/** The side more metrics name as their winner, a tie when as many name each */
|
|
89
|
+
export declare function determineOverallWinner(metrics: Record<string, Pick<MetricComparison, 'winner'>>): ComparisonWinner;
|
|
90
|
+
/** The names of the metrics any trial of the cases was scored on, in the order first seen */
|
|
91
|
+
export declare function scoredMetricNames(results: Iterable<ScoredTrial>): Set<string>;
|
|
92
|
+
/** A case's score of a metric: the mean over its trials the metric scored, none when it scored none */
|
|
93
|
+
export declare function caseScore(result: ScoredTrial | undefined, metricName: string): number | undefined;
|
|
94
|
+
/** The scores of a metric for each pair, side by side, as `pairedCaseScores` pairs them */
|
|
95
|
+
export declare function pairedScores(pairs: readonly CasePair[], metricName: string): [number[], number[]];
|
|
96
|
+
/**
|
|
97
|
+
* The cases whose score of a metric rose or fell from the first side to the second by more than
|
|
98
|
+
* `SCORE_EPSILON`, the largest moves first
|
|
99
|
+
*/
|
|
100
|
+
export declare function caseChanges(pairs: readonly CasePair[], metricName: string, inputOf: (index: number) => string): {
|
|
101
|
+
improved: CaseChange[];
|
|
102
|
+
regressed: CaseChange[];
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=comparison-stats.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comparison-stats.d.ts","sourceRoot":"","sources":["../src/comparison-stats.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAWrD;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,eAAe,GAAG,GAAG,GAAG,UAAU,GAAG,MAAM,CAAC;AAErF,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,YAAY,GAAG,KAAK,CAAC;AAEjE,MAAM,WAAW,gBAAgB;IAC/B,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,sFAAsF;IACtF,CAAC,EAAE,MAAM,CAAC;IACV,mFAAmF;IACnF,UAAU,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,EAAE,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAClC,4EAA4E;IAC5E,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,cAAc,CAAC;IACrB,6BAA6B;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,cAAc,EAAE,MAAM,CAAC;IACvB,wCAAwC;IACxC,WAAW,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,CAAC,EAAE,MAAM,CAAC;IACV,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,OAAO,CAAC;IACrB,gGAAgG;IAChG,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAED,+CAA+C;AAC/C,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,oBAAoB,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC,CAAC;AAE1F,qFAAqF;AACrF,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvD,MAAM,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;CACjC;AAED,gFAAgF;AAChF,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,IAAI,SAAS,CAAC,CAAC,GAAG,SAAS,EAAE,CAAC,GAAG,SAAS,CAAC,CAAC;AAEpG,+DAA+D;AAC/D,MAAM,WAAW,UAAU;IACzB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,6BAA6B;IAC7B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,sDAAsD;AACtD,eAAO,MAAM,aAAa,OAAO,CAAC;AAgFlC;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,EAC1D,MAAM,EAAE,qBAAqB,GAC5B,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAmBlC;AAED,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAAE,EAClB,UAAU,EAAE,MAAM,EAAE,EACpB,MAAM,EAAE,qBAAqB,GAC5B,kBAAkB,CAcpB;AAED,+EAA+E;AAC/E,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CAAC,GACxD,gBAAgB,CAYlB;AAED,6FAA6F;AAC7F,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAQ7E;AAED,uGAAuG;AACvG,wBAAgB,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUjG;AAqBD,2FAA2F;AAC3F,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,CAGjG;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,SAAS,QAAQ,EAAE,EAC1B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GACjC;IAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;IAAC,SAAS,EAAE,UAAU,EAAE,CAAA;CAAE,CAYrD"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { pairedTTest } from './stats/t-test.js';
|
|
2
|
+
import { mcnemarExact, mcnemarsTest } from './stats/mcnemar.js';
|
|
3
|
+
import { wilcoxonSignedRank } from './stats/wilcoxon.js';
|
|
4
|
+
import { bootstrapCI } from './stats/bootstrap.js';
|
|
5
|
+
import { cohensD } from './stats/effect-size.js';
|
|
6
|
+
import { holm } from './stats/holm.js';
|
|
7
|
+
import { mean } from './stats/percentiles.js';
|
|
8
|
+
import { pairedDifference } from './stats/precision.js';
|
|
9
|
+
import { truncateInput } from './reporters/format.js';
|
|
10
|
+
/** A score moves when it changes by more than this */
|
|
11
|
+
export const SCORE_EPSILON = 1e-9;
|
|
12
|
+
function isBinary(scores) {
|
|
13
|
+
return scores.every((s) => s === 0 || s === 1);
|
|
14
|
+
}
|
|
15
|
+
/** Below this many discordant pairs McNemar's test is exact */
|
|
16
|
+
const EXACT_MCNEMAR_BELOW = 25;
|
|
17
|
+
function pairedTest(baseline, challenger, continuous, binaryAllowed) {
|
|
18
|
+
const direction = Math.sign(mean(differencesOf(baseline, challenger)));
|
|
19
|
+
if (baseline.length < 2)
|
|
20
|
+
return { test: 'none', pValue: 1, direction };
|
|
21
|
+
if (binaryAllowed && isBinary(baseline) && isBinary(challenger)) {
|
|
22
|
+
let b = 0;
|
|
23
|
+
let c = 0;
|
|
24
|
+
for (let i = 0; i < baseline.length; i++) {
|
|
25
|
+
if (baseline[i] === 1 && challenger[i] === 0)
|
|
26
|
+
b++;
|
|
27
|
+
if (baseline[i] === 0 && challenger[i] === 1)
|
|
28
|
+
c++;
|
|
29
|
+
}
|
|
30
|
+
return b + c < EXACT_MCNEMAR_BELOW
|
|
31
|
+
? { test: 'mcnemar-exact', pValue: mcnemarExact(b, c).pValue, direction }
|
|
32
|
+
: { test: 'mcnemar', pValue: mcnemarsTest(b, c).pValue, direction };
|
|
33
|
+
}
|
|
34
|
+
if (continuous === 'wilcoxon') {
|
|
35
|
+
const { statistic, n, pValue } = wilcoxonSignedRank(baseline, challenger);
|
|
36
|
+
return { test: 'wilcoxon', pValue, direction: Math.sign((n * (n + 1)) / 4 - statistic) };
|
|
37
|
+
}
|
|
38
|
+
return { test: 't', pValue: pairedTTest(baseline, challenger).pValue, direction };
|
|
39
|
+
}
|
|
40
|
+
/** Challenger minus baseline per case, without float noise */
|
|
41
|
+
function differencesOf(baseline, challenger) {
|
|
42
|
+
return challenger.map((score, i) => pairedDifference(score, baseline[i]));
|
|
43
|
+
}
|
|
44
|
+
function meanOrNaN(values) {
|
|
45
|
+
return values.length === 0 ? Number.NaN : mean(values);
|
|
46
|
+
}
|
|
47
|
+
function compareMetric(name, baselineScores, challengerScores, config) {
|
|
48
|
+
const differences = differencesOf(baselineScores, challengerScores);
|
|
49
|
+
const { low, high } = bootstrapCI(differences, {
|
|
50
|
+
confidence: 1 - config.alpha,
|
|
51
|
+
...(config.seed !== undefined && { seed: `${String(config.seed)}:${name}` }),
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
baseline: meanOrNaN(baselineScores),
|
|
55
|
+
challenger: meanOrNaN(challengerScores),
|
|
56
|
+
n: baselineScores.length,
|
|
57
|
+
difference: meanOrNaN(differences),
|
|
58
|
+
ci: { low, high },
|
|
59
|
+
effectSize: cohensD(differences),
|
|
60
|
+
...pairedTest(baselineScores, challengerScores, config.test, true),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Compares each metric on its paired scores, then adjusts the p-values with Holm's correction
|
|
65
|
+
* across the metrics and decides each metric's winner
|
|
66
|
+
*/
|
|
67
|
+
export function compareMetrics(paired, config) {
|
|
68
|
+
const names = [...paired.keys()];
|
|
69
|
+
const compared = names.map((name) => {
|
|
70
|
+
const [baselineScores, challengerScores] = paired.get(name);
|
|
71
|
+
return compareMetric(name, baselineScores, challengerScores, config);
|
|
72
|
+
});
|
|
73
|
+
const adjusted = holm(compared.map((metric) => metric.pValue));
|
|
74
|
+
const decided = {};
|
|
75
|
+
for (const [at, name] of names.entries()) {
|
|
76
|
+
const { direction, ...metric } = compared[at];
|
|
77
|
+
const significant = adjusted[at] < config.alpha && direction !== 0;
|
|
78
|
+
decided[name] = {
|
|
79
|
+
...metric,
|
|
80
|
+
adjustedPValue: adjusted[at],
|
|
81
|
+
significant,
|
|
82
|
+
winner: !significant ? 'tie' : direction > 0 ? 'challenger' : 'baseline',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return decided;
|
|
86
|
+
}
|
|
87
|
+
export function compareResource(baseline, challenger, config) {
|
|
88
|
+
const difference = meanOrNaN(differencesOf(baseline, challenger));
|
|
89
|
+
const { test, pValue, direction } = pairedTest(baseline, challenger, config.test, false);
|
|
90
|
+
const significant = pValue < config.alpha && direction !== 0;
|
|
91
|
+
return {
|
|
92
|
+
baseline: meanOrNaN(baseline),
|
|
93
|
+
challenger: meanOrNaN(challenger),
|
|
94
|
+
n: baseline.length,
|
|
95
|
+
difference,
|
|
96
|
+
test,
|
|
97
|
+
pValue,
|
|
98
|
+
significant,
|
|
99
|
+
winner: !significant ? 'tie' : direction < 0 ? 'challenger' : 'baseline',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** The side more metrics name as their winner, a tie when as many name each */
|
|
103
|
+
export function determineOverallWinner(metrics) {
|
|
104
|
+
let baselineWins = 0;
|
|
105
|
+
let challengerWins = 0;
|
|
106
|
+
for (const mc of Object.values(metrics)) {
|
|
107
|
+
if (mc.winner === 'baseline')
|
|
108
|
+
baselineWins++;
|
|
109
|
+
if (mc.winner === 'challenger')
|
|
110
|
+
challengerWins++;
|
|
111
|
+
}
|
|
112
|
+
if (challengerWins > baselineWins)
|
|
113
|
+
return 'challenger';
|
|
114
|
+
if (baselineWins > challengerWins)
|
|
115
|
+
return 'baseline';
|
|
116
|
+
return 'tie';
|
|
117
|
+
}
|
|
118
|
+
/** The names of the metrics any trial of the cases was scored on, in the order first seen */
|
|
119
|
+
export function scoredMetricNames(results) {
|
|
120
|
+
const names = new Set();
|
|
121
|
+
for (const r of results) {
|
|
122
|
+
for (const trial of r.trials ?? [r]) {
|
|
123
|
+
for (const s of trial.scores)
|
|
124
|
+
names.add(s.name);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return names;
|
|
128
|
+
}
|
|
129
|
+
/** A case's score of a metric: the mean over its trials the metric scored, none when it scored none */
|
|
130
|
+
export function caseScore(result, metricName) {
|
|
131
|
+
if (!result)
|
|
132
|
+
return undefined;
|
|
133
|
+
const trials = result.trials ?? [result];
|
|
134
|
+
if (trials.every((trial) => trial.error !== undefined))
|
|
135
|
+
return undefined;
|
|
136
|
+
const scores = trials.flatMap((trial) => trial.error !== undefined
|
|
137
|
+
? [0]
|
|
138
|
+
: trial.scores.filter((s) => s.name === metricName).map((s) => s.score));
|
|
139
|
+
return scores.length === 0 ? undefined : mean(scores);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The scores of a metric for each pair, side by side, with the index of the pair. A case one side
|
|
143
|
+
* errored on counts as a 0 for that side, since a target that fails loses the case, and a case
|
|
144
|
+
* both sides errored on is left out, telling nothing about either
|
|
145
|
+
*/
|
|
146
|
+
function pairedCaseScores(pairs, metricName) {
|
|
147
|
+
const scored = [];
|
|
148
|
+
for (const [index, [first, second]] of pairs.entries()) {
|
|
149
|
+
const before = caseScore(first, metricName);
|
|
150
|
+
const after = caseScore(second, metricName);
|
|
151
|
+
if (before === undefined && after === undefined)
|
|
152
|
+
continue;
|
|
153
|
+
scored.push({ index, before: before ?? 0, after: after ?? 0 });
|
|
154
|
+
}
|
|
155
|
+
return scored;
|
|
156
|
+
}
|
|
157
|
+
/** The scores of a metric for each pair, side by side, as `pairedCaseScores` pairs them */
|
|
158
|
+
export function pairedScores(pairs, metricName) {
|
|
159
|
+
const scored = pairedCaseScores(pairs, metricName);
|
|
160
|
+
return [scored.map((s) => s.before), scored.map((s) => s.after)];
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The cases whose score of a metric rose or fell from the first side to the second by more than
|
|
164
|
+
* `SCORE_EPSILON`, the largest moves first
|
|
165
|
+
*/
|
|
166
|
+
export function caseChanges(pairs, metricName, inputOf) {
|
|
167
|
+
const improved = [];
|
|
168
|
+
const regressed = [];
|
|
169
|
+
for (const { index, before, after } of pairedCaseScores(pairs, metricName)) {
|
|
170
|
+
const delta = pairedDifference(after, before);
|
|
171
|
+
if (Math.abs(delta) <= SCORE_EPSILON)
|
|
172
|
+
continue;
|
|
173
|
+
const change = { index, input: truncateInput(inputOf(index)), before, after, delta };
|
|
174
|
+
(delta > 0 ? improved : regressed).push(change);
|
|
175
|
+
}
|
|
176
|
+
const byMove = (a, b) => Math.abs(b.delta) - Math.abs(a.delta) || a.index - b.index;
|
|
177
|
+
return { improved: improved.sort(byMove), regressed: regressed.sort(byMove) };
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=comparison-stats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comparison-stats.js","sourceRoot":"","sources":["../src/comparison-stats.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,7 @@ import type { MetricFn, StatisticalMetricFn } from './metrics/types.js';
|
|
|
3
3
|
import type { JudgeConfigInput } from './schema.js';
|
|
4
4
|
import { Dataset } from './datasets/index.js';
|
|
5
5
|
import type { PairwiseOptions, PairwiseResult } from './pairwise.js';
|
|
6
|
+
import type { ComparisonWinner, MetricComparison, ResourceComparison } from './comparison-stats.js';
|
|
6
7
|
export interface EvalComparisonOptions {
|
|
7
8
|
dataset: Dataset;
|
|
8
9
|
targets: {
|
|
@@ -48,57 +49,7 @@ export interface EvalComparisonRunOptions {
|
|
|
48
49
|
/** Cancels both runs; `run()` rejects with the signal's reason */
|
|
49
50
|
signal?: AbortSignal;
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
-
* The test a comparison ran: McNemar's (exact below 25 discordant pairs, chi-square with
|
|
53
|
-
* continuity correction from 25), the paired t-test, Wilcoxon signed-rank, or none with fewer
|
|
54
|
-
* than 2 cases
|
|
55
|
-
*/
|
|
56
|
-
export type ComparisonTest = 'mcnemar' | 'mcnemar-exact' | 't' | 'wilcoxon' | 'none';
|
|
57
|
-
export type ComparisonWinner = 'baseline' | 'challenger' | 'tie';
|
|
58
|
-
export interface MetricComparison {
|
|
59
|
-
baseline: number;
|
|
60
|
-
challenger: number;
|
|
61
|
-
/** Cases compared: those at least one side scored, an errored side counting as a 0 */
|
|
62
|
-
n: number;
|
|
63
|
-
/** Mean of the paired differences, challenger minus baseline */
|
|
64
|
-
difference: number;
|
|
65
|
-
/** Percentile bootstrap confidence interval of `difference` at `1 - alpha` */
|
|
66
|
-
ci: {
|
|
67
|
-
low: number;
|
|
68
|
-
high: number;
|
|
69
|
-
};
|
|
70
|
-
/** Cohen's d of the paired differences (dz), NaN with fewer than 2 cases */
|
|
71
|
-
effectSize: number;
|
|
72
|
-
test: ComparisonTest;
|
|
73
|
-
/** The test's own p-value */
|
|
74
|
-
pValue: number;
|
|
75
|
-
/** `pValue` after Holm's correction across the compared metrics */
|
|
76
|
-
adjustedPValue: number;
|
|
77
|
-
/** `adjustedPValue` is below `alpha` */
|
|
78
|
-
significant: boolean;
|
|
79
|
-
/**
|
|
80
|
-
* The higher side when significant, by the test's direction: the sign of `difference`, except
|
|
81
|
-
* for Wilcoxon's test, where it is the side with the larger rank sum and may differ from it
|
|
82
|
-
*/
|
|
83
|
-
winner: ComparisonWinner;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Latency or cost of the two sides, paired over the cases both answered, where lower is better.
|
|
87
|
-
* Not corrected for multiple comparisons and not part of the metrics' vote on the winner
|
|
88
|
-
*/
|
|
89
|
-
export interface ResourceComparison {
|
|
90
|
-
/** Mean per case, NaN when no case pairs up */
|
|
91
|
-
baseline: number;
|
|
92
|
-
challenger: number;
|
|
93
|
-
n: number;
|
|
94
|
-
/** Challenger minus baseline: below 0 when the challenger is cheaper or faster */
|
|
95
|
-
difference: number;
|
|
96
|
-
test: ComparisonTest;
|
|
97
|
-
pValue: number;
|
|
98
|
-
significant: boolean;
|
|
99
|
-
/** The lower side when significant, by the test's direction as for `MetricComparison.winner` */
|
|
100
|
-
winner: ComparisonWinner;
|
|
101
|
-
}
|
|
52
|
+
export type { ComparisonTest, ComparisonWinner, MetricComparison, ResourceComparison, } from './comparison-stats.js';
|
|
102
53
|
export interface ComparisonResult {
|
|
103
54
|
summary: {
|
|
104
55
|
winner: ComparisonWinner;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eval-comparison.d.ts","sourceRoot":"","sources":["../src/eval-comparison.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,
|
|
1
|
+
{"version":3,"file":"eval-comparison.d.ts","sourceRoot":"","sources":["../src/eval-comparison.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAoB,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACvF,OAAO,KAAK,EAAwC,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAEvF,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAIrC,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAUlE,OAAO,KAAK,EAEV,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EACnB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE;QACP,QAAQ,EAAE,UAAU,CAAC;QACrB,UAAU,EAAE,UAAU,CAAC;KACxB,CAAC;IACF,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC;IACrB,kBAAkB,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC3C,gEAAgE;IAChE,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,IAAI,CAAC,EAAE,GAAG,GAAG,UAAU,CAAC;IACxB,4FAA4F;IAC5F,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACvF;AAED,MAAM,WAAW,wBAAwB;IACvC,kEAAkE;IAClE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,YAAY,EACV,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE;QACP,MAAM,EAAE,gBAAgB,CAAC;QACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;QAC1C,yEAAyE;QACzE,OAAO,EAAE,kBAAkB,CAAC;QAC5B,sFAAsF;QACtF,IAAI,CAAC,EAAE,kBAAkB,CAAC;KAC3B,CAAC;IACF,QAAQ,EAAE,eAAe,CAAC;IAC1B,UAAU,EAAE,eAAe,CAAC;IAC5B,gEAAgE;IAChE,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAwB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAC9C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAe;gBAElC,IAAI,EAAE,qBAAqB;IAiCjC,GAAG,CAAC,OAAO,GAAE,wBAA6B,GAAG,OAAO,CAAC,gBAAgB,CAAC;CA8F7E"}
|