Skip to content

Commit

Permalink
Merge pull request #198 from my-game-plan/feature/opta-fixes
Browse files Browse the repository at this point in the history
Improvements to Opta and Wyscout deserializers
  • Loading branch information
koenvo authored Jul 13, 2023
2 parents 6315335 + e8ff8ca commit e4c892b
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 27 deletions.
37 changes: 29 additions & 8 deletions kloppy/infra/serializers/event/opta/deserializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
BodyPart,
PassType,
PassQualifier,
CounterAttackQualifier,
)
from kloppy.exceptions import DeserializationError
from kloppy.infra.serializers.event.deserializer import EventDataDeserializer
Expand Down Expand Up @@ -98,6 +99,8 @@
EVENT_QUALIFIER_SECOND_YELLOW_CARD = 32
EVENT_QUALIFIER_RED_CARD = 33

EVENT_QUALIFIER_COUNTER_ATTACK = 23

EVENT_QUALIFIER_TEAM_FORMATION = 130

event_type_names = {
Expand Down Expand Up @@ -218,16 +221,13 @@ def _parse_f24_datetime(dt_str: str) -> float:

def _parse_pass(raw_qualifiers: Dict[int, str], outcome: int) -> Dict:
if outcome:
receiver_coordinates = Point(
x=float(raw_qualifiers[140]), y=float(raw_qualifiers[141])
)
result = PassResult.COMPLETE
else:
result = PassResult.INCOMPLETE
# receiver_coordinates = None
receiver_coordinates = Point(
x=float(raw_qualifiers[140]), y=float(raw_qualifiers[141])
)
receiver_coordinates = Point(
x=float(raw_qualifiers[140]) if 140 in raw_qualifiers else 0,
y=float(raw_qualifiers[141]) if 141 in raw_qualifiers else 0,
)

qualifiers = _get_event_qualifiers(raw_qualifiers)

Expand Down Expand Up @@ -298,6 +298,14 @@ def _parse_shot(
# timestamp =
else:
result = ShotResult.GOAL
elif 82 in raw_qualifiers:
result = ShotResult.BLOCKED
elif type_id == EVENT_TYPE_SHOT_MISS:
result = ShotResult.OFF_TARGET
elif type_id == EVENT_TYPE_SHOT_POST:
result = ShotResult.OFF_TARGET
elif type_id == EVENT_TYPE_SHOT_SAVED:
result = ShotResult.SAVED
else:
result = None

Expand Down Expand Up @@ -377,6 +385,7 @@ def _get_event_qualifiers(raw_qualifiers: List) -> List[Qualifier]:
qualifiers.extend(_get_event_bodypart_qualifiers(raw_qualifiers))
qualifiers.extend(_get_event_pass_qualifiers(raw_qualifiers))
qualifiers.extend(_get_event_card_qualifiers(raw_qualifiers))
qualifiers.extend(_get_event_counter_attack_qualifiers(raw_qualifiers))
return qualifiers


Expand Down Expand Up @@ -448,6 +457,16 @@ def _get_event_card_qualifiers(raw_qualifiers: List) -> List[Qualifier]:
return qualifiers


def _get_event_counter_attack_qualifiers(
raw_qualifiers: List,
) -> List[Qualifier]:
qualifiers = []
if EVENT_QUALIFIER_COUNTER_ATTACK in raw_qualifiers:
qualifiers.append(CounterAttackQualifier(True))

return qualifiers


def _get_event_type_name(type_id: int) -> str:
return event_type_names.get(type_id, "unknown")

Expand Down Expand Up @@ -647,7 +666,9 @@ def deserialize(self, inputs: OptaInputs) -> EventDataset:
**clearance_event_kwargs,
**generic_event_kwargs,
)
elif type_id == EVENT_TYPE_FOUL_COMMITTED:
elif (type_id == EVENT_TYPE_FOUL_COMMITTED) and (
outcome == 0
):
event = self.event_factory.build_foul_committed(
result=None,
qualifiers=None,
Expand Down
37 changes: 31 additions & 6 deletions kloppy/infra/serializers/event/wyscout/deserializer_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
)
from kloppy.utils import performance_logging

from . import wyscout_tags
from ..deserializer import EventDataDeserializer
from .deserializer_v2 import WyscoutInputs

Expand Down Expand Up @@ -172,12 +171,10 @@ def _parse_foul(raw_event: Dict) -> Dict:
def _parse_card(raw_event: Dict) -> Dict:
qualifiers = _generic_qualifiers(raw_event)
card_type = None
if _has_tag(raw_event, wyscout_tags.RED_CARD):
card_type = CardType.RED
elif _has_tag(raw_event, wyscout_tags.YELLOW_CARD):
if _check_secondary_event_types(raw_event, ["yellow_card"]):
card_type = CardType.FIRST_YELLOW
elif _has_tag(raw_event, wyscout_tags.SECOND_YELLOW_CARD):
card_type = CardType.SECOND_YELLOW
elif _check_secondary_event_types(raw_event, ["red_card"]):
card_type = CardType.RED

return {"result": None, "qualifiers": qualifiers, "card_type": card_type}

Expand Down Expand Up @@ -343,6 +340,12 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset:
"period": periods[-1],
"timestamp": float(
raw_event["second"] + raw_event["minute"] * 60
)
if period_id == 1
else float(
raw_event["second"]
+ (raw_event["minute"] * 60)
- (60 * 45)
),
}

Expand Down Expand Up @@ -402,6 +405,28 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset:
event = self.event_factory.build_shot(
**set_piece_event_args, **generic_event_args
)
elif primary_event_type == "infraction":
if "foul" in secondary_event_types:
foul_event_args = _parse_foul(raw_event)
event = self.event_factory.build_foul_committed(
**foul_event_args, **generic_event_args
)
# We already append event to events
# as we potentially have a card and foul event for one raw event
if event and self.should_include_event(event):
events.append(transformer.transform_event(event))
continue
if (
"yellow_card" in secondary_event_types
or "red_card" in secondary_event_types
):
card_event_args = _parse_card(raw_event)
event = self.event_factory.build_card(
**card_event_args, **generic_event_args
)
if event and self.should_include_event(event):
events.append(transformer.transform_event(event))
continue

else:
event = self.event_factory.build_generic(
Expand Down
77 changes: 77 additions & 0 deletions kloppy/tests/files/wyscout_events_v3.json
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,83 @@
"name": "Bologna"
},
"videoTimestamp": "8.148438"
},{
"id": 1343788476,
"matchId": 2852835,
"matchPeriod": "1H",
"minute": 0,
"second": 52,
"matchTimestamp": "00:00:52.511",
"videoTimestamp": "53.511357",
"type": {
"primary": "infraction",
"secondary": [
"foul"
]
},
"location": {
"x": 65,
"y": 11
},
"team": {
"formation": "4-2-3-1",
"id": 3166,
"name": "Bologna"
},
"opponentTeam": {
"formation": "3-4-3",
"id": 3185,
"name": "Torino"
},
"player": {
"id": 20623,
"name": "R. Soriano",
"position": "AMF"
},
"pass": null,
"shot": null,
"groundDuel": null,
"aerialDuel": null,
"infraction": {
"yellowCard": false,
"redCard": false,
"type": "regular_foul",
"opponent": {
"id": 20583,
"name": "Danilo",
"position": "RCB"
}
},
"carry": null,
"possession": {
"attack": {
"flank": "right",
"withGoal": false,
"withShot": false,
"withShotOnGoal": false,
"xg": 0
},
"duration": "28.0201355",
"endLocation": {
"x": 83,
"y": 0
},
"eventIndex": 2,
"eventsNumber": 14,
"id": 663291836,
"startLocation": {
"x": 40,
"y": 68
},
"team": {
"formation": "4-2-3-1",
"id": 3166,
"name": "Bologna"
},
"types": [
"attack"
]
}
},
{
"id": 663291842,
Expand Down
6 changes: 6 additions & 0 deletions kloppy/tests/test_opta.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
DatasetType,
CardType,
FormationType,
CounterAttackQualifier,
)

from kloppy.domain.models.event import EventType
Expand Down Expand Up @@ -111,6 +112,11 @@ def test_correct_deserialization(self, f7_data: str, f24_data: str):
# Check OFFSIDE pass has end_coordinates
assert dataset.events[20].receiver_coordinates.x == 89.3 # 2360555167

# Check counterattack
assert (
CounterAttackQualifier(value=True) in dataset.events[17].qualifiers
) # 2318695229

def test_correct_normalized_deserialization(
self, f7_data: str, f24_data: str
):
Expand Down
21 changes: 8 additions & 13 deletions kloppy/tests/test_wyscout.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from pathlib import Path

import pytest
from kloppy.domain import Point, SetPieceType, SetPieceQualifier
from kloppy.domain.models import EventType
from kloppy.domain import Point, SetPieceType, SetPieceQualifier, EventType

from kloppy import wyscout

Expand All @@ -24,8 +23,13 @@ def test_correct_v3_deserialization(self, event_v3_data: Path):
coordinates="wyscout",
data_version="V3",
)
assert dataset.records[2].coordinates == Point(36.0, 78.0)
assert dataset.events[5].event_type == EventType.CLEARANCE
assert dataset.events[2].coordinates == Point(36.0, 78.0)
assert (
dataset.events[4].get_qualifier_value(SetPieceQualifier)
== SetPieceType.CORNER_KICK
)
assert dataset.events[5].event_type == EventType.FOUL_COMMITTED
assert dataset.events[6].event_type == EventType.CLEARANCE

def test_correct_normalized_v3_deserialization(self, event_v3_data: Path):
dataset = wyscout.load(event_data=event_v3_data, data_version="V3")
Expand All @@ -43,12 +47,3 @@ def test_correct_v2_deserialization(self, event_v2_data: Path):
def test_correct_auto_recognize_deserialization(self, event_v2_data: Path):
dataset = wyscout.load(event_data=event_v2_data, coordinates="wyscout")
assert dataset.records[2].coordinates == Point(29.0, 6.0)

def test_correct_handling_of_corner_shot(self, event_v3_data: Path):
dataset = wyscout.load(
event_data=event_v3_data, data_version="V3", event_types=["shot"]
)
assert (
dataset.events[0].get_qualifier_value(SetPieceQualifier)
== SetPieceType.CORNER_KICK
)

0 comments on commit e4c892b

Please sign in to comment.