From 12398095862f8bba729660df3125eee750dc6f56 Mon Sep 17 00:00:00 2001 From: driesdeprest Date: Tue, 16 Jan 2024 13:13:07 +0100 Subject: [PATCH 01/17] Add shot assist to data model & fix Opta & StatsBomb deserializers --- kloppy/domain/models/event.py | 2 ++ .../serializers/event/opta/deserializer.py | 11 +++++-- .../event/statsbomb/specification.py | 3 ++ kloppy/tests/files/opta_f24.xml | 8 +++++ kloppy/tests/test_adapter.py | 2 +- kloppy/tests/test_opta.py | 32 +++++++++++-------- kloppy/tests/test_statsbomb.py | 1 + 7 files changed, 42 insertions(+), 17 deletions(-) diff --git a/kloppy/domain/models/event.py b/kloppy/domain/models/event.py index c40147d3..9f629eb6 100644 --- a/kloppy/domain/models/event.py +++ b/kloppy/domain/models/event.py @@ -350,7 +350,9 @@ class PassType(Enum): THROUGH_BALL = "THROUGH_BALL" CHIPPED_PASS = "CHIPPED_PASS" FLICK_ON = "FLICK_ON" + SHOT_ASSIST = "SHOT_ASSIST" ASSIST = "ASSIST" + SHOT_ASSIST_2ND = "SHOT_ASSIST_2ND" ASSIST_2ND = "ASSIST_2ND" SWITCH_OF_PLAY = "SWITCH_OF_PLAY" diff --git a/kloppy/infra/serializers/event/opta/deserializer.py b/kloppy/infra/serializers/event/opta/deserializer.py index 01a81e25..c30296aa 100644 --- a/kloppy/infra/serializers/event/opta/deserializer.py +++ b/kloppy/infra/serializers/event/opta/deserializer.py @@ -126,7 +126,7 @@ EVENT_QUALIFIER_LAUNCH = 157 EVENT_QUALIFIER_FLICK_ON = 168 EVENT_QUALIFIER_SWITCH_OF_PLAY = 196 -EVENT_QUALIFIER_ASSIST = 210 +EVENT_QUALIFIER_SHOT_ASSIST = 210 EVENT_QUALIFIER_ASSIST_2ND = 218 EVENT_QUALIFIER_FIRST_YELLOW_CARD = 31 @@ -573,7 +573,6 @@ def _get_pass_qualifiers(raw_qualifiers: Dict[int, str]) -> List[Qualifier]: EVENT_QUALIFIER_THROUGH_BALL: PassType.THROUGH_BALL, EVENT_QUALIFIER_LAUNCH: PassType.LAUNCH, EVENT_QUALIFIER_FLICK_ON: PassType.FLICK_ON, - EVENT_QUALIFIER_ASSIST: PassType.ASSIST, EVENT_QUALIFIER_ASSIST_2ND: PassType.ASSIST_2ND, } for ( @@ -583,6 +582,14 @@ def _get_pass_qualifiers(raw_qualifiers: Dict[int, str]) -> List[Qualifier]: if sp_pass_qualifier in raw_qualifiers: qualifiers.append(PassQualifier(value=pass_qualifier_value)) + if EVENT_QUALIFIER_SHOT_ASSIST in raw_qualifiers: + qualifiers.append(PassQualifier(value=PassType.SHOT_ASSIST)) + shot_result_qualifier = int( + raw_qualifiers[EVENT_QUALIFIER_SHOT_ASSIST] + ) + if shot_result_qualifier == EVENT_TYPE_SHOT_GOAL: + qualifiers.append(PassQualifier(value=PassType.ASSIST)) + return qualifiers diff --git a/kloppy/infra/serializers/event/statsbomb/specification.py b/kloppy/infra/serializers/event/statsbomb/specification.py index 874834b3..9ff8ed8f 100644 --- a/kloppy/infra/serializers/event/statsbomb/specification.py +++ b/kloppy/infra/serializers/event/statsbomb/specification.py @@ -1226,8 +1226,11 @@ def _get_pass_qualifiers(pass_dict: Dict) -> List[PassQualifier]: add_qualifier(PassType.HEAD_PASS) elif body_part_id == BODYPART.KEEPER_ARM: add_qualifier(PassType.HAND_PASS) + if "shot_assist" in pass_dict: + add_qualifier(PassType.SHOT_ASSIST) if "goal_assist" in pass_dict: add_qualifier(PassType.ASSIST) + add_qualifier(PassType.SHOT_ASSIST) return qualifiers diff --git a/kloppy/tests/files/opta_f24.xml b/kloppy/tests/files/opta_f24.xml index 4b4c14ae..34f595f3 100644 --- a/kloppy/tests/files/opta_f24.xml +++ b/kloppy/tests/files/opta_f24.xml @@ -168,6 +168,14 @@ + + + + + + + + diff --git a/kloppy/tests/test_adapter.py b/kloppy/tests/test_adapter.py index 3f8c0fe3..5eb8bf17 100644 --- a/kloppy/tests/test_adapter.py +++ b/kloppy/tests/test_adapter.py @@ -57,4 +57,4 @@ def read_to_stream(self, url: str, output: BinaryIO): # Asserts borrowed from `test_opta.py` assert dataset.metadata.provider == Provider.OPTA assert dataset.dataset_type == DatasetType.EVENT - assert len(dataset.events) == 33 + assert len(dataset.events) == 34 diff --git a/kloppy/tests/test_opta.py b/kloppy/tests/test_opta.py index 59261a13..fcbfee7f 100644 --- a/kloppy/tests/test_opta.py +++ b/kloppy/tests/test_opta.py @@ -56,7 +56,7 @@ def test_correct_deserialization(self, f7_data: str, f24_data: str): ) assert dataset.metadata.provider == Provider.OPTA assert dataset.dataset_type == DatasetType.EVENT - assert len(dataset.events) == 33 + assert len(dataset.events) == 34 assert len(dataset.metadata.periods) == 5 assert ( dataset.events[10].ball_owning_team == dataset.metadata.teams[1] @@ -121,11 +121,15 @@ def test_correct_deserialization(self, f7_data: str, f24_data: str): assert ( dataset.events[5].qualifiers[0].value == PassType.CHIPPED_PASS ) # 1444075194 + assert dataset.events[17].get_qualifier_values(PassQualifier) == [ + PassType.SHOT_ASSIST, + PassType.ASSIST, + ] # 1666666666 assert ( - dataset.events[19].qualifiers[0].value == CardType.RED + dataset.events[20].qualifiers[0].value == CardType.RED ) # 2318695229 assert ( - dataset.events[21].event_type == EventType.CLEARANCE + dataset.events[22].event_type == EventType.CLEARANCE ) # 2498907287 # Check receiver coordinates for incomplete passes @@ -133,45 +137,45 @@ def test_correct_deserialization(self, f7_data: str, f24_data: str): assert dataset.events[6].receiver_coordinates.y == 68.2 # Check timestamp from qualifier in case of goal - assert dataset.events[17].timestamp == 139.65200018882751 # 2318695229 + assert dataset.events[18].timestamp == 139.65200018882751 # 2318695229 # assert dataset.events[17].coordinates_y == 12 # Check Own goal - assert dataset.events[18].result.value == "OWN_GOAL" # 2318697001 + assert dataset.events[19].result.value == "OWN_GOAL" # 2318697001 # Check OFFSIDE pass has end_coordinates - assert dataset.events[20].receiver_coordinates.x == 89.3 # 2360555167 + assert dataset.events[21].receiver_coordinates.x == 89.3 # 2360555167 # Check goalkeeper qualifiers assert ( - dataset.events[23].get_qualifier_value(GoalkeeperQualifier) + dataset.events[24].get_qualifier_value(GoalkeeperQualifier) == GoalkeeperActionType.SAVE ) assert ( - dataset.events[24].get_qualifier_value(GoalkeeperQualifier) + dataset.events[25].get_qualifier_value(GoalkeeperQualifier) == GoalkeeperActionType.CLAIM ) assert ( - dataset.events[25].get_qualifier_value(GoalkeeperQualifier) + dataset.events[26].get_qualifier_value(GoalkeeperQualifier) == GoalkeeperActionType.PUNCH ) assert ( - dataset.events[26].get_qualifier_value(GoalkeeperQualifier) + dataset.events[27].get_qualifier_value(GoalkeeperQualifier) == GoalkeeperActionType.PICK_UP ) assert ( - dataset.events[27].get_qualifier_value(GoalkeeperQualifier) + dataset.events[28].get_qualifier_value(GoalkeeperQualifier) == GoalkeeperActionType.SMOTHER ) assert ( - dataset.events[28].event_type == EventType.INTERCEPTION + dataset.events[29].event_type == EventType.INTERCEPTION ) # 2609934569 assert ( - dataset.events[29].event_type == EventType.MISCONTROL + dataset.events[30].event_type == EventType.MISCONTROL ) # 250913217 # Check counterattack assert ( - CounterAttackQualifier(value=True) in dataset.events[17].qualifiers + CounterAttackQualifier(value=True) in dataset.events[18].qualifiers ) # 2318695229 # Check DuelQualifiers diff --git a/kloppy/tests/test_statsbomb.py b/kloppy/tests/test_statsbomb.py index 4115f1f5..2ee7b3af 100644 --- a/kloppy/tests/test_statsbomb.py +++ b/kloppy/tests/test_statsbomb.py @@ -599,6 +599,7 @@ def test_pass_qualifiers(self, dataset: EventDataset): PassType.CROSS, PassType.HIGH_PASS, PassType.LONG_BALL, + PassType.SHOT_ASSIST, ] def test_set_piece(self, dataset: EventDataset): From f33f998025859b64b7d2db8715dd12f629cc78a4 Mon Sep 17 00:00:00 2001 From: driesdeprest Date: Mon, 22 Jan 2024 14:29:12 +0100 Subject: [PATCH 02/17] Add pressure event to data model & StatsBomb parser --- kloppy/domain/models/event.py | 20 +++++++++++++++++++ kloppy/domain/services/event_factory.py | 4 ++++ .../event/statsbomb/specification.py | 19 ++++++++++++++++++ kloppy/tests/test_statsbomb.py | 9 +++++++++ 4 files changed, 52 insertions(+) diff --git a/kloppy/domain/models/event.py b/kloppy/domain/models/event.py index c40147d3..22f4dfe3 100644 --- a/kloppy/domain/models/event.py +++ b/kloppy/domain/models/event.py @@ -213,6 +213,7 @@ class EventType(Enum): BALL_OUT (EventType): FOUL_COMMITTED (EventType): GOALKEEPER (EventType): + PRESSURE (EventType): FORMATION_CHANGE (EventType): """ @@ -234,6 +235,7 @@ class EventType(Enum): BALL_OUT = "BALL_OUT" FOUL_COMMITTED = "FOUL_COMMITTED" GOALKEEPER = "GOALKEEPER" + PRESSURE = "PRESSURE" FORMATION_CHANGE = "FORMATION_CHANGE" def __repr__(self): @@ -1001,6 +1003,24 @@ class GoalkeeperEvent(Event): event_name: str = "goalkeeper" +@dataclass(repr=False) +@docstring_inherit_attributes(Event) +class PressureEvent(Event): + """ + PressureEvent + + Attributes: + event_type (EventType): `EventType.Pressure` (See [`EventType`][kloppy.domain.models.event.EventType]) + event_name (str): `"pressure"`, + end_timestamp (float): + """ + + end_timestamp: float + + event_type: EventType = EventType.PRESSURE + event_name: str = "pressure" + + @dataclass(repr=False) class EventDataset(Dataset[Event]): """ diff --git a/kloppy/domain/services/event_factory.py b/kloppy/domain/services/event_factory.py index 33fe4f61..7afe01c3 100644 --- a/kloppy/domain/services/event_factory.py +++ b/kloppy/domain/services/event_factory.py @@ -23,6 +23,7 @@ SubstitutionEvent, GoalkeeperEvent, ) +from kloppy.domain.models.event import PressureEvent T = TypeVar("T") @@ -122,3 +123,6 @@ def build_substitution(self, **kwargs) -> SubstitutionEvent: def build_goalkeeper_event(self, **kwargs) -> GoalkeeperEvent: return create_event(GoalkeeperEvent, **kwargs) + + def build_pressure_event(self, **kwargs) -> PressureEvent: + return create_event(PressureEvent, **kwargs) diff --git a/kloppy/infra/serializers/event/statsbomb/specification.py b/kloppy/infra/serializers/event/statsbomb/specification.py index 874834b3..e1a020e9 100644 --- a/kloppy/infra/serializers/event/statsbomb/specification.py +++ b/kloppy/infra/serializers/event/statsbomb/specification.py @@ -1136,6 +1136,24 @@ def _create_events( return [recovery_event] +class PRESSURE(EVENT): + """StatsBomb 17/Pressure event.""" + + def _create_events( + self, event_factory: EventFactory, **generic_event_kwargs + ) -> List[Event]: + end_timestamp = generic_event_kwargs["timestamp"] + self.raw_event.get( + "duration", 0.0 + ) + pressure_event = event_factory.build_pressure_event( + result=None, + qualifiers=None, + end_timestamp=end_timestamp, + **generic_event_kwargs, + ) + return [pressure_event] + + class TACTICAL_SHIFT(EVENT): """StatsBomb 36/Tactical shift event.""" @@ -1279,6 +1297,7 @@ def event_decoder(raw_event: Dict) -> Union[EVENT, Dict]: EVENT_TYPE.PLAYER_ON: PLAYER_ON, EVENT_TYPE.PLAYER_OFF: PLAYER_OFF, EVENT_TYPE.BALL_RECOVERY: BALL_RECOVERY, + EVENT_TYPE.PRESSURE: PRESSURE, EVENT_TYPE.TACTICAL_SHIFT: TACTICAL_SHIFT, } event_type = EVENT_TYPE(raw_event["type"]) diff --git a/kloppy/tests/test_statsbomb.py b/kloppy/tests/test_statsbomb.py index 4115f1f5..e090050e 100644 --- a/kloppy/tests/test_statsbomb.py +++ b/kloppy/tests/test_statsbomb.py @@ -997,6 +997,15 @@ def test_card(self, dataset: EventDataset): assert foul_without_card.get_qualifier_value(CardQualifier) is None +class TestStatsBombPressureEvent: + """Tests related to deserializing 17/Pressure events""" + + def test_deserialize_all(self, dataset: EventDataset): + """It should deserialize all ball recovery events""" + events = dataset.find_all("pressure") + assert len(events) == 203 + + class TestStatsBombPlayerOffEvent: """Tests related to deserializing 19/Player Off events""" From 5dd84e1deb337166fb649743444819dc15dbbd43 Mon Sep 17 00:00:00 2001 From: driesdeprest Date: Thu, 25 Jan 2024 20:59:56 +0100 Subject: [PATCH 03/17] Merge master and simplify test --- kloppy/tests/test_opta.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/kloppy/tests/test_opta.py b/kloppy/tests/test_opta.py index e19a25a6..759ebced 100644 --- a/kloppy/tests/test_opta.py +++ b/kloppy/tests/test_opta.py @@ -244,7 +244,7 @@ class TestOptaPassEvent: def test_deserialize_all(self, dataset: EventDataset): """It should deserialize all clearance events""" events = dataset.find_all("pass") - assert len(events) == 14 + assert len(events) == 15 def test_receiver_coordinates(self, dataset: EventDataset): """Test if the receiver coordinates are correctly deserialized""" @@ -268,6 +268,11 @@ def test_pass_qualifiers(self, dataset: EventDataset): assert PassType.CHIPPED_PASS in chipped_pass.get_qualifier_values( PassQualifier ) + assist = dataset.get_event_by_id("1666666666") + assert PassType.ASSIST in assist.get_qualifier_values(PassQualifier) + assert PassType.SHOT_ASSIST in assist.get_qualifier_values( + PassQualifier + ) class TestOptaClearanceEvent: From d84159b116d977abc01cd573b00c89903cfe6101 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Sat, 27 Jan 2024 21:13:44 +0100 Subject: [PATCH 04/17] fix: Uniform implementation of timestamps The representation of timestamps was inconsistent between different data providers. This commit implements a uniform implementation between data providers. The timestamps of events and frames are now relative to the kick-off event in each period. This implies that timestamp are reset to zero for each period and that tracking frames before the kick-off event get a negative timestamp. The standard implementation for the start_timestamp and end_timestamp of periods is the absolute UTC datetime of respectively the kick-off and final whistle of the corresponding period. If the absolute datetime is not available, we use the offset between the start of the period's data feed and the kick-off for the start_timestamp, and the offset between the start of the period's data feed and the final whistle for the end_timestamp. BREAKING CHANGE: The Period.start_timestamp and Period.end_timestamp fields now have type datetime or timedelta instead of float BREAKING CHANGE: The DataRecord.timestamp field now has type timedelta instead of float BREAKING CHANGE: The Period.contains method was removed BREAKING CHANGE: The value of the Period.start_timestamp, Period.end_timestamp and DataRecord.timestamp fields changes for most dataproviders --- kloppy/domain/models/code.py | 3 +- kloppy/domain/models/common.py | 45 +- kloppy/domain/models/event.py | 2 +- kloppy/infra/serializers/code/sportscode.py | 23 +- .../event/datafactory/deserializer.py | 63 +- .../event/metrica/json_deserializer.py | 32 +- .../serializers/event/opta/deserializer.py | 8 +- .../serializers/event/sportec/deserializer.py | 63 +- .../serializers/event/statsbomb/helpers.py | 6 +- .../event/statsbomb/specification.py | 8 +- .../event/wyscout/deserializer_v2.py | 20 +- .../event/wyscout/deserializer_v3.py | 35 +- .../infra/serializers/tracking/metrica_csv.py | 14 +- .../tracking/metrica_epts/metadata.py | 10 +- .../tracking/metrica_epts/reader.py | 4 +- .../serializers/tracking/secondspectrum.py | 19 +- .../infra/serializers/tracking/skillcorner.py | 36 +- .../tracking/sportec/deserializer.py | 7 +- .../serializers/tracking/statsperform.py | 91 +- kloppy/infra/serializers/tracking/tracab.py | 18 +- .../tests/files/skillcorner_match_data.json | 938 +++++++++++++++++- kloppy/tests/test_datafactory.py | 39 +- kloppy/tests/test_metrica_csv.py | 39 +- kloppy/tests/test_metrica_epts.py | 24 + kloppy/tests/test_metrica_events.py | 46 +- kloppy/tests/test_opta.py | 23 +- kloppy/tests/test_secondspectrum.py | 28 +- kloppy/tests/test_skillcorner.py | 70 +- kloppy/tests/test_sportec.py | 64 +- kloppy/tests/test_statsbomb.py | 27 +- kloppy/tests/test_statsperform.py | 28 +- kloppy/tests/test_to_records.py | 5 +- kloppy/tests/test_tracab.py | 40 +- kloppy/tests/test_wyscout.py | 45 + kloppy/tests/test_xml.py | 33 +- 35 files changed, 1690 insertions(+), 266 deletions(-) diff --git a/kloppy/domain/models/code.py b/kloppy/domain/models/code.py index 264a07f3..7f48f788 100644 --- a/kloppy/domain/models/code.py +++ b/kloppy/domain/models/code.py @@ -1,3 +1,4 @@ +from datetime import timedelta from dataclasses import dataclass, field from typing import List, Dict, Callable, Union, Any @@ -26,7 +27,7 @@ class Code(DataRecord): code_id: str code: str - end_timestamp: float + end_timestamp: timedelta labels: Dict[str, Union[bool, str]] = field(default_factory=dict) @property diff --git a/kloppy/domain/models/common.py b/kloppy/domain/models/common.py index 485bd067..9ded940c 100644 --- a/kloppy/domain/models/common.py +++ b/kloppy/domain/models/common.py @@ -2,6 +2,7 @@ from abc import ABC, abstractmethod from collections import defaultdict from dataclasses import dataclass, field, replace +from datetime import datetime, timedelta from enum import Enum, Flag from typing import ( Dict, @@ -34,6 +35,7 @@ OrientationError, InvalidFilterError, KloppyParameterError, + KloppyError, ) @@ -331,31 +333,42 @@ class Period: Period Attributes: - id: `1` for first half, `2` for second half - start_timestamp: timestamp given by provider (can be unix timestamp or relative) - end_timestamp: timestamp given by provider (can be unix timestamp or relative) + id: `1` for first half, `2` for second half, `3` for first half of + overtime, `4` for second half of overtime, `5` for penalty shootout + start_timestamp: The UTC datetime of the kick-off or, if the + absolute datetime is not available, the offset between the start + of the data feed and the period's kick-off + end_timestamp: The UTC datetime of the final whistle or, if the + absolute datetime is not available, the offset between the start + of the data feed and the period's final whistle attacking_direction: See [`AttackingDirection`][kloppy.domain.models.common.AttackingDirection] """ id: int - start_timestamp: float - end_timestamp: float - attacking_direction: Optional[ - AttackingDirection - ] = AttackingDirection.NOT_SET - - def contains(self, timestamp: float): - return self.start_timestamp <= timestamp <= self.end_timestamp + start_timestamp: Union[datetime, timedelta] + end_timestamp: Union[datetime, timedelta] + attacking_direction: AttackingDirection = AttackingDirection.NOT_SET + + def contains(self, timestamp: datetime): + if isinstance(self.start_timestamp, datetime) and isinstance( + self.end_timestamp, datetime + ): + return self.start_timestamp <= timestamp <= self.end_timestamp + raise KloppyError( + "This method can only be used when start_timestamp and end_timmestamp are a datetime" + ) @property - def attacking_direction_set(self): + def attacking_direction_set(self) -> bool: return self.attacking_direction != AttackingDirection.NOT_SET - def set_attacking_direction(self, attacking_direction: AttackingDirection): + def set_attacking_direction( + self, attacking_direction: AttackingDirection + ) -> None: self.attacking_direction = attacking_direction @property - def duration(self): + def duration(self) -> timedelta: return self.end_timestamp - self.start_timestamp def __eq__(self, other): @@ -755,7 +768,7 @@ class DataRecord(ABC): Attributes: period: See [`Period`][kloppy.domain.models.common.Period] - timestamp: Timestamp of occurrence + timestamp: Timestamp of occurrence, relative to the period kick-off ball_owning_team: See [`Team`][kloppy.domain.models.common.Team] ball_state: See [`Team`][kloppy.domain.models.common.BallState] """ @@ -764,7 +777,7 @@ class DataRecord(ABC): prev_record: Optional["DataRecord"] = field(init=False) next_record: Optional["DataRecord"] = field(init=False) period: Period - timestamp: float + timestamp: timedelta ball_owning_team: Optional[Team] ball_state: Optional[BallState] diff --git a/kloppy/domain/models/event.py b/kloppy/domain/models/event.py index c40147d3..81583936 100644 --- a/kloppy/domain/models/event.py +++ b/kloppy/domain/models/event.py @@ -676,7 +676,7 @@ def matches(self, filter_) -> bool: return True def __str__(self): - m, s = divmod(self.timestamp, 60) + m, s = divmod(self.timestamp.total_seconds(), 60) event_type = ( self.__class__.__name__ diff --git a/kloppy/infra/serializers/code/sportscode.py b/kloppy/infra/serializers/code/sportscode.py index 41623f31..612bf8ca 100644 --- a/kloppy/infra/serializers/code/sportscode.py +++ b/kloppy/infra/serializers/code/sportscode.py @@ -1,4 +1,5 @@ import logging +from datetime import timedelta from typing import Union, IO, NamedTuple from lxml import objectify, etree @@ -50,15 +51,19 @@ def deserialize(self, inputs: SportsCodeInputs) -> CodeDataset: all_instances = objectify.fromstring(inputs.data.read()) codes = [] - period = Period(id=1, start_timestamp=0, end_timestamp=0) + period = Period( + id=1, + start_timestamp=timedelta(seconds=0), + end_timestamp=timedelta(seconds=0), + ) for instance in all_instances.ALL_INSTANCES.iterchildren(): - end_timestamp = float(instance.end) + end_timestamp = timedelta(seconds=float(instance.end)) code = Code( period=period, code_id=str(instance.ID), code=str(instance.code), - timestamp=float(instance.start), + timestamp=timedelta(seconds=float(instance.start)), end_timestamp=end_timestamp, labels=parse_labels(instance), ball_state=None, @@ -88,7 +93,7 @@ def serialize(self, dataset: CodeDataset) -> bytes: root = etree.Element("file") all_instances = etree.SubElement(root, "ALL_INSTANCES") for i, code in enumerate(dataset.codes): - relative_period_start = 0 + relative_period_start = timedelta(seconds=0) for period in dataset.metadata.periods: if period == code.period: break @@ -100,10 +105,16 @@ def serialize(self, dataset: CodeDataset) -> bytes: id_.text = code.code_id or str(i + 1) start = etree.SubElement(instance, "start") - start.text = str(relative_period_start + code.start_timestamp) + start.text = str( + relative_period_start.total_seconds() + + code.start_timestamp.total_seconds() + ) end = etree.SubElement(instance, "end") - end.text = str(relative_period_start + code.end_timestamp) + end.text = str( + relative_period_start.total_seconds() + + code.end_timestamp.total_seconds() + ) code_ = etree.SubElement(instance, "code") code_.text = code.code diff --git a/kloppy/infra/serializers/event/datafactory/deserializer.py b/kloppy/infra/serializers/event/datafactory/deserializer.py index 2bac39c0..e75c2621 100644 --- a/kloppy/infra/serializers/event/datafactory/deserializer.py +++ b/kloppy/infra/serializers/event/datafactory/deserializer.py @@ -1,5 +1,7 @@ import json import logging +from datetime import timedelta, datetime, timezone +from dataclasses import replace from typing import Dict, List, Tuple, Union, IO, NamedTuple from kloppy.domain import ( @@ -155,8 +157,10 @@ DF_EVENT_TYPE_PENALTY_SHOOTOUT_POST = 183 -def parse_str_ts(raw_event: Dict) -> float: - return raw_event["t"]["m"] * 60 + (raw_event["t"]["s"] or 0) +def parse_str_ts(raw_event: Dict) -> timedelta: + return timedelta( + seconds=raw_event["t"]["m"] * 60 + (raw_event["t"]["s"] or 0) + ) def _parse_coordinates(coordinates: Dict[str, float]) -> Point: @@ -397,8 +401,21 @@ def deserialize(self, inputs: DatafactoryInputs) -> EventDataset: # setup periods status = incidences.pop(DF_EVENT_CLASS_STATUS) # start timestamps are fixed - start_ts = {1: 0, 2: 45 * 60, 3: 90 * 60, 4: 105 * 60, 5: 120 * 60} + start_ts = { + 1: timedelta(minutes=0), + 2: timedelta(minutes=45), + 3: timedelta(minutes=90), + 4: timedelta(minutes=105), + 5: timedelta(minutes=120), + } # check for end status updates to setup periods + start_event_types = { + DF_EVENT_TYPE_STATUS_MATCH_START, + DF_EVENT_TYPE_STATUS_SECOND_HALF_START, + DF_EVENT_TYPE_STATUS_FIRST_EXTRA_START, + DF_EVENT_TYPE_STATUS_SECOND_EXTRA_START, + DF_EVENT_TYPE_STATUS_PENALTY_SHOOTOUT_START, + } end_event_types = { DF_EVENT_TYPE_STATUS_MATCH_END, DF_EVENT_TYPE_STATUS_FIRST_HALF_END, @@ -408,18 +425,36 @@ def deserialize(self, inputs: DatafactoryInputs) -> EventDataset: } periods = {} for status_update in status.values(): - if status_update["type"] not in end_event_types: + if status_update["type"] not in ( + start_event_types | end_event_types + ): continue + timestamp = datetime.strptime( + match["date"] + + status_update["time"] + + match["stadiumGMT"], + "%Y%m%d%H:%M:%S%z", + ).astimezone(timezone.utc) half = status_update["t"]["half"] - end_ts = parse_str_ts(status_update) - periods[half] = Period( - id=half, - start_timestamp=start_ts[half], - end_timestamp=end_ts, - attacking_direction=AttackingDirection.HOME_AWAY - if half % 2 == 1 - else AttackingDirection.AWAY_HOME, - ) + if status_update["type"] == DF_EVENT_TYPE_STATUS_MATCH_START: + half = 1 + if status_update["type"] in start_event_types: + periods[half] = Period( + id=half, + start_timestamp=timestamp, + end_timestamp=None, + attacking_direction=AttackingDirection.HOME_AWAY + if half % 2 == 1 + else AttackingDirection.AWAY_HOME, + ) + elif status_update["type"] in end_event_types: + if half not in periods: + raise DeserializationError( + f"Missing start event for period {half}" + ) + periods[half] = replace( + periods[half], end_timestamp=timestamp + ) # exclude goals, already listed as shots too incidences.pop(DF_EVENT_CLASS_GOALS) @@ -447,7 +482,7 @@ def deserialize(self, inputs: DatafactoryInputs) -> EventDataset: # skip invalid event continue - timestamp = parse_str_ts(raw_event) + timestamp = parse_str_ts(raw_event) - start_ts[period.id] if ( previous_event is not None and previous_event["t"]["half"] != raw_event["t"]["half"] diff --git a/kloppy/infra/serializers/event/metrica/json_deserializer.py b/kloppy/infra/serializers/event/metrica/json_deserializer.py index 3d846837..16461bc7 100644 --- a/kloppy/infra/serializers/event/metrica/json_deserializer.py +++ b/kloppy/infra/serializers/event/metrica/json_deserializer.py @@ -1,6 +1,7 @@ import logging import json from dataclasses import replace +from datetime import timedelta from typing import Dict, List, NamedTuple, IO, Optional from kloppy.domain import ( @@ -10,6 +11,7 @@ CarryResult, EventDataset, PassResult, + Period, Point, Provider, Qualifier, @@ -106,7 +108,11 @@ def _parse_subtypes(event: dict) -> List: def _parse_pass( - event: Dict, previous_event: Dict, subtypes: List, team: Team + period: Period, + event: Dict, + previous_event: Dict, + subtypes: List, + team: Team, ) -> Dict: event_type_id = event["type"]["id"] @@ -114,7 +120,9 @@ def _parse_pass( result = PassResult.COMPLETE receiver_player = team.get_player_by_id(event["to"]["id"]) receiver_coordinates = _parse_coordinates(event["end"]) - receive_timestamp = event["end"]["time"] + receive_timestamp = ( + timedelta(seconds=event["end"]["time"]) - period.start_timestamp + ) else: if event_type_id == MS_PASS_OUTCOME_OUT: result = PassResult.OUT @@ -208,11 +216,12 @@ def _parse_shot(event: Dict, previous_event: Dict, subtypes: List) -> Dict: return dict(result=result, qualifiers=qualifiers) -def _parse_carry(event: Dict) -> Dict: +def _parse_carry(period: Period, event: Dict) -> Dict: return dict( result=CarryResult.COMPLETE, end_coordinates=_parse_coordinates(event["end"]), - end_timestamp=event["end"]["time"], + end_timestamp=timedelta(seconds=event["end"]["time"]) + - period.start_timestamp, ) @@ -285,7 +294,8 @@ def deserialize(self, inputs: MetricaJsonEventDataInputs) -> EventDataset: generic_event_kwargs = dict( # from DataRecord period=period, - timestamp=raw_event["start"]["time"], + timestamp=timedelta(seconds=raw_event["start"]["time"]) + - period.start_timestamp, ball_owning_team=_parse_ball_owning_team(event_type, team), ball_state=BallState.ALIVE, # from Event @@ -301,6 +311,7 @@ def deserialize(self, inputs: MetricaJsonEventDataInputs) -> EventDataset: continue elif event_type in MS_PASS_TYPES: pass_event_kwargs = _parse_pass( + period=period, event=raw_event, previous_event=previous_event, subtypes=subtypes, @@ -332,7 +343,9 @@ def deserialize(self, inputs: MetricaJsonEventDataInputs) -> EventDataset: ) elif event_type == MS_EVENT_TYPE_CARRY: - carry_event_kwargs = _parse_carry(event=raw_event) + carry_event_kwargs = _parse_carry( + period=period, event=raw_event + ) event = self.event_factory.build_carry( qualifiers=None, **carry_event_kwargs, @@ -371,9 +384,10 @@ def deserialize(self, inputs: MetricaJsonEventDataInputs) -> EventDataset: generic_event_kwargs[ "coordinates" ] = _parse_coordinates(raw_event["end"]) - generic_event_kwargs["timestamp"] = raw_event["end"][ - "time" - ] + generic_event_kwargs["timestamp"] = ( + timedelta(seconds=raw_event["end"]["time"]) + - period.start_timestamp + ) event = self.event_factory.build_ball_out( result=None, diff --git a/kloppy/infra/serializers/event/opta/deserializer.py b/kloppy/infra/serializers/event/opta/deserializer.py index 0ee6c3a1..dacdb7b5 100644 --- a/kloppy/infra/serializers/event/opta/deserializer.py +++ b/kloppy/infra/serializers/event/opta/deserializer.py @@ -245,16 +245,14 @@ } -def _parse_f24_datetime(dt_str: str) -> float: +def _parse_f24_datetime(dt_str: str) -> datetime: def zero_pad_milliseconds(timestamp): parts = timestamp.split(".") return ".".join(parts[:-1] + ["{:03d}".format(int(parts[-1]))]) dt_str = zero_pad_milliseconds(dt_str) - return ( - datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%S.%f") - .replace(tzinfo=pytz.utc) - .timestamp() + return datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%S.%f").replace( + tzinfo=pytz.utc ) diff --git a/kloppy/infra/serializers/event/sportec/deserializer.py b/kloppy/infra/serializers/event/sportec/deserializer.py index fa29f753..dfe4be3d 100644 --- a/kloppy/infra/serializers/event/sportec/deserializer.py +++ b/kloppy/infra/serializers/event/sportec/deserializer.py @@ -1,5 +1,6 @@ from collections import OrderedDict from typing import Dict, List, NamedTuple, IO +from datetime import timedelta, datetime, timezone import logging from dateutil.parser import parse from lxml import objectify @@ -129,16 +130,23 @@ def sportec_metadata_from_xml_elm(match_root) -> SportecMetadata: periods = [ Period( id=1, - start_timestamp=SPORTEC_FIRST_HALF_STARTING_FRAME_ID / SPORTEC_FPS, - end_timestamp=SPORTEC_FIRST_HALF_STARTING_FRAME_ID / SPORTEC_FPS - + float(other_game_information["TotalTimeFirstHalf"]) / 1000, + start_timestamp=timedelta( + seconds=SPORTEC_FIRST_HALF_STARTING_FRAME_ID / SPORTEC_FPS + ), + end_timestamp=timedelta( + seconds=SPORTEC_FIRST_HALF_STARTING_FRAME_ID / SPORTEC_FPS + + float(other_game_information["TotalTimeFirstHalf"]) / 1000 + ), ), Period( id=2, - start_timestamp=SPORTEC_SECOND_HALF_STARTING_FRAME_ID - / SPORTEC_FPS, - end_timestamp=SPORTEC_SECOND_HALF_STARTING_FRAME_ID / SPORTEC_FPS - + float(other_game_information["TotalTimeSecondHalf"]) / 1000, + start_timestamp=timedelta( + seconds=SPORTEC_SECOND_HALF_STARTING_FRAME_ID / SPORTEC_FPS + ), + end_timestamp=timedelta( + seconds=SPORTEC_SECOND_HALF_STARTING_FRAME_ID / SPORTEC_FPS + + float(other_game_information["TotalTimeSecondHalf"]) / 1000 + ), ), ] @@ -148,21 +156,33 @@ def sportec_metadata_from_xml_elm(match_root) -> SportecMetadata: [ Period( id=3, - start_timestamp=SPORTEC_FIRST_EXTRA_HALF_STARTING_FRAME_ID - / SPORTEC_FPS, - end_timestamp=SPORTEC_FIRST_EXTRA_HALF_STARTING_FRAME_ID - / SPORTEC_FPS - + float(other_game_information["TotalTimeFirstHalfExtra"]) - / 1000, + start_timestamp=timedelta( + seconds=SPORTEC_FIRST_EXTRA_HALF_STARTING_FRAME_ID + / SPORTEC_FPS + ), + end_timestamp=timedelta( + seconds=SPORTEC_FIRST_EXTRA_HALF_STARTING_FRAME_ID + / SPORTEC_FPS + + float( + other_game_information["TotalTimeFirstHalfExtra"] + ) + / 1000 + ), ), Period( id=4, - start_timestamp=SPORTEC_SECOND_EXTRA_HALF_STARTING_FRAME_ID - / SPORTEC_FPS, - end_timestamp=SPORTEC_SECOND_EXTRA_HALF_STARTING_FRAME_ID - / SPORTEC_FPS - + float(other_game_information["TotalTimeSecondHalfExtra"]) - / 1000, + start_timestamp=timedelta( + seconds=SPORTEC_SECOND_EXTRA_HALF_STARTING_FRAME_ID + / SPORTEC_FPS + ), + end_timestamp=timedelta( + seconds=SPORTEC_SECOND_EXTRA_HALF_STARTING_FRAME_ID + / SPORTEC_FPS + + float( + other_game_information["TotalTimeSecondHalfExtra"] + ) + / 1000 + ), ), ] ) @@ -228,8 +248,8 @@ def _event_chain_from_xml_elm(event_elm): SPORTEC_EVENT_BODY_PART_RIGHT_FOOT = "rightLeg" -def _parse_datetime(dt_str: str) -> float: - return parse(dt_str).timestamp() +def _parse_datetime(dt_str: str) -> datetime: + return parse(dt_str).astimezone(timezone.utc) def _get_event_qualifiers(event_chain: Dict) -> List[Qualifier]: @@ -397,6 +417,7 @@ def deserialize(self, inputs: SportecEventDataInputs) -> EventDataset: for event_elm in event_root.iterchildren("Event"): event_chain = _event_chain_from_xml_elm(event_elm) timestamp = _parse_datetime(event_chain["Event"]["EventTime"]) + if ( SPORTEC_EVENT_NAME_KICKOFF in event_chain and "GameSection" diff --git a/kloppy/infra/serializers/event/statsbomb/helpers.py b/kloppy/infra/serializers/event/statsbomb/helpers.py index 0c991193..85edcae1 100644 --- a/kloppy/infra/serializers/event/statsbomb/helpers.py +++ b/kloppy/infra/serializers/event/statsbomb/helpers.py @@ -1,3 +1,4 @@ +from datetime import timedelta from typing import List, Dict, Optional from kloppy.domain import ( @@ -16,7 +17,7 @@ def parse_str_ts(timestamp: str) -> float: """Parse a HH:mm:ss string timestamp into number of seconds.""" h, m, s = timestamp.split(":") - return int(h) * 3600 + int(m) * 60 + float(s) + return timedelta(seconds=int(h) * 3600 + int(m) * 60 + float(s)) def get_team_by_id(team_id: int, teams: List[Team]) -> Team: @@ -121,7 +122,8 @@ def get_player_from_freeze_frame(player_data, team, i): FREEZE_FRAME_FPS = 25 frame_id = int( - event.period.start_timestamp + event.timestamp * FREEZE_FRAME_FPS + event.period.start_timestamp.total_seconds() + + event.timestamp.total_seconds() * FREEZE_FRAME_FPS ) return Frame( diff --git a/kloppy/infra/serializers/event/statsbomb/specification.py b/kloppy/infra/serializers/event/statsbomb/specification.py index 874834b3..0f28bda7 100644 --- a/kloppy/infra/serializers/event/statsbomb/specification.py +++ b/kloppy/infra/serializers/event/statsbomb/specification.py @@ -1,3 +1,4 @@ +from datetime import timedelta from enum import Enum, EnumMeta from typing import List, Dict, Optional, NamedTuple, Union @@ -361,7 +362,9 @@ def _create_events( pass_dict["end_location"], self.fidelity_version, ) - receive_timestamp = timestamp + self.raw_event.get("duration", 0.0) + receive_timestamp = timestamp + timedelta( + seconds=self.raw_event.get("duration", 0.0) + ) if "outcome" in pass_dict: outcome_id = pass_dict["outcome"]["id"] @@ -707,7 +710,8 @@ def _create_events( carry_dict = self.raw_event["carry"] carry_event = event_factory.build_carry( qualifiers=None, - end_timestamp=timestamp + self.raw_event.get("duration", 0), + end_timestamp=timestamp + + timedelta(seconds=self.raw_event.get("duration", 0)), result=CarryResult.COMPLETE, end_coordinates=parse_coordinates( carry_dict["end_location"], diff --git a/kloppy/infra/serializers/event/wyscout/deserializer_v2.py b/kloppy/infra/serializers/event/wyscout/deserializer_v2.py index 2ef37b64..4e1815cc 100644 --- a/kloppy/infra/serializers/event/wyscout/deserializer_v2.py +++ b/kloppy/infra/serializers/event/wyscout/deserializer_v2.py @@ -1,5 +1,7 @@ import json import logging +from dataclasses import replace +from datetime import timedelta from typing import Dict, List, Tuple, NamedTuple, IO, Optional from kloppy.domain import ( @@ -502,8 +504,12 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset: for idx, raw_event in enumerate(raw_events["events"]): next_event = None + next_period_id = None if (idx + 1) < len(raw_events["events"]): next_event = raw_events["events"][idx + 1] + next_period_id = int( + next_event["matchPeriod"].replace("H", "") + ) team_id = str(raw_event["teamId"]) player_id = str(raw_event["playerId"]) @@ -513,10 +519,18 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset: periods.append( Period( id=period_id, - start_timestamp=0, - end_timestamp=0, + start_timestamp=timedelta(seconds=0) + if len(periods) == 0 + else periods[-1].end_timestamp, + end_timestamp=None, ) ) + if next_period_id != period_id: + periods[-1] = replace( + periods[-1], + end_timestamp=periods[-1].start_timestamp + + timedelta(seconds=raw_event["eventSec"]), + ) generic_event_args = { "event_id": str(raw_event["id"]), @@ -532,7 +546,7 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset: "ball_owning_team": None, "ball_state": None, "period": periods[-1], - "timestamp": raw_event["eventSec"], + "timestamp": timedelta(seconds=raw_event["eventSec"]), } new_events = [] diff --git a/kloppy/infra/serializers/event/wyscout/deserializer_v3.py b/kloppy/infra/serializers/event/wyscout/deserializer_v3.py index a19ce11a..e57b6821 100644 --- a/kloppy/infra/serializers/event/wyscout/deserializer_v3.py +++ b/kloppy/infra/serializers/event/wyscout/deserializer_v3.py @@ -1,5 +1,7 @@ import json import logging +from dataclasses import replace +from datetime import timedelta from typing import Dict, List, Tuple, NamedTuple, IO from kloppy.domain import ( @@ -490,8 +492,12 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset: for idx, raw_event in enumerate(raw_events["events"]): next_event = None + next_period_id = None if (idx + 1) < len(raw_events["events"]): next_event = raw_events["events"][idx + 1] + next_period_id = int( + next_event["matchPeriod"].replace("H", "") + ) team_id = str(raw_event["team"]["id"]) team = teams[team_id] @@ -502,11 +508,24 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset: periods.append( Period( id=period_id, - start_timestamp=0, - end_timestamp=0, + start_timestamp=timedelta(seconds=0) + if len(periods) == 0 + else periods[-1].end_timestamp, + end_timestamp=None, ) ) + if next_period_id != period_id: + periods[-1] = replace( + periods[-1], + end_timestamp=periods[-1].start_timestamp + + timedelta( + seconds=float( + raw_event["second"] + raw_event["minute"] * 60 + ) + ), + ) + ball_owning_team = None if raw_event["possession"]: ball_owning_team = teams[ @@ -529,14 +548,10 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset: "ball_owning_team": ball_owning_team, "ball_state": None, "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) + "timestamp": timedelta( + seconds=float( + raw_event["second"] + raw_event["minute"] * 60 + ) ), } diff --git a/kloppy/infra/serializers/tracking/metrica_csv.py b/kloppy/infra/serializers/tracking/metrica_csv.py index fa61690c..cfc386ae 100644 --- a/kloppy/infra/serializers/tracking/metrica_csv.py +++ b/kloppy/infra/serializers/tracking/metrica_csv.py @@ -1,5 +1,6 @@ import logging from collections import namedtuple +from datetime import timedelta from typing import Tuple, Dict, Iterator, IO, NamedTuple from kloppy.domain import ( @@ -89,12 +90,16 @@ def __create_iterator( if period is None or period.id != period_id: period = Period( id=period_id, - start_timestamp=frame_id / frame_rate, - end_timestamp=frame_id / frame_rate, + start_timestamp=timedelta( + seconds=(frame_id - 1) / frame_rate + ), + end_timestamp=timedelta(seconds=frame_id / frame_rate), ) else: # consider not update this every frame for performance reasons - period.end_timestamp = frame_id / frame_rate + period.end_timestamp = timedelta( + seconds=frame_id / frame_rate + ) if frame_idx % frame_sample == 0: yield self.__PartialFrame( @@ -188,7 +193,8 @@ def deserialize( frame = Frame( frame_id=frame_id, - timestamp=frame_id / frame_rate - period.start_timestamp, + timestamp=timedelta(seconds=frame_id / frame_rate) + - period.start_timestamp, ball_coordinates=home_partial_frame.ball_coordinates, players_data=players_data, period=period, diff --git a/kloppy/infra/serializers/tracking/metrica_epts/metadata.py b/kloppy/infra/serializers/tracking/metrica_epts/metadata.py index f87ffc46..3a14ac20 100644 --- a/kloppy/infra/serializers/tracking/metrica_epts/metadata.py +++ b/kloppy/infra/serializers/tracking/metrica_epts/metadata.py @@ -1,4 +1,5 @@ from typing import IO +from datetime import timedelta from lxml import objectify import warnings @@ -96,9 +97,12 @@ def _load_periods( periods.append( Period( id=idx + 1, - start_timestamp=float(provider_params[start_key]) - / frame_rate, - end_timestamp=float(provider_params[end_key]) / frame_rate, + start_timestamp=timedelta( + seconds=float(provider_params[start_key]) / frame_rate + ), + end_timestamp=timedelta( + seconds=float(provider_params[end_key]) / frame_rate + ), attacking_direction=attacking_direction, ) ) diff --git a/kloppy/infra/serializers/tracking/metrica_epts/reader.py b/kloppy/infra/serializers/tracking/metrica_epts/reader.py index 86e81ddf..628a5b52 100644 --- a/kloppy/infra/serializers/tracking/metrica_epts/reader.py +++ b/kloppy/infra/serializers/tracking/metrica_epts/reader.py @@ -1,5 +1,6 @@ import re from typing import List, Tuple, Set, Iterator, IO +from datetime import timedelta from kloppy.utils import Readable @@ -92,7 +93,7 @@ def to_float(v): } frame_id = int(row[frame_name]) if frame_id <= end_frame_id: - timestamp = frame_id / metadata.frame_rate + timestamp = timedelta(seconds=frame_id / metadata.frame_rate) del row[frame_name] row["frame_id"] = frame_id @@ -102,6 +103,7 @@ def to_float(v): for period in periods: if period.start_timestamp <= timestamp <= period.end_timestamp: row["period_id"] = period.id + row["timestamp"] -= period.start_timestamp break yield row diff --git a/kloppy/infra/serializers/tracking/secondspectrum.py b/kloppy/infra/serializers/tracking/secondspectrum.py index e1f0dcb0..5c771dea 100644 --- a/kloppy/infra/serializers/tracking/secondspectrum.py +++ b/kloppy/infra/serializers/tracking/secondspectrum.py @@ -1,5 +1,6 @@ import json import logging +from datetime import timedelta from typing import Tuple, Dict, Optional, Union, NamedTuple, IO from lxml import objectify @@ -56,7 +57,7 @@ def provider(self) -> Provider: @classmethod def _frame_from_framedata(cls, teams, period, frame_data): frame_id = frame_data["frameIdx"] - frame_timestamp = frame_data["gameClock"] + frame_timestamp = timedelta(seconds=frame_data["gameClock"]) if frame_data["ball"]["xyz"]: ball_x, ball_y, ball_z = frame_data["ball"]["xyz"] @@ -137,8 +138,12 @@ def deserialize(self, inputs: SecondSpectrumInputs) -> TrackingDataset: periods.append( Period( id=int(period["number"]), - start_timestamp=start_frame_id, - end_timestamp=end_frame_id, + start_timestamp=timedelta( + seconds=start_frame_id / frame_rate + ), + end_timestamp=timedelta( + seconds=end_frame_id / frame_rate + ), ) ) else: @@ -158,8 +163,12 @@ def deserialize(self, inputs: SecondSpectrumInputs) -> TrackingDataset: periods.append( Period( id=int(period.attrib["iId"]), - start_timestamp=start_frame_id, - end_timestamp=end_frame_id, + start_timestamp=timedelta( + seconds=start_frame_id / frame_rate + ), + end_timestamp=timedelta( + seconds=end_frame_id / frame_rate + ), ) ) diff --git a/kloppy/infra/serializers/tracking/skillcorner.py b/kloppy/infra/serializers/tracking/skillcorner.py index a255e501..ecbd1e13 100644 --- a/kloppy/infra/serializers/tracking/skillcorner.py +++ b/kloppy/infra/serializers/tracking/skillcorner.py @@ -1,4 +1,5 @@ import logging +from datetime import timedelta from typing import List, Dict, Tuple, NamedTuple, IO, Optional, Union from enum import Enum, Flag from collections import Counter @@ -33,6 +34,9 @@ logger = logging.getLogger(__name__) +frame_rate = 10 + + class SkillCornerInputs(NamedTuple): meta_data: IO[bytes] raw_data: IO[bytes] @@ -72,6 +76,20 @@ def _get_frame_data( frame_id = frame["frame"] frame_time = cls._timestamp_from_timestring(frame["time"]) + if frame_period == 1: + frame_time -= timedelta(seconds=0) + elif frame_period == 2: + frame_time -= timedelta(seconds=45 * 60) + # TODO: check if the below is correct; just guessing here + elif frame_period == 3: + frame_time -= timedelta(seconds=90 * 60) + elif frame_period == 4: + frame_time -= timedelta(seconds=105 * 60) + elif frame_period == 5: + frame_time -= timedelta(seconds=120 * 60) + else: + raise ValueError(f"Unknown period id {frame_period}") + ball_coordinates = None players_data = {} @@ -137,7 +155,7 @@ def _get_frame_data( return Frame( frame_id=frame_id, - timestamp=frame_time - periods[frame_period].start_timestamp, + timestamp=frame_time, ball_coordinates=ball_coordinates, players_data=players_data, period=periods[frame_period], @@ -152,10 +170,12 @@ def _timestamp_from_timestring(cls, timestring): if len(parts) == 2: m, s = parts - return 60 * float(m) + float(s) + return timedelta(seconds=60 * float(m) + float(s)) elif len(parts) == 3: h, m, s = parts - return 3600 * float(h) + 60 * float(m) + float(s) + return timedelta( + seconds=3600 * float(h) + 60 * float(m) + float(s) + ) else: raise ValueError("Invalid timestring format") @@ -223,11 +243,11 @@ def __get_periods(cls, tracking): periods[period] = Period( id=period, - start_timestamp=cls._timestamp_from_timestring( - _frames[0]["time"] + start_timestamp=timedelta( + seconds=_frames[0]["frame"] / frame_rate ), - end_timestamp=cls._timestamp_from_timestring( - _frames[-1]["time"] + end_timestamp=timedelta( + seconds=_frames[-1]["frame"] / frame_rate ), ) return periods @@ -396,8 +416,6 @@ def _iter(): self._set_skillcorner_attacking_directions(frames, periods) - frame_rate = 10 - orientation = ( Orientation.HOME_TEAM if periods[1].attacking_direction == AttackingDirection.HOME_AWAY diff --git a/kloppy/infra/serializers/tracking/sportec/deserializer.py b/kloppy/infra/serializers/tracking/sportec/deserializer.py index 45b05b1f..2a0ed163 100644 --- a/kloppy/infra/serializers/tracking/sportec/deserializer.py +++ b/kloppy/infra/serializers/tracking/sportec/deserializer.py @@ -1,6 +1,7 @@ import logging from collections import defaultdict from typing import NamedTuple, Optional, Union, IO +from datetime import timedelta from lxml import objectify @@ -156,11 +157,11 @@ def _iter(): if i % sample == 0: yield Frame( frame_id=frame_id, - timestamp=( - ( + timestamp=timedelta( + seconds=( frame_id # Do subtraction with integers to prevent floating errors - - period.start_timestamp + - period.start_timestamp.seconds * sportec_metadata.fps ) / sportec_metadata.fps diff --git a/kloppy/infra/serializers/tracking/statsperform.py b/kloppy/infra/serializers/tracking/statsperform.py index 6c5d6687..283593b9 100644 --- a/kloppy/infra/serializers/tracking/statsperform.py +++ b/kloppy/infra/serializers/tracking/statsperform.py @@ -1,5 +1,6 @@ import json import logging +from datetime import datetime, timedelta from typing import IO, Any, Dict, List, NamedTuple, Optional, Union from lxml import objectify @@ -49,29 +50,6 @@ def __init__( def provider(self) -> Provider: return Provider.STATSPERFORM - @classmethod - def __get_periods(cls, tracking): - """Gets the Periods contained in the tracking data.""" - period_data = {} - for line in tracking: - time_info = line.split(";")[1].split(",") - period_id = int(time_info[1]) - frame_id = int(time_info[0]) - if period_id not in period_data: - period_data[period_id] = set() - period_data[period_id].add(frame_id) - - periods = { - period_id: Period( - id=period_id, - start_timestamp=min(frame_ids), - end_timestamp=max(frame_ids), - ) - for period_id, frame_ids in period_data.items() - } - - return periods - @classmethod def __get_frame_rate(cls, tracking): """gets the frame rate of the tracking data""" @@ -96,7 +74,9 @@ def _frame_from_framedata(cls, teams_list, period, frame_data): frame_info = components[0].split(";") frame_id = int(frame_info[0]) - frame_timestamp = int(frame_info[1].split(",")[0]) / 1000 + frame_timestamp = timedelta( + seconds=int(frame_info[1].split(",")[0]) / 1000 + ) match_status = int(frame_info[1].split(",")[2]) ball_state = BallState.ALIVE if match_status == 0 else BallState.DEAD @@ -147,6 +127,26 @@ def _frame_from_framedata(cls, teams_list, period, frame_data): other_data={}, ) + @staticmethod + def __parse_periods_from_xml(match: Any) -> List[Dict[str, Any]]: + parsed_periods = [] + live_data = match.liveData + match_details = live_data.matchDetails + periods = match_details.periods + for period in periods.iterchildren(tag="period"): + parsed_periods.append( + { + "id": int(period.get("id")), + "start_timestamp": datetime.strptime( + period.get("start"), "%Y-%m-%dT%H:%M:%SZ" + ), + "end_timestamp": datetime.strptime( + period.get("end"), "%Y-%m-%dT%H:%M:%SZ" + ), + } + ) + return parsed_periods + @staticmethod def __parse_teams_from_xml(match: Any) -> List[Dict[str, Any]]: parsed_teams = [] @@ -190,6 +190,28 @@ def __parse_players_from_xml(match: Any) -> List[Dict[str, Any]]: ) return parsed_players + @staticmethod + def __parse_periods_from_json( + match: Dict[str, Any] + ) -> List[Dict[str, Any]]: + parsed_periods = [] + live_data = match["liveData"] + match_details = live_data["matchDetails"] + periods = match_details["period"] + for period in periods: + parsed_periods.append( + { + "id": period["id"], + "start_timestamp": datetime.strptime( + period["start"], "%Y-%m-%dT%H:%M:%SZ" + ), + "end_timestamp": datetime.strptime( + period["end"], "%Y-%m-%dT%H:%M:%SZ" + ), + } + ) + return parsed_periods + @staticmethod def __parse_teams_from_json(match: Dict[str, Any]) -> List[Dict[str, Any]]: parsed_teams = [] @@ -239,13 +261,24 @@ def deserialize(self, inputs: StatsPerformInputs) -> TrackingDataset: with performance_logging("Loading meta data", logger=logger): if meta_data.decode("utf-8")[0] == "<": match = objectify.fromstring(meta_data) + parsed_periods = self.__parse_periods_from_xml(match) parsed_teams = self.__parse_teams_from_xml(match) parsed_players = self.__parse_players_from_xml(match) else: match = json.loads(meta_data) + parsed_periods = self.__parse_periods_from_json(match) parsed_teams = self.__parse_teams_from_json(match) parsed_players = self.__parse_players_from_json(match) + periods = {} + for parsed_period in parsed_periods: + period_id = parsed_period["id"] + periods[period_id] = Period( + id=period_id, + start_timestamp=parsed_period["start_timestamp"], + end_timestamp=parsed_period["end_timestamp"], + ) + teams = {} for parsed_team in parsed_teams: team_id = parsed_team["team_id"] @@ -279,8 +312,6 @@ def deserialize(self, inputs: StatsPerformInputs) -> TrackingDataset: pitch_size_length = 100 pitch_size_width = 100 - periods = self.__get_periods(tracking_data) - transformer = self.get_transformer( length=pitch_size_length, width=pitch_size_width, @@ -292,13 +323,11 @@ def _iter(): for line_ in tracking_data: splits = line_.split(";")[1].split(",") - frame_id = int(splits[0]) period_id = int(splits[1]) period_ = periods[period_id] - if period_.contains(frame_id / frame_rate): - if n % sample == 0: - yield period_, line_ - n += 1 + if n % sample == 0: + yield period_, line_ + n += 1 frames = [] for n, frame_data in enumerate(_iter(), start=1): diff --git a/kloppy/infra/serializers/tracking/tracab.py b/kloppy/infra/serializers/tracking/tracab.py index f7a0e162..8f61a51b 100644 --- a/kloppy/infra/serializers/tracking/tracab.py +++ b/kloppy/infra/serializers/tracking/tracab.py @@ -1,4 +1,5 @@ import logging +from datetime import timedelta from typing import Tuple, Dict, NamedTuple, IO, Optional, Union from lxml import objectify @@ -112,7 +113,8 @@ def _frame_from_line(cls, teams, period, line, frame_rate): return Frame( frame_id=frame_id, - timestamp=frame_id / frame_rate - period.start_timestamp, + timestamp=timedelta(seconds=frame_id / frame_rate) + - period.start_timestamp, ball_coordinates=Point3D( float(ball_x), float(ball_y), float(ball_z) ), @@ -150,8 +152,12 @@ def deserialize(self, inputs: TRACABInputs) -> TrackingDataset: periods.append( Period( id=int(period.attrib["iId"]), - start_timestamp=start_frame_id / frame_rate, - end_timestamp=end_frame_id / frame_rate, + start_timestamp=timedelta( + seconds=start_frame_id / frame_rate + ), + end_timestamp=timedelta( + seconds=end_frame_id / frame_rate + ), ) ) @@ -174,7 +180,11 @@ def _iter(): continue for period_ in periods: - if period_.contains(frame_id / frame_rate): + if ( + period_.start_timestamp + <= timedelta(seconds=frame_id / frame_rate) + <= period_.end_timestamp + ): if n % sample == 0: yield period_, line_ n += 1 diff --git a/kloppy/tests/files/skillcorner_match_data.json b/kloppy/tests/files/skillcorner_match_data.json index 9ec7c9ea..fee78fef 100644 --- a/kloppy/tests/files/skillcorner_match_data.json +++ b/kloppy/tests/files/skillcorner_match_data.json @@ -1 +1,937 @@ -{"referees": [{"first_name": "Felix", "referee_role": 0, "start_time": "00:00:00", "trackable_object": 22396, "last_name": "Zwayer", "end_time": null, "replaced_by": null, "id": 246}], "date_time": "2019-11-09T17:30:00Z", "home_team": {"acronym": "BMU", "id": 100, "short_name": "Bayern Munchen", "name": "FC Bayern Munchen"}, "away_team": {"acronym": "DOR", "id": 103, "short_name": "Dortmund", "name": "Borussia Dortmund"}, "away_team_kit": {"jersey_color": "#f4e422", "name": "ucl", "season": {"name": "2018/2019", "id": 5, "end_date": "2019-07-31", "start_date": "2018-08-01"}, "team_id": 103, "number_color": "#000000", "id": 524}, "home_team_coach": {"first_name": "Hans-Dieter", "last_name": "Flick", "id": 840}, "status": "closed", "pitch_length": 105, "players": [{"injured": false, "own_goal": 0, "last_name": "Delaney", "goal": 0, "red_card": 0, "number": 6, "id": 11192, "trackable_object": 11216, "team_id": 103, "birthday": "1991-09-03", "end_time": null, "first_name": "Thomas", "player_role": {"acronym": "SUB", "id": 17, "name": "Substitute"}, "start_time": null, "yellow_card": 0, "team_player_id": 7632}, {"injured": false, "own_goal": 0, "last_name": "Alcantara", "goal": 0, "red_card": 0, "number": 6, "id": 10247, "trackable_object": 10257, "team_id": 100, "birthday": "1991-04-11", "end_time": null, "first_name": "Thiago", "player_role": {"acronym": "RM", "id": 10, "name": "Right Midfield"}, "start_time": "01:11:33", "yellow_card": 0, "team_player_id": 209}, {"injured": false, "own_goal": 0, "last_name": "Mai", "goal": 0, "red_card": 0, "number": 33, "id": 22148, "trackable_object": 22391, "team_id": 100, "birthday": "2000-03-31", "end_time": null, "first_name": "Lukas", "player_role": {"acronym": "SUB", "id": 17, "name": "Substitute"}, "start_time": null, "yellow_card": 0, "team_player_id": 18532}, {"injured": false, "own_goal": 0, "last_name": "Alaba", "goal": 0, "red_card": 0, "number": 27, "id": 2395, "trackable_object": 2405, "team_id": 100, "birthday": "1992-06-24", "end_time": null, "first_name": "David", "player_role": {"acronym": "LCB", "id": 3, "name": "Left Center Back"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 1334}, {"injured": false, "own_goal": 0, "last_name": "Neuer", "goal": 0, "red_card": 0, "number": 1, "id": 6627, "trackable_object": 6637, "team_id": 100, "birthday": "1986-03-27", "end_time": null, "first_name": "Manuel", "player_role": {"acronym": "GK", "id": 0, "name": "Goalkeeper"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 255}, {"injured": false, "own_goal": 0, "last_name": "Reus", "goal": 0, "red_card": 0, "number": 11, "id": 6796, "trackable_object": 6806, "team_id": 103, "birthday": "1989-05-31", "end_time": null, "first_name": "Marco", "player_role": {"acronym": "CF", "id": 15, "name": "Center Forward"}, "start_time": "01:00:29", "yellow_card": 1, "team_player_id": 568}, {"injured": false, "own_goal": 0, "last_name": "Larsen", "goal": 0, "red_card": 0, "number": 34, "id": 12749, "trackable_object": 12910, "team_id": 103, "birthday": "1998-09-19", "end_time": null, "first_name": "Jaco Bruun", "player_role": {"acronym": "SUB", "id": 17, "name": "Substitute"}, "start_time": null, "yellow_card": 0, "team_player_id": 7639}, {"injured": false, "own_goal": 1, "last_name": "Hummels", "goal": 0, "red_card": 0, "number": 15, "id": 7207, "trackable_object": 7217, "team_id": 103, "birthday": "1988-12-16", "end_time": null, "first_name": "Mats", "player_role": {"acronym": "LCB", "id": 3, "name": "Left Center Back"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 1638}, {"injured": false, "own_goal": 0, "last_name": "Cuisance", "goal": 0, "red_card": 0, "number": 11, "id": 19127, "trackable_object": 19324, "team_id": 100, "birthday": "1999-08-16", "end_time": null, "first_name": "Mickael", "player_role": {"acronym": "SUB", "id": 17, "name": "Substitute"}, "start_time": null, "yellow_card": 0, "team_player_id": 21498}, {"injured": false, "own_goal": 0, "last_name": "Tolisso", "goal": 0, "red_card": 0, "number": 24, "id": 1999, "trackable_object": 2009, "team_id": 100, "birthday": "1994-08-03", "end_time": null, "first_name": "Corentin", "player_role": {"acronym": "SUB", "id": 17, "name": "Substitute"}, "start_time": null, "yellow_card": 0, "team_player_id": 4405}, {"injured": false, "own_goal": 0, "last_name": "B\u00fcrki", "goal": 0, "red_card": 0, "number": 1, "id": 9252, "trackable_object": 9262, "team_id": 103, "birthday": "1990-11-14", "end_time": null, "first_name": "Roman", "player_role": {"acronym": "GK", "id": 0, "name": "Goalkeeper"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 4030}, {"injured": false, "own_goal": 0, "last_name": "Hakimi", "goal": 0, "red_card": 0, "number": 5, "id": 11495, "trackable_object": 11559, "team_id": 103, "birthday": "1998-11-04", "end_time": null, "first_name": "Achraf", "player_role": {"acronym": "RWB", "id": 6, "name": "Right Wing Back"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 7628}, {"injured": false, "own_goal": 0, "last_name": "Akanji", "goal": 0, "red_card": 0, "number": 16, "id": 6607, "trackable_object": 6617, "team_id": 103, "birthday": "1995-07-19", "end_time": null, "first_name": "Manuel", "player_role": {"acronym": "RCB", "id": 4, "name": "Right Center Back"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 7630}, {"injured": false, "own_goal": 0, "last_name": "Hitz", "goal": 0, "red_card": 0, "number": 35, "id": 7090, "trackable_object": 7100, "team_id": 103, "birthday": "1987-09-18", "end_time": null, "first_name": "Marwin", "player_role": {"acronym": "SUB", "id": 17, "name": "Substitute"}, "start_time": null, "yellow_card": 0, "team_player_id": 7625}, {"injured": false, "own_goal": 0, "last_name": "Zagadou", "goal": 0, "red_card": 0, "number": 2, "id": 12747, "trackable_object": 12908, "team_id": 103, "birthday": "1999-06-03", "end_time": null, "first_name": "Dan-Axel", "player_role": {"acronym": "SUB", "id": 17, "name": "Substitute"}, "start_time": null, "yellow_card": 0, "team_player_id": 7626}, {"injured": false, "own_goal": 0, "last_name": "M\u00fcller", "goal": 0, "red_card": 0, "number": 25, "id": 10308, "trackable_object": 10318, "team_id": 100, "birthday": "1989-09-13", "end_time": null, "first_name": "Thomas", "player_role": {"acronym": "CM", "id": 8, "name": "Center Midfield"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 1538}, {"injured": false, "own_goal": 0, "last_name": "Kimmich", "goal": 0, "red_card": 0, "number": 32, "id": 5472, "trackable_object": 5482, "team_id": 100, "birthday": "1995-02-08", "end_time": null, "first_name": "Joshua", "player_role": {"acronym": "LM", "id": 9, "name": "Left Midfield"}, "start_time": "00:00:00", "yellow_card": 1, "team_player_id": 2176}, {"injured": false, "own_goal": 0, "last_name": "Hazard", "goal": 0, "red_card": 0, "number": 23, "id": 10326, "trackable_object": 10336, "team_id": 103, "birthday": "1993-03-29", "end_time": null, "first_name": "Thorgan", "player_role": {"acronym": "LW", "id": 12, "name": "Left Winger"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 21468}, {"injured": false, "own_goal": 0, "last_name": "Brandt", "goal": 0, "red_card": 0, "number": 19, "id": 5568, "trackable_object": 5578, "team_id": 103, "birthday": "1996-05-02", "end_time": null, "first_name": "Julian", "player_role": {"acronym": "AM", "id": 11, "name": "Attacking Midfield"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 20558}, {"injured": false, "own_goal": 0, "last_name": "Weigl", "goal": 0, "red_card": 0, "number": 33, "id": 5585, "trackable_object": 5595, "team_id": 103, "birthday": "1995-09-08", "end_time": "01:00:45", "first_name": "Julian", "player_role": {"acronym": "RM", "id": 10, "name": "Right Midfield"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 2448}, {"injured": false, "own_goal": 0, "last_name": "Witsel", "goal": 0, "red_card": 0, "number": 28, "id": 1138, "trackable_object": 1148, "team_id": 103, "birthday": "1989-01-12", "end_time": null, "first_name": "Axel", "player_role": {"acronym": "LM", "id": 9, "name": "Left Midfield"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 7637}, {"injured": false, "own_goal": 0, "last_name": "Schulz", "goal": 0, "red_card": 0, "number": 14, "id": 7969, "trackable_object": 7979, "team_id": 103, "birthday": "1993-04-01", "end_time": null, "first_name": "Nico", "player_role": {"acronym": "LWB", "id": 5, "name": "Left Wing Back"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 20194}, {"injured": false, "own_goal": 0, "last_name": "Ulreich", "goal": 0, "red_card": 0, "number": 26, "id": 10177, "trackable_object": 10187, "team_id": 100, "birthday": "1988-08-03", "end_time": null, "first_name": "Sven", "player_role": {"acronym": "SUB", "id": 17, "name": "Substitute"}, "start_time": null, "yellow_card": 0, "team_player_id": 4401}, {"injured": false, "own_goal": 0, "last_name": "Lewandowski", "goal": 2, "red_card": 0, "number": 9, "id": 9106, "trackable_object": 9116, "team_id": 100, "birthday": "1988-08-21", "end_time": null, "first_name": "Robert", "player_role": {"acronym": "CF", "id": 15, "name": "Center Forward"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 3261}, {"injured": false, "own_goal": 0, "last_name": "Coman", "goal": 0, "red_card": 0, "number": 29, "id": 5922, "trackable_object": 5932, "team_id": 100, "birthday": "1996-06-13", "end_time": "01:14:53", "first_name": "Kingsley", "player_role": {"acronym": "LW", "id": 12, "name": "Left Winger"}, "start_time": "00:00:00", "yellow_card": 1, "team_player_id": 3474}, {"injured": false, "own_goal": 0, "last_name": "Goretzka", "goal": 0, "red_card": 0, "number": 18, "id": 6158, "trackable_object": 6168, "team_id": 100, "birthday": "1995-02-06", "end_time": "01:11:33", "first_name": "Leon", "player_role": {"acronym": "RM", "id": 10, "name": "Right Midfield"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 7662}, {"injured": false, "own_goal": 0, "last_name": "Davies", "goal": 0, "red_card": 0, "number": 19, "id": 17902, "trackable_object": 18099, "team_id": 100, "birthday": "2000-11-02", "end_time": null, "first_name": "Alphonso", "player_role": {"acronym": "LWB", "id": 5, "name": "Left Wing Back"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 18534}, {"injured": false, "own_goal": 0, "last_name": "Martinez", "goal": 0, "red_card": 0, "number": 8, "id": 4812, "trackable_object": 4822, "team_id": 100, "birthday": "1988-09-02", "end_time": null, "first_name": "Javier", "player_role": {"acronym": "RCB", "id": 4, "name": "Right Center Back"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 254}, {"injured": false, "own_goal": 0, "last_name": "Pavard", "goal": 0, "red_card": 0, "number": 5, "id": 1298, "trackable_object": 1308, "team_id": 100, "birthday": "1996-03-28", "end_time": null, "first_name": "Benjamin", "player_role": {"acronym": "RWB", "id": 6, "name": "Right Wing Back"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 20195}, {"injured": false, "own_goal": 0, "last_name": "Coutinho", "goal": 0, "red_card": 0, "number": 10, "id": 8675, "trackable_object": 8685, "team_id": 100, "birthday": "1992-06-12", "end_time": null, "first_name": "Philippe", "player_role": {"acronym": "RW", "id": 13, "name": "Right Winger"}, "start_time": "01:09:30", "yellow_card": 0, "team_player_id": 21497}, {"injured": false, "own_goal": 0, "last_name": "Perisic", "goal": 0, "red_card": 0, "number": 14, "id": 4545, "trackable_object": 4555, "team_id": 100, "birthday": "1989-02-02", "end_time": null, "first_name": "Ivan", "player_role": {"acronym": "LW", "id": 12, "name": "Left Winger"}, "start_time": "01:14:53", "yellow_card": 0, "team_player_id": 21499}, {"injured": false, "own_goal": 0, "last_name": "Guerreiro", "goal": 0, "red_card": 0, "number": 13, "id": 8869, "trackable_object": 8879, "team_id": 103, "birthday": "1993-12-22", "end_time": null, "first_name": "Raphael", "player_role": {"acronym": "RW", "id": 13, "name": "Right Winger"}, "start_time": "00:35:17", "yellow_card": 0, "team_player_id": 4065}, {"injured": false, "own_goal": 0, "last_name": "Alcacer", "goal": 0, "red_card": 0, "number": 9, "id": 3566, "trackable_object": 3576, "team_id": 103, "birthday": "1993-08-30", "end_time": null, "first_name": "Francisco", "player_role": {"acronym": "RM", "id": 10, "name": "Right Midfield"}, "start_time": "01:00:45", "yellow_card": 0, "team_player_id": 7640}, {"injured": false, "own_goal": 0, "last_name": "Sancho", "goal": 0, "red_card": 0, "number": 7, "id": 12788, "trackable_object": 12950, "team_id": 103, "birthday": "2000-03-25", "end_time": "00:35:17", "first_name": "Jadon", "player_role": {"acronym": "RW", "id": 13, "name": "Right Winger"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 7730}, {"injured": false, "own_goal": 0, "last_name": "Piszczek", "goal": 0, "red_card": 0, "number": 26, "id": 6492, "trackable_object": 6502, "team_id": 103, "birthday": "1985-06-03", "end_time": null, "first_name": "Lukasz", "player_role": {"acronym": "SUB", "id": 17, "name": "Substitute"}, "start_time": null, "yellow_card": 0, "team_player_id": 2798}, {"injured": false, "own_goal": 0, "last_name": "Gnabry", "goal": 1, "red_card": 0, "number": 22, "id": 9724, "trackable_object": 9734, "team_id": 100, "birthday": "1995-07-14", "end_time": "01:09:30", "first_name": "Serge", "player_role": {"acronym": "RW", "id": 13, "name": "Right Winger"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 7663}, {"injured": false, "own_goal": 0, "last_name": "Dahoud", "goal": 0, "red_card": 0, "number": 8, "id": 6554, "trackable_object": 6564, "team_id": 103, "birthday": "1996-01-01", "end_time": null, "first_name": "Mahmoud", "player_role": {"acronym": "SUB", "id": 17, "name": "Substitute"}, "start_time": null, "yellow_card": 0, "team_player_id": 7635}, {"injured": false, "own_goal": 0, "last_name": "G\u00f6tze", "goal": 0, "red_card": 0, "number": 10, "id": 6890, "trackable_object": 6900, "team_id": 103, "birthday": "1992-06-03", "end_time": "01:00:29", "first_name": "Mario", "player_role": {"acronym": "CF", "id": 15, "name": "Center Forward"}, "start_time": "00:00:00", "yellow_card": 0, "team_player_id": 7633}], "away_team_coach": {"first_name": "Lucien", "last_name": "Favre", "id": 110}, "ball": {"trackable_object": 55}, "pitch_width": 68, "competition_edition": {"season": {"name": "2019/2020", "id": 6, "end_date": "2020-07-31", "start_date": "2019-08-01"}, "id": 77, "competition": {"id": 6, "name": "Bundesliga", "area": "GER"}, "name": "Bundesliga 2019-2020"}, "stadium": {"city": "Munich", "capacity": 75000, "id": 40, "name": "Allianz Arena"}, "home_team_kit": {"jersey_color": "#e4070c", "name": "home", "season": {"name": "2017/2018", "id": 4, "end_date": "2018-07-31", "start_date": "2017-08-01"}, "team_id": 100, "number_color": "#ffffff", "id": 57}, "competition_round": {"potential_overtime": false, "round_number": 11, "id": 170, "name": "Round 11"}, "away_team_score": 0, "id": 2417, "home_team_score": 4} \ No newline at end of file +{ + "referees": [ + { + "first_name": "Felix", + "referee_role": 0, + "start_time": "00:00:00", + "trackable_object": 22396, + "last_name": "Zwayer", + "end_time": null, + "replaced_by": null, + "id": 246 + } + ], + "date_time": "2019-11-09T17:30:00Z", + "home_team": { + "acronym": "BMU", + "id": 100, + "short_name": "Bayern Munchen", + "name": "FC Bayern Munchen" + }, + "away_team": { + "acronym": "DOR", + "id": 103, + "short_name": "Dortmund", + "name": "Borussia Dortmund" + }, + "away_team_kit": { + "jersey_color": "#f4e422", + "name": "ucl", + "season": { + "name": "2018/2019", + "id": 5, + "end_date": "2019-07-31", + "start_date": "2018-08-01" + }, + "team_id": 103, + "number_color": "#000000", + "id": 524 + }, + "home_team_coach": { + "first_name": "Hans-Dieter", + "last_name": "Flick", + "id": 840 + }, + "status": "closed", + "pitch_length": 105, + "players": [ + { + "injured": false, + "own_goal": 0, + "last_name": "Delaney", + "goal": 0, + "red_card": 0, + "number": 6, + "id": 11192, + "trackable_object": 11216, + "team_id": 103, + "birthday": "1991-09-03", + "end_time": null, + "first_name": "Thomas", + "player_role": { + "acronym": "SUB", + "id": 17, + "name": "Substitute" + }, + "start_time": null, + "yellow_card": 0, + "team_player_id": 7632 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Alcantara", + "goal": 0, + "red_card": 0, + "number": 6, + "id": 10247, + "trackable_object": 10257, + "team_id": 100, + "birthday": "1991-04-11", + "end_time": null, + "first_name": "Thiago", + "player_role": { + "acronym": "RM", + "id": 10, + "name": "Right Midfield" + }, + "start_time": "01:11:33", + "yellow_card": 0, + "team_player_id": 209 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Mai", + "goal": 0, + "red_card": 0, + "number": 33, + "id": 22148, + "trackable_object": 22391, + "team_id": 100, + "birthday": "2000-03-31", + "end_time": null, + "first_name": "Lukas", + "player_role": { + "acronym": "SUB", + "id": 17, + "name": "Substitute" + }, + "start_time": null, + "yellow_card": 0, + "team_player_id": 18532 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Alaba", + "goal": 0, + "red_card": 0, + "number": 27, + "id": 2395, + "trackable_object": 2405, + "team_id": 100, + "birthday": "1992-06-24", + "end_time": null, + "first_name": "David", + "player_role": { + "acronym": "LCB", + "id": 3, + "name": "Left Center Back" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 1334 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Neuer", + "goal": 0, + "red_card": 0, + "number": 1, + "id": 6627, + "trackable_object": 6637, + "team_id": 100, + "birthday": "1986-03-27", + "end_time": null, + "first_name": "Manuel", + "player_role": { + "acronym": "GK", + "id": 0, + "name": "Goalkeeper" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 255 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Reus", + "goal": 0, + "red_card": 0, + "number": 11, + "id": 6796, + "trackable_object": 6806, + "team_id": 103, + "birthday": "1989-05-31", + "end_time": null, + "first_name": "Marco", + "player_role": { + "acronym": "CF", + "id": 15, + "name": "Center Forward" + }, + "start_time": "01:00:29", + "yellow_card": 1, + "team_player_id": 568 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Larsen", + "goal": 0, + "red_card": 0, + "number": 34, + "id": 12749, + "trackable_object": 12910, + "team_id": 103, + "birthday": "1998-09-19", + "end_time": null, + "first_name": "Jaco Bruun", + "player_role": { + "acronym": "SUB", + "id": 17, + "name": "Substitute" + }, + "start_time": null, + "yellow_card": 0, + "team_player_id": 7639 + }, + { + "injured": false, + "own_goal": 1, + "last_name": "Hummels", + "goal": 0, + "red_card": 0, + "number": 15, + "id": 7207, + "trackable_object": 7217, + "team_id": 103, + "birthday": "1988-12-16", + "end_time": null, + "first_name": "Mats", + "player_role": { + "acronym": "LCB", + "id": 3, + "name": "Left Center Back" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 1638 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Cuisance", + "goal": 0, + "red_card": 0, + "number": 11, + "id": 19127, + "trackable_object": 19324, + "team_id": 100, + "birthday": "1999-08-16", + "end_time": null, + "first_name": "Mickael", + "player_role": { + "acronym": "SUB", + "id": 17, + "name": "Substitute" + }, + "start_time": null, + "yellow_card": 0, + "team_player_id": 21498 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Tolisso", + "goal": 0, + "red_card": 0, + "number": 24, + "id": 1999, + "trackable_object": 2009, + "team_id": 100, + "birthday": "1994-08-03", + "end_time": null, + "first_name": "Corentin", + "player_role": { + "acronym": "SUB", + "id": 17, + "name": "Substitute" + }, + "start_time": null, + "yellow_card": 0, + "team_player_id": 4405 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "B\u00fcrki", + "goal": 0, + "red_card": 0, + "number": 1, + "id": 9252, + "trackable_object": 9262, + "team_id": 103, + "birthday": "1990-11-14", + "end_time": null, + "first_name": "Roman", + "player_role": { + "acronym": "GK", + "id": 0, + "name": "Goalkeeper" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 4030 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Hakimi", + "goal": 0, + "red_card": 0, + "number": 5, + "id": 11495, + "trackable_object": 11559, + "team_id": 103, + "birthday": "1998-11-04", + "end_time": null, + "first_name": "Achraf", + "player_role": { + "acronym": "RWB", + "id": 6, + "name": "Right Wing Back" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 7628 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Akanji", + "goal": 0, + "red_card": 0, + "number": 16, + "id": 6607, + "trackable_object": 6617, + "team_id": 103, + "birthday": "1995-07-19", + "end_time": null, + "first_name": "Manuel", + "player_role": { + "acronym": "RCB", + "id": 4, + "name": "Right Center Back" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 7630 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Hitz", + "goal": 0, + "red_card": 0, + "number": 35, + "id": 7090, + "trackable_object": 7100, + "team_id": 103, + "birthday": "1987-09-18", + "end_time": null, + "first_name": "Marwin", + "player_role": { + "acronym": "SUB", + "id": 17, + "name": "Substitute" + }, + "start_time": null, + "yellow_card": 0, + "team_player_id": 7625 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Zagadou", + "goal": 0, + "red_card": 0, + "number": 2, + "id": 12747, + "trackable_object": 12908, + "team_id": 103, + "birthday": "1999-06-03", + "end_time": null, + "first_name": "Dan-Axel", + "player_role": { + "acronym": "SUB", + "id": 17, + "name": "Substitute" + }, + "start_time": null, + "yellow_card": 0, + "team_player_id": 7626 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "M\u00fcller", + "goal": 0, + "red_card": 0, + "number": 25, + "id": 10308, + "trackable_object": 10318, + "team_id": 100, + "birthday": "1989-09-13", + "end_time": null, + "first_name": "Thomas", + "player_role": { + "acronym": "CM", + "id": 8, + "name": "Center Midfield" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 1538 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Kimmich", + "goal": 0, + "red_card": 0, + "number": 32, + "id": 5472, + "trackable_object": 5482, + "team_id": 100, + "birthday": "1995-02-08", + "end_time": null, + "first_name": "Joshua", + "player_role": { + "acronym": "LM", + "id": 9, + "name": "Left Midfield" + }, + "start_time": "00:00:00", + "yellow_card": 1, + "team_player_id": 2176 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Hazard", + "goal": 0, + "red_card": 0, + "number": 23, + "id": 10326, + "trackable_object": 10336, + "team_id": 103, + "birthday": "1993-03-29", + "end_time": null, + "first_name": "Thorgan", + "player_role": { + "acronym": "LW", + "id": 12, + "name": "Left Winger" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 21468 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Brandt", + "goal": 0, + "red_card": 0, + "number": 19, + "id": 5568, + "trackable_object": 5578, + "team_id": 103, + "birthday": "1996-05-02", + "end_time": null, + "first_name": "Julian", + "player_role": { + "acronym": "AM", + "id": 11, + "name": "Attacking Midfield" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 20558 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Weigl", + "goal": 0, + "red_card": 0, + "number": 33, + "id": 5585, + "trackable_object": 5595, + "team_id": 103, + "birthday": "1995-09-08", + "end_time": "01:00:45", + "first_name": "Julian", + "player_role": { + "acronym": "RM", + "id": 10, + "name": "Right Midfield" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 2448 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Witsel", + "goal": 0, + "red_card": 0, + "number": 28, + "id": 1138, + "trackable_object": 1148, + "team_id": 103, + "birthday": "1989-01-12", + "end_time": null, + "first_name": "Axel", + "player_role": { + "acronym": "LM", + "id": 9, + "name": "Left Midfield" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 7637 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Schulz", + "goal": 0, + "red_card": 0, + "number": 14, + "id": 7969, + "trackable_object": 7979, + "team_id": 103, + "birthday": "1993-04-01", + "end_time": null, + "first_name": "Nico", + "player_role": { + "acronym": "LWB", + "id": 5, + "name": "Left Wing Back" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 20194 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Ulreich", + "goal": 0, + "red_card": 0, + "number": 26, + "id": 10177, + "trackable_object": 10187, + "team_id": 100, + "birthday": "1988-08-03", + "end_time": null, + "first_name": "Sven", + "player_role": { + "acronym": "SUB", + "id": 17, + "name": "Substitute" + }, + "start_time": null, + "yellow_card": 0, + "team_player_id": 4401 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Lewandowski", + "goal": 2, + "red_card": 0, + "number": 9, + "id": 9106, + "trackable_object": 9116, + "team_id": 100, + "birthday": "1988-08-21", + "end_time": null, + "first_name": "Robert", + "player_role": { + "acronym": "CF", + "id": 15, + "name": "Center Forward" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 3261 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Coman", + "goal": 0, + "red_card": 0, + "number": 29, + "id": 5922, + "trackable_object": 5932, + "team_id": 100, + "birthday": "1996-06-13", + "end_time": "01:14:53", + "first_name": "Kingsley", + "player_role": { + "acronym": "LW", + "id": 12, + "name": "Left Winger" + }, + "start_time": "00:00:00", + "yellow_card": 1, + "team_player_id": 3474 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Goretzka", + "goal": 0, + "red_card": 0, + "number": 18, + "id": 6158, + "trackable_object": 6168, + "team_id": 100, + "birthday": "1995-02-06", + "end_time": "01:11:33", + "first_name": "Leon", + "player_role": { + "acronym": "RM", + "id": 10, + "name": "Right Midfield" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 7662 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Davies", + "goal": 0, + "red_card": 0, + "number": 19, + "id": 17902, + "trackable_object": 18099, + "team_id": 100, + "birthday": "2000-11-02", + "end_time": null, + "first_name": "Alphonso", + "player_role": { + "acronym": "LWB", + "id": 5, + "name": "Left Wing Back" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 18534 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Martinez", + "goal": 0, + "red_card": 0, + "number": 8, + "id": 4812, + "trackable_object": 4822, + "team_id": 100, + "birthday": "1988-09-02", + "end_time": null, + "first_name": "Javier", + "player_role": { + "acronym": "RCB", + "id": 4, + "name": "Right Center Back" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 254 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Pavard", + "goal": 0, + "red_card": 0, + "number": 5, + "id": 1298, + "trackable_object": 1308, + "team_id": 100, + "birthday": "1996-03-28", + "end_time": null, + "first_name": "Benjamin", + "player_role": { + "acronym": "RWB", + "id": 6, + "name": "Right Wing Back" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 20195 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Coutinho", + "goal": 0, + "red_card": 0, + "number": 10, + "id": 8675, + "trackable_object": 8685, + "team_id": 100, + "birthday": "1992-06-12", + "end_time": null, + "first_name": "Philippe", + "player_role": { + "acronym": "RW", + "id": 13, + "name": "Right Winger" + }, + "start_time": "01:09:30", + "yellow_card": 0, + "team_player_id": 21497 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Perisic", + "goal": 0, + "red_card": 0, + "number": 14, + "id": 4545, + "trackable_object": 4555, + "team_id": 100, + "birthday": "1989-02-02", + "end_time": null, + "first_name": "Ivan", + "player_role": { + "acronym": "LW", + "id": 12, + "name": "Left Winger" + }, + "start_time": "01:14:53", + "yellow_card": 0, + "team_player_id": 21499 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Guerreiro", + "goal": 0, + "red_card": 0, + "number": 13, + "id": 8869, + "trackable_object": 8879, + "team_id": 103, + "birthday": "1993-12-22", + "end_time": null, + "first_name": "Raphael", + "player_role": { + "acronym": "RW", + "id": 13, + "name": "Right Winger" + }, + "start_time": "00:35:17", + "yellow_card": 0, + "team_player_id": 4065 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Alcacer", + "goal": 0, + "red_card": 0, + "number": 9, + "id": 3566, + "trackable_object": 3576, + "team_id": 103, + "birthday": "1993-08-30", + "end_time": null, + "first_name": "Francisco", + "player_role": { + "acronym": "RM", + "id": 10, + "name": "Right Midfield" + }, + "start_time": "01:00:45", + "yellow_card": 0, + "team_player_id": 7640 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Sancho", + "goal": 0, + "red_card": 0, + "number": 7, + "id": 12788, + "trackable_object": 12950, + "team_id": 103, + "birthday": "2000-03-25", + "end_time": "00:35:17", + "first_name": "Jadon", + "player_role": { + "acronym": "RW", + "id": 13, + "name": "Right Winger" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 7730 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Piszczek", + "goal": 0, + "red_card": 0, + "number": 26, + "id": 6492, + "trackable_object": 6502, + "team_id": 103, + "birthday": "1985-06-03", + "end_time": null, + "first_name": "Lukasz", + "player_role": { + "acronym": "SUB", + "id": 17, + "name": "Substitute" + }, + "start_time": null, + "yellow_card": 0, + "team_player_id": 2798 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Gnabry", + "goal": 1, + "red_card": 0, + "number": 22, + "id": 9724, + "trackable_object": 9734, + "team_id": 100, + "birthday": "1995-07-14", + "end_time": "01:09:30", + "first_name": "Serge", + "player_role": { + "acronym": "RW", + "id": 13, + "name": "Right Winger" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 7663 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "Dahoud", + "goal": 0, + "red_card": 0, + "number": 8, + "id": 6554, + "trackable_object": 6564, + "team_id": 103, + "birthday": "1996-01-01", + "end_time": null, + "first_name": "Mahmoud", + "player_role": { + "acronym": "SUB", + "id": 17, + "name": "Substitute" + }, + "start_time": null, + "yellow_card": 0, + "team_player_id": 7635 + }, + { + "injured": false, + "own_goal": 0, + "last_name": "G\u00f6tze", + "goal": 0, + "red_card": 0, + "number": 10, + "id": 6890, + "trackable_object": 6900, + "team_id": 103, + "birthday": "1992-06-03", + "end_time": "01:00:29", + "first_name": "Mario", + "player_role": { + "acronym": "CF", + "id": 15, + "name": "Center Forward" + }, + "start_time": "00:00:00", + "yellow_card": 0, + "team_player_id": 7633 + } + ], + "away_team_coach": { + "first_name": "Lucien", + "last_name": "Favre", + "id": 110 + }, + "ball": { + "trackable_object": 55 + }, + "pitch_width": 68, + "competition_edition": { + "season": { + "name": "2019/2020", + "id": 6, + "end_date": "2020-07-31", + "start_date": "2019-08-01" + }, + "id": 77, + "competition": { + "id": 6, + "name": "Bundesliga", + "area": "GER" + }, + "name": "Bundesliga 2019-2020" + }, + "stadium": { + "city": "Munich", + "capacity": 75000, + "id": 40, + "name": "Allianz Arena" + }, + "home_team_kit": { + "jersey_color": "#e4070c", + "name": "home", + "season": { + "name": "2017/2018", + "id": 4, + "end_date": "2018-07-31", + "start_date": "2017-08-01" + }, + "team_id": 100, + "number_color": "#ffffff", + "id": 57 + }, + "competition_round": { + "potential_overtime": false, + "round_number": 11, + "id": 170, + "name": "Round 11" + }, + "away_team_score": 0, + "id": 2417, + "home_team_score": 4 +} diff --git a/kloppy/tests/test_datafactory.py b/kloppy/tests/test_datafactory.py index ba2c029e..699defba 100644 --- a/kloppy/tests/test_datafactory.py +++ b/kloppy/tests/test_datafactory.py @@ -1,3 +1,5 @@ +from datetime import timedelta, datetime, timezone + import pytest from kloppy.domain import ( @@ -43,18 +45,35 @@ def test_correct_deserialization(self, event_data: str): assert player.position is None # not set assert player.starting - assert dataset.metadata.periods[0] == Period( - id=1, - start_timestamp=0, - end_timestamp=2912, - attacking_direction=AttackingDirection.HOME_AWAY, + assert dataset.metadata.periods[0].id == 1 + assert dataset.metadata.periods[0].start_timestamp == datetime( + 2011, 11, 11, 9, 0, 13, 0, timezone.utc + ) + assert dataset.metadata.periods[0].end_timestamp == datetime( + 2011, 11, 11, 9, 48, 45, 0, timezone.utc + ) + assert ( + dataset.metadata.periods[0].attacking_direction + == AttackingDirection.HOME_AWAY ) - assert dataset.metadata.periods[1] == Period( - id=2, - start_timestamp=2700, - end_timestamp=5710, - attacking_direction=AttackingDirection.AWAY_HOME, + assert dataset.metadata.periods[1].id == 2 + assert dataset.metadata.periods[1].start_timestamp == datetime( + 2011, 11, 11, 10, 3, 45, 0, timezone.utc ) + assert dataset.metadata.periods[1].end_timestamp == datetime( + 2011, 11, 11, 10, 53, 55, 0, timezone.utc + ) + assert ( + dataset.metadata.periods[1].attacking_direction + == AttackingDirection.AWAY_HOME + ) + + assert dataset.events[0].timestamp == timedelta( + seconds=3 + ) # kickoff first half + assert dataset.events[473].timestamp == timedelta( + seconds=4 + ) # kickoff second half assert dataset.events[0].coordinates == Point(0.01, 0.01) diff --git a/kloppy/tests/test_metrica_csv.py b/kloppy/tests/test_metrica_csv.py index fe4c741a..eeb4ee2b 100644 --- a/kloppy/tests/test_metrica_csv.py +++ b/kloppy/tests/test_metrica_csv.py @@ -1,3 +1,5 @@ +from datetime import timedelta + import pytest from kloppy.domain import ( @@ -32,18 +34,35 @@ def test_correct_deserialization(self, home_data: str, away_data: str): assert len(dataset.records) == 6 assert len(dataset.metadata.periods) == 2 assert dataset.metadata.orientation == Orientation.FIXED_HOME_AWAY - assert dataset.metadata.periods[0] == Period( - id=1, - start_timestamp=0.04, - end_timestamp=0.12, - attacking_direction=AttackingDirection.HOME_AWAY, + assert dataset.metadata.periods[0].id == 1 + assert dataset.metadata.periods[0].start_timestamp == timedelta( + seconds=0.0 + ) + assert dataset.metadata.periods[0].end_timestamp == timedelta( + seconds=0.12 + ) + assert ( + dataset.metadata.periods[0].attacking_direction + == AttackingDirection.HOME_AWAY ) - assert dataset.metadata.periods[1] == Period( - id=2, - start_timestamp=5800.16, - end_timestamp=5800.24, - attacking_direction=AttackingDirection.AWAY_HOME, + assert dataset.metadata.periods[1].id == 2 + assert dataset.metadata.periods[1].start_timestamp == timedelta( + seconds=5800.12 ) + assert dataset.metadata.periods[1].end_timestamp == timedelta( + seconds=5800.24 + ) + assert ( + dataset.metadata.periods[1].attacking_direction + == AttackingDirection.AWAY_HOME + ) + + # check timestamps + assert dataset.records[0].frame_id == 1 # period 1 + assert dataset.records[0].timestamp == timedelta(seconds=0.04) + assert dataset.records[1].timestamp == timedelta(seconds=0.08) + assert dataset.records[3].frame_id == 145004 # period 2 + assert dataset.records[3].timestamp == timedelta(seconds=0.04) # make sure data is loaded correctly (including flip y-axis) home_player = dataset.metadata.teams[0].players[0] diff --git a/kloppy/tests/test_metrica_epts.py b/kloppy/tests/test_metrica_epts.py index f1696460..bdb02a78 100644 --- a/kloppy/tests/test_metrica_epts.py +++ b/kloppy/tests/test_metrica_epts.py @@ -1,4 +1,5 @@ import re +from datetime import datetime, timedelta import pytest from pandas import DataFrame @@ -115,8 +116,31 @@ def test_correct_deserialization(self, meta_data: str, raw_data: str): assert len(dataset.records) == 100 assert len(dataset.metadata.periods) == 2 + assert dataset.metadata.periods[0].id == 1 + assert dataset.metadata.periods[0].start_timestamp == timedelta( + seconds=18 + ) + assert dataset.metadata.periods[0].end_timestamp == timedelta( + seconds=19.96 + ) + assert dataset.metadata.periods[1].id == 2 + assert dataset.metadata.periods[1].start_timestamp == timedelta( + seconds=26 + ) + assert dataset.metadata.periods[1].end_timestamp == timedelta( + seconds=27.96 + ) assert dataset.metadata.orientation is Orientation.HOME_TEAM + assert dataset.records[0].frame_id == 450 + assert dataset.records[0].timestamp == timedelta( + seconds=0 + ) # kickoff first half + assert dataset.records[50].frame_id == 650 + assert dataset.records[50].timestamp == timedelta( + seconds=0 + ) # kickoff second half + assert dataset.records[0].players_data[ first_player ].coordinates == Point(x=0.30602, y=0.97029) diff --git a/kloppy/tests/test_metrica_events.py b/kloppy/tests/test_metrica_events.py index 08d2d0f3..fd385467 100644 --- a/kloppy/tests/test_metrica_events.py +++ b/kloppy/tests/test_metrica_events.py @@ -1,4 +1,5 @@ from pathlib import Path +from datetime import timedelta import pytest from kloppy import metrica @@ -45,18 +46,43 @@ def test_metadata(self, dataset: EventDataset): assert str(player) == "Track_11" assert player.position.name == "Goalkeeper" - assert dataset.metadata.periods[0] == Period( - id=1, - start_timestamp=14.44, - end_timestamp=2783.76, - attacking_direction=AttackingDirection.NOT_SET, + assert dataset.metadata.periods[0].id == 1 + assert dataset.metadata.periods[0].start_timestamp == timedelta( + seconds=18 ) - assert dataset.metadata.periods[1] == Period( - id=2, - start_timestamp=2803.6, - end_timestamp=5742.12, - attacking_direction=AttackingDirection.NOT_SET, + assert dataset.metadata.periods[0].end_timestamp == timedelta( + seconds=19.96 ) + assert ( + dataset.metadata.periods[0].attacking_direction + == AttackingDirection.HOME_AWAY + ) + assert dataset.metadata.periods[1].id == 2 + assert dataset.metadata.periods[1].start_timestamp == timedelta( + seconds=26 + ) + assert dataset.metadata.periods[1].end_timestamp == timedelta( + seconds=27.96 + ) + assert ( + dataset.metadata.periods[1].attacking_direction + == AttackingDirection.AWAY_HOME + ) + + def test_timestamps(self, dataset: EventDataset): + """It should parse the timestamps correctly.""" + # note: these timestamps are odd because the metadata and event data + # are from different matches + assert dataset.events[0].timestamp == timedelta( + seconds=14.44 + ) - timedelta( + seconds=450 / 25 + ) # kickoff first half + assert dataset.events[1749].timestamp == timedelta( + seconds=2803.6 + ) - timedelta( + seconds=650 / 25 + ) # kickoff second half def test_coordinates(self, dataset: EventDataset): """It should parse the coordinates of events correctly.""" diff --git a/kloppy/tests/test_opta.py b/kloppy/tests/test_opta.py index e19a25a6..762b565c 100644 --- a/kloppy/tests/test_opta.py +++ b/kloppy/tests/test_opta.py @@ -1,5 +1,5 @@ import math -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta import pytest @@ -59,18 +59,12 @@ def dataset(base_dir) -> EventDataset: def test_parse_f24_datetime(): """Test if the F24 datetime is correctly parsed""" # timestamps have millisecond precision - assert ( - _parse_f24_datetime("2018-09-23T15:02:13.608") - == datetime( - 2018, 9, 23, 15, 2, 13, 608000, tzinfo=timezone.utc - ).timestamp() + assert _parse_f24_datetime("2018-09-23T15:02:13.608") == datetime( + 2018, 9, 23, 15, 2, 13, 608000, tzinfo=timezone.utc ) # milliseconds are not left-padded - assert ( - _parse_f24_datetime("2018-09-23T15:02:14.39") - == datetime( - 2018, 9, 23, 15, 2, 14, 39000, tzinfo=timezone.utc - ).timestamp() + assert _parse_f24_datetime("2018-09-23T15:02:14.39") == datetime( + 2018, 9, 23, 15, 2, 14, 39000, tzinfo=timezone.utc ) @@ -192,6 +186,13 @@ def test_generic_attributes(self, dataset: EventDataset): ) assert event.ball_state == BallState.ALIVE + def test_timestamp(self, dataset): + """It should set the correct timestamp, reset to zero after each period""" + kickoff_p1 = dataset.get_event_by_id("1510681159") + assert kickoff_p1.timestamp == timedelta(seconds=0.431) + kickoff_p2 = dataset.get_event_by_id("1209571018") + assert kickoff_p2.timestamp == timedelta(seconds=1.557) + def test_correct_normalized_deserialization(self, base_dir): """Test if the normalized deserialization is correct""" dataset = opta.load( diff --git a/kloppy/tests/test_secondspectrum.py b/kloppy/tests/test_secondspectrum.py index f556bff9..c8bc7f78 100644 --- a/kloppy/tests/test_secondspectrum.py +++ b/kloppy/tests/test_secondspectrum.py @@ -1,4 +1,5 @@ import logging +from datetime import timedelta from pathlib import Path import pytest @@ -48,24 +49,39 @@ def test_correct_deserialization( # Check the Periods assert dataset.metadata.periods[0].id == 1 - assert dataset.metadata.periods[0].start_timestamp == 0 - assert dataset.metadata.periods[0].end_timestamp == 2982240 + assert dataset.metadata.periods[0].start_timestamp == timedelta( + seconds=0 + ) + assert dataset.metadata.periods[0].end_timestamp == timedelta( + seconds=2982240 / 25 + ) assert ( dataset.metadata.periods[0].attacking_direction == AttackingDirection.AWAY_HOME ) assert dataset.metadata.periods[1].id == 2 - assert dataset.metadata.periods[1].start_timestamp == 3907360 - assert dataset.metadata.periods[1].end_timestamp == 6927840 + assert dataset.metadata.periods[1].start_timestamp == timedelta( + seconds=3907360 / 25 + ) + assert dataset.metadata.periods[1].end_timestamp == timedelta( + seconds=6927840 / 25 + ) assert ( dataset.metadata.periods[1].attacking_direction == AttackingDirection.HOME_AWAY ) # Check some timestamps - assert dataset.records[0].timestamp == 0 # First frame - assert dataset.records[20].timestamp == 320.0 # Later frame + assert dataset.records[0].timestamp == timedelta( + seconds=0 + ) # First frame + assert dataset.records[20].timestamp == timedelta( + seconds=320.0 + ) # Later frame + assert dataset.records[187].timestamp == timedelta( + seconds=9.72 + ) # Second period # Check some players home_player = dataset.metadata.teams[0].players[2] diff --git a/kloppy/tests/test_skillcorner.py b/kloppy/tests/test_skillcorner.py index 0688f033..fec09520 100644 --- a/kloppy/tests/test_skillcorner.py +++ b/kloppy/tests/test_skillcorner.py @@ -1,3 +1,4 @@ +from datetime import timedelta from pathlib import Path import pytest @@ -26,77 +27,92 @@ def raw_data(self, base_dir) -> str: def test_correct_deserialization(self, raw_data: Path, meta_data: Path): dataset = skillcorner.load( - meta_data=meta_data, raw_data=raw_data, coordinates="skillcorner" + meta_data=meta_data, + raw_data=raw_data, + coordinates="skillcorner", + include_empty_frames=True, ) assert dataset.metadata.provider == Provider.SKILLCORNER assert dataset.dataset_type == DatasetType.TRACKING - assert len(dataset.records) == 34783 + assert len(dataset.records) == 55632 assert len(dataset.metadata.periods) == 2 assert dataset.metadata.orientation == Orientation.AWAY_TEAM - assert dataset.metadata.periods[0] == Period( - id=1, - start_timestamp=0.0, - end_timestamp=2753.3, - attacking_direction=AttackingDirection.AWAY_HOME, + assert dataset.metadata.periods[0].id == 1 + assert dataset.metadata.periods[0].start_timestamp == timedelta( + seconds=1411 / 10 + ) + assert dataset.metadata.periods[0].end_timestamp == timedelta( + seconds=28944 / 10 + ) + assert ( + dataset.metadata.periods[0].attacking_direction + == AttackingDirection.AWAY_HOME ) - assert dataset.metadata.periods[1] == Period( - id=2, - start_timestamp=2700.0, - end_timestamp=5509.7, - attacking_direction=AttackingDirection.HOME_AWAY, + assert dataset.metadata.periods[1].id == 2 + assert dataset.metadata.periods[1].start_timestamp == timedelta( + seconds=39979 / 10 + ) + assert dataset.metadata.periods[1].end_timestamp == timedelta( + seconds=68076 / 10 + ) + assert ( + dataset.metadata.periods[1].attacking_direction + == AttackingDirection.HOME_AWAY ) - # are frames with wrong camera views and pregame skipped? - assert dataset.records[0].timestamp == 11.2 + assert dataset.records[0].frame_id == 1411 + assert dataset.records[0].timestamp == timedelta(seconds=0) + assert dataset.records[27534].frame_id == 39979 + assert dataset.records[27534].timestamp == timedelta(seconds=0) # make sure skillcorner ID is used as player ID assert dataset.metadata.teams[0].players[0].player_id == "10247" # make sure data is loaded correctly home_player = dataset.metadata.teams[0].players[2] - assert dataset.records[0].players_data[ + assert dataset.records[112].players_data[ home_player ].coordinates == Point(x=33.8697315398, y=-9.55742259253) away_player = dataset.metadata.teams[1].players[9] - assert dataset.records[0].players_data[ + assert dataset.records[112].players_data[ away_player ].coordinates == Point(x=25.9863082795, y=27.3013598578) - assert dataset.records[1].ball_coordinates == Point3D( + assert dataset.records[113].ball_coordinates == Point3D( x=30.5914728131, y=35.3622277834, z=2.24371228757 ) # check that missing ball-z_coordinate is identified as None - assert dataset.records[38].ball_coordinates == Point3D( + assert dataset.records[150].ball_coordinates == Point3D( x=11.6568802848, y=24.7214038909, z=None ) # check that 'ball_z' column is included in to_pandas dataframe - # frame = _frame_to_pandas_row_converter(dataset.records[38]) + # frame = _frame_to_pandas_row_converter(dataset.records[150]) # assert "ball_z" in frame.keys() # make sure player data is only in the frame when the player is in view assert "home_1" not in [ player.player_id - for player in dataset.records[0].players_data.keys() + for player in dataset.records[112].players_data.keys() ] assert "away_1" not in [ player.player_id - for player in dataset.records[0].players_data.keys() + for player in dataset.records[112].players_data.keys() ] # are anonymous players loaded correctly? home_anon_75 = [ player - for player in dataset.records[87].players_data + for player in dataset.records[197].players_data if player.player_id == "home_anon_75" ] assert home_anon_75 == [ player - for player in dataset.records[88].players_data + for player in dataset.records[200].players_data if player.player_id == "home_anon_75" ] @@ -116,3 +132,11 @@ def test_correct_normalized_deserialization( assert dataset.records[0].players_data[ home_player ].coordinates == Point(x=0.8225688718076191, y=0.6405503322430882) + + def test_skip_empty_frames(self, meta_data: str, raw_data: str): + dataset = skillcorner.load( + meta_data=meta_data, raw_data=raw_data, include_empty_frames=False + ) + + assert len(dataset.records) == 34783 + assert dataset.records[0].timestamp == timedelta(seconds=11.2) diff --git a/kloppy/tests/test_sportec.py b/kloppy/tests/test_sportec.py index d19613eb..1ca73837 100644 --- a/kloppy/tests/test_sportec.py +++ b/kloppy/tests/test_sportec.py @@ -1,3 +1,4 @@ +from datetime import datetime, timedelta, timezone from pathlib import Path import pytest @@ -50,18 +51,33 @@ def test_correct_event_data_deserialization( assert dataset.events[28].result == ShotResult.OWN_GOAL assert dataset.metadata.orientation == Orientation.FIXED_HOME_AWAY - assert dataset.metadata.periods[0] == Period( - id=1, - start_timestamp=1591381800.21, - end_timestamp=1591384584.0, - attacking_direction=AttackingDirection.HOME_AWAY, + assert dataset.metadata.periods[0].id == 1 + assert dataset.metadata.periods[0].start_timestamp == datetime( + 2020, 6, 5, 18, 30, 0, 210000, tzinfo=timezone.utc ) - assert dataset.metadata.periods[1] == Period( - id=2, - start_timestamp=1591385607.01, - end_timestamp=1591388598.0, - attacking_direction=AttackingDirection.AWAY_HOME, + assert dataset.metadata.periods[0].end_timestamp == datetime( + 2020, 6, 5, 19, 16, 24, 0, tzinfo=timezone.utc ) + assert ( + dataset.metadata.periods[0].attacking_direction + == AttackingDirection.HOME_AWAY + ) + assert dataset.metadata.periods[1].id == 2 + assert dataset.metadata.periods[1].start_timestamp == datetime( + 2020, 6, 5, 19, 33, 27, 10000, tzinfo=timezone.utc + ) + assert dataset.metadata.periods[1].end_timestamp == datetime( + 2020, 6, 5, 20, 23, 18, 0, tzinfo=timezone.utc + ) + assert ( + dataset.metadata.periods[1].attacking_direction + == AttackingDirection.AWAY_HOME + ) + + # Check the timestamps + assert dataset.events[0].timestamp == timedelta(seconds=0) + assert dataset.events[1].timestamp == timedelta(seconds=3.123) + assert dataset.events[25].timestamp == timedelta(seconds=0) player = dataset.metadata.teams[0].players[0] assert player.player_id == "DFL-OBJ-00001D" @@ -123,6 +139,28 @@ def test_load_metadata(self, raw_data: Path, meta_data: Path): assert dataset.metadata.provider == Provider.SPORTEC assert dataset.dataset_type == DatasetType.TRACKING assert len(dataset.metadata.periods) == 2 + assert dataset.metadata.periods[0].id == 1 + assert dataset.metadata.periods[0].start_timestamp == timedelta( + seconds=400 + ) + assert dataset.metadata.periods[0].end_timestamp == timedelta( + seconds=400 + 2786.2 + ) + assert ( + dataset.metadata.periods[0].attacking_direction + == AttackingDirection.HOME_AWAY + ) + assert dataset.metadata.periods[1].id == 2 + assert dataset.metadata.periods[1].start_timestamp == timedelta( + seconds=4000 + ) + assert dataset.metadata.periods[1].end_timestamp == timedelta( + seconds=4000 + 2996.68 + ) + assert ( + dataset.metadata.periods[1].attacking_direction + == AttackingDirection.AWAY_HOME + ) def test_load_frames(self, raw_data: Path, meta_data: Path): dataset = sportec.load_tracking( @@ -133,7 +171,7 @@ def test_load_frames(self, raw_data: Path, meta_data: Path): ) home_team, away_team = dataset.metadata.teams - assert dataset.frames[0].timestamp == 0.0 + assert dataset.frames[0].timestamp == timedelta(seconds=0) assert dataset.frames[0].ball_owning_team == away_team assert dataset.frames[0].ball_state == BallState.DEAD assert dataset.frames[0].ball_coordinates == Point3D( @@ -165,8 +203,8 @@ def test_load_frames(self, raw_data: Path, meta_data: Path): second_period = dataset.metadata.periods[1] for frame in dataset: if frame.period == second_period: - assert ( - frame.timestamp == 0 + assert frame.timestamp == timedelta( + seconds=0 ), "First frame must start at timestamp 0.0" break else: diff --git a/kloppy/tests/test_statsbomb.py b/kloppy/tests/test_statsbomb.py index 4115f1f5..f3f31b14 100644 --- a/kloppy/tests/test_statsbomb.py +++ b/kloppy/tests/test_statsbomb.py @@ -1,5 +1,6 @@ import os from collections import defaultdict +from datetime import timedelta from pathlib import Path from typing import cast @@ -151,7 +152,9 @@ def test_periods(self, dataset): """It should create the periods""" assert len(dataset.metadata.periods) == 2 assert dataset.metadata.periods[0].id == 1 - assert dataset.metadata.periods[0].start_timestamp == 0.0 + assert dataset.metadata.periods[0].start_timestamp == parse_str_ts( + "00:00:00.000" + ) assert dataset.metadata.periods[0].end_timestamp == parse_str_ts( "00:47:38.122" ) @@ -217,6 +220,17 @@ def test_generic_attributes(self, dataset: EventDataset): assert event.timestamp == parse_str_ts("00:41:31.122") assert event.ball_state == BallState.ALIVE + def test_timestamp(self, dataset): + """It should set the correct timestamp, reset to zero after each period""" + kickoff_p1 = dataset.get_event_by_id( + "8022c113-e349-4b0b-b4a7-a3bb662535f8" + ) + assert kickoff_p1.timestamp == parse_str_ts("00:00:00.840") + kickoff_p2 = dataset.get_event_by_id( + "b3199171-507c-42a3-b4c4-9e609d7a98f6" + ) + assert kickoff_p2.timestamp == parse_str_ts("00:00:00.848") + def test_related_events(self, dataset: EventDataset): """Test whether related events are properly linked""" carry_event = dataset.get_event_by_id( @@ -571,10 +585,9 @@ def test_open_play(self, dataset: EventDataset): # A pass should have end coordinates assert pass_event.receiver_coordinates == Point(86.15, 53.35) # A pass should have an end timestamp - assert ( - pass_event.receive_timestamp - == parse_str_ts("00:35:21.533") + 0.634066 - ) + assert pass_event.receive_timestamp == parse_str_ts( + "00:35:21.533" + ) + timedelta(seconds=0.634066) # A pass should have a receiver assert ( pass_event.receiver_player.name @@ -819,7 +832,9 @@ def test_attributes(self, dataset: EventDataset): # A carry should have an end location assert carry.end_coordinates == Point(21.65, 54.85) # A carry should have an end timestamp - assert carry.end_timestamp == parse_str_ts("00:20:11.457") + 1.365676 + assert carry.end_timestamp == parse_str_ts("00:20:11.457") + timedelta( + seconds=1.365676 + ) class TestStatsBombDuelEvent: diff --git a/kloppy/tests/test_statsperform.py b/kloppy/tests/test_statsperform.py index 68770e8b..ce7528ed 100644 --- a/kloppy/tests/test_statsperform.py +++ b/kloppy/tests/test_statsperform.py @@ -1,4 +1,5 @@ from pathlib import Path +from datetime import datetime, timedelta import pytest @@ -53,24 +54,39 @@ def test_correct_deserialization(self, meta_data: Path, raw_data: Path): # Check the periods assert dataset.metadata.periods[1].id == 1 - assert dataset.metadata.periods[1].start_timestamp == 0 - assert dataset.metadata.periods[1].end_timestamp == 2500 + assert dataset.metadata.periods[1].start_timestamp == datetime( + 2020, 8, 23, 11, 0, 10 + ) + assert dataset.metadata.periods[1].end_timestamp == datetime( + 2020, 8, 23, 11, 48, 15 + ) assert ( dataset.metadata.periods[1].attacking_direction == AttackingDirection.AWAY_HOME ) assert dataset.metadata.periods[2].id == 2 - assert dataset.metadata.periods[2].start_timestamp == 0 - assert dataset.metadata.periods[2].end_timestamp == 6500 + assert dataset.metadata.periods[2].start_timestamp == datetime( + 2020, 8, 23, 12, 6, 22 + ) + assert dataset.metadata.periods[2].end_timestamp == datetime( + 2020, 8, 23, 12, 56, 30 + ) assert ( dataset.metadata.periods[2].attacking_direction == AttackingDirection.HOME_AWAY ) # Check some timestamps - assert dataset.records[0].timestamp == 0 # First frame - assert dataset.records[20].timestamp == 2.0 # Later frame + assert dataset.records[0].timestamp == timedelta( + seconds=0 + ) # First frame + assert dataset.records[20].timestamp == timedelta( + seconds=2.0 + ) # Later frame + assert dataset.records[26].timestamp == timedelta( + seconds=0 + ) # Second period # Check some players home_team = dataset.metadata.teams[0] diff --git a/kloppy/tests/test_to_records.py b/kloppy/tests/test_to_records.py index 545abf0d..81e1a6bf 100644 --- a/kloppy/tests/test_to_records.py +++ b/kloppy/tests/test_to_records.py @@ -1,4 +1,5 @@ from pathlib import Path +from datetime import timedelta import pytest from kloppy import statsbomb @@ -57,7 +58,7 @@ def test_string_columns(self, dataset: EventDataset): "timestamp", "coordinates_x", "coordinates" ) assert records[0] == { - "timestamp": 0.098, + "timestamp": timedelta(seconds=0.098), "coordinates_x": 60.5, "coordinates": Point(x=60.5, y=40.5), } @@ -75,7 +76,7 @@ def test_string_wildcard_columns(self, dataset: EventDataset): DistanceToOwnGoalTransformer(), ) assert records[0] == { - "timestamp": 0.098, + "timestamp": timedelta(seconds=0.098), "player_id": "6581", "coordinates_x": 60.5, "coordinates_y": 40.5, diff --git a/kloppy/tests/test_tracab.py b/kloppy/tests/test_tracab.py index a97d9d8f..c69671b5 100644 --- a/kloppy/tests/test_tracab.py +++ b/kloppy/tests/test_tracab.py @@ -1,4 +1,5 @@ from pathlib import Path +from datetime import timedelta import pytest @@ -35,25 +36,42 @@ def test_correct_deserialization(self, meta_data: Path, raw_data: Path): only_alive=False, ) + # Check metadata assert dataset.metadata.provider == Provider.TRACAB assert dataset.dataset_type == DatasetType.TRACKING assert len(dataset.records) == 6 assert len(dataset.metadata.periods) == 2 assert dataset.metadata.orientation == Orientation.FIXED_HOME_AWAY - assert dataset.metadata.periods[0] == Period( - id=1, - start_timestamp=4.0, - end_timestamp=4.08, - attacking_direction=AttackingDirection.HOME_AWAY, + assert dataset.metadata.periods[0].id == 1 + assert dataset.metadata.periods[0].start_timestamp == timedelta( + seconds=100 / 25 ) - - assert dataset.metadata.periods[1] == Period( - id=2, - start_timestamp=8.0, - end_timestamp=8.08, - attacking_direction=AttackingDirection.AWAY_HOME, + assert dataset.metadata.periods[0].end_timestamp == timedelta( + seconds=102 / 25 + ) + assert ( + dataset.metadata.periods[0].attacking_direction + == AttackingDirection.HOME_AWAY + ) + assert dataset.metadata.periods[1].id == 2 + assert dataset.metadata.periods[1].start_timestamp == timedelta( + seconds=200 / 25 ) + assert dataset.metadata.periods[1].end_timestamp == timedelta( + seconds=202 / 25 + ) + assert ( + dataset.metadata.periods[1].attacking_direction + == AttackingDirection.AWAY_HOME + ) + + # Check frame ids and timestamps + assert dataset.records[0].frame_id == 100 + assert dataset.records[0].timestamp == timedelta(seconds=0) + assert dataset.records[3].frame_id == 200 + assert dataset.records[3].timestamp == timedelta(seconds=0) + # Check frame data player_home_19 = dataset.metadata.teams[0].get_player_by_jersey_number( 19 ) diff --git a/kloppy/tests/test_wyscout.py b/kloppy/tests/test_wyscout.py index 3cd6b516..32df5670 100644 --- a/kloppy/tests/test_wyscout.py +++ b/kloppy/tests/test_wyscout.py @@ -1,3 +1,4 @@ +from datetime import datetime, timedelta from pathlib import Path import pytest @@ -58,6 +59,28 @@ def dataset(self, event_v2_data) -> EventDataset: ) return dataset + def test_metadata(self, dataset: EventDataset): + assert dataset.metadata.periods[0].id == 1 + assert dataset.metadata.periods[0].start_timestamp == timedelta( + seconds=0 + ) + assert dataset.metadata.periods[0].end_timestamp == timedelta( + seconds=2863.708369 + ) + assert dataset.metadata.periods[1].id == 2 + assert dataset.metadata.periods[1].start_timestamp == timedelta( + seconds=2863.708369 + ) + assert dataset.metadata.periods[1].end_timestamp == timedelta( + seconds=2863.708369 + ) + timedelta(seconds=2999.70982) + + def test_timestamps(self, dataset: EventDataset): + kickoff_p1 = dataset.get_event_by_id("190078343") + assert kickoff_p1.timestamp == timedelta(seconds=2.643377) + kickoff_p2 = dataset.get_event_by_id("190079822") + assert kickoff_p2.timestamp == timedelta(seconds=0) + def test_shot_event(self, dataset: EventDataset): shot_event = dataset.get_event_by_id("190079151") assert ( @@ -139,6 +162,28 @@ def dataset(self, event_v3_data: Path) -> EventDataset: ) return dataset + def test_metadata(self, dataset: EventDataset): + assert dataset.metadata.periods[0].id == 1 + assert dataset.metadata.periods[0].start_timestamp == timedelta( + seconds=0 + ) + assert dataset.metadata.periods[0].end_timestamp == timedelta( + minutes=20, seconds=47 + ) + assert dataset.metadata.periods[1].id == 2 + assert dataset.metadata.periods[1].start_timestamp == timedelta( + minutes=20, seconds=47 + ) + assert dataset.metadata.periods[1].end_timestamp == timedelta( + minutes=20, seconds=47 + ) + timedelta(minutes=50, seconds=30) + + def test_timestamps(self, dataset: EventDataset): + kickoff_p1 = dataset.get_event_by_id(663292348) + assert kickoff_p1.timestamp == timedelta(minutes=0, seconds=1) + kickoff_p2 = dataset.get_event_by_id(1331979498) + assert kickoff_p2.timestamp == timedelta(minutes=1, seconds=0) + def test_coordinates(self, dataset: EventDataset): assert dataset.records[2].coordinates == Point(36.0, 78.0) diff --git a/kloppy/tests/test_xml.py b/kloppy/tests/test_xml.py index 4169c4b7..957bdc74 100644 --- a/kloppy/tests/test_xml.py +++ b/kloppy/tests/test_xml.py @@ -1,4 +1,5 @@ import os +from datetime import timedelta from pandas import DataFrame from pandas._testing import assert_frame_equal @@ -14,7 +15,9 @@ def test_correct_deserialization(self, base_dir): assert len(dataset.metadata.periods) == 1 - assert dataset.metadata.periods[0].start_timestamp == 0 + assert dataset.metadata.periods[0].start_timestamp == timedelta( + seconds=0 + ) assert ( dataset.metadata.periods[0].end_timestamp == dataset.codes[-1].end_timestamp @@ -23,8 +26,8 @@ def test_correct_deserialization(self, base_dir): assert len(dataset.codes) == 3 assert dataset.codes[0].code_id == "P1" assert dataset.codes[0].code == "PASS" - assert dataset.codes[0].timestamp == 3.6 - assert dataset.codes[0].end_timestamp == 9.7 + assert dataset.codes[0].timestamp == timedelta(seconds=3.6) + assert dataset.codes[0].end_timestamp == timedelta(seconds=9.7) assert dataset.codes[0].labels == { "Team": "Henkie", "Packing.Value": 1, @@ -37,8 +40,16 @@ def test_correct_deserialization(self, base_dir): { "code_id": ["P1", "P2", "P3"], "period_id": [1, 1, 1], - "timestamp": [3.6, 68.3, 103.6], - "end_timestamp": [9.7, 74.5, 109.6], + "timestamp": [ + timedelta(seconds=3.6), + timedelta(seconds=68.3), + timedelta(seconds=103.6), + ], + "end_timestamp": [ + timedelta(seconds=9.7), + timedelta(seconds=74.5), + timedelta(seconds=109.6), + ], "code": ["PASS", "PASS", "SHOT"], "Team": ["Henkie", "Henkie", "Henkie"], "Packing.Value": [1, 3, None], @@ -56,8 +67,16 @@ def test_correct_serialization(self, base_dir): # Make sure that data in Period 2 get the timestamp corrected dataset.metadata.periods = [ - Period(id=1, start_timestamp=0, end_timestamp=45 * 60), - Period(id=2, start_timestamp=45 * 60 + 10, end_timestamp=90 * 60), + Period( + id=1, + start_timestamp=timedelta(seconds=0), + end_timestamp=timedelta(minutes=45), + ), + Period( + id=2, + start_timestamp=timedelta(minutes=45, seconds=10), + end_timestamp=timedelta(minutes=90), + ), ] dataset.codes[1].period = dataset.metadata.periods[1] From ed909fc71928da8184c50af9dd9a42cea5b1517f Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Sat, 27 Jan 2024 22:24:50 +0100 Subject: [PATCH 05/17] fix: Wyscout v3 timestamps --- .../serializers/event/wyscout/deserializer_v3.py | 11 ++++++++++- kloppy/tests/test_wyscout.py | 5 ++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/kloppy/infra/serializers/event/wyscout/deserializer_v3.py b/kloppy/infra/serializers/event/wyscout/deserializer_v3.py index e57b6821..d4e8a0ce 100644 --- a/kloppy/infra/serializers/event/wyscout/deserializer_v3.py +++ b/kloppy/infra/serializers/event/wyscout/deserializer_v3.py @@ -475,6 +475,14 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset: event["id"] = event["type"]["primary"] periods = [] + # start timestamps are fixed + start_ts = { + 1: timedelta(minutes=0), + 2: timedelta(minutes=45), + 3: timedelta(minutes=90), + 4: timedelta(minutes=105), + 5: timedelta(minutes=120), + } with performance_logging("parse data", logger=logger): home_team_id, away_team_id = raw_events["teams"].keys() @@ -552,7 +560,8 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset: seconds=float( raw_event["second"] + raw_event["minute"] * 60 ) - ), + ) + - start_ts[period_id], } primary_event_type = raw_event["type"]["primary"] diff --git a/kloppy/tests/test_wyscout.py b/kloppy/tests/test_wyscout.py index 32df5670..4bcc58a3 100644 --- a/kloppy/tests/test_wyscout.py +++ b/kloppy/tests/test_wyscout.py @@ -181,8 +181,11 @@ def test_metadata(self, dataset: EventDataset): def test_timestamps(self, dataset: EventDataset): kickoff_p1 = dataset.get_event_by_id(663292348) assert kickoff_p1.timestamp == timedelta(minutes=0, seconds=1) + # Note: the test file is incorrect. The second period start at 45:00 kickoff_p2 = dataset.get_event_by_id(1331979498) - assert kickoff_p2.timestamp == timedelta(minutes=1, seconds=0) + assert kickoff_p2.timestamp == timedelta( + minutes=1, seconds=0 + ) - timedelta(minutes=45) def test_coordinates(self, dataset: EventDataset): assert dataset.records[2].coordinates == Point(36.0, 78.0) From aac51e555534cf3ab73d11195c6893631305aea3 Mon Sep 17 00:00:00 2001 From: driesdeprest Date: Mon, 29 Jan 2024 13:22:09 +0100 Subject: [PATCH 06/17] Add support for JSON Tracab data --- kloppy/_providers/tracab.py | 39 +- .../serializers/tracking/tracab/__init__.py | 3 + .../serializers/tracking/tracab/common.py | 6 + .../{tracab.py => tracab/tracab_dat.py} | 12 +- .../tracking/tracab/tracab_json.py | 253 ++ kloppy/tests/files/tracab_meta.json | 464 ++ kloppy/tests/files/tracab_raw.json | 3934 +++++++++++++++++ kloppy/tests/test_tracab.py | 127 +- 8 files changed, 4814 insertions(+), 24 deletions(-) create mode 100644 kloppy/infra/serializers/tracking/tracab/__init__.py create mode 100644 kloppy/infra/serializers/tracking/tracab/common.py rename kloppy/infra/serializers/tracking/{tracab.py => tracab/tracab_dat.py} (96%) create mode 100644 kloppy/infra/serializers/tracking/tracab/tracab_json.py create mode 100644 kloppy/tests/files/tracab_meta.json create mode 100644 kloppy/tests/files/tracab_raw.json diff --git a/kloppy/_providers/tracab.py b/kloppy/_providers/tracab.py index 8a9abef6..3c8f7c7d 100644 --- a/kloppy/_providers/tracab.py +++ b/kloppy/_providers/tracab.py @@ -1,8 +1,13 @@ -from typing import Optional +from typing import Optional, Union, Type + from kloppy.domain import TrackingDataset -from kloppy.infra.serializers.tracking.tracab import ( - TRACABDeserializer, +from kloppy.infra.serializers.tracking.tracab.tracab_dat import ( + TRACABDatDeserializer, + TRACABInputs, +) +from kloppy.infra.serializers.tracking.tracab.tracab_json import ( + TRACABJSONDeserializer, TRACABInputs, ) from kloppy.io import FileLike, open_as_file @@ -15,8 +20,16 @@ def load( limit: Optional[int] = None, coordinates: Optional[str] = None, only_alive: Optional[bool] = True, + data_version: Optional[str] = None, ) -> TrackingDataset: - deserializer = TRACABDeserializer( + if data_version == "dat": + deserializer_class = TRACABDatDeserializer + elif data_version == "json": + deserializer_class = TRACABJSONDeserializer + else: + deserializer_class = identify_deserializer(meta_data, raw_data) + + deserializer = deserializer_class( sample_rate=sample_rate, limit=limit, coordinate_system=coordinates, @@ -28,3 +41,21 @@ def load( return deserializer.deserialize( inputs=TRACABInputs(meta_data=meta_data_fp, raw_data=raw_data_fp) ) + + +def identify_deserializer( + meta_data: FileLike, + raw_data: FileLike, +) -> Union[Type[TRACABDatDeserializer], Type[TRACABJSONDeserializer]]: + deserializer = None + if "xml" in meta_data.name and "dat" in raw_data.name: + deserializer = TRACABDatDeserializer + if "json" in meta_data.name and "json" in raw_data.name: + deserializer = TRACABJSONDeserializer + + if deserializer is None: + raise ValueError( + "Tracab data version could not be recognized, please specify" + ) + + return deserializer diff --git a/kloppy/infra/serializers/tracking/tracab/__init__.py b/kloppy/infra/serializers/tracking/tracab/__init__.py new file mode 100644 index 00000000..ea341638 --- /dev/null +++ b/kloppy/infra/serializers/tracking/tracab/__init__.py @@ -0,0 +1,3 @@ +from .common import TRACABInputs +from .tracab_dat import TRACABDatDeserializer +from .tracab_json import TRACABJSONDeserializer diff --git a/kloppy/infra/serializers/tracking/tracab/common.py b/kloppy/infra/serializers/tracking/tracab/common.py new file mode 100644 index 00000000..0e2e2797 --- /dev/null +++ b/kloppy/infra/serializers/tracking/tracab/common.py @@ -0,0 +1,6 @@ +from typing import NamedTuple, IO + + +class TRACABInputs(NamedTuple): + meta_data: IO[bytes] + raw_data: IO[bytes] diff --git a/kloppy/infra/serializers/tracking/tracab.py b/kloppy/infra/serializers/tracking/tracab/tracab_dat.py similarity index 96% rename from kloppy/infra/serializers/tracking/tracab.py rename to kloppy/infra/serializers/tracking/tracab/tracab_dat.py index f7a0e162..eac9f698 100644 --- a/kloppy/infra/serializers/tracking/tracab.py +++ b/kloppy/infra/serializers/tracking/tracab/tracab_dat.py @@ -1,5 +1,5 @@ import logging -from typing import Tuple, Dict, NamedTuple, IO, Optional, Union +from typing import Dict, Optional, Union from lxml import objectify @@ -25,17 +25,13 @@ from kloppy.utils import Readable, performance_logging -from .deserializer import TrackingDataDeserializer +from .common import TRACABInputs +from ..deserializer import TrackingDataDeserializer logger = logging.getLogger(__name__) -class TRACABInputs(NamedTuple): - meta_data: IO[bytes] - raw_data: IO[bytes] - - -class TRACABDeserializer(TrackingDataDeserializer[TRACABInputs]): +class TRACABDatDeserializer(TrackingDataDeserializer[TRACABInputs]): def __init__( self, limit: Optional[int] = None, diff --git a/kloppy/infra/serializers/tracking/tracab/tracab_json.py b/kloppy/infra/serializers/tracking/tracab/tracab_json.py new file mode 100644 index 00000000..78b1bb5e --- /dev/null +++ b/kloppy/infra/serializers/tracking/tracab/tracab_json.py @@ -0,0 +1,253 @@ +import logging +import json +import html +from typing import Dict, Optional, Union + +from kloppy.domain import ( + TrackingDataset, + DatasetFlag, + AttackingDirection, + Frame, + Point, + Point3D, + Team, + BallState, + Period, + Orientation, + Metadata, + Ground, + Player, + Provider, + PlayerData, + Position, +) +from kloppy.exceptions import DeserializationError + +from kloppy.utils import Readable, performance_logging + +from .common import TRACABInputs +from ..deserializer import TrackingDataDeserializer + +logger = logging.getLogger(__name__) + + +class TRACABJSONDeserializer(TrackingDataDeserializer[TRACABInputs]): + def __init__( + self, + limit: Optional[int] = None, + sample_rate: Optional[float] = None, + coordinate_system: Optional[Union[str, Provider]] = None, + only_alive: Optional[bool] = True, + ): + super().__init__(limit, sample_rate, coordinate_system) + self.only_alive = only_alive + + @property + def provider(self) -> Provider: + return Provider.TRACAB + + @classmethod + def _create_frame(cls, teams, period, raw_frame, frame_rate): + frame_id = raw_frame["FrameCount"] + raw_players_data = raw_frame["PlayerPositions"] + raw_ball_position = raw_frame["BallPosition"][0] + + players_data = {} + for player_data in raw_players_data: + if player_data["Team"] == 1: + team = teams[0] + elif player_data["Team"] == 0: + team = teams[1] + elif player_data["Team"] in (-1, 4): + continue + else: + raise DeserializationError( + f"Unknown Player Team ID: {player_data['Team']}" + ) + + jersey_no = player_data["JerseyNumber"] + x = player_data["X"] + y = player_data["Y"] + speed = player_data["Speed"] + + player = team.get_player_by_jersey_number(jersey_no) + if player: + players_data[player] = PlayerData( + coordinates=Point(float(x), float(y)), speed=float(speed) + ) + else: + # continue + raise DeserializationError( + f"Player not found for player jersey no {jersey_no} of team: {team.name}" + ) + + ball_x = raw_ball_position["X"] + ball_y = raw_ball_position["Y"] + ball_z = raw_ball_position["Z"] + ball_speed = raw_ball_position["Speed"] + if raw_ball_position["BallOwningTeam"] == "H": + ball_owning_team = teams[0] + elif raw_ball_position["BallOwningTeam"] == "A": + ball_owning_team = teams[1] + else: + raise DeserializationError( + f"Unknown ball owning team: {raw_ball_position['BallOwningTeam']}" + ) + if raw_ball_position["BallStatus"] == "Alive": + ball_state = BallState.ALIVE + elif raw_ball_position["BallStatus"] == "Dead": + ball_state = BallState.DEAD + else: + raise DeserializationError( + f"Unknown ball state: {raw_ball_position['BallStatus']}" + ) + + return Frame( + frame_id=frame_id, + timestamp=frame_id / frame_rate - period.start_timestamp, + ball_coordinates=Point3D( + float(ball_x), float(ball_y), float(ball_z) + ), + ball_state=ball_state, + ball_owning_team=ball_owning_team, + ball_speed=ball_speed, + players_data=players_data, + period=period, + other_data={}, + ) + + @staticmethod + def __validate_inputs(inputs: Dict[str, Readable]): + if "metadata" not in inputs: + raise ValueError("Please specify a value for 'metadata'") + if "raw_data" not in inputs: + raise ValueError("Please specify a value for 'raw_data'") + + def create_team(self, team_data, ground): + team = Team( + team_id=str(team_data["TeamID"]), + name=html.unescape(team_data["ShortName"]), + ground=ground, + ) + + def parse_player_position( + starting_position: str, current_position: str + ): + if starting_position != "S": + return Position( + position_id=starting_position, name=starting_position + ) + elif current_position != "S" and current_position != "O": + return Position( + position_id=current_position, name=current_position + ) + else: + return None + + team.players = [ + Player( + player_id=str(player["PlayerID"]), + team=team, + first_name=html.unescape(player["FirstName"]), + last_name=html.unescape(player["LastName"]), + name=html.unescape( + player["FirstName"] + " " + player["LastName"] + ), + jersey_no=int(player["JerseyNo"]), + starting=True if player["StartingPosition"] != "S" else False, + position=parse_player_position( + player["StartingPosition"], player["CurrentPosition"] + ), + ) + for player in team_data["Players"] + ] + + return team + + def deserialize(self, inputs: TRACABInputs) -> TrackingDataset: + meta_data = json.load(inputs.meta_data) + raw_data = json.load(inputs.raw_data) + + with performance_logging("Loading metadata", logger=logger): + frame_rate = meta_data["FrameRate"] + pitch_size_width = meta_data["PitchShortSide"] / 100 + pitch_size_length = meta_data["PitchLongSide"] / 100 + + periods = [] + for period_id in [1, 2, 3, 4]: + period_start_frame = meta_data[f"Phase{period_id}StartFrame"] + period_end_frame = meta_data[f"Phase{period_id}EndFrame"] + if period_start_frame != 0 or period_end_frame != 0: + periods.append( + Period( + id=period_id, + start_timestamp=period_start_frame / frame_rate, + end_timestamp=period_end_frame / frame_rate, + attacking_direction=AttackingDirection.HOME_AWAY + if meta_data[f"Phase{period_id}HomeGKLeft"] + else AttackingDirection.AWAY_HOME, + ) + ) + + home_team = self.create_team(meta_data["HomeTeam"], Ground.HOME) + away_team = self.create_team(meta_data["AwayTeam"], Ground.AWAY) + teams = [home_team, away_team] + + transformer = self.get_transformer( + length=pitch_size_length, width=pitch_size_width + ) + + with performance_logging("Loading data", logger=logger): + raw_data = raw_data["FrameData"] + + def _iter(): + n = 0 + sample = 1.0 / self.sample_rate + + for frame in raw_data: + if ( + self.only_alive + and frame["BallPosition"][0]["BallStatus"] == "Dead" + ): + continue + + frame_id = frame["FrameCount"] + for _period in periods: + if _period.contains(frame_id / frame_rate): + if n % sample == 0: + yield _period, frame + n += 1 + + frames = [] + for n, (_period, _frame) in enumerate(_iter()): + frame = self._create_frame(teams, _period, _frame, frame_rate) + + frame = transformer.transform_frame(frame) + + frames.append(frame) + + if self.limit and n >= self.limit: + break + + orientation = ( + Orientation.HOME_TEAM + if periods[1].attacking_direction == AttackingDirection.HOME_AWAY + else Orientation.AWAY_TEAM + ) + + metadata = Metadata( + teams=teams, + periods=periods, + pitch_dimensions=transformer.get_to_coordinate_system().pitch_dimensions, + score=None, + frame_rate=frame_rate, + orientation=orientation, + provider=Provider.TRACAB, + flags=DatasetFlag.BALL_OWNING_TEAM | DatasetFlag.BALL_STATE, + coordinate_system=transformer.get_to_coordinate_system(), + ) + + return TrackingDataset( + records=frames, + metadata=metadata, + ) diff --git a/kloppy/tests/files/tracab_meta.json b/kloppy/tests/files/tracab_meta.json new file mode 100644 index 00000000..5ce9242f --- /dev/null +++ b/kloppy/tests/files/tracab_meta.json @@ -0,0 +1,464 @@ +{ + "GameID": 1, + "CompetitionID": 1, + "SeasonID": 2023, + "FrameRate": 25, + "PitchShortSide": 6800, + "PitchLongSide": 10500, + "Phase1StartFrame": 1848508, + "Phase1EndFrame": 1916408, + "Phase2StartFrame": 1942114, + "Phase2EndFrame": 2017933, + "Phase3StartFrame": 0, + "Phase3EndFrame": 0, + "Phase4StartFrame": 0, + "Phase4EndFrame": 0, + "Phase5StartFrame": 0, + "Phase5EndFrame": 0, + "Phase1HomeGKLeft": false, + "Phase2HomeGKLeft": true, + "Phase3HomeGKLeft": null, + "Phase4HomeGKLeft": null, + "Phase5HomeGKLeft": null, + "HomeTeam": { + "LongName": "Long Name Home", + "ShortName": "Short Name Home", + "TeamID": 1, + "Players": [ + { + "PlayerID": 8216, + "FirstName": "Player", + "LastName": "One", + "JerseyNo": 1, + "StartFrameCount": 1848508, + "EndFrameCount": 2017933, + "StartingPosition": "G", + "CurrentPosition": "G" + }, + { + "PlayerID": 8302, + "FirstName": "Player", + "LastName": "Two", + "JerseyNo": 2, + "StartFrameCount": 1848508, + "EndFrameCount": 2017933, + "StartingPosition": "D", + "CurrentPosition": "D" + }, + { + "PlayerID": 12544, + "FirstName": "Player", + "LastName": "Three", + "JerseyNo": 5, + "StartFrameCount": 1848508, + "EndFrameCount": 2017933, + "StartingPosition": "D", + "CurrentPosition": "D" + }, + { + "PlayerID": 12142, + "FirstName": "Player", + "LastName": "Four", + "JerseyNo": 7, + "StartFrameCount": 1848508, + "EndFrameCount": 1976302, + "StartingPosition": "M", + "CurrentPosition": "O" + }, + { + "PlayerID": 8172, + "FirstName": "Player", + "LastName": "Five", + "JerseyNo": 8, + "StartFrameCount": 1848508, + "EndFrameCount": 2017933, + "StartingPosition": "M", + "CurrentPosition": "M" + }, + { + "PlayerID": 12736, + "FirstName": "Player", + "LastName": "Six", + "JerseyNo": 13, + "StartFrameCount": 1848508, + "EndFrameCount": 2017933, + "StartingPosition": "D", + "CurrentPosition": "D" + }, + { + "PlayerID": 13282, + "FirstName": "Player", + "LastName": "Seven", + "JerseyNo": 16, + "StartFrameCount": 1848508, + "EndFrameCount": 2017933, + "StartingPosition": "M", + "CurrentPosition": "M" + }, + { + "PlayerID": 12170, + "FirstName": "Player", + "LastName": "Eight", + "JerseyNo": 20, + "StartFrameCount": 1848508, + "EndFrameCount": 2001211, + "StartingPosition": "M", + "CurrentPosition": "O" + }, + { + "PlayerID": 12524, + "FirstName": "Player", + "LastName": "Nine", + "JerseyNo": 55, + "StartFrameCount": 1848508, + "EndFrameCount": 1976390, + "StartingPosition": "D", + "CurrentPosition": "O" + }, + { + "PlayerID": 9340, + "FirstName": "Player", + "LastName": "Ten", + "JerseyNo": 77, + "StartFrameCount": 1848508, + "EndFrameCount": 2017933, + "StartingPosition": "M", + "CurrentPosition": "M" + }, + { + "PlayerID": 11909, + "FirstName": "Player", + "LastName": "Eleven", + "JerseyNo": 90, + "StartFrameCount": 1848508, + "EndFrameCount": 1976494, + "StartingPosition": "A", + "CurrentPosition": "O" + }, + { + "PlayerID": 12814, + "FirstName": "Player", + "LastName": "Twelve", + "JerseyNo": 22, + "StartFrameCount": 0, + "EndFrameCount": 0, + "StartingPosition": "S", + "CurrentPosition": "S" + }, + { + "PlayerID": 11650, + "FirstName": "Player", + "LastName": "Thirteen", + "JerseyNo": 6, + "StartFrameCount": 2001211, + "EndFrameCount": 2017933, + "StartingPosition": "S", + "CurrentPosition": "M" + }, + { + "PlayerID": 8183, + "FirstName": "Player", + "LastName": "Fourteen", + "JerseyNo": 11, + "StartFrameCount": 0, + "EndFrameCount": 0, + "StartingPosition": "S", + "CurrentPosition": "S" + }, + { + "PlayerID": 13245, + "FirstName": "Player", + "LastName": "Fifteen", + "JerseyNo": 17, + "StartFrameCount": 0, + "EndFrameCount": 0, + "StartingPosition": "S", + "CurrentPosition": "S" + }, + { + "PlayerID": 13246, + "FirstName": "Player", + "LastName": "Sixteen", + "JerseyNo": 18, + "StartFrameCount": 1976494, + "EndFrameCount": 2017933, + "StartingPosition": "S", + "CurrentPosition": "A" + }, + { + "PlayerID": 12809, + "FirstName": "Player", + "LastName": "Seventeen", + "JerseyNo": 21, + "StartFrameCount": 1976302, + "EndFrameCount": 2017933, + "StartingPosition": "S", + "CurrentPosition": "M" + }, + { + "PlayerID": 8106, + "FirstName": "Player", + "LastName": "Eighteen", + "JerseyNo": 25, + "StartFrameCount": 0, + "EndFrameCount": 0, + "StartingPosition": "S", + "CurrentPosition": "S" + }, + { + "PlayerID": 12816, + "FirstName": "Player", + "LastName": "Nineteen", + "JerseyNo": 27, + "StartFrameCount": 1976390, + "EndFrameCount": 2017933, + "StartingPosition": "S", + "CurrentPosition": "D" + }, + { + "PlayerID": 12102, + "FirstName": "Player", + "LastName": "Twenty", + "JerseyNo": 39, + "StartFrameCount": 0, + "EndFrameCount": 0, + "StartingPosition": "S", + "CurrentPosition": "S" + } + ] + }, + "AwayTeam": { + "LongName": "Long Name Away", + "ShortName": "Short Name Away", + "TeamID": 2, + "Players": [ + { + "PlayerID": 10524, + "FirstName": "Away Player", + "LastName": "One", + "JerseyNo": 12, + "StartFrameCount": 1848508, + "EndFrameCount": 2017933, + "StartingPosition": "G", + "CurrentPosition": "G" + }, + { + "PlayerID": 12660, + "FirstName": "Away Player", + "LastName": "Two", + "JerseyNo": 4, + "StartFrameCount": 1848508, + "EndFrameCount": 2017933, + "StartingPosition": "D", + "CurrentPosition": "D" + }, + { + "PlayerID": 13275, + "FirstName": "Away Player", + "LastName": "Three", + "JerseyNo": 5, + "StartFrameCount": 1848508, + "EndFrameCount": 2017933, + "StartingPosition": "D", + "CurrentPosition": "D" + }, + { + "PlayerID": 12314, + "FirstName": "Away Player", + "LastName": "Four", + "JerseyNo": 9, + "StartFrameCount": 1848508, + "EndFrameCount": 2017933, + "StartingPosition": "A", + "CurrentPosition": "A" + }, + { + "PlayerID": 12352, + "FirstName": "Away Player", + "LastName": "Five", + "JerseyNo": 17, + "StartFrameCount": 1848508, + "EndFrameCount": 2011156, + "StartingPosition": "M", + "CurrentPosition": "O" + }, + { + "PlayerID": 12535, + "FirstName": "Away Player", + "LastName": "Six", + "JerseyNo": 19, + "StartFrameCount": 1848508, + "EndFrameCount": 2017933, + "StartingPosition": "D", + "CurrentPosition": "D" + }, + { + "PlayerID": 11979, + "FirstName": "Away Player", + "LastName": "Seven", + "JerseyNo": 22, + "StartFrameCount": 1848508, + "EndFrameCount": 1941284, + "StartingPosition": "M", + "CurrentPosition": "O" + }, + { + "PlayerID": 12128, + "FirstName": "Away Player", + "LastName": "Eight", + "JerseyNo": 24, + "StartFrameCount": 1848508, + "EndFrameCount": 2017933, + "StartingPosition": "M", + "CurrentPosition": "M" + }, + { + "PlayerID": 13281, + "FirstName": "Away Player", + "LastName": "Nine", + "JerseyNo": 26, + "StartFrameCount": 1848508, + "EndFrameCount": 1984022, + "StartingPosition": "M", + "CurrentPosition": "O" + }, + { + "PlayerID": 9080, + "FirstName": "Away Player", + "LastName": "Ten", + "JerseyNo": 27, + "StartFrameCount": 1848508, + "EndFrameCount": 2017933, + "StartingPosition": "D", + "CurrentPosition": "D" + }, + { + "PlayerID": 12604, + "FirstName": "Away Player", + "LastName": "Eleven", + "JerseyNo": 33, + "StartFrameCount": 1848508, + "EndFrameCount": 1940954, + "StartingPosition": "M", + "CurrentPosition": "O" + }, + { + "PlayerID": 12360, + "FirstName": "Away Player", + "LastName": "Twelve", + "JerseyNo": 35, + "StartFrameCount": 0, + "EndFrameCount": 0, + "StartingPosition": "S", + "CurrentPosition": "S" + }, + { + "PlayerID": 12282, + "FirstName": "Away Player", + "LastName": "Thirteen", + "JerseyNo": 2, + "StartFrameCount": 1940954, + "EndFrameCount": 2017933, + "StartingPosition": "S", + "CurrentPosition": "D" + }, + { + "PlayerID": 13283, + "FirstName": "Away Player", + "LastName": "Fourteen", + "JerseyNo": 3, + "StartFrameCount": 0, + "EndFrameCount": 0, + "StartingPosition": "S", + "CurrentPosition": "S" + }, + { + "PlayerID": 13276, + "FirstName": "Away Player", + "LastName": "Fifteen", + "JerseyNo": 7, + "StartFrameCount": 0, + "EndFrameCount": 0, + "StartingPosition": "S", + "CurrentPosition": "S" + }, + { + "PlayerID": 13142, + "FirstName": "Away Player", + "LastName": "Sixteen", + "JerseyNo": 21, + "StartFrameCount": 2011156, + "EndFrameCount": 2017933, + "StartingPosition": "S", + "CurrentPosition": "M" + }, + { + "PlayerID": 13307, + "FirstName": "Away Player", + "LastName": "Seventeen", + "JerseyNo": 23, + "StartFrameCount": 1984022, + "EndFrameCount": 2017933, + "StartingPosition": "S", + "CurrentPosition": "M" + }, + { + "PlayerID": 12595, + "FirstName": "Away Player", + "LastName": "Eighteen", + "JerseyNo": 25, + "StartFrameCount": 0, + "EndFrameCount": 0, + "StartingPosition": "S", + "CurrentPosition": "S" + }, + { + "PlayerID": 9654, + "FirstName": "Away Player", + "LastName": "Nineteen", + "JerseyNo": 28, + "StartFrameCount": 1941284, + "EndFrameCount": 2017933, + "StartingPosition": "S", + "CurrentPosition": "M" + }, + { + "PlayerID": 12636, + "FirstName": "Away Player", + "LastName": "Twenty", + "JerseyNo": 31, + "StartFrameCount": 0, + "EndFrameCount": 0, + "StartingPosition": "S", + "CurrentPosition": "S" + } + ] + }, + "Referees": [ + { + "PlayerID": 0, + "FirstName": "Referee", + "LastName": "Main", + "JerseyNo": 1, + "StartFrameCount": 1848508, + "EndFrameCount": 2017933 + }, + { + "PlayerID": 9999991, + "FirstName": "Referee", + "LastName": "Head", + "JerseyNo": 0, + "StartFrameCount": 1848508, + "EndFrameCount": 2017933 + }, + { + "PlayerID": 9999993, + "FirstName": "Referee", + "LastName": "2nd Assistant", + "JerseyNo": 2, + "StartFrameCount": 1848508, + "EndFrameCount": 2017933 + } + ], + "Kickoff": "2023-12-15 20:32:20" +} \ No newline at end of file diff --git a/kloppy/tests/files/tracab_raw.json b/kloppy/tests/files/tracab_raw.json new file mode 100644 index 00000000..249656e5 --- /dev/null +++ b/kloppy/tests/files/tracab_raw.json @@ -0,0 +1,3934 @@ +{ + "FrameData": [ + { + "FrameCount": 1848508, + "GameRunning": 1, + "Phase": 2, + "PlayerPositions": [ + { + "Team": 0, + "JerseyNumber": 9, + "X": 6, + "Y": -913, + "Speed": 1.02 + }, + { + "Team": 0, + "JerseyNumber": 19, + "X": -1947, + "Y": 2020, + "Speed": 0.46 + }, + { + "Team": 1, + "JerseyNumber": 20, + "X": 256, + "Y": -958, + "Speed": 0.09 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 55, + "X": 1738, + "Y": -2548, + "Speed": 0.51 + }, + { + "Team": 1, + "JerseyNumber": 5, + "X": 2221, + "Y": -603, + "Speed": 0.28 + }, + { + "Team": 0, + "JerseyNumber": 12, + "X": -4722, + "Y": 28, + "Speed": 0.38 + }, + { + "Team": 1, + "JerseyNumber": 13, + "X": 1580, + "Y": 2284, + "Speed": 0.42 + }, + { + "Team": 0, + "JerseyNumber": 17, + "X": -777, + "Y": 759, + "Speed": 0.56 + }, + { + "Team": 0, + "JerseyNumber": 5, + "X": -2123, + "Y": 639, + "Speed": 0.1 + }, + { + "Team": 1, + "JerseyNumber": 1, + "X": 5270, + "Y": 27, + "Speed": 0.15 + }, + { + "Team": 1, + "JerseyNumber": 7, + "X": 18, + "Y": 3182, + "Speed": 0.45 + }, + { + "Team": 1, + "JerseyNumber": 2, + "X": 2242, + "Y": 815, + "Speed": 0.33 + }, + { + "Team": 1, + "JerseyNumber": 8, + "X": 866, + "Y": -108, + "Speed": 0.24 + }, + { + "Team": 0, + "JerseyNumber": 33, + "X": -926, + "Y": -31, + "Speed": 0.13 + }, + { + "Team": 1, + "JerseyNumber": 16, + "X": -46, + "Y": 7, + "Speed": 0.27 + }, + { + "Team": 0, + "JerseyNumber": 4, + "X": -2008, + "Y": -633, + "Speed": 0.35 + }, + { + "Team": 0, + "JerseyNumber": 27, + "X": -1924, + "Y": -2321, + "Speed": 0.32 + }, + { + "Team": 1, + "JerseyNumber": 90, + "X": 43, + "Y": 1066, + "Speed": 0.32 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 77, + "X": 53, + "Y": -3165, + "Speed": 0.32 + }, + { + "Team": 0, + "JerseyNumber": 26, + "X": -667, + "Y": -726, + "Speed": 0.92 + }, + { + "Team": 0, + "JerseyNumber": 24, + "X": -404, + "Y": -2581, + "Speed": 0.2 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 22, + "X": -364, + "Y": 1432, + "Speed": 0.48 + } + ], + "BallPosition": [ + { + "BallContactInfo": "SetHome", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 11, + "X": 2710, + "Y": 3722, + "Z": 11 + }, + { + "BallContactInfo": "SetHome", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 11, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "SetHome", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 11, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "SetHome", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 11, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "SetHome", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 11, + "X": 5550, + "Y": 4400, + "Z": 0 + } + ] + }, + { + "FrameCount": 1848509, + "GameRunning": 1, + "Phase": 2, + "PlayerPositions": [ + { + "Team": 0, + "JerseyNumber": 9, + "X": 14, + "Y": -905, + "Speed": 1.33 + }, + { + "Team": 0, + "JerseyNumber": 19, + "X": -1946, + "Y": 2023, + "Speed": 0.47 + }, + { + "Team": 1, + "JerseyNumber": 20, + "X": 256, + "Y": -957, + "Speed": 0.13 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 55, + "X": 1736, + "Y": -2552, + "Speed": 0.5 + }, + { + "Team": 1, + "JerseyNumber": 5, + "X": 2221, + "Y": -602, + "Speed": 0.28 + }, + { + "Team": 0, + "JerseyNumber": 12, + "X": -4722, + "Y": 28, + "Speed": 0.35 + }, + { + "Team": 1, + "JerseyNumber": 13, + "X": 1581, + "Y": 2284, + "Speed": 0.42 + }, + { + "Team": 0, + "JerseyNumber": 17, + "X": -775, + "Y": 761, + "Speed": 0.62 + }, + { + "Team": 0, + "JerseyNumber": 5, + "X": -2123, + "Y": 641, + "Speed": 0.13 + }, + { + "Team": 1, + "JerseyNumber": 1, + "X": 5270, + "Y": 28, + "Speed": 0.15 + }, + { + "Team": 1, + "JerseyNumber": 7, + "X": 14, + "Y": 3186, + "Speed": 0.49 + }, + { + "Team": 1, + "JerseyNumber": 2, + "X": 2244, + "Y": 813, + "Speed": 0.33 + }, + { + "Team": 1, + "JerseyNumber": 8, + "X": 865, + "Y": -107, + "Speed": 0.24 + }, + { + "Team": 0, + "JerseyNumber": 33, + "X": -926, + "Y": -30, + "Speed": 0.15 + }, + { + "Team": 1, + "JerseyNumber": 16, + "X": -41, + "Y": 9, + "Speed": 0.27 + }, + { + "Team": 0, + "JerseyNumber": 4, + "X": -2005, + "Y": -633, + "Speed": 0.35 + }, + { + "Team": 0, + "JerseyNumber": 27, + "X": -1923, + "Y": -2323, + "Speed": 0.35 + }, + { + "Team": 1, + "JerseyNumber": 90, + "X": 41, + "Y": 1066, + "Speed": 0.37 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 77, + "X": 50, + "Y": -3164, + "Speed": 0.38 + }, + { + "Team": 0, + "JerseyNumber": 26, + "X": -663, + "Y": -727, + "Speed": 0.96 + }, + { + "Team": 0, + "JerseyNumber": 24, + "X": -404, + "Y": -2579, + "Speed": 0.26 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 22, + "X": -366, + "Y": 1434, + "Speed": 0.51 + } + ], + "BallPosition": [ + { + "BallContactInfo": "SetHome", + "BallOwningTeam": "H", + "BallStatus": "Alive", + "Speed": 11, + "X": 2710, + "Y": 3722, + "Z": 11 + }, + { + "BallContactInfo": "SetHome", + "BallOwningTeam": "H", + "BallStatus": "Alive", + "Speed": 11, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "SetHome", + "BallOwningTeam": "H", + "BallStatus": "Alive", + "Speed": 11, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "SetHome", + "BallOwningTeam": "H", + "BallStatus": "Alive", + "Speed": 11, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "SetHome", + "BallOwningTeam": "H", + "BallStatus": "Alive", + "Speed": 11, + "X": 5550, + "Y": 4400, + "Z": 0 + } + ] + }, + { + "FrameCount": 1848510, + "GameRunning": 1, + "Phase": 2, + "PlayerPositions": [ + { + "Team": 0, + "JerseyNumber": 9, + "X": 24, + "Y": -897, + "Speed": 1.58 + }, + { + "Team": 0, + "JerseyNumber": 19, + "X": -1946, + "Y": 2026, + "Speed": 0.48 + }, + { + "Team": 1, + "JerseyNumber": 20, + "X": 258, + "Y": -957, + "Speed": 0.13 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 55, + "X": 1735, + "Y": -2555, + "Speed": 0.54 + }, + { + "Team": 1, + "JerseyNumber": 5, + "X": 2221, + "Y": -601, + "Speed": 0.28 + }, + { + "Team": 0, + "JerseyNumber": 12, + "X": -4722, + "Y": 29, + "Speed": 0.32 + }, + { + "Team": 1, + "JerseyNumber": 13, + "X": 1583, + "Y": 2284, + "Speed": 0.42 + }, + { + "Team": 0, + "JerseyNumber": 17, + "X": -771, + "Y": 763, + "Speed": 0.64 + }, + { + "Team": 0, + "JerseyNumber": 5, + "X": -2123, + "Y": 642, + "Speed": 0.08 + }, + { + "Team": 1, + "JerseyNumber": 1, + "X": 5270, + "Y": 30, + "Speed": 0.13 + }, + { + "Team": 1, + "JerseyNumber": 7, + "X": 9, + "Y": 3190, + "Speed": 0.62 + }, + { + "Team": 1, + "JerseyNumber": 2, + "X": 2247, + "Y": 811, + "Speed": 0.36 + }, + { + "Team": 1, + "JerseyNumber": 8, + "X": 864, + "Y": -106, + "Speed": 0.24 + }, + { + "Team": 0, + "JerseyNumber": 33, + "X": -926, + "Y": -28, + "Speed": 0.15 + }, + { + "Team": 1, + "JerseyNumber": 16, + "X": -39, + "Y": 11, + "Speed": 0.3 + }, + { + "Team": 0, + "JerseyNumber": 4, + "X": -2002, + "Y": -631, + "Speed": 0.35 + }, + { + "Team": 0, + "JerseyNumber": 27, + "X": -1921, + "Y": -2325, + "Speed": 0.34 + }, + { + "Team": 1, + "JerseyNumber": 90, + "X": 39, + "Y": 1065, + "Speed": 0.4 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 77, + "X": 47, + "Y": -3160, + "Speed": 0.44 + }, + { + "Team": 0, + "JerseyNumber": 26, + "X": -658, + "Y": -727, + "Speed": 0.99 + }, + { + "Team": 0, + "JerseyNumber": 24, + "X": -404, + "Y": -2578, + "Speed": 0.29 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 22, + "X": -367, + "Y": 1437, + "Speed": 0.57 + } + ], + "BallPosition": [ + { + "BallContactInfo": "SetHome", + "BallOwningTeam": "H", + "BallStatus": "Alive", + "Speed": 11, + "X": 2710, + "Y": 3722, + "Z": 11 + }, + { + "BallContactInfo": "SetHome", + "BallOwningTeam": "H", + "BallStatus": "Alive", + "Speed": 11, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "SetHome", + "BallOwningTeam": "H", + "BallStatus": "Alive", + "Speed": 11, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "SetHome", + "BallOwningTeam": "H", + "BallStatus": "Alive", + "Speed": 11, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "SetHome", + "BallOwningTeam": "H", + "BallStatus": "Alive", + "Speed": 11, + "X": 5550, + "Y": 4400, + "Z": 0 + } + ] + }, + { + "FrameCount": 1916408, + "GameRunning": 1, + "Phase": 2, + "PlayerPositions": [ + { + "Team": 1, + "JerseyNumber": 2, + "X": -702, + "Y": 726, + "Speed": 1.8 + }, + { + "Team": 0, + "JerseyNumber": 5, + "X": -4476, + "Y": 411, + "Speed": 1.58 + }, + { + "Team": 1, + "JerseyNumber": 5, + "X": -497, + "Y": 222, + "Speed": 1.06 + }, + { + "Team": 0, + "JerseyNumber": 9, + "X": -1477, + "Y": 796, + "Speed": 1.29 + }, + { + "Team": 1, + "JerseyNumber": 13, + "X": -2980, + "Y": 1199, + "Speed": 1.46 + }, + { + "Team": 0, + "JerseyNumber": 17, + "X": -3596, + "Y": 430, + "Speed": 0.13 + }, + { + "Team": 0, + "JerseyNumber": 19, + "X": -3807, + "Y": 488, + "Speed": 1.18 + }, + { + "Team": 0, + "JerseyNumber": 33, + "X": -3245, + "Y": 1361, + "Speed": 1.45 + }, + { + "Team": 0, + "JerseyNumber": 24, + "X": -3102, + "Y": -716, + "Speed": 1.19 + }, + { + "Team": 1, + "JerseyNumber": 90, + "X": -3766, + "Y": 799, + "Speed": 1.42 + }, + { + "Team": 0, + "JerseyNumber": 22, + "X": -3721, + "Y": 1330, + "Speed": 1.63 + }, + { + "Team": 1, + "JerseyNumber": 1, + "X": 3968, + "Y": -182, + "Speed": 1.72 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": -4059, + "Y": -3008, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 7, + "X": -3327, + "Y": 1367, + "Speed": 1.67 + }, + { + "Team": 1, + "JerseyNumber": 8, + "X": -2218, + "Y": 876, + "Speed": 1.93 + }, + { + "Team": 0, + "JerseyNumber": 12, + "X": -5144, + "Y": 419, + "Speed": 0.83 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": 2510, + "Y": -3219, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 4, + "X": -4027, + "Y": 44, + "Speed": 1.26 + }, + { + "Team": 0, + "JerseyNumber": 27, + "X": -3655, + "Y": -689, + "Speed": 1.59 + }, + { + "Team": 1, + "JerseyNumber": 16, + "X": -3176, + "Y": -642, + "Speed": 1.24 + }, + { + "Team": 1, + "JerseyNumber": 55, + "X": -2167, + "Y": -1363, + "Speed": 1.48 + }, + { + "Team": 1, + "JerseyNumber": 20, + "X": -2822, + "Y": -268, + "Speed": 1.28 + }, + { + "Team": 0, + "JerseyNumber": 26, + "X": -3187, + "Y": 630, + "Speed": 1.34 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 77, + "X": -3343, + "Y": -326, + "Speed": 1.9 + } + ], + "BallPosition": [ + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 106.9, + "X": -4490, + "Y": 436, + "Z": 5 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 106.9, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 106.9, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 106.9, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 106.9, + "X": 5550, + "Y": 4400, + "Z": 0 + } + ] + }, + { + "FrameCount": 1916409, + "GameRunning": 0, + "Phase": 3, + "PlayerPositions": [ + { + "Team": 1, + "JerseyNumber": 2, + "X": -699, + "Y": 721, + "Speed": 1.77 + }, + { + "Team": 0, + "JerseyNumber": 5, + "X": -4473, + "Y": 407, + "Speed": 1.58 + }, + { + "Team": 1, + "JerseyNumber": 5, + "X": -501, + "Y": 225, + "Speed": 1.08 + }, + { + "Team": 0, + "JerseyNumber": 9, + "X": -1477, + "Y": 790, + "Speed": 1.31 + }, + { + "Team": 1, + "JerseyNumber": 13, + "X": -2977, + "Y": 1193, + "Speed": 1.46 + }, + { + "Team": 0, + "JerseyNumber": 17, + "X": -3596, + "Y": 430, + "Speed": 0.13 + }, + { + "Team": 0, + "JerseyNumber": 19, + "X": -3804, + "Y": 484, + "Speed": 1.18 + }, + { + "Team": 0, + "JerseyNumber": 33, + "X": -3243, + "Y": 1356, + "Speed": 1.47 + }, + { + "Team": 0, + "JerseyNumber": 24, + "X": -3097, + "Y": -717, + "Speed": 1.23 + }, + { + "Team": 1, + "JerseyNumber": 90, + "X": -3762, + "Y": 796, + "Speed": 1.4 + }, + { + "Team": 0, + "JerseyNumber": 22, + "X": -3720, + "Y": 1324, + "Speed": 1.62 + }, + { + "Team": 1, + "JerseyNumber": 1, + "X": 3974, + "Y": -184, + "Speed": 1.71 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": -4061, + "Y": -3001, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 7, + "X": -3323, + "Y": 1360, + "Speed": 1.7 + }, + { + "Team": 1, + "JerseyNumber": 8, + "X": -2216, + "Y": 869, + "Speed": 1.95 + }, + { + "Team": 0, + "JerseyNumber": 12, + "X": -5140, + "Y": 417, + "Speed": 0.83 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": 2516, + "Y": -3213, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 4, + "X": -4024, + "Y": 40, + "Speed": 1.23 + }, + { + "Team": 0, + "JerseyNumber": 27, + "X": -3650, + "Y": -693, + "Speed": 1.61 + }, + { + "Team": 1, + "JerseyNumber": 16, + "X": -3171, + "Y": -642, + "Speed": 1.23 + }, + { + "Team": 1, + "JerseyNumber": 55, + "X": -2161, + "Y": -1368, + "Speed": 1.53 + }, + { + "Team": 1, + "JerseyNumber": 20, + "X": -2819, + "Y": -273, + "Speed": 1.26 + }, + { + "Team": 0, + "JerseyNumber": 26, + "X": -3183, + "Y": 627, + "Speed": 1.31 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 77, + "X": -3338, + "Y": -331, + "Speed": 1.86 + } + ], + "BallPosition": [ + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 85.18, + "X": -4494, + "Y": 435, + "Z": 2 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 85.18, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 85.18, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 85.18, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 85.18, + "X": 5550, + "Y": 4400, + "Z": 0 + } + ] + }, + { + "FrameCount": 1916410, + "GameRunning": 0, + "Phase": 3, + "PlayerPositions": [ + { + "Team": 1, + "JerseyNumber": 2, + "X": -696, + "Y": 714, + "Speed": 1.77 + }, + { + "Team": 0, + "JerseyNumber": 5, + "X": -4470, + "Y": 404, + "Speed": 1.58 + }, + { + "Team": 1, + "JerseyNumber": 5, + "X": -503, + "Y": 228, + "Speed": 1.03 + }, + { + "Team": 0, + "JerseyNumber": 9, + "X": -1479, + "Y": 785, + "Speed": 1.3 + }, + { + "Team": 1, + "JerseyNumber": 13, + "X": -2975, + "Y": 1189, + "Speed": 1.41 + }, + { + "Team": 0, + "JerseyNumber": 17, + "X": -3596, + "Y": 430, + "Speed": 0.13 + }, + { + "Team": 0, + "JerseyNumber": 19, + "X": -3801, + "Y": 480, + "Speed": 1.18 + }, + { + "Team": 0, + "JerseyNumber": 33, + "X": -3242, + "Y": 1353, + "Speed": 1.47 + }, + { + "Team": 0, + "JerseyNumber": 24, + "X": -3092, + "Y": -715, + "Speed": 1.2 + }, + { + "Team": 1, + "JerseyNumber": 90, + "X": -3757, + "Y": 793, + "Speed": 1.4 + }, + { + "Team": 0, + "JerseyNumber": 22, + "X": -3717, + "Y": 1317, + "Speed": 1.64 + }, + { + "Team": 1, + "JerseyNumber": 1, + "X": 3980, + "Y": -186, + "Speed": 1.72 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": -4064, + "Y": -2993, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 7, + "X": -3320, + "Y": 1353, + "Speed": 1.7 + }, + { + "Team": 1, + "JerseyNumber": 8, + "X": -2212, + "Y": 862, + "Speed": 1.95 + }, + { + "Team": 0, + "JerseyNumber": 12, + "X": -5138, + "Y": 416, + "Speed": 0.83 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": 2522, + "Y": -3208, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 4, + "X": -4019, + "Y": 36, + "Speed": 1.23 + }, + { + "Team": 0, + "JerseyNumber": 27, + "X": -3644, + "Y": -696, + "Speed": 1.63 + }, + { + "Team": 1, + "JerseyNumber": 16, + "X": -3167, + "Y": -642, + "Speed": 1.22 + }, + { + "Team": 1, + "JerseyNumber": 55, + "X": -2158, + "Y": -1371, + "Speed": 1.53 + }, + { + "Team": 1, + "JerseyNumber": 20, + "X": -2816, + "Y": -277, + "Speed": 1.26 + }, + { + "Team": 0, + "JerseyNumber": 26, + "X": -3179, + "Y": 622, + "Speed": 1.31 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 77, + "X": -3332, + "Y": -334, + "Speed": 1.83 + } + ], + "BallPosition": [ + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 78.03, + "X": -4498, + "Y": 440, + "Z": 1 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 78.03, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 78.03, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 78.03, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 78.03, + "X": 5550, + "Y": 4400, + "Z": 0 + } + ] + }, + { + "FrameCount": 1916411, + "GameRunning": 0, + "Phase": 3, + "PlayerPositions": [ + { + "Team": 1, + "JerseyNumber": 2, + "X": -693, + "Y": 708, + "Speed": 1.75 + }, + { + "Team": 0, + "JerseyNumber": 5, + "X": -4467, + "Y": 400, + "Speed": 1.56 + }, + { + "Team": 1, + "JerseyNumber": 5, + "X": -506, + "Y": 231, + "Speed": 1.04 + }, + { + "Team": 0, + "JerseyNumber": 9, + "X": -1479, + "Y": 779, + "Speed": 1.31 + }, + { + "Team": 1, + "JerseyNumber": 13, + "X": -2971, + "Y": 1184, + "Speed": 1.46 + }, + { + "Team": 0, + "JerseyNumber": 17, + "X": -3595, + "Y": 431, + "Speed": 0.1 + }, + { + "Team": 0, + "JerseyNumber": 19, + "X": -3797, + "Y": 477, + "Speed": 1.18 + }, + { + "Team": 0, + "JerseyNumber": 33, + "X": -3241, + "Y": 1350, + "Speed": 1.47 + }, + { + "Team": 0, + "JerseyNumber": 24, + "X": -3088, + "Y": -715, + "Speed": 1.2 + }, + { + "Team": 1, + "JerseyNumber": 90, + "X": -3753, + "Y": 791, + "Speed": 1.38 + }, + { + "Team": 0, + "JerseyNumber": 22, + "X": -3715, + "Y": 1310, + "Speed": 1.64 + }, + { + "Team": 1, + "JerseyNumber": 1, + "X": 3986, + "Y": -187, + "Speed": 1.71 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": -4065, + "Y": -2986, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 7, + "X": -3318, + "Y": 1346, + "Speed": 1.72 + }, + { + "Team": 1, + "JerseyNumber": 8, + "X": -2210, + "Y": 854, + "Speed": 1.93 + }, + { + "Team": 0, + "JerseyNumber": 12, + "X": -5135, + "Y": 415, + "Speed": 0.83 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": 2529, + "Y": -3201, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 4, + "X": -4016, + "Y": 33, + "Speed": 1.21 + }, + { + "Team": 0, + "JerseyNumber": 27, + "X": -3639, + "Y": -700, + "Speed": 1.64 + }, + { + "Team": 1, + "JerseyNumber": 16, + "X": -3160, + "Y": -642, + "Speed": 1.22 + }, + { + "Team": 1, + "JerseyNumber": 55, + "X": -2153, + "Y": -1375, + "Speed": 1.52 + }, + { + "Team": 1, + "JerseyNumber": 20, + "X": -2813, + "Y": -282, + "Speed": 1.29 + }, + { + "Team": 0, + "JerseyNumber": 26, + "X": -3175, + "Y": 618, + "Speed": 1.31 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 77, + "X": -3326, + "Y": -339, + "Speed": 1.83 + } + ], + "BallPosition": [ + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 41.64, + "X": -4494, + "Y": 436, + "Z": 2 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 41.64, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 41.64, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 41.64, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 41.64, + "X": 5550, + "Y": 4400, + "Z": 0 + } + ] + }, + { + "FrameCount": 1916412, + "GameRunning": 0, + "Phase": 3, + "PlayerPositions": [ + { + "Team": 1, + "JerseyNumber": 2, + "X": -690, + "Y": 701, + "Speed": 1.72 + }, + { + "Team": 0, + "JerseyNumber": 5, + "X": -4463, + "Y": 395, + "Speed": 1.53 + }, + { + "Team": 1, + "JerseyNumber": 5, + "X": -509, + "Y": 234, + "Speed": 1.04 + }, + { + "Team": 0, + "JerseyNumber": 9, + "X": -1479, + "Y": 772, + "Speed": 1.31 + }, + { + "Team": 1, + "JerseyNumber": 13, + "X": -2968, + "Y": 1179, + "Speed": 1.46 + }, + { + "Team": 0, + "JerseyNumber": 17, + "X": -3594, + "Y": 430, + "Speed": 0.13 + }, + { + "Team": 0, + "JerseyNumber": 19, + "X": -3794, + "Y": 473, + "Speed": 1.21 + }, + { + "Team": 0, + "JerseyNumber": 33, + "X": -3240, + "Y": 1348, + "Speed": 1.43 + }, + { + "Team": 0, + "JerseyNumber": 24, + "X": -3084, + "Y": -715, + "Speed": 1.2 + }, + { + "Team": 1, + "JerseyNumber": 90, + "X": -3748, + "Y": 788, + "Speed": 1.36 + }, + { + "Team": 0, + "JerseyNumber": 22, + "X": -3712, + "Y": 1302, + "Speed": 1.64 + }, + { + "Team": 1, + "JerseyNumber": 1, + "X": 3994, + "Y": -189, + "Speed": 1.71 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": -4067, + "Y": -2979, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 7, + "X": -3314, + "Y": 1340, + "Speed": 1.72 + }, + { + "Team": 1, + "JerseyNumber": 8, + "X": -2207, + "Y": 847, + "Speed": 1.93 + }, + { + "Team": 0, + "JerseyNumber": 12, + "X": -5133, + "Y": 413, + "Speed": 0.83 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": 2534, + "Y": -3197, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 4, + "X": -4012, + "Y": 27, + "Speed": 1.21 + }, + { + "Team": 0, + "JerseyNumber": 27, + "X": -3634, + "Y": -704, + "Speed": 1.64 + }, + { + "Team": 1, + "JerseyNumber": 16, + "X": -3155, + "Y": -642, + "Speed": 1.22 + }, + { + "Team": 1, + "JerseyNumber": 55, + "X": -2149, + "Y": -1379, + "Speed": 1.49 + }, + { + "Team": 1, + "JerseyNumber": 20, + "X": -2809, + "Y": -288, + "Speed": 1.31 + }, + { + "Team": 0, + "JerseyNumber": 26, + "X": -3171, + "Y": 612, + "Speed": 1.31 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 77, + "X": -3320, + "Y": -342, + "Speed": 1.78 + } + ], + "BallPosition": [ + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 29.98, + "X": -4492, + "Y": 433, + "Z": 3 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 29.98, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 29.98, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 29.98, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 29.98, + "X": 5550, + "Y": 4400, + "Z": 0 + } + ] + }, + { + "FrameCount": 1916413, + "GameRunning": 0, + "Phase": 3, + "PlayerPositions": [ + { + "Team": 1, + "JerseyNumber": 2, + "X": -687, + "Y": 695, + "Speed": 1.71 + }, + { + "Team": 0, + "JerseyNumber": 5, + "X": -4460, + "Y": 391, + "Speed": 1.53 + }, + { + "Team": 1, + "JerseyNumber": 5, + "X": -513, + "Y": 236, + "Speed": 1.02 + }, + { + "Team": 0, + "JerseyNumber": 9, + "X": -1480, + "Y": 766, + "Speed": 1.31 + }, + { + "Team": 1, + "JerseyNumber": 13, + "X": -2965, + "Y": 1174, + "Speed": 1.46 + }, + { + "Team": 0, + "JerseyNumber": 17, + "X": -3594, + "Y": 430, + "Speed": 0.13 + }, + { + "Team": 0, + "JerseyNumber": 19, + "X": -3791, + "Y": 469, + "Speed": 1.18 + }, + { + "Team": 0, + "JerseyNumber": 33, + "X": -3239, + "Y": 1346, + "Speed": 1.41 + }, + { + "Team": 0, + "JerseyNumber": 24, + "X": -3079, + "Y": -715, + "Speed": 1.19 + }, + { + "Team": 1, + "JerseyNumber": 90, + "X": -3744, + "Y": 785, + "Speed": 1.38 + }, + { + "Team": 0, + "JerseyNumber": 22, + "X": -3709, + "Y": 1295, + "Speed": 1.65 + }, + { + "Team": 1, + "JerseyNumber": 1, + "X": 3999, + "Y": -191, + "Speed": 1.68 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": -4068, + "Y": -2972, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 7, + "X": -3312, + "Y": 1332, + "Speed": 1.73 + }, + { + "Team": 1, + "JerseyNumber": 8, + "X": -2204, + "Y": 841, + "Speed": 1.91 + }, + { + "Team": 0, + "JerseyNumber": 12, + "X": -5129, + "Y": 411, + "Speed": 0.8 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": 2540, + "Y": -3192, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 4, + "X": -4007, + "Y": 22, + "Speed": 1.23 + }, + { + "Team": 0, + "JerseyNumber": 27, + "X": -3629, + "Y": -708, + "Speed": 1.64 + }, + { + "Team": 1, + "JerseyNumber": 16, + "X": -3147, + "Y": -646, + "Speed": 1.26 + }, + { + "Team": 1, + "JerseyNumber": 55, + "X": -2143, + "Y": -1382, + "Speed": 1.52 + }, + { + "Team": 1, + "JerseyNumber": 20, + "X": -2806, + "Y": -293, + "Speed": 1.31 + }, + { + "Team": 0, + "JerseyNumber": 26, + "X": -3167, + "Y": 608, + "Speed": 1.31 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 77, + "X": -3313, + "Y": -346, + "Speed": 1.78 + } + ], + "BallPosition": [ + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 114.88, + "X": -4489, + "Y": 429, + "Z": 4 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 114.88, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 114.88, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 114.88, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 114.88, + "X": 5550, + "Y": 4400, + "Z": 0 + } + ] + }, + { + "FrameCount": 1916414, + "GameRunning": 0, + "Phase": 3, + "PlayerPositions": [ + { + "Team": 1, + "JerseyNumber": 2, + "X": -685, + "Y": 688, + "Speed": 1.71 + }, + { + "Team": 0, + "JerseyNumber": 5, + "X": -4456, + "Y": 385, + "Speed": 1.56 + }, + { + "Team": 1, + "JerseyNumber": 5, + "X": -516, + "Y": 239, + "Speed": 1.02 + }, + { + "Team": 0, + "JerseyNumber": 9, + "X": -1481, + "Y": 760, + "Speed": 1.3 + }, + { + "Team": 1, + "JerseyNumber": 13, + "X": -2961, + "Y": 1169, + "Speed": 1.46 + }, + { + "Team": 0, + "JerseyNumber": 17, + "X": -3593, + "Y": 430, + "Speed": 0.1 + }, + { + "Team": 0, + "JerseyNumber": 19, + "X": -3787, + "Y": 464, + "Speed": 1.23 + }, + { + "Team": 0, + "JerseyNumber": 33, + "X": -3237, + "Y": 1344, + "Speed": 1.4 + }, + { + "Team": 0, + "JerseyNumber": 24, + "X": -3075, + "Y": -715, + "Speed": 1.19 + }, + { + "Team": 1, + "JerseyNumber": 90, + "X": -3740, + "Y": 781, + "Speed": 1.38 + }, + { + "Team": 0, + "JerseyNumber": 22, + "X": -3706, + "Y": 1288, + "Speed": 1.65 + }, + { + "Team": 1, + "JerseyNumber": 1, + "X": 4005, + "Y": -194, + "Speed": 1.68 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": -4069, + "Y": -2965, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 7, + "X": -3310, + "Y": 1325, + "Speed": 1.75 + }, + { + "Team": 1, + "JerseyNumber": 8, + "X": -2202, + "Y": 834, + "Speed": 1.9 + }, + { + "Team": 0, + "JerseyNumber": 12, + "X": -5125, + "Y": 407, + "Speed": 0.82 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": 2545, + "Y": -3186, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 4, + "X": -4004, + "Y": 18, + "Speed": 1.23 + }, + { + "Team": 0, + "JerseyNumber": 27, + "X": -3623, + "Y": -712, + "Speed": 1.66 + }, + { + "Team": 1, + "JerseyNumber": 16, + "X": -3140, + "Y": -647, + "Speed": 1.29 + }, + { + "Team": 1, + "JerseyNumber": 55, + "X": -2140, + "Y": -1385, + "Speed": 1.49 + }, + { + "Team": 1, + "JerseyNumber": 20, + "X": -2804, + "Y": -298, + "Speed": 1.34 + }, + { + "Team": 0, + "JerseyNumber": 26, + "X": -3163, + "Y": 604, + "Speed": 1.31 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 77, + "X": -3308, + "Y": -349, + "Speed": 1.74 + } + ], + "BallPosition": [ + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 121.28, + "X": -4486, + "Y": 425, + "Z": 4 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 121.28, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 121.28, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 121.28, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 121.28, + "X": 5550, + "Y": 4400, + "Z": 0 + } + ] + }, + { + "FrameCount": 1916415, + "GameRunning": 0, + "Phase": 3, + "PlayerPositions": [ + { + "Team": 1, + "JerseyNumber": 2, + "X": -683, + "Y": 682, + "Speed": 1.68 + }, + { + "Team": 0, + "JerseyNumber": 5, + "X": -4451, + "Y": 381, + "Speed": 1.55 + }, + { + "Team": 1, + "JerseyNumber": 5, + "X": -519, + "Y": 242, + "Speed": 1.04 + }, + { + "Team": 0, + "JerseyNumber": 9, + "X": -1481, + "Y": 754, + "Speed": 1.3 + }, + { + "Team": 1, + "JerseyNumber": 13, + "X": -2958, + "Y": 1164, + "Speed": 1.46 + }, + { + "Team": 0, + "JerseyNumber": 17, + "X": -3593, + "Y": 431, + "Speed": 0.06 + }, + { + "Team": 0, + "JerseyNumber": 19, + "X": -3784, + "Y": 459, + "Speed": 1.23 + }, + { + "Team": 0, + "JerseyNumber": 33, + "X": -3236, + "Y": 1341, + "Speed": 1.28 + }, + { + "Team": 0, + "JerseyNumber": 24, + "X": -3069, + "Y": -714, + "Speed": 1.23 + }, + { + "Team": 1, + "JerseyNumber": 90, + "X": -3734, + "Y": 778, + "Speed": 1.4 + }, + { + "Team": 0, + "JerseyNumber": 22, + "X": -3704, + "Y": 1282, + "Speed": 1.64 + }, + { + "Team": 1, + "JerseyNumber": 1, + "X": 4012, + "Y": -195, + "Speed": 1.68 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": -4070, + "Y": -2957, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 7, + "X": -3308, + "Y": 1317, + "Speed": 1.78 + }, + { + "Team": 1, + "JerseyNumber": 8, + "X": -2200, + "Y": 828, + "Speed": 1.88 + }, + { + "Team": 0, + "JerseyNumber": 12, + "X": -5122, + "Y": 406, + "Speed": 0.82 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": 2550, + "Y": -3181, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 4, + "X": -4000, + "Y": 13, + "Speed": 1.26 + }, + { + "Team": 0, + "JerseyNumber": 27, + "X": -3616, + "Y": -716, + "Speed": 1.66 + }, + { + "Team": 1, + "JerseyNumber": 16, + "X": -3134, + "Y": -649, + "Speed": 1.33 + }, + { + "Team": 1, + "JerseyNumber": 55, + "X": -2136, + "Y": -1389, + "Speed": 1.46 + }, + { + "Team": 1, + "JerseyNumber": 20, + "X": -2801, + "Y": -304, + "Speed": 1.36 + }, + { + "Team": 0, + "JerseyNumber": 26, + "X": -3159, + "Y": 599, + "Speed": 1.33 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 77, + "X": -3302, + "Y": -352, + "Speed": 1.71 + } + ], + "BallPosition": [ + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 360.73, + "X": -4460, + "Y": 403, + "Z": 2 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 360.73, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 360.73, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 360.73, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 360.73, + "X": 5550, + "Y": 4400, + "Z": 0 + } + ] + }, + { + "FrameCount": 1916416, + "GameRunning": 0, + "Phase": 3, + "PlayerPositions": [ + { + "Team": 1, + "JerseyNumber": 2, + "X": -679, + "Y": 676, + "Speed": 1.7 + }, + { + "Team": 0, + "JerseyNumber": 5, + "X": -4448, + "Y": 377, + "Speed": 1.55 + }, + { + "Team": 1, + "JerseyNumber": 5, + "X": -522, + "Y": 244, + "Speed": 0.99 + }, + { + "Team": 0, + "JerseyNumber": 9, + "X": -1482, + "Y": 748, + "Speed": 1.33 + }, + { + "Team": 1, + "JerseyNumber": 13, + "X": -2955, + "Y": 1159, + "Speed": 1.49 + }, + { + "Team": 0, + "JerseyNumber": 17, + "X": -3592, + "Y": 430, + "Speed": 0.07 + }, + { + "Team": 0, + "JerseyNumber": 19, + "X": -3781, + "Y": 456, + "Speed": 1.26 + }, + { + "Team": 0, + "JerseyNumber": 33, + "X": -3233, + "Y": 1337, + "Speed": 1.26 + }, + { + "Team": 0, + "JerseyNumber": 24, + "X": -3065, + "Y": -714, + "Speed": 1.19 + }, + { + "Team": 1, + "JerseyNumber": 90, + "X": -3730, + "Y": 774, + "Speed": 1.42 + }, + { + "Team": 0, + "JerseyNumber": 22, + "X": -3701, + "Y": 1275, + "Speed": 1.67 + }, + { + "Team": 1, + "JerseyNumber": 1, + "X": 4018, + "Y": -198, + "Speed": 1.69 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": -4070, + "Y": -2949, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 7, + "X": -3307, + "Y": 1310, + "Speed": 1.77 + }, + { + "Team": 1, + "JerseyNumber": 8, + "X": -2198, + "Y": 821, + "Speed": 1.87 + }, + { + "Team": 0, + "JerseyNumber": 12, + "X": -5118, + "Y": 404, + "Speed": 0.84 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": 2555, + "Y": -3175, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 4, + "X": -3997, + "Y": 9, + "Speed": 1.28 + }, + { + "Team": 0, + "JerseyNumber": 27, + "X": -3610, + "Y": -719, + "Speed": 1.68 + }, + { + "Team": 1, + "JerseyNumber": 16, + "X": -3127, + "Y": -652, + "Speed": 1.38 + }, + { + "Team": 1, + "JerseyNumber": 55, + "X": -2131, + "Y": -1392, + "Speed": 1.46 + }, + { + "Team": 1, + "JerseyNumber": 20, + "X": -2798, + "Y": -309, + "Speed": 1.39 + }, + { + "Team": 0, + "JerseyNumber": 26, + "X": -3156, + "Y": 595, + "Speed": 1.33 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 77, + "X": -3296, + "Y": -356, + "Speed": 1.71 + } + ], + "BallPosition": [ + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 437.01, + "X": -4452, + "Y": 392, + "Z": 4 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 437.01, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 437.01, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 437.01, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 437.01, + "X": 5550, + "Y": 4400, + "Z": 0 + } + ] + }, + { + "FrameCount": 1916417, + "GameRunning": 0, + "Phase": 3, + "PlayerPositions": [ + { + "Team": 1, + "JerseyNumber": 2, + "X": -677, + "Y": 670, + "Speed": 1.68 + }, + { + "Team": 0, + "JerseyNumber": 5, + "X": -4443, + "Y": 372, + "Speed": 1.53 + }, + { + "Team": 1, + "JerseyNumber": 5, + "X": -524, + "Y": 246, + "Speed": 0.97 + }, + { + "Team": 0, + "JerseyNumber": 9, + "X": -1482, + "Y": 742, + "Speed": 1.36 + }, + { + "Team": 1, + "JerseyNumber": 13, + "X": -2951, + "Y": 1154, + "Speed": 1.49 + }, + { + "Team": 0, + "JerseyNumber": 17, + "X": -3591, + "Y": 430, + "Speed": 0.09 + }, + { + "Team": 0, + "JerseyNumber": 19, + "X": -3777, + "Y": 451, + "Speed": 1.26 + }, + { + "Team": 0, + "JerseyNumber": 33, + "X": -3233, + "Y": 1334, + "Speed": 1.14 + }, + { + "Team": 0, + "JerseyNumber": 24, + "X": -3059, + "Y": -715, + "Speed": 1.23 + }, + { + "Team": 1, + "JerseyNumber": 90, + "X": -3725, + "Y": 770, + "Speed": 1.42 + }, + { + "Team": 0, + "JerseyNumber": 22, + "X": -3699, + "Y": 1269, + "Speed": 1.64 + }, + { + "Team": 1, + "JerseyNumber": 1, + "X": 4025, + "Y": -201, + "Speed": 1.69 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": -4071, + "Y": -2942, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 7, + "X": -3304, + "Y": 1302, + "Speed": 1.78 + }, + { + "Team": 1, + "JerseyNumber": 8, + "X": -2196, + "Y": 815, + "Speed": 1.87 + }, + { + "Team": 0, + "JerseyNumber": 12, + "X": -5116, + "Y": 401, + "Speed": 0.84 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": 2561, + "Y": -3171, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 4, + "X": -3994, + "Y": 4, + "Speed": 1.31 + }, + { + "Team": 0, + "JerseyNumber": 27, + "X": -3604, + "Y": -722, + "Speed": 1.66 + }, + { + "Team": 1, + "JerseyNumber": 16, + "X": -3119, + "Y": -656, + "Speed": 1.45 + }, + { + "Team": 1, + "JerseyNumber": 55, + "X": -2127, + "Y": -1396, + "Speed": 1.44 + }, + { + "Team": 1, + "JerseyNumber": 20, + "X": -2795, + "Y": -313, + "Speed": 1.39 + }, + { + "Team": 0, + "JerseyNumber": 26, + "X": -3151, + "Y": 591, + "Speed": 1.36 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 77, + "X": -3289, + "Y": -361, + "Speed": 1.73 + } + ], + "BallPosition": [ + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 378.66, + "X": -4452, + "Y": 394, + "Z": 2 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 378.66, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 378.66, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 378.66, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 378.66, + "X": 5550, + "Y": 4400, + "Z": 0 + } + ] + }, + { + "FrameCount": 1916418, + "GameRunning": 0, + "Phase": 3, + "PlayerPositions": [ + { + "Team": 1, + "JerseyNumber": 2, + "X": -675, + "Y": 664, + "Speed": 1.68 + }, + { + "Team": 0, + "JerseyNumber": 5, + "X": -4439, + "Y": 367, + "Speed": 1.55 + }, + { + "Team": 1, + "JerseyNumber": 5, + "X": -526, + "Y": 248, + "Speed": 0.94 + }, + { + "Team": 0, + "JerseyNumber": 9, + "X": -1482, + "Y": 737, + "Speed": 1.39 + }, + { + "Team": 1, + "JerseyNumber": 13, + "X": -2948, + "Y": 1148, + "Speed": 1.51 + }, + { + "Team": 0, + "JerseyNumber": 17, + "X": -3591, + "Y": 429, + "Speed": 0.09 + }, + { + "Team": 0, + "JerseyNumber": 19, + "X": -3775, + "Y": 448, + "Speed": 1.26 + }, + { + "Team": 0, + "JerseyNumber": 33, + "X": -3231, + "Y": 1330, + "Speed": 1.1 + }, + { + "Team": 0, + "JerseyNumber": 24, + "X": -3054, + "Y": -715, + "Speed": 1.23 + }, + { + "Team": 1, + "JerseyNumber": 90, + "X": -3720, + "Y": 766, + "Speed": 1.43 + }, + { + "Team": 0, + "JerseyNumber": 22, + "X": -3695, + "Y": 1263, + "Speed": 1.64 + }, + { + "Team": 1, + "JerseyNumber": 1, + "X": 4032, + "Y": -202, + "Speed": 1.72 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": -4071, + "Y": -2935, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 7, + "X": -3302, + "Y": 1295, + "Speed": 1.8 + }, + { + "Team": 1, + "JerseyNumber": 8, + "X": -2193, + "Y": 807, + "Speed": 1.88 + }, + { + "Team": 0, + "JerseyNumber": 12, + "X": -5113, + "Y": 400, + "Speed": 0.87 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": 2566, + "Y": -3165, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 4, + "X": -3990, + "Y": 0, + "Speed": 1.33 + }, + { + "Team": 0, + "JerseyNumber": 27, + "X": -3599, + "Y": -723, + "Speed": 1.66 + }, + { + "Team": 1, + "JerseyNumber": 16, + "X": -3112, + "Y": -659, + "Speed": 1.51 + }, + { + "Team": 1, + "JerseyNumber": 55, + "X": -2122, + "Y": -1398, + "Speed": 1.44 + }, + { + "Team": 1, + "JerseyNumber": 20, + "X": -2792, + "Y": -319, + "Speed": 1.42 + }, + { + "Team": 0, + "JerseyNumber": 26, + "X": -3147, + "Y": 587, + "Speed": 1.4 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 77, + "X": -3283, + "Y": -366, + "Speed": 1.76 + } + ], + "BallPosition": [ + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 151.74, + "X": -4456, + "Y": 385, + "Z": 8 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 151.74, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 151.74, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 151.74, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 151.74, + "X": 5550, + "Y": 4400, + "Z": 0 + } + ] + }, + { + "FrameCount": 1916419, + "GameRunning": 0, + "Phase": 3, + "PlayerPositions": [ + { + "Team": 1, + "JerseyNumber": 2, + "X": -672, + "Y": 657, + "Speed": 1.7 + }, + { + "Team": 0, + "JerseyNumber": 5, + "X": -4434, + "Y": 363, + "Speed": 1.53 + }, + { + "Team": 1, + "JerseyNumber": 5, + "X": -528, + "Y": 251, + "Speed": 0.94 + }, + { + "Team": 0, + "JerseyNumber": 9, + "X": -1482, + "Y": 730, + "Speed": 1.42 + }, + { + "Team": 1, + "JerseyNumber": 13, + "X": -2945, + "Y": 1144, + "Speed": 1.49 + }, + { + "Team": 0, + "JerseyNumber": 17, + "X": -3590, + "Y": 429, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 19, + "X": -3773, + "Y": 444, + "Speed": 1.26 + }, + { + "Team": 0, + "JerseyNumber": 33, + "X": -3231, + "Y": 1328, + "Speed": 0.99 + }, + { + "Team": 0, + "JerseyNumber": 24, + "X": -3048, + "Y": -715, + "Speed": 1.27 + }, + { + "Team": 1, + "JerseyNumber": 90, + "X": -3714, + "Y": 761, + "Speed": 1.45 + }, + { + "Team": 0, + "JerseyNumber": 22, + "X": -3693, + "Y": 1258, + "Speed": 1.64 + }, + { + "Team": 1, + "JerseyNumber": 1, + "X": 4037, + "Y": -206, + "Speed": 1.7 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": -4071, + "Y": -2928, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 7, + "X": -3300, + "Y": 1288, + "Speed": 1.82 + }, + { + "Team": 1, + "JerseyNumber": 8, + "X": -2190, + "Y": 800, + "Speed": 1.88 + }, + { + "Team": 0, + "JerseyNumber": 12, + "X": -5110, + "Y": 399, + "Speed": 0.86 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": 2573, + "Y": -3160, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 4, + "X": -3986, + "Y": -4, + "Speed": 1.33 + }, + { + "Team": 0, + "JerseyNumber": 27, + "X": -3592, + "Y": -725, + "Speed": 1.66 + }, + { + "Team": 1, + "JerseyNumber": 16, + "X": -3107, + "Y": -660, + "Speed": 1.54 + }, + { + "Team": 1, + "JerseyNumber": 55, + "X": -2118, + "Y": -1402, + "Speed": 1.44 + }, + { + "Team": 1, + "JerseyNumber": 20, + "X": -2788, + "Y": -324, + "Speed": 1.45 + }, + { + "Team": 0, + "JerseyNumber": 26, + "X": -3144, + "Y": 583, + "Speed": 1.43 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 77, + "X": -3277, + "Y": -370, + "Speed": 1.79 + } + ], + "BallPosition": [ + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 66.2, + "X": -4453, + "Y": 385, + "Z": 6 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 66.2, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 66.2, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 66.2, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 66.2, + "X": 5550, + "Y": 4400, + "Z": 0 + } + ] + }, + { + "FrameCount": 1916420, + "GameRunning": 0, + "Phase": 3, + "PlayerPositions": [ + { + "Team": 1, + "JerseyNumber": 2, + "X": -669, + "Y": 650, + "Speed": 1.7 + }, + { + "Team": 0, + "JerseyNumber": 5, + "X": -4430, + "Y": 358, + "Speed": 1.53 + }, + { + "Team": 1, + "JerseyNumber": 5, + "X": -531, + "Y": 253, + "Speed": 0.92 + }, + { + "Team": 0, + "JerseyNumber": 9, + "X": -1482, + "Y": 723, + "Speed": 1.45 + }, + { + "Team": 1, + "JerseyNumber": 13, + "X": -2940, + "Y": 1140, + "Speed": 1.51 + }, + { + "Team": 0, + "JerseyNumber": 17, + "X": -3590, + "Y": 429, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 19, + "X": -3770, + "Y": 440, + "Speed": 1.28 + }, + { + "Team": 0, + "JerseyNumber": 33, + "X": -3230, + "Y": 1324, + "Speed": 0.97 + }, + { + "Team": 0, + "JerseyNumber": 24, + "X": -3043, + "Y": -714, + "Speed": 1.31 + }, + { + "Team": 1, + "JerseyNumber": 90, + "X": -3710, + "Y": 759, + "Speed": 1.43 + }, + { + "Team": 0, + "JerseyNumber": 22, + "X": -3689, + "Y": 1252, + "Speed": 1.67 + }, + { + "Team": 1, + "JerseyNumber": 1, + "X": 4044, + "Y": -209, + "Speed": 1.7 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": -4071, + "Y": -2920, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 7, + "X": -3298, + "Y": 1282, + "Speed": 1.82 + }, + { + "Team": 1, + "JerseyNumber": 8, + "X": -2188, + "Y": 792, + "Speed": 1.9 + }, + { + "Team": 0, + "JerseyNumber": 12, + "X": -5107, + "Y": 397, + "Speed": 0.89 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": 2577, + "Y": -3156, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 4, + "X": -3983, + "Y": -8, + "Speed": 1.33 + }, + { + "Team": 0, + "JerseyNumber": 27, + "X": -3588, + "Y": -726, + "Speed": 1.63 + }, + { + "Team": 1, + "JerseyNumber": 16, + "X": -3101, + "Y": -661, + "Speed": 1.53 + }, + { + "Team": 1, + "JerseyNumber": 55, + "X": -2113, + "Y": -1404, + "Speed": 1.44 + }, + { + "Team": 1, + "JerseyNumber": 20, + "X": -2785, + "Y": -329, + "Speed": 1.47 + }, + { + "Team": 0, + "JerseyNumber": 26, + "X": -3140, + "Y": 579, + "Speed": 1.43 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 77, + "X": -3270, + "Y": -375, + "Speed": 1.84 + } + ], + "BallPosition": [ + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 176.34, + "X": -4447, + "Y": 374, + "Z": 6 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 176.34, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 176.34, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 176.34, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 176.34, + "X": 5550, + "Y": 4400, + "Z": 0 + } + ] + }, + { + "FrameCount": 2017933, + "GameRunning": 1, + "Phase": 3, + "PlayerPositions": [ + { + "Team": 1, + "JerseyNumber": 1, + "X": -3081, + "Y": -272, + "Speed": 1.45 + }, + { + "Team": 0, + "JerseyNumber": 23, + "X": 1461, + "Y": 268, + "Speed": 0.59 + }, + { + "Team": 1, + "JerseyNumber": 21, + "X": 2711, + "Y": 2210, + "Speed": 1.08 + }, + { + "Team": 0, + "JerseyNumber": 24, + "X": 1321, + "Y": 1321, + "Speed": 1.89 + }, + { + "Team": 1, + "JerseyNumber": 6, + "X": 1090, + "Y": -955, + "Speed": 0.98 + }, + { + "Team": 1, + "JerseyNumber": 27, + "X": 2060, + "Y": -2279, + "Speed": 1.29 + }, + { + "Team": 0, + "JerseyNumber": 5, + "X": 1956, + "Y": -420, + "Speed": 1.82 + }, + { + "Team": 0, + "JerseyNumber": 28, + "X": 1294, + "Y": 1030, + "Speed": 0.82 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": -1954, + "Y": -3164, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 13, + "X": -124, + "Y": -1318, + "Speed": 1.02 + }, + { + "Team": 1, + "JerseyNumber": 8, + "X": 881, + "Y": 734, + "Speed": 1.32 + }, + { + "Team": 0, + "JerseyNumber": 21, + "X": 2314, + "Y": -1471, + "Speed": 1.07 + }, + { + "Team": 0, + "JerseyNumber": 9, + "X": 390, + "Y": -194, + "Speed": 0.82 + }, + { + "Team": 0, + "JerseyNumber": 4, + "X": 2369, + "Y": 737, + "Speed": 1.5 + }, + { + "Team": 1, + "JerseyNumber": 77, + "X": 2681, + "Y": -1870, + "Speed": 0.16 + }, + { + "Team": 1, + "JerseyNumber": 18, + "X": 2680, + "Y": -260, + "Speed": 1.13 + }, + { + "Team": 1, + "JerseyNumber": 2, + "X": 2560, + "Y": 1766, + "Speed": 1.3 + }, + { + "Team": 0, + "JerseyNumber": 12, + "X": 4688, + "Y": 313, + "Speed": 1.59 + }, + { + "Team": 1, + "JerseyNumber": 16, + "X": 925, + "Y": -1402, + "Speed": 0.9 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": -1493, + "Y": -2956, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 2, + "X": 2002, + "Y": -230, + "Speed": 1.55 + }, + { + "Team": 1, + "JerseyNumber": 5, + "X": -18, + "Y": 579, + "Speed": 1.18 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": -2793, + "Y": -3303, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 19, + "X": 2458, + "Y": -1083, + "Speed": 1.49 + }, + { + "Team": 0, + "JerseyNumber": 27, + "X": 2624, + "Y": 1571, + "Speed": 1.36 + }, + { + "Team": -1, + "JerseyNumber": 0, + "X": -2402, + "Y": -3174, + "Speed": 0 + } + ], + "BallPosition": [ + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 275.15, + "X": 2600, + "Y": 1577, + "Z": 10 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 275.15, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 275.15, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 275.15, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "", + "BallOwningTeam": "H", + "BallStatus": "Dead", + "Speed": 275.15, + "X": 5550, + "Y": 4400, + "Z": 0 + } + ] + } + ], + "PackageID": "d27d5170-bab1-11ee-b39d-d10edbcdad1e", + "Filename": "TF10-55-2023-13495-1848508-2017931.json", + "GameID": "13495", + "CompetitionID": "55", + "SeasonID": "2023", + "PackageTime": "1706098339", + "NumberOfFrames": 169423, + "PitchShortSide": 680, + "PitchLongSide": 1050 +} \ No newline at end of file diff --git a/kloppy/tests/test_tracab.py b/kloppy/tests/test_tracab.py index a97d9d8f..02caf5e8 100644 --- a/kloppy/tests/test_tracab.py +++ b/kloppy/tests/test_tracab.py @@ -18,19 +18,122 @@ from kloppy import tracab -class TestTracabTracking: - @pytest.fixture - def meta_data(self, base_dir) -> str: - return base_dir / "files/tracab_meta.xml" +@pytest.fixture(scope="session") +def json_meta_data(base_dir: Path) -> Path: + return base_dir / "files" / "tracab_meta.json" - @pytest.fixture - def raw_data(self, base_dir) -> str: - return base_dir / "files/tracab_raw.dat" - def test_correct_deserialization(self, meta_data: Path, raw_data: Path): +@pytest.fixture(scope="session") +def json_raw_data(base_dir: Path) -> Path: + return base_dir / "files" / "tracab_raw.json" + + +@pytest.fixture(scope="session") +def xml_meta_data(base_dir: Path) -> Path: + return base_dir / "files" / "tracab_meta.xml" + + +@pytest.fixture(scope="session") +def dat_raw_data(base_dir: Path) -> Path: + return base_dir / "files" / "tracab_raw.dat" + + +def test_correct_auto_recognize_deserialization( + json_meta_data: Path, + json_raw_data: Path, + xml_meta_data: Path, + dat_raw_data: Path, +): + dataset = tracab.load( + meta_data=json_meta_data, raw_data=json_raw_data, only_alive=False + ) + assert len(dataset.records) == 5 + dataset = tracab.load( + meta_data=xml_meta_data, raw_data=dat_raw_data, only_alive=False + ) + assert len(dataset.records) == 6 + + +class TestTracabJSONTracking: + def test_correct_deserialization( + self, json_meta_data: Path, json_raw_data: Path + ): + dataset = tracab.load( + meta_data=json_meta_data, + raw_data=json_raw_data, + coordinates="tracab", + only_alive=False, + data_version="json", + ) + assert dataset.metadata.provider == Provider.TRACAB + assert dataset.dataset_type == DatasetType.TRACKING + assert len(dataset.records) == 5 + assert len(dataset.metadata.periods) == 2 + assert dataset.metadata.periods[0] == Period( + id=1, + start_timestamp=73940.32, + end_timestamp=76656.32, + attacking_direction=AttackingDirection.AWAY_HOME, + ) + + assert dataset.metadata.periods[1] == Period( + id=2, + start_timestamp=77684.56, + end_timestamp=80717.32, + attacking_direction=AttackingDirection.HOME_AWAY, + ) + + player_home_1 = dataset.metadata.teams[0].get_player_by_jersey_number( + 1 + ) + assert dataset.records[0].players_data[ + player_home_1 + ].coordinates == Point(x=5270.0, y=27.0) + + player_away_12 = dataset.metadata.teams[1].get_player_by_jersey_number( + 12 + ) + assert dataset.records[0].players_data[ + player_away_12 + ].coordinates == Point(x=-4722.0, y=28.0) + assert dataset.records[0].ball_state == BallState.DEAD + assert dataset.records[1].ball_state == BallState.ALIVE + # Shouldn't this be closer to (0,0,0)? + assert dataset.records[1].ball_coordinates == Point3D( + x=2710.0, y=3722.0, z=11.0 + ) + + # make sure player data is only in the frame when the player is at the pitch + assert "12170" in [ + player.player_id + for player in dataset.records[0].players_data.keys() + ] + assert "12170" not in [ + player.player_id + for player in dataset.records[4].players_data.keys() + ] + + def test_correct_normalized_deserialization( + self, json_meta_data: Path, json_raw_data: Path + ): + dataset = tracab.load( + meta_data=json_meta_data, raw_data=json_raw_data, only_alive=False + ) + player_home_1 = dataset.metadata.teams[0].get_player_by_jersey_number( + 1 + ) + assert dataset.records[0].players_data[ + player_home_1 + ].coordinates == Point(x=1.0019047619047619, y=0.49602941176470583) + + +class TestTracabDATTracking: + def test_correct_deserialization( + self, xml_meta_data: Path, dat_raw_data: Path + ): dataset = tracab.load( - meta_data=meta_data, - raw_data=raw_data, + meta_data=xml_meta_data, + raw_data=dat_raw_data, coordinates="tracab", only_alive=False, ) @@ -90,10 +193,10 @@ def test_correct_deserialization(self, meta_data: Path, raw_data: Path): ] def test_correct_normalized_deserialization( - self, meta_data: Path, raw_data: Path + self, xml_meta_data: Path, dat_raw_data: Path ): dataset = tracab.load( - meta_data=meta_data, raw_data=raw_data, only_alive=False + meta_data=xml_meta_data, raw_data=dat_raw_data, only_alive=False ) player_home_19 = dataset.metadata.teams[0].get_player_by_jersey_number( From ce740830f83a023dd3555ecb8b6afccc5fa8f585 Mon Sep 17 00:00:00 2001 From: driesdeprest Date: Mon, 29 Jan 2024 13:58:49 +0100 Subject: [PATCH 07/17] Add starting events of second half to test file --- kloppy/tests/files/tracab_raw.json | 472 +++++++++++++++++++++++++++++ 1 file changed, 472 insertions(+) diff --git a/kloppy/tests/files/tracab_raw.json b/kloppy/tests/files/tracab_raw.json index 249656e5..deda81bc 100644 --- a/kloppy/tests/files/tracab_raw.json +++ b/kloppy/tests/files/tracab_raw.json @@ -3685,6 +3685,478 @@ } ] }, + { + "FrameCount": 1942114, + "GameRunning": 1, + "Phase": 3, + "PlayerPositions": [ + { + "Team": 1, + "JerseyNumber": 1, + "X": -5281, + "Y": 16, + "Speed": 0.29 + }, + { + "Team": 0, + "JerseyNumber": 4, + "X": 2371, + "Y": 48, + "Speed": 0.12 + }, + { + "Team": 0, + "JerseyNumber": 26, + "X": 23, + "Y": -1557, + "Speed": 1.16 + }, + { + "Team": 0, + "JerseyNumber": 24, + "X": -3, + "Y": 2589, + "Speed": 0.52 + }, + { + "Team": 1, + "JerseyNumber": 77, + "X": -292, + "Y": 1960, + "Speed": 0.15 + }, + { + "Team": 1, + "JerseyNumber": 55, + "X": -1706, + "Y": 1835, + "Speed": 0.86 + }, + { + "Team": 0, + "JerseyNumber": 5, + "X": 1988, + "Y": -1654, + "Speed": 0.06 + }, + { + "Team": 1, + "JerseyNumber": 7, + "X": -59, + "Y": -1822, + "Speed": 0.04 + }, + { + "Team": 1, + "JerseyNumber": 90, + "X": -18, + "Y": -952, + "Speed": 0.13 + }, + { + "Team": 1, + "JerseyNumber": 2, + "X": -1931, + "Y": -597, + "Speed": 0.6 + }, + { + "Team": 0, + "JerseyNumber": 17, + "X": 1235, + "Y": -677, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 8, + "X": -916, + "Y": -245, + "Speed": 0.57 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 5, + "X": -1911, + "Y": 447, + "Speed": 0.58 + }, + { + "Team": 0, + "JerseyNumber": 9, + "X": -38, + "Y": -47, + "Speed": 0.32 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 12, + "X": 4790, + "Y": 5, + "Speed": 0.03 + }, + { + "Team": 0, + "JerseyNumber": 27, + "X": 871, + "Y": 3027, + "Speed": 0.3 + }, + { + "Team": 0, + "JerseyNumber": 28, + "X": 1837, + "Y": 1159, + "Speed": 0.42 + }, + { + "Team": 0, + "JerseyNumber": 2, + "X": 604, + "Y": -36, + "Speed": 0.1 + }, + { + "Team": 1, + "JerseyNumber": 13, + "X": -1638, + "Y": -1782, + "Speed": 0.09 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 16, + "X": -2, + "Y": 895, + "Speed": 0.41 + }, + { + "Team": 0, + "JerseyNumber": 19, + "X": 514, + "Y": -3189, + "Speed": 0.19 + }, + { + "Team": 1, + "JerseyNumber": 20, + "X": -734, + "Y": 542, + "Speed": 0.16 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + } + ], + "BallPosition": [ + { + "BallContactInfo": "SetAway", + "BallOwningTeam": "A", + "BallStatus": "Dead", + "Speed": 807.18, + "X": 240, + "Y": -6, + "Z": 15 + }, + { + "BallContactInfo": "SetAway", + "BallOwningTeam": "A", + "BallStatus": "Dead", + "Speed": 807.18, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "SetAway", + "BallOwningTeam": "A", + "BallStatus": "Dead", + "Speed": 807.18, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "SetAway", + "BallOwningTeam": "A", + "BallStatus": "Dead", + "Speed": 807.18, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "SetAway", + "BallOwningTeam": "A", + "BallStatus": "Dead", + "Speed": 807.18, + "X": 5550, + "Y": 4400, + "Z": 0 + } + ] + }, + { + "FrameCount": 1942115, + "GameRunning": 1, + "Phase": 3, + "PlayerPositions": [ + { + "Team": 1, + "JerseyNumber": 1, + "X": -5280, + "Y": 18, + "Speed": 0.37 + }, + { + "Team": 0, + "JerseyNumber": 4, + "X": 2369, + "Y": 48, + "Speed": 0.12 + }, + { + "Team": 0, + "JerseyNumber": 26, + "X": 23, + "Y": -1551, + "Speed": 1.19 + }, + { + "Team": 0, + "JerseyNumber": 24, + "X": -6, + "Y": 2592, + "Speed": 0.57 + }, + { + "Team": 1, + "JerseyNumber": 77, + "X": -290, + "Y": 1960, + "Speed": 0.13 + }, + { + "Team": 1, + "JerseyNumber": 55, + "X": -1706, + "Y": 1834, + "Speed": 0.82 + }, + { + "Team": 0, + "JerseyNumber": 5, + "X": 1988, + "Y": -1654, + "Speed": 0.06 + }, + { + "Team": 1, + "JerseyNumber": 7, + "X": -59, + "Y": -1820, + "Speed": 0.08 + }, + { + "Team": 1, + "JerseyNumber": 90, + "X": -14, + "Y": -948, + "Speed": 0.2 + }, + { + "Team": 1, + "JerseyNumber": 2, + "X": -1929, + "Y": -595, + "Speed": 0.62 + }, + { + "Team": 0, + "JerseyNumber": 17, + "X": 1237, + "Y": -676, + "Speed": 0.03 + }, + { + "Team": 1, + "JerseyNumber": 8, + "X": -915, + "Y": -246, + "Speed": 0.57 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 5, + "X": -1909, + "Y": 447, + "Speed": 0.58 + }, + { + "Team": 0, + "JerseyNumber": 9, + "X": -41, + "Y": -47, + "Speed": 0.36 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 0, + "JerseyNumber": 12, + "X": 4790, + "Y": 5, + "Speed": 0.03 + }, + { + "Team": 0, + "JerseyNumber": 27, + "X": 869, + "Y": 3029, + "Speed": 0.35 + }, + { + "Team": 0, + "JerseyNumber": 28, + "X": 1839, + "Y": 1159, + "Speed": 0.44 + }, + { + "Team": 0, + "JerseyNumber": 2, + "X": 603, + "Y": -37, + "Speed": 0.13 + }, + { + "Team": 1, + "JerseyNumber": 13, + "X": -1639, + "Y": -1783, + "Speed": 0.13 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + }, + { + "Team": 1, + "JerseyNumber": 16, + "X": 1, + "Y": 889, + "Speed": 0.48 + }, + { + "Team": 0, + "JerseyNumber": 19, + "X": 512, + "Y": -3190, + "Speed": 0.26 + }, + { + "Team": 1, + "JerseyNumber": 20, + "X": -734, + "Y": 541, + "Speed": 0.18 + }, + { + "Team": 4, + "JerseyNumber": 0, + "X": 5550, + "Y": 4400, + "Speed": 0 + } + ], + "BallPosition": [ + { + "BallContactInfo": "SetAway", + "BallOwningTeam": "A", + "BallStatus": "Dead", + "Speed": 747.39, + "X": 269, + "Y": -7, + "Z": 18 + }, + { + "BallContactInfo": "SetAway", + "BallOwningTeam": "A", + "BallStatus": "Dead", + "Speed": 747.39, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "SetAway", + "BallOwningTeam": "A", + "BallStatus": "Dead", + "Speed": 747.39, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "SetAway", + "BallOwningTeam": "A", + "BallStatus": "Dead", + "Speed": 747.39, + "X": 5550, + "Y": 4400, + "Z": 0 + }, + { + "BallContactInfo": "SetAway", + "BallOwningTeam": "A", + "BallStatus": "Dead", + "Speed": 747.39, + "X": 5550, + "Y": 4400, + "Z": 0 + } + ] + }, { "FrameCount": 2017933, "GameRunning": 1, From 7c776df6e54868dcef52b18eea66d1c33af83acc Mon Sep 17 00:00:00 2001 From: driesdeprest Date: Mon, 29 Jan 2024 14:50:40 +0100 Subject: [PATCH 08/17] Fix failing tests --- kloppy/tests/test_tracab.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kloppy/tests/test_tracab.py b/kloppy/tests/test_tracab.py index 02caf5e8..5ab30cc2 100644 --- a/kloppy/tests/test_tracab.py +++ b/kloppy/tests/test_tracab.py @@ -47,7 +47,7 @@ def test_correct_auto_recognize_deserialization( dataset = tracab.load( meta_data=json_meta_data, raw_data=json_raw_data, only_alive=False ) - assert len(dataset.records) == 5 + assert len(dataset.records) == 7 dataset = tracab.load( meta_data=xml_meta_data, raw_data=dat_raw_data, only_alive=False ) @@ -67,7 +67,7 @@ def test_correct_deserialization( ) assert dataset.metadata.provider == Provider.TRACAB assert dataset.dataset_type == DatasetType.TRACKING - assert len(dataset.records) == 5 + assert len(dataset.records) == 7 assert len(dataset.metadata.periods) == 2 assert dataset.metadata.periods[0] == Period( id=1, @@ -110,7 +110,7 @@ def test_correct_deserialization( ] assert "12170" not in [ player.player_id - for player in dataset.records[4].players_data.keys() + for player in dataset.records[6].players_data.keys() ] def test_correct_normalized_deserialization( From a0db47fd24f970d8d4455481ec5fe0f69dd01449 Mon Sep 17 00:00:00 2001 From: driesdeprest Date: Mon, 29 Jan 2024 14:55:18 +0100 Subject: [PATCH 09/17] Fix orientation --- kloppy/infra/serializers/tracking/tracab/tracab_json.py | 6 +++--- kloppy/tests/test_tracab.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/kloppy/infra/serializers/tracking/tracab/tracab_json.py b/kloppy/infra/serializers/tracking/tracab/tracab_json.py index 78b1bb5e..77188452 100644 --- a/kloppy/infra/serializers/tracking/tracab/tracab_json.py +++ b/kloppy/infra/serializers/tracking/tracab/tracab_json.py @@ -230,9 +230,9 @@ def _iter(): break orientation = ( - Orientation.HOME_TEAM - if periods[1].attacking_direction == AttackingDirection.HOME_AWAY - else Orientation.AWAY_TEAM + Orientation.FIXED_HOME_AWAY + if periods[0].attacking_direction == AttackingDirection.HOME_AWAY + else Orientation.FIXED_AWAY_HOME ) metadata = Metadata( diff --git a/kloppy/tests/test_tracab.py b/kloppy/tests/test_tracab.py index 5ab30cc2..daaf91a9 100644 --- a/kloppy/tests/test_tracab.py +++ b/kloppy/tests/test_tracab.py @@ -75,13 +75,13 @@ def test_correct_deserialization( end_timestamp=76656.32, attacking_direction=AttackingDirection.AWAY_HOME, ) - assert dataset.metadata.periods[1] == Period( id=2, start_timestamp=77684.56, end_timestamp=80717.32, attacking_direction=AttackingDirection.HOME_AWAY, ) + assert dataset.metadata.orientation == Orientation.FIXED_AWAY_HOME player_home_1 = dataset.metadata.teams[0].get_player_by_jersey_number( 1 From e8aafb1922454c8c06d11327e83a4deeb03e78cd Mon Sep 17 00:00:00 2001 From: driesdeprest Date: Mon, 12 Feb 2024 12:02:37 +0100 Subject: [PATCH 10/17] Don't cast floats to floats, use tuple instead of list, fix test --- .../serializers/tracking/tracab/tracab_json.py | 8 +++----- kloppy/tests/test_tracab.py | 17 +++++++++++------ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/kloppy/infra/serializers/tracking/tracab/tracab_json.py b/kloppy/infra/serializers/tracking/tracab/tracab_json.py index 77188452..8a600c3c 100644 --- a/kloppy/infra/serializers/tracking/tracab/tracab_json.py +++ b/kloppy/infra/serializers/tracking/tracab/tracab_json.py @@ -73,7 +73,7 @@ def _create_frame(cls, teams, period, raw_frame, frame_rate): player = team.get_player_by_jersey_number(jersey_no) if player: players_data[player] = PlayerData( - coordinates=Point(float(x), float(y)), speed=float(speed) + coordinates=Point(x, y), speed=speed ) else: # continue @@ -105,9 +105,7 @@ def _create_frame(cls, teams, period, raw_frame, frame_rate): return Frame( frame_id=frame_id, timestamp=frame_id / frame_rate - period.start_timestamp, - ball_coordinates=Point3D( - float(ball_x), float(ball_y), float(ball_z) - ), + ball_coordinates=Point3D(ball_x, ball_y, ball_z), ball_state=ball_state, ball_owning_team=ball_owning_team, ball_speed=ball_speed, @@ -174,7 +172,7 @@ def deserialize(self, inputs: TRACABInputs) -> TrackingDataset: pitch_size_length = meta_data["PitchLongSide"] / 100 periods = [] - for period_id in [1, 2, 3, 4]: + for period_id in (1, 2, 3, 4): period_start_frame = meta_data[f"Phase{period_id}StartFrame"] period_end_frame = meta_data[f"Phase{period_id}EndFrame"] if period_start_frame != 0 or period_end_frame != 0: diff --git a/kloppy/tests/test_tracab.py b/kloppy/tests/test_tracab.py index daaf91a9..e6d3435c 100644 --- a/kloppy/tests/test_tracab.py +++ b/kloppy/tests/test_tracab.py @@ -2,6 +2,11 @@ import pytest +from kloppy._providers.tracab import ( + identify_deserializer, + TRACABJSONDeserializer, + TRACABDatDeserializer, +) from kloppy.domain import ( Period, AttackingDirection, @@ -44,14 +49,14 @@ def test_correct_auto_recognize_deserialization( xml_meta_data: Path, dat_raw_data: Path, ): - dataset = tracab.load( - meta_data=json_meta_data, raw_data=json_raw_data, only_alive=False + tracab_json_deserializer = identify_deserializer( + meta_data=json_meta_data, raw_data=json_raw_data ) - assert len(dataset.records) == 7 - dataset = tracab.load( - meta_data=xml_meta_data, raw_data=dat_raw_data, only_alive=False + assert tracab_json_deserializer == TRACABJSONDeserializer + tracab_dat_deserializer = identify_deserializer( + meta_data=xml_meta_data, raw_data=dat_raw_data ) - assert len(dataset.records) == 6 + assert tracab_dat_deserializer == TRACABDatDeserializer class TestTracabJSONTracking: From b88255e366a87f419c96882116f1fed9289362d5 Mon Sep 17 00:00:00 2001 From: driesdeprest Date: Mon, 12 Feb 2024 12:47:09 +0100 Subject: [PATCH 11/17] Recognize 3 as a failing Team --- kloppy/infra/serializers/tracking/tracab/tracab_json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kloppy/infra/serializers/tracking/tracab/tracab_json.py b/kloppy/infra/serializers/tracking/tracab/tracab_json.py index 8a600c3c..7f99815c 100644 --- a/kloppy/infra/serializers/tracking/tracab/tracab_json.py +++ b/kloppy/infra/serializers/tracking/tracab/tracab_json.py @@ -58,7 +58,7 @@ def _create_frame(cls, teams, period, raw_frame, frame_rate): team = teams[0] elif player_data["Team"] == 0: team = teams[1] - elif player_data["Team"] in (-1, 4): + elif player_data["Team"] in (-1, 3, 4): continue else: raise DeserializationError( From 3ae63cc6ce8e48c983d3d9587a762f86eb03f8f7 Mon Sep 17 00:00:00 2001 From: driesdeprest Date: Thu, 15 Feb 2024 10:21:53 +0100 Subject: [PATCH 12/17] Removing SHOT_ASSIST_2ND --- kloppy/domain/models/event.py | 1 - 1 file changed, 1 deletion(-) diff --git a/kloppy/domain/models/event.py b/kloppy/domain/models/event.py index 9f629eb6..afde414e 100644 --- a/kloppy/domain/models/event.py +++ b/kloppy/domain/models/event.py @@ -352,7 +352,6 @@ class PassType(Enum): FLICK_ON = "FLICK_ON" SHOT_ASSIST = "SHOT_ASSIST" ASSIST = "ASSIST" - SHOT_ASSIST_2ND = "SHOT_ASSIST_2ND" ASSIST_2ND = "ASSIST_2ND" SWITCH_OF_PLAY = "SWITCH_OF_PLAY" From 87a9684b762dab17ba2e65e16b7f255724d251a0 Mon Sep 17 00:00:00 2001 From: driesdeprest Date: Thu, 22 Feb 2024 12:09:23 +0100 Subject: [PATCH 13/17] Orientation refactor --- .../tracking/tracab/tracab_json.py | 25 +++++++++++++------ kloppy/tests/test_tracab.py | 4 +-- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/kloppy/infra/serializers/tracking/tracab/tracab_json.py b/kloppy/infra/serializers/tracking/tracab/tracab_json.py index 7f99815c..8ff02866 100644 --- a/kloppy/infra/serializers/tracking/tracab/tracab_json.py +++ b/kloppy/infra/serializers/tracking/tracab/tracab_json.py @@ -1,4 +1,5 @@ import logging +import warnings import json import html from typing import Dict, Optional, Union @@ -20,6 +21,7 @@ Provider, PlayerData, Position, + attacking_direction_from_frame, ) from kloppy.exceptions import DeserializationError @@ -181,9 +183,6 @@ def deserialize(self, inputs: TRACABInputs) -> TrackingDataset: id=period_id, start_timestamp=period_start_frame / frame_rate, end_timestamp=period_end_frame / frame_rate, - attacking_direction=AttackingDirection.HOME_AWAY - if meta_data[f"Phase{period_id}HomeGKLeft"] - else AttackingDirection.AWAY_HOME, ) ) @@ -227,11 +226,21 @@ def _iter(): if self.limit and n >= self.limit: break - orientation = ( - Orientation.FIXED_HOME_AWAY - if periods[0].attacking_direction == AttackingDirection.HOME_AWAY - else Orientation.FIXED_AWAY_HOME - ) + try: + first_frame = next( + frame for frame in frames if frame.period.id == 1 + ) + orientation = ( + Orientation.HOME_AWAY + if attacking_direction_from_frame(first_frame) + == AttackingDirection.LTR + else Orientation.AWAY_HOME + ) + except StopIteration: + warnings.warn( + "Could not determine orientation of dataset, defaulting to NOT_SET" + ) + orientation = Orientation.NOT_SET metadata = Metadata( teams=teams, diff --git a/kloppy/tests/test_tracab.py b/kloppy/tests/test_tracab.py index 8a9c7a0b..fd9dbd00 100644 --- a/kloppy/tests/test_tracab.py +++ b/kloppy/tests/test_tracab.py @@ -78,15 +78,13 @@ def test_correct_deserialization( id=1, start_timestamp=73940.32, end_timestamp=76656.32, - attacking_direction=AttackingDirection.AWAY_HOME, ) assert dataset.metadata.periods[1] == Period( id=2, start_timestamp=77684.56, end_timestamp=80717.32, - attacking_direction=AttackingDirection.HOME_AWAY, ) - assert dataset.metadata.orientation == Orientation.FIXED_AWAY_HOME + assert dataset.metadata.orientation == Orientation.AWAY_HOME player_home_1 = dataset.metadata.teams[0].get_player_by_jersey_number( 1 From c8e7e3ca2d984d6de15200c12817f5538f6a0c8a Mon Sep 17 00:00:00 2001 From: driesdeprest Date: Thu, 22 Feb 2024 12:12:40 +0100 Subject: [PATCH 14/17] data version -> file format name change --- kloppy/_providers/tracab.py | 8 ++++---- kloppy/tests/test_tracab.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/kloppy/_providers/tracab.py b/kloppy/_providers/tracab.py index 3c8f7c7d..e4747143 100644 --- a/kloppy/_providers/tracab.py +++ b/kloppy/_providers/tracab.py @@ -20,11 +20,11 @@ def load( limit: Optional[int] = None, coordinates: Optional[str] = None, only_alive: Optional[bool] = True, - data_version: Optional[str] = None, + file_format: Optional[str] = None, ) -> TrackingDataset: - if data_version == "dat": + if file_format == "dat": deserializer_class = TRACABDatDeserializer - elif data_version == "json": + elif file_format == "json": deserializer_class = TRACABJSONDeserializer else: deserializer_class = identify_deserializer(meta_data, raw_data) @@ -55,7 +55,7 @@ def identify_deserializer( if deserializer is None: raise ValueError( - "Tracab data version could not be recognized, please specify" + "Tracab file format could not be recognized, please specify" ) return deserializer diff --git a/kloppy/tests/test_tracab.py b/kloppy/tests/test_tracab.py index fd9dbd00..dca29cb1 100644 --- a/kloppy/tests/test_tracab.py +++ b/kloppy/tests/test_tracab.py @@ -68,7 +68,7 @@ def test_correct_deserialization( raw_data=json_raw_data, coordinates="tracab", only_alive=False, - data_version="json", + file_format="json", ) assert dataset.metadata.provider == Provider.TRACAB assert dataset.dataset_type == DatasetType.TRACKING From 8fb129f4e41e415d2ed7b3c37285b2a981518c41 Mon Sep 17 00:00:00 2001 From: driesdeprest Date: Thu, 14 Mar 2024 12:00:36 +0100 Subject: [PATCH 15/17] fix typo --- kloppy/tests/test_statsbomb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kloppy/tests/test_statsbomb.py b/kloppy/tests/test_statsbomb.py index 066f80cd..5dbc3d68 100644 --- a/kloppy/tests/test_statsbomb.py +++ b/kloppy/tests/test_statsbomb.py @@ -993,7 +993,7 @@ class TestStatsBombPressureEvent: """Tests related to deserializing 17/Pressure events""" def test_deserialize_all(self, dataset: EventDataset): - """It should deserialize all ball recovery events""" + """It should deserialize all pressure events""" events = dataset.find_all("pressure") assert len(events) == 203 From 4834dc8ec7ffe53de570a2b0c08ab7e12f342956 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Wed, 3 Apr 2024 13:32:37 +0200 Subject: [PATCH 16/17] Fix typo --- kloppy/domain/models/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kloppy/domain/models/common.py b/kloppy/domain/models/common.py index 04b304dc..6c9603df 100644 --- a/kloppy/domain/models/common.py +++ b/kloppy/domain/models/common.py @@ -261,7 +261,7 @@ def contains(self, timestamp: datetime): ): return self.start_timestamp <= timestamp <= self.end_timestamp raise KloppyError( - "This method can only be used when start_timestamp and end_timmestamp are a datetime" + "This method can only be used when start_timestamp and end_timestamp are a datetime" ) @property From dc9007885853f414c4fd4791ec9878a454cd87a2 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Wed, 3 Apr 2024 14:12:25 +0200 Subject: [PATCH 17/17] Linting --- .../infra/serializers/tracking/tracab/tracab_json.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/kloppy/infra/serializers/tracking/tracab/tracab_json.py b/kloppy/infra/serializers/tracking/tracab/tracab_json.py index bad6d651..6fcb82b5 100644 --- a/kloppy/infra/serializers/tracking/tracab/tracab_json.py +++ b/kloppy/infra/serializers/tracking/tracab/tracab_json.py @@ -107,7 +107,8 @@ def _create_frame(cls, teams, period, raw_frame, frame_rate): return Frame( frame_id=frame_id, - timestamp=timedelta(seconds=frame_id / frame_rate) - period.start_timestamp, + timestamp=timedelta(seconds=frame_id / frame_rate) + - period.start_timestamp, ball_coordinates=Point3D(ball_x, ball_y, ball_z), ball_state=ball_state, ball_owning_team=ball_owning_team, @@ -182,8 +183,12 @@ def deserialize(self, inputs: TRACABInputs) -> TrackingDataset: periods.append( Period( id=period_id, - start_timestamp=timedelta(seconds=period_start_frame / frame_rate), - end_timestamp=timedelta(seconds=period_end_frame / frame_rate), + start_timestamp=timedelta( + seconds=period_start_frame / frame_rate + ), + end_timestamp=timedelta( + seconds=period_end_frame / frame_rate + ), ) )