Skip to content

Fixture statistic calculations

Fixture statistics are deterministic projections of accepted cricket events. They are calculated when requested; there is no manually editable statistic total and no persisted cache in the Basic implementation.

Repeated-read cache

Issue #293 adds a bounded cache-aside projection for the public GET /api/v1/fixtures/{fixtureId}/statistics response without contributors. This is the repeated, expensive statistic path measured in the representative workload. Contributor traces are deliberately excluded: they are explicit audit requests, can be much larger, and are not a repeated browse path.

The cache key is sat:v2:fixture-statistics:fixture:{fixtureId}:v{dataVersion}. The API contract, fixture scope, and authoritative data version are therefore all part of the key; this public-only cache has no bearer token, account, or consumer identity in either keys or values. Entries contain only the same public response returned by the API and expire after 60 seconds.

fixture_statistics_cache_version is advanced in the same database transaction as an accepted direct submission, correction, batch publication, or Cricsheet match ingest (ingest since issue

592). An entry for a prior version becomes unreachable

immediately, and the transaction also removes it. Cache expiry is a recovery bound: if an unexpected writer misses version advancement, a later read derives the current PostgreSQL value within 60 seconds. PostgreSQL delivery rows remain authoritative; cache rows are disposable and are never edited as statistics.

For two identical public reads, the uncached path derives the fixture twice. The cache-aside path derives once, writes one versioned response, and serves the second request from its one-row cache lookup. apps/backend/tests/unit/fixture-statistics.service.test.ts asserts that source derivation is called once; the database correction test asserts that an existing version-41 entry is removed and the authoritative version advances to 42.

Because the cache is disposable and PostgreSQL is authoritative, neither cache operation may fail a response the authoritative path can still produce. A failed cache read falls through to derivation. A failed cache write leaves the derived response untouched and costs only a repeated derivation for the next reader. Issue #430 records why: the write is on the request's critical path, so awaiting it without this rule turns a fully derived fixture into a failed public read. apps/backend/tests/database/fixture-statistics.database.test.ts runs the cache's own SQL, asserting that a cold miss falls through rather than publishing an empty result, that the value a hit returns still satisfies the published contract after its jsonb round trip, and that advancing the version makes the stored entry unreachable.

Correction refresh dependencies

Issue #286 makes correction refresh behaviour explicit without changing that authoritative live-derivation model. A correction transaction records one fixture dependency and the participant aggregate dependencies whose inputs the corrected delivery feeds. The participant set is the union of every previous and replacement striker, non-striker, bowler, dismissed player and identified fielder, because participant aggregates consume all of those relationships. A role correction therefore records both the previous and the replacement person. For each affected participant, the journal records the fixture's season, competition, and career scopes; it never records another fixture, season, competition, or participant. Direct corrections through the API and batch corrections published by the worker derive this set with the same shared functions in @sport-analytics/batch-processing (issue #592).

The immutable statistics_refresh_dependency rows are committed with the replacement delivery and are returned as refreshedScopes by the direct correction API. They record exactly which scopes a correction affects and are the invalidation input for a future materialized projection or cache. Nothing reads them yet, and no statistic is stored or recomputed from them. The current public endpoints still calculate from delivery_current, so their values are immediately the same values a full recomputation would produce.

Publication input

The derivation repository applies these rules before calculation:

  1. The fixture's originating submission must have status = accepted.
  2. Only the accepted, live delivery revision is selected for each natural key (innings, over_number, position_in_over). A protected correction atomically supersedes the prior row, so its replacement is the sole calculation input without any manual statistic edit.
  3. Events are ordered by innings ordinal and then innings_sequence. ball_number is display-only and is never used for order.
  4. Standard fixture batting, bowling and team aggregates exclude innings marked is_super_over. A super over is a tie-breaking procedure rather than a standard match innings, so including it would distort ordinary player and team statistics. The response makes this scope explicit with superOversIncluded: false.

Super-over scopes

These scope names describe possible future API behavior; they are not currently implemented or recorded as client-approved requirements.

The current fixture-statistics response represents standard aggregates and excludes all super-over innings.

A future super-over-only scope may calculate statistics exclusively from innings marked is_super_over. A future standard-and-super-over-combined scope may include both types, but it must be explicitly named and requested; it must never become the default.

Fixture outcome information may still identify an eliminator winner even when the super-over deliveries are excluded from batting and bowling aggregates. The season, competition and career aggregates added under Issue #285 consume the same standard-innings boundary, taken from apps/backend/src/modules/statistics/super-over-scope.ts, which is the single place the exclusion is decided. See docs/statistics/participant-aggregates.md.

This is the team's current implementation default for Issue #104. Client confirmation of the convention remains pending, which is why the boundary is named in one module rather than repeated at each call site.

Basic calculations

Result Calculation
Team total Sum runs_total for the innings, plus penalty_pre and penalty_post
Wickets lost Count terminal dismissals; retired hurt and retired not out do not count
Innings legal balls Count deliveries with neither wides nor no-balls
Innings overs Format progress from delivery over identity, legal balls, fixture balls_per_over, and any miscounted-over override
Innings run rate Team total / legal balls × balls_per_over, rounded to two decimals
Total extras Sum delivery runs_extras plus innings pre/post penalty runs
Extras breakdown Wides, no-balls, byes, leg-byes, and delivery plus innings penalty runs
Batter runs Sum runs_off_bat for deliveries where the participant is striker
Balls faced Count striker deliveries with no wide; a no-ball still counts as faced
Strike rate Batter runs / balls faced × 100, rounded to two decimal places
Fours and sixes Count 4 or 6 runs_off_bat, excluding non_boundary deliveries
Runs conceded runs_off_bat + wides + no-balls; byes and leg-byes only off a wide
Legal balls bowled Count deliveries with neither wides nor no-balls
Overs bowled completeOvers.remainingBalls, using the fixture's balls_per_over
Economy rate Runs conceded / legal balls × balls_per_over, rounded to two decimals
Bowler wickets Count wickets whose dismissal_kind.credits_bowler value is true
Fixture outcome Accepted fixture outcome fact, represented as a typed result and margin

Strike rate and economy rate are null when their denominator is zero. This distinguishes an undefined rate from a real rate of zero.

Innings run rate follows the same rule and is null until a legal ball has been bowled. Extras are always present, including when every component is zero. Runs recorded as byes or leg-byes on a wide are reported as wides under Law 22.6; byes off a no-ball remain byes. The extras penaltyRuns component combines delivery-level penalties with the innings-level pre/post penalties already included in totalRuns.

The legalBalls count is the denominator for run rate, while overs is the presentation value. They are intentionally separate: a five- or seven-ball over recorded in innings_miscounted_over can advance the human-readable over without pretending the fixture always uses six balls or losing the exact number of legal deliveries.

Statistic resources and traceability

Each innings and participant projection has a stable opaque statisticId. The ID is a deterministic hash of the fixture and scope identifiers, so replaying the same fixture preserves its resource references.

Published statistics retain stable competitor and participant identifiers while also carrying the readable relationship names needed for presentation. Innings totals include competitorName; participant statistics include participantName and the participant's competitorName where known; and fixture outcomes include winnerCompetitorName and eliminatorCompetitorName where those relationships exist.

Normal list and detail responses return sourceEventCount only. Calling either endpoint with includeContributors=true adds the accepted, ordered delivery records used by that projection. Each trace record includes the stable delivery eventId, innings and sequence references, striker and bowler identifiers and readable names, run components, extras, boundary flag and credited-bowler wicket count. This is enough to reproduce every published metric while allowing user-facing traces to identify the players without exposing submission ownership or internal audit data.

For an innings total, metrics.deliveryRuns is traceable to delivery event IDs while metrics.penaltyRuns is traceable to the returned inningsId, because the approved schema records pre/post penalties at innings level rather than inventing a delivery for them. The same trace exposes the deliveries behind wickets, legal balls, run rate, and delivery extras; miscounted-over and innings-penalty context remains authoritative innings metadata.

Each innings statistic also returns a required metrics.powerplay field. It is null when the standard innings has no authoritative marker metadata. Otherwise it contains the retained ranges, runs, terminal wickets lost, legal balls, formatted overs and run rate calculated from accepted current deliveries whose source ball label lies inside any inclusive range. Wides and no-balls add runs but not legal balls, the fixture's balls-per-over controls overs and rate, and a zero-legal-ball rate is null. When contributors are requested, the nested powerplay trace contains only those accepted events. This makes an absent marker observably different from a real marked range that produces zeros. Super-over innings remain excluded before either standard or powerplay derivation.

Example response fragment:

{
  "metrics": {
    "powerplay": {
      "ranges": [{ "fromBall": 0.1, "toBall": 5.6, "type": "mandatory" }],
      "runs": 62,
      "wicketsLost": 1,
      "legalBalls": 36,
      "overs": "6.0",
      "runRate": 10.33,
      "sourceEventCount": 37
    }
  }
}

Public frontend

Opening /fixtures/{fixtureId} automatically requests and displays the fixture's Basic statistics below its named teams and match metadata. The combined overview shows the typed outcome, completeness state, warnings, innings totals, available player batting and bowling metrics, and participating players without a separate statistics action. The previous /fixtures/{fixtureId}/statistics route remains available for compatible deep links.

The Powerplay section compares the API-supplied runs, wickets, formatted overs and run rate for each innings with authoritative marker metadata. It labels the team and one-based innings number, keeps the phase distinct from full-innings totals, and explains that the interface never assumes the first six overs. An innings whose metrics.powerplay value is null receives an explicit unavailable message; the frontend does not reconstruct a range or replace missing metadata with zeroes.

The statistics API exposes readable team and player names alongside stable identifiers so public interfaces can present cricket identities without additional name-resolution requests. Identifiers remain internal to API requests, routes, and React keys rather than visible page content.

Each result links to /fixtures/{fixtureId}/statistics/{statisticId}. That route opts into includeContributors=true and presents the accepted delivery references and run components used by the calculation. It does not expose submission ownership, account information, pending or rejected events, or internal audit data. The calculation trace identifies deliveries by readable match order and player names; stable event identifiers remain internal React keys.

Failure states

The statistics section must always end in a state the reader can act on. A failed request shows the section error state, the reason reported by the API or the browser, and a retry control; the rest of the match overview keeps rendering. Because the application mounts no error boundary of its own, a failure to display an otherwise valid response would unmount the whole match overview and leave a blank page with no error and no retry, so the section carries its own boundary and presents such a failure in the same actionable error state. Issue #430 records the blank-section behaviour this replaces.

Incomplete data

An accepted but incomplete fixture returns HTTP 200 with status: partial. Warning codes cover:

  • source-declared missing fields;
  • no standard innings;
  • no accepted delivery events;
  • an innings with no accepted delivery events; and
  • a participant whose fixture competitor cannot be established.

The response still contains every result that can be derived safely. An unpublished fixture, an unknown fixture, or an unknown statistic resource returns 404.

Golden fixture

The backend unit suite includes a manually verified representative fixture covering wides, no-balls, byes, a non-boundary four, a bowler-credited wicket, innings penalty runs and both batting and bowling roles. It also replays the same events in reverse input order and asserts an identical result, while the repository test verifies accepted-revision filtering and occurrence ordering.

AI Declaration

The preceding calculation, API and public-interface documentation was generated, reviewed and edited with the assistance of Codex[GPT-5.6 Sol] and ChatGPT-Web[GPT-5.6 Sol]. The live-revision correction rule and selective refresh dependencies were updated with the assistance of Codex[GPT-5]. The versioned public fixture-statistics cache was documented with the assistance of Codex[GPT-5]. The issue #623 wide-run rule was documented with the assistance of Claude-Code[Claude Opus 5]. The innings scorecard context for issue #631 was documented with the assistance of Codex[GPT-5]. The issue #592 correction dependency participant set and ingest version advancement were documented with the assistance of Claude-Code[Claude Opus 5]. The issue #633 powerplay derivation was documented with the assistance of Codex[GPT-5]. The issue #634 public powerplay presentation was documented with the assistance of Codex[GPT-5].