Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Wyscout v3] Estimate shot result coordinates #320

130 changes: 107 additions & 23 deletions kloppy/infra/serializers/event/wyscout/deserializer_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging
from dataclasses import replace
from datetime import timedelta
from typing import Dict, List, Tuple, NamedTuple, IO
from typing import Dict, List, Tuple, NamedTuple, IO, Optional

from kloppy.domain import (
BallOutEvent,
Expand Down Expand Up @@ -49,6 +49,7 @@

from ..deserializer import EventDataDeserializer
from .deserializer_v2 import WyscoutInputs
from . import wyscout_tags
fubininho marked this conversation as resolved.
Show resolved Hide resolved


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -99,6 +100,82 @@ def _parse_team(raw_events, wyId: str, ground: Ground) -> Team:
return team


def _create_shot_result_coordinates(raw_event: Dict) -> Optional[Point]:
"""Estimate the shot end location from the Wyscout tags.

Wyscout does not provide end-coordinates of shots. Instead shots on goal
are tagged with a zone. This function maps each of these zones to
a coordinate. The zones and corresponding y-coordinate are depicted below.


olt | ot | ort
--------------------------------
||=================||
-------------------------------
|| g;l | gt | grt ||
--------------------------------
ol || gcl | gc | gcr || or
--------------------------------
olb || glb | gb | gln || grb

40 45 50 55 60 (y-coordinate of zone)
44.62 55.38 (y-coordiante of post)
"""
if (
raw_event["shot"]["goalZone"]
== wyscout_tags.ShotZoneResults.GoalBottomCenter
or raw_event["shot"]["goalZone"]
== wyscout_tags.ShotZoneResults.GoalCenter
or raw_event["shot"]["goalZone"]
== wyscout_tags.ShotZoneResults.GoalTopCenter
):
return Point(100.0, 50.0)
if (
raw_event["shot"]["goalZone"]
== wyscout_tags.ShotZoneResults.GoalBottomRight
or raw_event["shot"]["goalZone"]
== wyscout_tags.ShotZoneResults.GoalCenterRight
or raw_event["shot"]["goalZone"]
== wyscout_tags.ShotZoneResults.GoalTopRight
):
return Point(100.0, 55.0)
if (
raw_event["shot"]["goalZone"]
== wyscout_tags.ShotZoneResults.GoalBottomLeft
or raw_event["shot"]["goalZone"]
== wyscout_tags.ShotZoneResults.GoalCenterLeft
or raw_event["shot"]["goalZone"]
== wyscout_tags.ShotZoneResults.GoalTopLeft
):
return Point(100.0, 45.0)
if raw_event["shot"]["goalZone"] == wyscout_tags.ShotZoneResults.OutTop:
return Point(100.0, 50.0)
if (
raw_event["shot"]["goalZone"]
== wyscout_tags.ShotZoneResults.OutRightTop
or raw_event["shot"]["goalZone"]
== wyscout_tags.ShotZoneResults.OutRight
or raw_event["shot"]["goalZone"]
== wyscout_tags.ShotZoneResults.OutBottomRight
):
return Point(100.0, 60.0)
if (
raw_event["shot"]["goalZone"]
== wyscout_tags.ShotZoneResults.OutLeftTop
or raw_event["shot"]["goalZone"]
== wyscout_tags.ShotZoneResults.OutLeft
or raw_event["shot"]["goalZone"]
== wyscout_tags.ShotZoneResults.OutBottomLeft
):
return Point(100.0, 40.0)
if raw_event["shot"]["goalZone"] == wyscout_tags.ShotZoneResults.Blocked:
return Point(
x=float(raw_event["location"]["x"]),
y=float(raw_event["positions"]["y"]),
)
return None


def _generic_qualifiers(raw_event: Dict) -> List[Qualifier]:
qualifiers: List[Qualifier] = []

Expand Down Expand Up @@ -131,10 +208,7 @@ def _parse_shot(raw_event: Dict) -> Dict:

return {
"result": result,
"result_coordinates": Point(
x=float(0),
y=float(0),
),
"result_coordinates": _create_shot_result_coordinates(raw_event),
"qualifiers": qualifiers,
}

Expand Down Expand Up @@ -187,12 +261,14 @@ def _parse_pass(raw_event: Dict, next_event: Dict, team: Team) -> Dict:
"qualifiers": _pass_qualifiers(raw_event),
"receive_timestamp": None,
"receiver_player": receiver_player,
"receiver_coordinates": Point(
x=float(raw_event["pass"]["endLocation"]["x"]),
y=float(raw_event["pass"]["endLocation"]["y"]),
)
if len(raw_event["pass"]["endLocation"]) > 1
else None,
"receiver_coordinates": (
Point(
x=float(raw_event["pass"]["endLocation"]["x"]),
y=float(raw_event["pass"]["endLocation"]["y"]),
)
if len(raw_event["pass"]["endLocation"]) > 1
else None
),
}


Expand Down Expand Up @@ -516,9 +592,11 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset:
periods.append(
Period(
id=period_id,
start_timestamp=timedelta(seconds=0)
if len(periods) == 0
else periods[-1].end_timestamp,
start_timestamp=(
timedelta(seconds=0)
if len(periods) == 0
else periods[-1].end_timestamp
),
end_timestamp=None,
)
)
Expand All @@ -539,20 +617,26 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset:
ball_owning_team = teams[
str(raw_event["possession"]["team"]["id"])
]
else:
ball_owning_team = team # TODO: this solve the issue of ball owning team when transforming to spald, but it's not correct
fubininho marked this conversation as resolved.
Show resolved Hide resolved

generic_event_args = {
"event_id": raw_event["id"],
"raw_event": raw_event,
"coordinates": Point(
x=float(raw_event["location"]["x"]),
y=float(raw_event["location"]["y"]),
)
if raw_event["location"]
else None,
"coordinates": (
Point(
x=float(raw_event["location"]["x"]),
y=float(raw_event["location"]["y"]),
)
if raw_event["location"]
else None
),
"team": team,
"player": players[team_id][player_id]
if player_id != INVALID_PLAYER
else None,
"player": (
players[team_id][player_id]
if player_id != INVALID_PLAYER
else None
),
"ball_owning_team": ball_owning_team,
"ball_state": None,
"period": periods[-1],
Expand Down
22 changes: 22 additions & 0 deletions kloppy/infra/serializers/event/wyscout/wyscout_tags.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from enum import Enum

GOAL = 101
OWN_GOAL = 102

Expand Down Expand Up @@ -87,3 +89,23 @@
DANGEROUS_BALL_LOST = 2001

BLOCKED = 2101


class ShotZoneResults(Enum):
fubininho marked this conversation as resolved.
Show resolved Hide resolved
GoalBottomLeft = "glb"
GoalBottomRight = "gbr"
GoalBottomCenter = "gbc"
GoalCenterLeft = "gcl"
GoalCenter = "gc"
GoalCenterRight = "gcr"
GoalTopLeft = "gtl"
GoalTopRight = "gtr"
GoalTopCenter = "gtc"
OutBottomRight = "obr"
OutBottomLeft = "obl"
OutRight = "or"
OutLeft = "ol"
OutLeftTop = "olt"
OutTop = "ot"
OutRightTop = "ort"
Blocked = "bc"
Loading