Skip to content

Team Report

Match report for both teams: average formation shape, possession split, territory and analysis quality.

Request

GET /analyses/{id}/team-report
Parameter Type Required Description
id integer Yes Analysis ID

The first request computes the report and caches it server-side; later requests are fast. The response is deterministic per (analysis_id, schema_version) except for teams[].name, which is resolved at read time.

Response

{
  "schema_version": 1,
  "engine_version": "0.5.44",
  "coordinate_frame": "attacking",
  "orientation_source": "game_meta",

  "params": {
    "sample_interval_ms": 40,
    "sprint_min_kmh": 25.2,
    "sprint_min_s": 1.0,
    "top_speed_percentile": 0.99,
    "reliable_seconds_floor": 600.0,
    "hull_central_fraction": 0.8,
    "max_sample_gap_ms": 400,
    "sequence_gap_ms": 5000,
    "sequence_shot_slack_ms": 1000,
    "pitch": { "length_m": 105.0, "width_m": 68.0 }
  },

  "teams": [
    {
      "team": 0,
      "name": "Team A",
      "kit": {
        "primary": "red",
        "primary_rgb": [200, 16, 46],
        "secondary": "white",
        "secondary_rgb": [255, 255, 255],
        "pattern": "solid"
      },
      "average_positions": [
        {
          "player_id": "0-10",
          "jersey": 10,
          "label": "10",
          "role": "player",
          "position": { "x": 61.7, "y": 41.5 },
          "n_frames": 124510
        },
        {
          "player_id": "0-11",
          "jersey": 11,
          "label": "11",
          "role": "player",
          "position": { "x": 53.6, "y": 18.0 },
          "n_frames": 35332
        },
        {
          "player_id": "0-GK",
          "jersey": null,
          "label": "GK",
          "role": "goalkeeper",
          "position": { "x": 9.4, "y": 34.1 },
          "n_frames": 121904
        }
        // ... one entry per detected player on this team
      ]
    },
    {
      "team": 1,
      "name": "Team B",
      "kit": {
        "primary": "sky_blue",
        "primary_rgb": [108, 171, 221],
        "secondary": "navy",
        "secondary_rgb": [28, 40, 82],
        "pattern": "stripes"
      },
      "average_positions": [
        {
          "player_id": "1-9",
          "jersey": 9,
          "label": "9",
          "role": "player",
          "position": { "x": 68.2, "y": 33.4 },
          "n_frames": 88959
        }
        // ... one entry per detected player on this team
      ]
    }
  ],

  "possession": [
    { "team": 0,    "seconds": 2831.8, "pct": 49.64 },
    { "team": 1,    "seconds": 2475.5, "pct": 43.39 },
    { "team": null, "seconds": 397.5,  "pct": 6.97 }
  ],

  "territory": [
    { "zone": "attacking_third", "seconds": 1204.6 },
    { "zone": "defensive_third", "seconds": 1388.2 },
    { "zone": "middle_third",    "seconds": 2411.9 }
  ],

  "quality": {
    "score": 71.0,
    "grade": "fair",
    "reasons": [
      { "code": "pitch_not_located", "severity": "major", "segments": 10, "impact": 29 }
    ]
  }
}

Top level

Field Type Description
schema_version integer Version of this response shape. See Schema version
engine_version string Pipeline version the analysis was produced with
coordinate_frame string attacking, attacking_first_half or camera. See Coordinates
orientation_source string | null Where the attacking direction came from: hub (the video's left_start_team, the confirmed side when one was set, else the engine's derived value), game_meta (engine record) or tracking (inferred from the ends each team occupied). Null when none could resolve it; coordinate_frame is then camera and no rotation is applied

params

Thresholds the report was computed with, identical to the Player Report. Read edges and thresholds from here rather than hardcoding them.

teams

Always two entries, team: 0 first.

Field Type Description
team integer 0 or 1, the shirt-cluster label. Resolve to home/away via Teams & Sides
name string Display name, resolved from team identity at read time. Confirming sides after the analysis finished is reflected on the next request. Team 0 / Team 1 while identity is unknown
kit object | null Kit colours, same shape as card.kit on the player report. Null when the analysis carries no kit read
average_positions array One entry per detected player on the team, numeric jerseys ascending then non-numeric suffixes such as 0-GK, the order Player IDs and Summary use. A player with no detected tracking is absent

Average position object

Field Type Unit Description
player_id string Player ID, e.g. 0-10. Accepted by Player Report
jersey int | null Shirt number. Null for non-numeric ids such as 0-GK
label string Id suffix, e.g. "10" or "GK"
role string goalkeeper or player
position {x, y} m Mean position over the whole match. For a per-half mean read position.avg_position from the Player Report
n_frames integer Frames behind the mean. A substitute detected for ten minutes appears in this list next to a player detected for ninety; scale the marker by n_frames or drop entries below a threshold of your own

possession

Three entries in fixed order, team 0, team 1, then unknown, aligned with teams[] by index and by team. The same figures Summary returns under possession.summary.

Field Type Unit Description
team int | null 0, 1, or null for unknown possession. A typical match has 5-15% in the null bucket. See Possession
seconds float s Time in possession
pct float Share of the match, 0-100

territory

Time the ball spent in each longitudinal third, sorted by zone name. One match-wide split, not a per-team one; for a team's own territory profile combine the possession filters on Query Intervals. [] when the analysis carries no pitch-zone spans.

Field Type Unit Description
zone string Pitch-zone name from the analysis, currently defensive_third, middle_third or attacking_third, relative to the team in possession. Treat it as an open string and render an unfamiliar name as-is. See Ball Pitchzone
seconds float s Time the ball spent in that zone

quality

Reliability of the analysis behind the report, the same shape as quality on Get Analysis. Null when the analysis was never scored.

Field Type Description
score float | null Overall quality, 0-100
grade string | null good, fair, degraded or unreliable
reasons array Factors that lowered the score. [] when there are none

Reason object

Field Type Description
code string Reason identifier, e.g. pitch_not_located
severity string | null e.g. minor, major
segments int | null Match segments affected
impact int | null Weight of this factor on the score

Coordinates

Coordinates are metres in x / y fields and points are {x, y}. Positions are rotated so that each team attacks from x = 0 towards x = 105, as in the Player Report. coordinate_frame states how far that rotation could be resolved:

Value Meaning
attacking Both halves oriented for each team
attacking_first_half The first-half direction applied to the whole match because the moment the teams changed ends could not be located. Second-half positions are mirrored
camera No attacking direction. Raw camera-frame coordinates as in Pitch Coordinates; both teams are then already in the same frame

Warning

In attacking and attacking_first_half the rotation is per team, so teams[0].average_positions and teams[1].average_positions are in different frames. To draw both on one pitch as opponents, mirror one team through the centre point: x' = params.pitch.length_m - x, y' = params.pitch.width_m - y.

Warning

Every position here is a whole-match mean. In attacking_first_half the halves are in opposite frames, so each mean averages a player's first-half position with the mirror image of their second-half one and the formation shape pulls towards the halfway line. Use the per-half means from the Player Report instead.

Time

Durations are seconds in seconds, distances end in _m, speeds in _kmh, and timestamps are milliseconds from match start in fields ending _ms. Divide a _ms value by 1000 to align with the start / end seconds of Events and Query Intervals.

Schema version

schema_version is the same integer the Player Report carries and is bumped for both reports together when a field is added, removed or renamed, or a threshold in params changes. Currently 1. Key caches on (analysis_id, schema_version), and refresh when team identity changes because teams[].name moves without the version moving.