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 746d7412..6c9603df 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, ) @@ -238,21 +240,32 @@ class Period: Period Attributes: - id: `1` for first half, `2` for second half, `3` for first overtime, - `4` for second overtime, and `5` for penalty shootouts - 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 + start_timestamp: Union[datetime, timedelta] + end_timestamp: Union[datetime, timedelta] - def contains(self, timestamp: float): - return self.start_timestamp <= timestamp <= self.end_timestamp + 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_timestamp are a datetime" + ) @property - def duration(self): + def duration(self) -> timedelta: return self.end_timestamp - self.start_timestamp def __eq__(self, other): @@ -811,7 +824,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] """ @@ -820,7 +833,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 d05c5b83..73d6e1c9 100644 --- a/kloppy/domain/models/event.py +++ b/kloppy/domain/models/event.py @@ -701,7 +701,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 99d8d289..808cf20a 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,15 +425,33 @@ 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, - ) + 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, + ) + 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) @@ -444,7 +479,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 d2408339..44ca6690 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 5f43ff37..420e2821 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 cadd101b..2196e47f 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"], @@ -1142,9 +1146,10 @@ class 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 + end_timestamp = generic_event_kwargs["timestamp"] + timedelta( + seconds=self.raw_event.get("duration", 0.0) ) + pressure_event = event_factory.build_pressure_event( result=None, qualifiers=None, 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..d4e8a0ce 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 ( @@ -473,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() @@ -490,8 +500,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 +516,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,15 +556,12 @@ 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 + "timestamp": timedelta( + seconds=float( + raw_event["second"] + raw_event["minute"] * 60 + ) ) - if period_id == 1 - else float( - raw_event["second"] - + (raw_event["minute"] * 60) - - (60 * 45) - ), + - start_ts[period_id], } primary_event_type = raw_event["type"]["primary"] diff --git a/kloppy/infra/serializers/tracking/metrica_csv.py b/kloppy/infra/serializers/tracking/metrica_csv.py index 163c8d30..4bcdb34e 100644 --- a/kloppy/infra/serializers/tracking/metrica_csv.py +++ b/kloppy/infra/serializers/tracking/metrica_csv.py @@ -1,6 +1,7 @@ import logging import warnings from collections import namedtuple +from datetime import timedelta from typing import Tuple, Dict, Iterator, IO, NamedTuple from kloppy.domain import ( @@ -90,12 +91,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( @@ -189,7 +194,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 4200fbe2..0ffb00c6 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 @@ -89,9 +90,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 + ), ) ) else: 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 a5dfb1a0..d9f4121b 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 import warnings from typing import Tuple, Dict, Optional, Union, NamedTuple, IO @@ -57,7 +58,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"] @@ -138,8 +139,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: @@ -159,8 +164,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 bbe3ca3d..91e5800f 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 import warnings from typing import List, Dict, Tuple, NamedTuple, IO, Optional, Union from enum import Enum, Flag @@ -34,6 +35,9 @@ logger = logging.getLogger(__name__) +frame_rate = 10 + + class SkillCornerInputs(NamedTuple): meta_data: IO[bytes] raw_data: IO[bytes] @@ -73,6 +77,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 = {} @@ -138,7 +156,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], @@ -153,10 +171,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") @@ -222,11 +242,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 diff --git a/kloppy/infra/serializers/tracking/sportec/deserializer.py b/kloppy/infra/serializers/tracking/sportec/deserializer.py index 3458edfd..10934a27 100644 --- a/kloppy/infra/serializers/tracking/sportec/deserializer.py +++ b/kloppy/infra/serializers/tracking/sportec/deserializer.py @@ -2,6 +2,7 @@ import warnings from collections import defaultdict from typing import NamedTuple, Optional, Union, IO +from datetime import timedelta from lxml import objectify @@ -157,11 +158,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 dada36da..b780dbf1 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 import warnings from typing import IO, Any, Dict, List, NamedTuple, Optional, Union @@ -50,29 +51,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""" @@ -97,7 +75,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 @@ -148,6 +128,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 = [] @@ -191,6 +191,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 = [] @@ -240,13 +262,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"] @@ -280,8 +313,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, @@ -293,13 +324,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/tracab_dat.py b/kloppy/infra/serializers/tracking/tracab/tracab_dat.py index 5b61cba3..f47a73d8 100644 --- a/kloppy/infra/serializers/tracking/tracab/tracab_dat.py +++ b/kloppy/infra/serializers/tracking/tracab/tracab_dat.py @@ -1,4 +1,5 @@ import logging +from datetime import timedelta import warnings from typing import Tuple, Dict, NamedTuple, IO, Optional, Union @@ -109,7 +110,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) ), @@ -147,8 +149,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 + ), ) ) @@ -171,7 +177,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/infra/serializers/tracking/tracab/tracab_json.py b/kloppy/infra/serializers/tracking/tracab/tracab_json.py index 8ff02866..6fcb82b5 100644 --- a/kloppy/infra/serializers/tracking/tracab/tracab_json.py +++ b/kloppy/infra/serializers/tracking/tracab/tracab_json.py @@ -2,6 +2,7 @@ import warnings import json import html +from datetime import timedelta from typing import Dict, Optional, Union from kloppy.domain import ( @@ -106,7 +107,8 @@ def _create_frame(cls, teams, period, raw_frame, 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(ball_x, ball_y, ball_z), ball_state=ball_state, ball_owning_team=ball_owning_team, @@ -181,8 +183,12 @@ def deserialize(self, inputs: TRACABInputs) -> TrackingDataset: periods.append( Period( id=period_id, - start_timestamp=period_start_frame / frame_rate, - end_timestamp=period_end_frame / frame_rate, + start_timestamp=timedelta( + seconds=period_start_frame / frame_rate + ), + end_timestamp=timedelta( + seconds=period_end_frame / frame_rate + ), ) ) @@ -210,7 +216,11 @@ def _iter(): frame_id = frame["FrameCount"] 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, frame 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 731a5aab..2daec1af 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,16 +45,27 @@ 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, + 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[1] == Period( - id=2, - start_timestamp=2700, - end_timestamp=5710, + 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.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 57ce3495..48f66e04 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,16 +34,27 @@ 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.HOME_AWAY - assert dataset.metadata.periods[0] == Period( - id=1, - start_timestamp=0.04, - end_timestamp=0.12, + 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[1] == Period( - id=2, - start_timestamp=5800.16, - end_timestamp=5800.24, + 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 + ) + + # 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 e626be2c..6031b451 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_AWAY + 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 fef37456..f02d5236 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,16 +46,35 @@ 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, + 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, + 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 + ) + + 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 17402ba4..15c63d46 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 ) @@ -191,6 +185,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 b7116232..9ce0ddb0 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,16 +49,31 @@ 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[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 + ) # 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 8b09dee8..4e715e7f 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,75 +27,84 @@ 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_HOME - assert dataset.metadata.periods[0] == Period( - id=1, - start_timestamp=0.0, - end_timestamp=2753.3, + 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[1].id == 2 + assert dataset.metadata.periods[1].start_timestamp == timedelta( + seconds=39979 / 10 ) - assert dataset.metadata.periods[1] == Period( - id=2, - start_timestamp=2700.0, - end_timestamp=5509.7, + assert dataset.metadata.periods[1].end_timestamp == timedelta( + seconds=68076 / 10 ) - # 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" ] @@ -114,3 +124,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 e8990538..5eb36646 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,17 +51,26 @@ def test_correct_event_data_deserialization( assert dataset.events[28].result == ShotResult.OWN_GOAL assert dataset.metadata.orientation == Orientation.HOME_AWAY - assert dataset.metadata.periods[0] == Period( - id=1, - start_timestamp=1591381800.21, - end_timestamp=1591384584.0, + 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, + assert dataset.metadata.periods[0].end_timestamp == datetime( + 2020, 6, 5, 19, 16, 24, 0, tzinfo=timezone.utc + ) + 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 ) + # 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" assert player.jersey_no == 1 @@ -121,6 +131,20 @@ 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[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 + ) def test_load_frames(self, raw_data: Path, meta_data: Path): dataset = sportec.load_tracking( @@ -131,7 +155,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( @@ -163,8 +187,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 f4b57999..4160041f 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" ) @@ -209,6 +212,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( @@ -563,10 +577,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 @@ -812,7 +825,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 b23f70f3..58d0d128 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,16 +54,31 @@ 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[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 + ) # 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 dca29cb1..2976feaf 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 @@ -74,15 +75,19 @@ def test_correct_deserialization( assert dataset.dataset_type == DatasetType.TRACKING assert len(dataset.records) == 7 assert len(dataset.metadata.periods) == 2 - assert dataset.metadata.periods[0] == Period( - id=1, - start_timestamp=73940.32, - end_timestamp=76656.32, + assert dataset.metadata.periods[0].id == 1 + assert dataset.metadata.periods[0].start_timestamp == timedelta( + seconds=73940.32 ) - assert dataset.metadata.periods[1] == Period( - id=2, - start_timestamp=77684.56, - end_timestamp=80717.32, + assert dataset.metadata.periods[0].end_timestamp == timedelta( + seconds=76656.32 + ) + assert dataset.metadata.periods[1].id == 2 + assert dataset.metadata.periods[1].start_timestamp == timedelta( + seconds=77684.56 + ) + assert dataset.metadata.periods[1].end_timestamp == timedelta( + seconds=80717.32 ) assert dataset.metadata.orientation == Orientation.AWAY_HOME @@ -141,23 +146,34 @@ def test_correct_deserialization( 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.HOME_AWAY - assert dataset.metadata.periods[0] == Period( - id=1, - start_timestamp=4.0, - end_timestamp=4.08, + 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, + assert dataset.metadata.periods[0].end_timestamp == timedelta( + seconds=102 / 25 ) + 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 + ) + + # 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..4bcc58a3 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,31 @@ 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) + # 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 + ) - timedelta(minutes=45) + 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]