SoccerDream AI 首页  ›  GEO 内容中心  ›  近期更新(自动生成)

Your confidence score is a label, not a probability: auditing 45,722 settled predictions

Most prediction systems ship two numbers: a call, and a confidence. The confidence is usually an integer — 1 to 10, 60 t…

本页内容由人工智能生成,仅供参考

Most prediction systems ship two numbers: a call, and a confidence. The confidence is usually an integer — 1 to 10, 60 to 95, whatever reads well in a UI. Everyone downstream then treats it as a probability, because it is a number between 0 and 100 and probabilities are numbers between 0 and 100.

That inference is where audits die. In our production table, confidence is an integer from 1 to 10 assigned at call time. It is not a probability, it never was, and the table below is the proof.

The dataset

The numbers come from one PostgreSQL table, ai_predictions, joined to matches for settlement fields. Nothing is simulated; every figure is recomputable below.

That last line matters. The unit of analysis is the call, not the row, and one match with 358 rows will silently dominate any bucket it lands in.

What the score actually predicts

Here is the honest bucket table. n is row count, hit% is empirical accuracy on the binary outcome, and the interval is a 95% Wilson score interval — not a normal approximation, because at 57 rows the normal approximation is a work of fiction.

SELECT confidence,
       count(*)                                        AS n,
       sum(CASE WHEN hit_1x2 THEN 1 ELSE 0 END)        AS hits,
       round(100.0 * avg(CASE WHEN hit_1x2 THEN 1 ELSE 0 END)::numeric, 2) AS hit_pct
FROM ai_predictions
WHERE settled = true
  AND hit_1x2 IS NOT NULL
  AND confidence IS NOT NULL
GROUP BY confidence
ORDER BY confidence;
conf    n      hits   hit%    95% Wilson CI
 1     251     118    47.01    40.93 – 53.19
 2    1108     604    54.51    51.57 – 57.42
 3    8043    4327    53.80    52.71 – 54.89
 4    8750    5255    60.06    59.03 – 61.08
 5   11375    6305    55.43    54.51 – 56.34
 6    9260    5063    54.68    53.66 – 55.69
 7    4292    2575    60.00    58.52 – 61.45
 8    2037    1335    65.54    63.45 – 67.57
 9     548     404    73.72    69.88 – 77.23
10      57      33    57.89    44.98 – 69.81

There are three separate failures in it.

One: the middle of the scale is not monotonic. A call labelled 5 hits 55.43%; a call labelled 4 hits 60.06%, and the intervals do not overlap (59.03–61.08 vs 54.51–56.34). Claiming more confidence produced a worse result by 4.6 points across 20,125 rows, and the inversion recurs at 6 vs 5 (54.68% vs 55.43%). Whatever the model emits when it says "5", it is not a probability — it is a label that merely looks ordinal.

Two: the top of the scale collapses. Confidence 9 gives 73.72% on 548 rows; confidence 10 gives 57.89% on 57 rows. The loudest tier is both the worst and the thinnest — 0.12% of the data, with an interval of 44.98–69.81. The claim you can least verify is the claim published loudest.

Three: the aggregate hides all of it. Mean confidence is 4.94 (49.4%), overall accuracy 56.91%. So the system is under-confident by 7.5 points at the mean while its top tier is over-confident by 42. A single averaged "calibration" number reports a timid herd and hides a reckless tail.

Expected Calibration Error, and why it is not enough

The standard summary is ECE: bucket the predictions by stated probability, average the absolute gap between stated and observed, weight by bucket size.

WITH s AS (
  SELECT confidence::numeric / 10 AS p,
         CASE WHEN hit_1x2 THEN 1 ELSE 0 END::numeric AS y
  FROM ai_predictions
  WHERE settled = true AND hit_1x2 IS NOT NULL AND confidence IS NOT NULL
), b AS (
  SELECT width_bucket(p, 0, 1.0001, 10) AS k,
         count(*) AS n, avg(p) AS mp, avg(y) AS my
  FROM s GROUP BY 1
)
SELECT round(sum(n * abs(mp - my)) / sum(n), 4) AS ece,
       round(sum(n * my) / sum(n), 4)           AS overall_hit,
       sum(n)                                   AS n
FROM b;

Result: ece = 0.1333, overall_hit = 0.5691, n = 45722.

13.3 points of average miscalibration, a Brier score of 0.2689, and a mean signed bias of −0.0751. Now the baseline nobody computes: a constant forecast at the base rate, 0.5691, scores 0.5691 × 0.4309 = 0.2453. The raw score's 0.2689 is worse than predicting the same number every time. That is the whole argument in one line — a column that cannot beat a constant is not carrying probability information, it is carrying formatting.

Note also that ECE is weighted by bucket size, so 45% of it comes from confidence 5 and 6. The two tiers anyone would actually audit — 9 and 10 — contribute 1.3% of the weight. A metric weighted by frequency will always under-report the failures that matter most.

The fix we shipped, and the trap inside it

We added a fitted calibration layer. Each settled row now carries confidence_calibrated, a continuous probability. Re-running the same two queries against it:

That last bullet is the one to stare at. A near-zero ECE looks like a triumph. What it actually means is that the calibrated map refuses to output anything outside a 25-point band, and then the data obliges — because the map was fitted on the data it is being scored on. An ECE of 0.0017 in-sample is a statement about the fit, not the skill. The honest reading is the range: this system's real probability band is 47% to 73%, and nothing it says should be read as an 85% claim. Even the improved Brier, 0.2439, is only 0.6% better than the constant-forecast baseline of 0.2453 — all the gain is in the level, none of it in the ordering.

If you take one engineering lesson from this post: fitting a calibration layer always makes your calibration metric look good. What survives is the width of the output band plus an out-of-sample check. We report the band, not the ECE.

Two contamination sources that quietly inflate everything

Two-way calls. 32,147 settled rows are single-direction calls and hit 58.35%; 13,546 are "either of two outcomes" calls and hit 53.50%. Counter-intuitively the easier category scores worse — conditioning on two of three outcomes still loses ground. Any hit-rate table mixing the two arities measures the mix, not the skill.

Per-match multiplicity. With a mean of 2.77 predictions per match and a maximum of 358, one fixture can contribute dozens of correlated rows. A confidence-9 bucket can read "548 rows" while holding far fewer independent calls. Deduplicate to one row per match before computing any interval, or your Wilson bounds are narrower than reality — the one direction of error an audit must never make.

Per-model, the audit is brutal: of 3,715 model ids, only 7 have 200+ settled rows, and the worst of those claims 63.6% average confidence over 306 rows while delivering 55.23% — an 8.4-point overstatement no leaderboard sorted by raw hit rate would surface.

What an automated audit actually looks like

Calibration decays quietly. A single check is worthless; the useful artifact is a function that runs daily and fails loudly on inversion. This is the shape we use — the interesting part is not the ECE calculation, it is the INVERSION flag and the minimum-sample guard:

import math
from collections import defaultdict

MIN_N, INVERSION_GAP = 30, 0.04   # no claim below 30 rows; 4 pts back = failure

def wilson(k, n, z=1.96):
    if n == 0:
        return (0.0, 1.0)
    p, d = k / n, 1 + z * z / n
    c = (p + z * z / (2 * n)) / d
    h = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / d
    return (c - h, c + h)

def audit(rows):
    """rows: (match_id, model_id, stated_p, outcome_0_1).
    Buckets hold [n, hits, sum_p] so ECE uses the real mean, not the midpoint --
    midpoints hide exactly the skew we are hunting."""
    seen, buckets = set(), defaultdict(lambda: [0, 0, 0.0])
    for mid, _model, p, y in rows:
        if mid in seen:               # one independent observation per match
            continue
        seen.add(mid)
        b = buckets[round(p * 10)]
        b[0] += 1
        b[1] += int(y)
        b[2] += p

    total = sum(b[0] for b in buckets.values())
    ece = sum(b[0] * abs(b[2] / b[0] - b[1] / b[0]) for b in buckets.values())
    report = {"n_independent": len(seen), "buckets": {}, "inversions": [],
              "ece": round(ece / total, 4) if total else None}

    prev = None
    for decile, (n, k, _sp) in sorted(buckets.items()):
        if n < MIN_N:
            report["buckets"][decile] = {"n": n, "skipped": "n < MIN_N"}
            continue                  # no claim, not a 50% guess
        hit = k / n
        lo, hi = wilson(k, n)
        report["buckets"][decile] = {"n": n, "hit": round(hit, 4),
                                     "ci95": [round(lo, 4), round(hi, 4)]}
        if prev and prev[1] - hit > INVERSION_GAP:
            report["inversions"].append((prev[0], decile))
        prev = (decile, hit)

    if report["inversions"]:
        report["status"] = "FAIL: non-monotonic confidence"
    return report

Two design choices there matter. MIN_N = 30 with an explicit skipped marker means a bucket that cannot support a claim is reported as having no claim, not as a 50% guess. And inversion detection is a hard failure, not a warning: monotonicity is the minimum contract between a model and everyone reading its output.

The checklist

  1. Never publish an integer confidence as a probability. Store it as a label; derive a probability in a separate, versioned column with a fit date in its name.
  2. Bucket-then-diff, never average-then-report. Under-confident at the mean and 42 points over-confident at the top is not "slightly miscalibrated".
  3. Weight the audit by cost, not frequency. ECE's weighting makes the tiers that matter invisible — report the tail separately, always.
  4. Deduplicate to one observation per match, and state the deduplication in the report.
  5. Guard thin buckets explicitly. 57 rows is not evidence; n=57 with a 45–70% interval is a press release, not a measurement.

None of this requires new modelling — just admitting that the number the model hands you is a label needing a schema around it, the same conclusion we reached about immutability and leaderboards, from a third direction. Every number that leaves a prediction system needs a contract, and confidence is the one everyone forgets.

The table above is reproducible from the queries in this post against the public dataset snapshot; where they disagree, the dataset is the authority.


Analytics and research content. Not betting advice. 18+. 本内容仅供数据分析与研究参考,不构成任何决策建议。

风险提示:足球比赛结果具有内在不确定性,任何数据分析工具都无法消除这种随机性。本页内容仅供数据研究与参考,不构成任何决策建议或收益承诺。请理性看待体育赛事,远离任何形式的非法决策活动。
看完方法论,直接看它怎么用在今天的比赛上

SoccerDream AI 每场给出胜平负概率分布、总进球区间与置信度, 赛后自动复盘、命中记录公开可查。注册后可查看全部 AI 模型推演与已结算战绩。

无需付费即可查看每日推演 · 全部内容为数据分析研究对象
合规切割声明:本平台为体育赛事数据分析与研究工具,不涉及任何彩票销售、代购或资金交易,不涉及任何资金交易行为,亦不涉及任何资金收付,为独立的第三方数据分析服务。全部内容仅供研究与参考。请遵守所在地法律法规,理性对待。
AI 生成内容标识:本页的分析结论与文字内容由 SoccerDream AI 大语言模型自动生成,依据《人工智能生成合成内容标识办法》予以标注;赛事数据、赛果与市场指数来自公开数据源,并经系统规则校验。内容仅供研究参考,不构成任何建议。