Wyscout adapter

Wyscout: Public tag-heavy data, made usable.

Wyscout's public dataset is tag-driven — events carry lists of numeric tags you have to interpret to get meaning. The adapter does the semantic work for you: interceptions are materialised from tag combinations, set-piece shots are dispatched from sub-event IDs, and duels collapse into the right canonical kind.

Input shape
Wyscout public match data + lightweight match info; optional player/team lookups for lineups.
Status
Primary provider
Raw feed
Wyscout
Provider-shaped. Their coordinates, their qualifier codes, their outcome strings.
Messy
Campos adapter
fromWyscout.shots(matchData, matchInfo)
Flips direction, standardises outcomes, filters own-goals and shootouts, resolves player labels.
One call
Typed data
ShotEvent[]
Drop into <ShotMap />, your own React view, a server-side report, or an analysis script.
Ready
The tag-heavy one

Wyscout public data carries a tags array on every event with numeric codes. Interceptions, set-piece shots, clearance intents — you don't get them as their own event types. The adapter materialises them from tag combinations so your chart code sees clean canonical events.

Get it running

One import, one call per surface.

import { fromWyscout } from "@withqwerty/campos-adapters";

const events = fromWyscout.events(matchData, matchInfo);
const shots = fromWyscout.shots(matchData, matchInfo);
const passes = fromWyscout.passes(matchData, matchInfo);
const lineups = fromWyscout.matchLineups(matchData, { players, teams });
What this adapter ships

The calls you can make today.

Shipped
events() returns Event[]

Every recognised match event in a canonical shape.

Partial
shots() returns ShotEvent[]

Just the shots, ready to plot.

Wyscout notes: Some placement inferred from tags; no xG in the public path.

Partial
passes() returns PassEvent[]

Pass trajectories with start, end, and result.

Wyscout notes: Result taxonomy still needs adapter-side review for PassSonar parity.

Partial
matchLineups() returns MatchLineups

Home and away team sheets with starters and bench.

Wyscout notes: Starters, bench, labels, and substitution metadata — no formation label, captain, shirt numbers, or coordinates yet.

What you can build with this

Charts that drop straight onto this adapter's output.

Supported cards plot from this adapter with no extra work. Partial cards plot, but read the scope note. Dimmed cards need a surface this provider doesn't ship.

Semantic reconstruction, not just coordinate scaling

Tags and sub-events, turned into canonical kinds.

Interception detection (tag 1401) layers onto passes, duels, clearances, and touch events. Ground attacking duels with the right sub-event ID become take-ons rather than plain duels. Set-piece shots are dispatched from sub-event IDs even when the top-level event ID matches something else. This is the real work the Wyscout adapter does.

[
  {
    "kind": "pass",
    "id": "2500040:241083854",
    "matchId": "2500040",
    "teamId": "1619",
    "playerId": "383",
    "playerName": null,
    "minute": 0,
    "addedMinute": null,
    "second": 1,
    "period": 1,
    "x": 52,
    "y": 48,
    "provider": "wyscout",
    "providerEventId": "241083854",
    "sourceMeta": {
      "eventId": 8,
      "eventName": "Pass",
      "subEventId": 85,
      "subEventName": "Simple pass",
      "tags": [
        1801
      ]
    },
    "endX": 27,
    "endY": 50,
    "length": 25.08,
    "angle": 3.0618,
    "recipient": null,
    "passResult": "complete",
    "passType": "ground",
    "isAssist": false
  },
  {
    "kind": "shot",
    "id": "2500040:241084564",
    "matchId": "2500040",
    "teamId": "1609",
    "playerId": "49876",
    "playerName": null,
    "minute": 34,
    "addedMinute": null,
    "second": 45,
    "period": 1,
    "x": 74,
    "y": 61,
    "provider": "wyscout",
    "providerEventId": "241084564",
    "sourceMeta": {
      "eventId": 3,
      "eventName": "Free Kick",
      "subEventId": 33,
      "subEventName": "Free kick shot",
      "tags": [
        401,
        1101,
        201,
        1215,
        1802
      ]
    },
    "endX": 0,
    "endY": 100,
    "xg": null,
    "xgot": null,
    "outcome": "off-target",
    "bodyPart": "left-foot",
    "isOwnGoal": false,
    "isPenalty": false,
    "context": "direct-free-kick",
    "goalMouthY": 40,
    "goalMouthZ": 3.5
  }
]
Current scope

What's honest about this adapter today.

Current public-dataset scope

Public research data, not an official API surface.

The current Wyscout path is the public research dataset plus optional player and team lookups. It's a useful source for many pitch charts, but it's deliberately not framed as full official-API parity.

Why some surfaces are partial

xG absent, result taxonomy still needs review.

No per-shot xG in the public path. Pass result semantics still need adapter-side normalisation review before PassSonar will claim full parity. Shot placement is inferred from tags in several cases rather than observed end locations.

Lineups today

Starters and bench, no formation label or shirts yet.

matchLineups() reads match.teamsData.formation with optional player and team lookups. Delivers starters, bench, substitution metadata, and coarse role codes. Formation labels, captain, shirt numbers, and coordinates are not in the public source.

Lineage

What this adapter borrows from whom.

Concrete credits for the projects that did the underlying research or laid the reference tables we read from.

kloppy Python

Provider type-ID → canonical kind mapping, coordinate transformers, and direction-by-period normalisation patterns. Our reference for Opta, StatsBomb, Wyscout, Stats Perform, and Sportec.

socceraction Python

SPADL canonical action taxonomy as a reference when we decided which Wyscout tag combinations collapse into which canonical event.

mplsoccer Python

Pitch-standardisation conventions we cross-checked our canonical Campos coordinate frame against.

Raw example — what the payload looks like before and after
Raw shot event
{
  "id": 241084564,
  "eventId": 3,
  "subEventName": "Free kick shot",
  "tags": [
    {
      "id": 401
    },
    {
      "id": 1101
    },
    {
      "id": 201
    },
    {
      "id": 1215
    },
    {
      "id": 1802
    }
  ],
  "playerId": 49876,
  "positions": [
    {
      "y": 39,
      "x": 74
    },
    {
      "y": 0,
      "x": 0
    }
  ],
  "matchId": 2500040,
  "eventName": "Free Kick",
  "teamId": 1609,
  "matchPeriod": "1H",
  "eventSec": 2085.050844,
  "subEventId": 33
}
Raw pass event
{
  "id": 241083854,
  "eventId": 8,
  "subEventName": "Simple pass",
  "tags": [
    {
      "id": 1801
    }
  ],
  "playerId": 383,
  "positions": [
    {
      "y": 52,
      "x": 52
    },
    {
      "y": 50,
      "x": 27
    }
  ],
  "matchId": 2500040,
  "eventName": "Pass",
  "teamId": 1619,
  "matchPeriod": "1H",
  "eventSec": 1.9439610000000016,
  "subEventId": 85
}
Match info
{
  "matchId": "2500040"
}
Canonical shot output
[
  {
    "kind": "shot",
    "id": "2500040:241084564",
    "matchId": "2500040",
    "teamId": "1609",
    "playerId": "49876",
    "playerName": null,
    "minute": 34,
    "addedMinute": null,
    "second": 45,
    "period": 1,
    "x": 74,
    "y": 61,
    "provider": "wyscout",
    "providerEventId": "241084564",
    "sourceMeta": {
      "eventId": 3,
      "eventName": "Free Kick",
      "subEventId": 33,
      "subEventName": "Free kick shot",
      "tags": [
        401,
        1101,
        201,
        1215,
        1802
      ]
    },
    "endX": 0,
    "endY": 100,
    "xg": null,
    "xgot": null,
    "outcome": "off-target",
    "bodyPart": "left-foot",
    "isOwnGoal": false,
    "isPenalty": false,
    "context": "direct-free-kick",
    "goalMouthY": 40,
    "goalMouthZ": 3.5
  }
]
Compare providers