diff --git a/kloppy/infra/serializers/event/opta/deserializer.py b/kloppy/infra/serializers/event/opta/deserializer.py index 3acedb36..b78937f1 100644 --- a/kloppy/infra/serializers/event/opta/deserializer.py +++ b/kloppy/infra/serializers/event/opta/deserializer.py @@ -31,6 +31,7 @@ BodyPart, PassType, PassQualifier, + CounterAttackQualifier, ) from kloppy.exceptions import DeserializationError from kloppy.infra.serializers.event.deserializer import EventDataDeserializer @@ -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 = { @@ -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) @@ -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 @@ -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 @@ -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") @@ -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, diff --git a/kloppy/infra/serializers/event/wyscout/deserializer_v3.py b/kloppy/infra/serializers/event/wyscout/deserializer_v3.py index 23d4c3e0..7e8538a1 100644 --- a/kloppy/infra/serializers/event/wyscout/deserializer_v3.py +++ b/kloppy/infra/serializers/event/wyscout/deserializer_v3.py @@ -27,7 +27,6 @@ ) from kloppy.utils import performance_logging -from . import wyscout_tags from ..deserializer import EventDataDeserializer from .deserializer_v2 import WyscoutInputs @@ -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} @@ -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) ), } @@ -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( diff --git a/kloppy/tests/files/wyscout_events_v3.json b/kloppy/tests/files/wyscout_events_v3.json index dc39832a..7740c741 100644 --- a/kloppy/tests/files/wyscout_events_v3.json +++ b/kloppy/tests/files/wyscout_events_v3.json @@ -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, diff --git a/kloppy/tests/test_opta.py b/kloppy/tests/test_opta.py index 8bd4f6e8..79024bcd 100644 --- a/kloppy/tests/test_opta.py +++ b/kloppy/tests/test_opta.py @@ -13,6 +13,7 @@ DatasetType, CardType, FormationType, + CounterAttackQualifier, ) from kloppy.domain.models.event import EventType @@ -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 ): diff --git a/kloppy/tests/test_wyscout.py b/kloppy/tests/test_wyscout.py index f4ebf9c9..da706067 100644 --- a/kloppy/tests/test_wyscout.py +++ b/kloppy/tests/test_wyscout.py @@ -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 @@ -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") @@ -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 - )