diff --git a/kloppy/_providers/statsperform.py b/kloppy/_providers/statsperform.py index 231c3fec..b3766dd9 100644 --- a/kloppy/_providers/statsperform.py +++ b/kloppy/_providers/statsperform.py @@ -1,6 +1,6 @@ from typing import Optional -from kloppy.domain import TrackingDataset +from kloppy.domain import TrackingDataset, Provider from kloppy.infra.serializers.tracking.statsperform import ( StatsPerformDeserializer, StatsPerformInputs, @@ -11,12 +11,23 @@ def load( meta_data: FileLike, # Stats Perform MA1 file - xml or json - single game, live data & lineups raw_data: FileLike, # Stats Perform MA25 file - txt - tracking data + provider_name: str = "sportvu", + pitch_length: Optional[float] = None, + pitch_width: Optional[float] = None, sample_rate: Optional[float] = None, limit: Optional[int] = None, coordinates: Optional[str] = None, only_alive: Optional[bool] = False, ) -> TrackingDataset: + if pitch_length is None or pitch_width is None: + if coordinates is None or coordinates != provider_name: + raise ValueError( + "Please provide the pitch dimensions " + "('pitch_length', 'pitch_width') " + f"or set 'coordinates' to '{provider_name}'" + ) deserializer = StatsPerformDeserializer( + provider=Provider[provider_name.upper()], sample_rate=sample_rate, limit=limit, coordinate_system=coordinates, @@ -27,6 +38,9 @@ def load( ) as raw_data_fp: return deserializer.deserialize( inputs=StatsPerformInputs( - meta_data=meta_data_fp, raw_data=raw_data_fp + meta_data=meta_data_fp, + raw_data=raw_data_fp, + pitch_length=pitch_length, + pitch_width=pitch_width, ) ) diff --git a/kloppy/domain/models/common.py b/kloppy/domain/models/common.py index 3d9e973d..46fddfb1 100644 --- a/kloppy/domain/models/common.py +++ b/kloppy/domain/models/common.py @@ -96,6 +96,7 @@ class Provider(Enum): KLOPPY: DATAFACTORY: STATSPERFORM: + SPORTVU: """ METRICA = "metrica" @@ -109,6 +110,7 @@ class Provider(Enum): KLOPPY = "kloppy" DATAFACTORY = "datafactory" STATSPERFORM = "statsperform" + SPORTVU = "sportvu" OTHER = "other" def __str__(self): @@ -764,10 +766,10 @@ def pitch_dimensions(self) -> PitchDimensions: @dataclass -class StatsPerformCoordinateSystem(CoordinateSystem): +class SportVUCoordinateSystem(CoordinateSystem): @property def provider(self) -> Provider: - return Provider.STATSPERFORM + return Provider.SportVU @property def origin(self) -> Origin: @@ -779,13 +781,12 @@ def vertical_orientation(self) -> VerticalOrientation: @property def pitch_dimensions(self) -> PitchDimensions: - # FIXME: This does not seem correct - return NormalizedPitchDimensions( - x_dim=Dimension(0, 100), - y_dim=Dimension(0, 100), - pitch_length=105, - pitch_width=68, - standardized=True, + return MetricPitchDimensions( + x_dim=Dimension(0, self.pitch_length), + y_dim=Dimension(0, self.pitch_width), + pitch_length=self.pitch_length, + pitch_width=self.pitch_width, + standardized=False, ) @@ -838,7 +839,7 @@ def build_coordinate_system( Provider.SKILLCORNER: SkillCornerCoordinateSystem, Provider.DATAFACTORY: DatafactoryCoordinateSystem, Provider.SECONDSPECTRUM: SecondSpectrumCoordinateSystem, - Provider.STATSPERFORM: StatsPerformCoordinateSystem, + Provider.SPORTVU: SportVUCoordinateSystem, } if provider in coordinate_systems: diff --git a/kloppy/infra/serializers/tracking/statsperform.py b/kloppy/infra/serializers/tracking/statsperform.py index 2265f5ad..ecee6b6e 100644 --- a/kloppy/infra/serializers/tracking/statsperform.py +++ b/kloppy/infra/serializers/tracking/statsperform.py @@ -1,7 +1,7 @@ import json import logging -from datetime import datetime, timedelta import warnings +from datetime import datetime, timedelta from typing import IO, Any, Dict, List, NamedTuple, Optional, Union from lxml import objectify @@ -34,11 +34,14 @@ class StatsPerformInputs(NamedTuple): meta_data: IO[bytes] raw_data: IO[bytes] + pitch_length: Optional[float] = None + pitch_width: Optional[float] = None class StatsPerformDeserializer(TrackingDataDeserializer[StatsPerformInputs]): def __init__( self, + provider: Provider, limit: Optional[int] = None, sample_rate: Optional[float] = None, coordinate_system: Optional[Union[str, Provider]] = None, @@ -46,14 +49,15 @@ def __init__( ): super().__init__(limit, sample_rate, coordinate_system) self.only_alive = only_alive + self._provider = provider @property def provider(self) -> Provider: - return Provider.STATSPERFORM + return self._provider @classmethod def __get_frame_rate(cls, tracking): - """gets the frame rate of the tracking data""" + """Infer the frame rate of the tracking data.""" frame_numbers = [ int(line.split(";")[1].split(",")[0]) for line in tracking[1:] @@ -81,7 +85,6 @@ def _frame_from_framedata(cls, teams_list, period, frame_data): match_status = int(frame_info[1].split(",")[2]) ball_state = BallState.ALIVE if match_status == 0 else BallState.DEAD - ball_owning_team = None if len(components) > 2: ball_data = components[2].split(";")[0].split(",") @@ -122,7 +125,7 @@ def _frame_from_framedata(cls, teams_list, period, frame_data): timestamp=frame_timestamp, ball_coordinates=ball_coordinates, ball_state=ball_state, - ball_owning_team=ball_owning_team, + ball_owning_team=None, players_data=players_data, period=period, other_data={}, @@ -311,7 +314,10 @@ def deserialize(self, inputs: StatsPerformInputs) -> TrackingDataset: with performance_logging("Loading tracking data", logger=logger): frame_rate = self.__get_frame_rate(tracking_data) - transformer = self.get_transformer() + transformer = self.get_transformer( + pitch_length=inputs.pitch_length, + pitch_width=inputs.pitch_width, + ) def _iter(): n = 0 @@ -332,6 +338,8 @@ def _iter(): teams_list, period, frame_data ) frame = transformer.transform_frame(frame) + if self.only_alive and frame.ball_state == BallState.DEAD: + continue frames.append(frame) if self.limit and n >= self.limit: @@ -355,13 +363,13 @@ def _iter(): meta_data = Metadata( teams=teams_list, - periods=periods, + periods=list(periods.values()), pitch_dimensions=transformer.get_to_coordinate_system().pitch_dimensions, score=None, frame_rate=frame_rate, orientation=orientation, provider=Provider.STATSPERFORM, - flags=DatasetFlag.BALL_OWNING_TEAM | DatasetFlag.BALL_STATE, + flags=DatasetFlag.BALL_STATE, coordinate_system=transformer.get_to_coordinate_system(), ) diff --git a/kloppy/tests/files/statsperform_ma25_tracking.txt b/kloppy/tests/files/statsperform_ma25_tracking.txt index f4de7a34..1df79296 100644 --- a/kloppy/tests/files/statsperform_ma25_tracking.txt +++ b/kloppy/tests/files/statsperform_ma25_tracking.txt @@ -1,4 +1,4 @@ -1598184000000;0,1,0:0,a2s2c6anax9wnlsw1s6vunl5h,9,52.803,23.617;0,28x7i681wkjueanvkevew3bh1,17,60.148,43.669;0,4rlw04tze791jwt623h5ikg5x,26,67.742,15.807;0,72m5u1rlgh6e229zpfvp7f5jp,11,52.546,14.632;0,3djmsd2atvq16ysmocq7twsfd,20,68.518,30.182;0,4wqekpzlcztenasxob7a6tyax,7,56.573,51.2;0,dmrj53hcw8vcffxx0lfudbob9,2,68.441,51.598;0,aipyotc87m5bdvmvizd5n69k9,8,60.517,24.892;0,5g5wwp5luxo1rz1kp6chvz0x6,32,68.689,39.75;0,b0wb7gf04synxlkq7o8gvzkoa,22,61.64,34.088;1,9nhlgi0k783bq8a2704bu6n6d,7,45.468,27.411;1,976riwm0dz0e74d4l28y3ttcl,5,32.324,30.735;1,3sc349yey596xp2j6xlyt0frp,32,38.523,60.736;1,19djqex4nu3lz6vw08f3fwv6x,3,35.582,8.521;1,fe4b3ric67hz0hhih3tjf2eh,24,52.453,5.164;1,72d5uxwcmvhd6mzthxuvev1sl,2,30.595,44.022;1,8j2vmplbpj983xbxibhyvidx,8,52.21,55.676;1,8qmm84tue6kuz8e5nhhdhmz8p,15,44.648,40.073;1,3mqhkh4iz4kesvmqui3hupgmi,11,52.347,43.999;1,e6ok0deqkoe80184iu509gzu2,27,52.58,33.089;3,6wfwy94p5bm0zv3aku0urfq39,40,100.245,32.224;4,6ekdnbnk56xlxforb5owt3dn9,1,5.268,33.556;:52.35,33.25,0; +1598184000000;0,1,1:0,a2s2c6anax9wnlsw1s6vunl5h,9,52.803,23.617;0,28x7i681wkjueanvkevew3bh1,17,60.148,43.669;0,4rlw04tze791jwt623h5ikg5x,26,67.742,15.807;0,72m5u1rlgh6e229zpfvp7f5jp,11,52.546,14.632;0,3djmsd2atvq16ysmocq7twsfd,20,68.518,30.182;0,4wqekpzlcztenasxob7a6tyax,7,56.573,51.2;0,dmrj53hcw8vcffxx0lfudbob9,2,68.441,51.598;0,aipyotc87m5bdvmvizd5n69k9,8,60.517,24.892;0,5g5wwp5luxo1rz1kp6chvz0x6,32,68.689,39.75;0,b0wb7gf04synxlkq7o8gvzkoa,22,61.64,34.088;1,9nhlgi0k783bq8a2704bu6n6d,7,45.468,27.411;1,976riwm0dz0e74d4l28y3ttcl,5,32.324,30.735;1,3sc349yey596xp2j6xlyt0frp,32,38.523,60.736;1,19djqex4nu3lz6vw08f3fwv6x,3,35.582,8.521;1,fe4b3ric67hz0hhih3tjf2eh,24,52.453,5.164;1,72d5uxwcmvhd6mzthxuvev1sl,2,30.595,44.022;1,8j2vmplbpj983xbxibhyvidx,8,52.21,55.676;1,8qmm84tue6kuz8e5nhhdhmz8p,15,44.648,40.073;1,3mqhkh4iz4kesvmqui3hupgmi,11,52.347,43.999;1,e6ok0deqkoe80184iu509gzu2,27,52.58,33.089;3,6wfwy94p5bm0zv3aku0urfq39,40,100.245,32.224;4,6ekdnbnk56xlxforb5owt3dn9,1,5.268,33.556;:52.35,33.25,0; 1598184000100;100,1,0:0,a2s2c6anax9wnlsw1s6vunl5h,9,52.558,23.752;0,28x7i681wkjueanvkevew3bh1,17,60.104,43.669;0,4rlw04tze791jwt623h5ikg5x,26,67.72,15.741;0,72m5u1rlgh6e229zpfvp7f5jp,11,52.469,14.815;0,3djmsd2atvq16ysmocq7twsfd,20,68.527,30.17;0,4wqekpzlcztenasxob7a6tyax,7,56.361,51.728;0,dmrj53hcw8vcffxx0lfudbob9,2,68.417,51.641;0,aipyotc87m5bdvmvizd5n69k9,8,60.413,24.993;0,5g5wwp5luxo1rz1kp6chvz0x6,32,68.616,39.82;0,b0wb7gf04synxlkq7o8gvzkoa,22,61.575,34.241;1,9nhlgi0k783bq8a2704bu6n6d,7,45.597,27.465;1,976riwm0dz0e74d4l28y3ttcl,5,32.249,30.732;1,3sc349yey596xp2j6xlyt0frp,32,38.542,60.789;1,19djqex4nu3lz6vw08f3fwv6x,3,35.579,8.49;1,fe4b3ric67hz0hhih3tjf2eh,24,52.553,5.199;1,72d5uxwcmvhd6mzthxuvev1sl,2,30.6,44.031;1,8j2vmplbpj983xbxibhyvidx,8,52.289,55.702;1,8qmm84tue6kuz8e5nhhdhmz8p,15,44.67,39.953;1,3mqhkh4iz4kesvmqui3hupgmi,11,52.515,43.883;1,e6ok0deqkoe80184iu509gzu2,27,52.809,33.005;3,6wfwy94p5bm0zv3aku0urfq39,40,99.886,32.433;4,6ekdnbnk56xlxforb5owt3dn9,1,5.265,33.529;:50.615,35.325,0; 1598184000200;200,1,0:0,a2s2c6anax9wnlsw1s6vunl5h,9,52.31,23.901;0,28x7i681wkjueanvkevew3bh1,17,60.053,43.669;0,4rlw04tze791jwt623h5ikg5x,26,67.7,15.684;0,72m5u1rlgh6e229zpfvp7f5jp,11,52.38,14.997;0,3djmsd2atvq16ysmocq7twsfd,20,68.531,30.149;0,4wqekpzlcztenasxob7a6tyax,7,56.184,52.14;0,dmrj53hcw8vcffxx0lfudbob9,2,68.397,51.679;0,aipyotc87m5bdvmvizd5n69k9,8,60.286,25.073;0,5g5wwp5luxo1rz1kp6chvz0x6,32,68.528,39.879;0,b0wb7gf04synxlkq7o8gvzkoa,22,61.503,34.386;1,9nhlgi0k783bq8a2704bu6n6d,7,45.724,27.517;1,976riwm0dz0e74d4l28y3ttcl,5,32.171,30.726;1,3sc349yey596xp2j6xlyt0frp,32,38.56,60.853;1,19djqex4nu3lz6vw08f3fwv6x,3,35.578,8.463;1,fe4b3ric67hz0hhih3tjf2eh,24,52.654,5.237;1,72d5uxwcmvhd6mzthxuvev1sl,2,30.607,44.051;1,8j2vmplbpj983xbxibhyvidx,8,52.389,55.725;1,8qmm84tue6kuz8e5nhhdhmz8p,15,44.688,39.835;1,3mqhkh4iz4kesvmqui3hupgmi,11,52.682,43.767;1,e6ok0deqkoe80184iu509gzu2,27,53.024,32.937;3,6wfwy94p5bm0zv3aku0urfq39,40,99.547,32.617;4,6ekdnbnk56xlxforb5owt3dn9,1,5.264,33.502;:49.63,36.14,0; 1598184000300;300,1,0:0,a2s2c6anax9wnlsw1s6vunl5h,9,52.059,24.071;0,28x7i681wkjueanvkevew3bh1,17,59.994,43.668;0,4rlw04tze791jwt623h5ikg5x,26,67.683,15.641;0,72m5u1rlgh6e229zpfvp7f5jp,11,52.278,15.18;0,3djmsd2atvq16ysmocq7twsfd,20,68.531,30.124;0,4wqekpzlcztenasxob7a6tyax,7,56.042,52.435;0,dmrj53hcw8vcffxx0lfudbob9,2,68.384,51.711;0,aipyotc87m5bdvmvizd5n69k9,8,60.138,25.134;0,5g5wwp5luxo1rz1kp6chvz0x6,32,68.425,39.928;0,b0wb7gf04synxlkq7o8gvzkoa,22,61.424,34.524;1,9nhlgi0k783bq8a2704bu6n6d,7,45.849,27.567;1,976riwm0dz0e74d4l28y3ttcl,5,32.092,30.716;1,3sc349yey596xp2j6xlyt0frp,32,38.578,60.931;1,19djqex4nu3lz6vw08f3fwv6x,3,35.581,8.445;1,fe4b3ric67hz0hhih3tjf2eh,24,52.755,5.279;1,72d5uxwcmvhd6mzthxuvev1sl,2,30.616,44.086;1,8j2vmplbpj983xbxibhyvidx,8,52.515,55.745;1,8qmm84tue6kuz8e5nhhdhmz8p,15,44.7,39.718;1,3mqhkh4iz4kesvmqui3hupgmi,11,52.85,43.651;1,e6ok0deqkoe80184iu509gzu2,27,53.227,32.885;3,6wfwy94p5bm0zv3aku0urfq39,40,99.231,32.775;4,6ekdnbnk56xlxforb5owt3dn9,1,5.268,33.476;:48.725,36.625,0; @@ -89,4 +89,4 @@ 1598187606200;6200,2,0:0,a2s2c6anax9wnlsw1s6vunl5h,9,66.074,35.263;0,28x7i681wkjueanvkevew3bh1,17,57.533,24.73;0,4rlw04tze791jwt623h5ikg5x,26,37.165,50.894;0,72m5u1rlgh6e229zpfvp7f5jp,11,56.565,67.502;0,3djmsd2atvq16ysmocq7twsfd,20,30.799,41.437;0,dmrj53hcw8vcffxx0lfudbob9,2,37.405,11.317;0,aipyotc87m5bdvmvizd5n69k9,8,55.174,50.009;0,ct32113pfx5q9avf2c0x208ru,37,52.491,1.57;0,5g5wwp5luxo1rz1kp6chvz0x6,32,31.691,25.184;0,b0wb7gf04synxlkq7o8gvzkoa,22,42.75,41.405;1,9nhlgi0k783bq8a2704bu6n6d,7,53.564,44.821;1,976riwm0dz0e74d4l28y3ttcl,5,64.396,41.295;1,3sc349yey596xp2j6xlyt0frp,32,65.845,13.949;1,19djqex4nu3lz6vw08f3fwv6x,3,61.011,56.618;1,fe4b3ric67hz0hhih3tjf2eh,24,45.577,50.88;1,72d5uxwcmvhd6mzthxuvev1sl,2,66.011,31.464;1,8j2vmplbpj983xbxibhyvidx,8,49.894,18.36;1,8qmm84tue6kuz8e5nhhdhmz8p,15,55.71,31.465;1,3mqhkh4iz4kesvmqui3hupgmi,11,43.074,32.381;1,e6ok0deqkoe80184iu509gzu2,27,46.193,44.575;3,6wfwy94p5bm0zv3aku0urfq39,40,15.461,33.905;4,6ekdnbnk56xlxforb5owt3dn9,1,95.061,33.729;:33.3,27.31,0; 1598187606300;6300,2,0:0,a2s2c6anax9wnlsw1s6vunl5h,9,66.222,35.177;0,28x7i681wkjueanvkevew3bh1,17,57.625,24.512;0,4rlw04tze791jwt623h5ikg5x,26,37.21,50.924;0,72m5u1rlgh6e229zpfvp7f5jp,11,56.806,67.487;0,3djmsd2atvq16ysmocq7twsfd,20,30.883,41.415;0,dmrj53hcw8vcffxx0lfudbob9,2,37.373,11.158;0,aipyotc87m5bdvmvizd5n69k9,8,55.424,49.979;0,ct32113pfx5q9avf2c0x208ru,37,52.388,1.487;0,5g5wwp5luxo1rz1kp6chvz0x6,32,31.783,25.141;0,b0wb7gf04synxlkq7o8gvzkoa,22,42.885,41.138;1,9nhlgi0k783bq8a2704bu6n6d,7,53.705,44.473;1,976riwm0dz0e74d4l28y3ttcl,5,64.458,41.012;1,3sc349yey596xp2j6xlyt0frp,32,65.809,13.545;1,19djqex4nu3lz6vw08f3fwv6x,3,61.202,56.343;1,fe4b3ric67hz0hhih3tjf2eh,24,45.764,50.684;1,72d5uxwcmvhd6mzthxuvev1sl,2,66.016,31.061;1,8j2vmplbpj983xbxibhyvidx,8,49.952,18.189;1,8qmm84tue6kuz8e5nhhdhmz8p,15,55.762,30.999;1,3mqhkh4iz4kesvmqui3hupgmi,11,42.948,32.095;1,e6ok0deqkoe80184iu509gzu2,27,46.179,44.201;3,6wfwy94p5bm0zv3aku0urfq39,40,15.534,33.777;4,6ekdnbnk56xlxforb5owt3dn9,1,94.962,33.718;:33.21,26.995,0; 1598187606400;6400,2,0:0,a2s2c6anax9wnlsw1s6vunl5h,9,66.359,35.09;0,28x7i681wkjueanvkevew3bh1,17,57.711,24.288;0,4rlw04tze791jwt623h5ikg5x,26,37.266,50.945;0,72m5u1rlgh6e229zpfvp7f5jp,11,57.05,67.478;0,3djmsd2atvq16ysmocq7twsfd,20,30.965,41.395;0,dmrj53hcw8vcffxx0lfudbob9,2,37.349,10.999;0,aipyotc87m5bdvmvizd5n69k9,8,55.669,49.938;0,ct32113pfx5q9avf2c0x208ru,37,52.276,1.425;0,5g5wwp5luxo1rz1kp6chvz0x6,32,31.885,25.069;0,b0wb7gf04synxlkq7o8gvzkoa,22,43.024,40.854;1,9nhlgi0k783bq8a2704bu6n6d,7,53.857,44.125;1,976riwm0dz0e74d4l28y3ttcl,5,64.538,40.746;1,3sc349yey596xp2j6xlyt0frp,32,65.753,13.164;1,19djqex4nu3lz6vw08f3fwv6x,3,61.403,56.084;1,fe4b3ric67hz0hhih3tjf2eh,24,45.958,50.493;1,72d5uxwcmvhd6mzthxuvev1sl,2,66.026,30.66;1,8j2vmplbpj983xbxibhyvidx,8,50.008,17.993;1,8qmm84tue6kuz8e5nhhdhmz8p,15,55.819,30.524;1,3mqhkh4iz4kesvmqui3hupgmi,11,42.827,31.806;1,e6ok0deqkoe80184iu509gzu2,27,46.166,43.833;3,6wfwy94p5bm0zv3aku0urfq39,40,15.605,33.654;4,6ekdnbnk56xlxforb5owt3dn9,1,94.863,33.706;:32.71,25.44,0; -1598187606500;6500,2,0:0,a2s2c6anax9wnlsw1s6vunl5h,9,66.487,35.002;0,28x7i681wkjueanvkevew3bh1,17,57.792,24.059;0,4rlw04tze791jwt623h5ikg5x,26,37.335,50.96;0,72m5u1rlgh6e229zpfvp7f5jp,11,57.299,67.476;0,3djmsd2atvq16ysmocq7twsfd,20,31.048,41.376;0,dmrj53hcw8vcffxx0lfudbob9,2,37.337,10.84;0,aipyotc87m5bdvmvizd5n69k9,8,55.908,49.884;0,ct32113pfx5q9avf2c0x208ru,37,52.156,1.386;0,5g5wwp5luxo1rz1kp6chvz0x6,32,31.999,24.969;0,b0wb7gf04synxlkq7o8gvzkoa,22,43.169,40.555;1,9nhlgi0k783bq8a2704bu6n6d,7,54.023,43.781;1,976riwm0dz0e74d4l28y3ttcl,5,64.638,40.495;1,3sc349yey596xp2j6xlyt0frp,32,65.676,12.813;1,19djqex4nu3lz6vw08f3fwv6x,3,61.613,55.843;1,fe4b3ric67hz0hhih3tjf2eh,24,46.159,50.309;1,72d5uxwcmvhd6mzthxuvev1sl,2,66.042,30.263;1,8j2vmplbpj983xbxibhyvidx,8,50.063,17.769;1,8qmm84tue6kuz8e5nhhdhmz8p,15,55.883,30.038;1,3mqhkh4iz4kesvmqui3hupgmi,11,42.713,31.513;1,e6ok0deqkoe80184iu509gzu2,27,46.156,43.47;3,6wfwy94p5bm0zv3aku0urfq39,40,15.675,33.533;4,6ekdnbnk56xlxforb5owt3dn9,1,94.763,33.697;:32.605,24.855,0; \ No newline at end of file +1598187606500;6500,2,0:0,a2s2c6anax9wnlsw1s6vunl5h,9,66.487,35.002;0,28x7i681wkjueanvkevew3bh1,17,57.792,24.059;0,4rlw04tze791jwt623h5ikg5x,26,37.335,50.96;0,72m5u1rlgh6e229zpfvp7f5jp,11,57.299,67.476;0,3djmsd2atvq16ysmocq7twsfd,20,31.048,41.376;0,dmrj53hcw8vcffxx0lfudbob9,2,37.337,10.84;0,aipyotc87m5bdvmvizd5n69k9,8,55.908,49.884;0,ct32113pfx5q9avf2c0x208ru,37,52.156,1.386;0,5g5wwp5luxo1rz1kp6chvz0x6,32,31.999,24.969;0,b0wb7gf04synxlkq7o8gvzkoa,22,43.169,40.555;1,9nhlgi0k783bq8a2704bu6n6d,7,54.023,43.781;1,976riwm0dz0e74d4l28y3ttcl,5,64.638,40.495;1,3sc349yey596xp2j6xlyt0frp,32,65.676,12.813;1,19djqex4nu3lz6vw08f3fwv6x,3,61.613,55.843;1,fe4b3ric67hz0hhih3tjf2eh,24,46.159,50.309;1,72d5uxwcmvhd6mzthxuvev1sl,2,66.042,30.263;1,8j2vmplbpj983xbxibhyvidx,8,50.063,17.769;1,8qmm84tue6kuz8e5nhhdhmz8p,15,55.883,30.038;1,3mqhkh4iz4kesvmqui3hupgmi,11,42.713,31.513;1,e6ok0deqkoe80184iu509gzu2,27,46.156,43.47;3,6wfwy94p5bm0zv3aku0urfq39,40,15.675,33.533;4,6ekdnbnk56xlxforb5owt3dn9,1,94.763,33.697;:32.605,24.855,0; diff --git a/kloppy/tests/test_statsperform.py b/kloppy/tests/test_statsperform.py index 58d0d128..74f8412a 100644 --- a/kloppy/tests/test_statsperform.py +++ b/kloppy/tests/test_statsperform.py @@ -1,86 +1,64 @@ -from pathlib import Path from datetime import datetime, timedelta +from pathlib import Path import pytest from kloppy import statsperform from kloppy.domain import ( - AttackingDirection, - DatasetType, Orientation, Point, Point3D, Provider, + TrackingDataset, + DatasetFlag, + SportVUCoordinateSystem, ) -@pytest.fixture -def meta_data_xml(base_dir) -> Path: - return base_dir / "files/statsperform_ma1_metadata.xml" +@pytest.fixture(scope="module") +def meta_data_xml(base_dir: Path) -> Path: + return base_dir / "files" / "statsperform_ma1_metadata.xml" -@pytest.fixture -def meta_data_json(base_dir) -> Path: - return base_dir / "files/statsperform_ma1_metadata.json" +@pytest.fixture(scope="module") +def meta_data_json(base_dir: Path) -> Path: + return base_dir / "files" / "statsperform_ma1_metadata.json" -@pytest.fixture -def raw_data(base_dir) -> Path: - return base_dir / "files/statsperform_ma25_tracking.txt" +@pytest.fixture(scope="module") +def raw_data(base_dir: Path) -> Path: + return base_dir / "files" / "statsperform_ma25_tracking.txt" + + +@pytest.fixture(scope="module", params=["xml", "json"]) +def dataset( + request: pytest.FixtureRequest, + raw_data: Path, + meta_data_xml: Path, + meta_data_json: Path, +) -> TrackingDataset: + return statsperform.load( + meta_data=meta_data_xml if request.param == "xml" else meta_data_json, + raw_data=raw_data, + only_alive=False, + provider_name="sportvu", + coordinates="sportvu", + ) -@pytest.mark.parametrize( - "meta_data", - [ - pytest.lazy_fixture("meta_data_xml"), - pytest.lazy_fixture("meta_data_json"), - ], -) class TestStatsPerformTracking: - def test_correct_deserialization(self, meta_data: Path, raw_data: Path): - dataset = statsperform.load( - meta_data=meta_data, - raw_data=raw_data, - only_alive=False, - coordinates="statsperform", - ) + """Tests related to deserializing tracking data delivered by StatsPerform.""" - # Check provider, type, shape, orientation + def test_provider(self, dataset: TrackingDataset): assert dataset.metadata.provider == Provider.STATSPERFORM - assert dataset.dataset_type == DatasetType.TRACKING - assert len(dataset.records) == 92 - assert len(dataset.metadata.periods) == 2 - assert dataset.metadata.orientation == Orientation.AWAY_HOME - # Check the periods - assert dataset.metadata.periods[1].id == 1 - 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 == datetime( - 2020, 8, 23, 12, 6, 22 - ) - assert dataset.metadata.periods[2].end_timestamp == datetime( - 2020, 8, 23, 12, 56, 30 - ) + def test_orientation(self, dataset: TrackingDataset): + assert dataset.metadata.orientation == Orientation.AWAY_HOME - # Check some timestamps - 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 + def test_framerate(self, dataset: TrackingDataset): + assert dataset.metadata.frame_rate == 10.0 - # Check some players + def test_teams(self, dataset: TrackingDataset): home_team = dataset.metadata.teams[0] home_player = home_team.players[2] assert home_player.player_id == "5g5wwp5luxo1rz1kp6chvz0x6" @@ -109,30 +87,121 @@ def test_correct_deserialization(self, meta_data: Path, raw_data: Path): assert not away_substitute.starting assert away_substitute.team == away_team - # Check the ball - assert dataset.records[1].ball_coordinates == Point3D( - x=50.615, y=35.325, z=0.0 + def test_periods(self, dataset: TrackingDataset): + assert len(dataset.metadata.periods) == 2 + assert dataset.metadata.periods[0].id == 1 + assert dataset.metadata.periods[0].start_timestamp == datetime( + 2020, 8, 23, 11, 0, 10 + ) + assert dataset.metadata.periods[0].end_timestamp == datetime( + 2020, 8, 23, 11, 48, 15 ) - # Check pitch dimensions + assert dataset.metadata.periods[1].id == 2 + assert dataset.metadata.periods[1].start_timestamp == datetime( + 2020, 8, 23, 12, 6, 22 + ) + assert dataset.metadata.periods[1].end_timestamp == datetime( + 2020, 8, 23, 12, 56, 30 + ) + + def test_flags(self, dataset): + assert dataset.metadata.flags == DatasetFlag.BALL_STATE + + def test_coordinate_system_without_pitch_dimensions( + self, raw_data: Path, meta_data_xml: Path + ): + dataset = statsperform.load( + meta_data=meta_data_xml, + raw_data=raw_data, + provider_name="sportvu", + coordinates="sportvu", + ) + coordinate_system = dataset.metadata.coordinate_system pitch_dimensions = dataset.metadata.pitch_dimensions + assert coordinate_system == SportVUCoordinateSystem( + # StatsPerform does not provide pitch dimensions + pitch_length=None, + pitch_width=None, + ) assert pitch_dimensions.x_dim.min == 0 - assert pitch_dimensions.x_dim.max == 100 + assert pitch_dimensions.x_dim.max == None assert pitch_dimensions.y_dim.min == 0 - assert pitch_dimensions.y_dim.max == 100 + assert pitch_dimensions.y_dim.max == None - def test_correct_normalized_deserialization( - self, meta_data: str, raw_data: str + def test_coordinate_system_with_pitch_dimensions( + self, raw_data: Path, meta_data_xml: Path ): dataset = statsperform.load( - meta_data=meta_data, + meta_data=meta_data_xml, raw_data=raw_data, - only_alive=False, + provider_name="sportvu", + coordinates="sportvu", + pitch_length=105, + pitch_width=68, + ) + coordinate_system = dataset.metadata.coordinate_system + pitch_dimensions = dataset.metadata.pitch_dimensions + assert coordinate_system == SportVUCoordinateSystem( + # StatsPerform does not provide pitch dimensions + pitch_length=105, + pitch_width=68, + ) + assert pitch_dimensions.x_dim.min == 0 + assert pitch_dimensions.x_dim.max == 105 + assert pitch_dimensions.y_dim.min == 0 + assert pitch_dimensions.y_dim.max == 68 + + def test_deserialize_all(self, dataset: TrackingDataset): + assert len(dataset.records) == 92 + + def test_deserialize_only_alive(self, raw_data: Path, meta_data_xml: Path): + dataset = statsperform.load( + provider_name="sportvu", + meta_data=meta_data_xml, + raw_data=raw_data, + only_alive=True, + coordinates="sportvu", + ) + assert len(dataset.records) == 91 + + def test_timestamps(self, dataset: TrackingDataset): + 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 + + def test_ball_coordinates(self, dataset: TrackingDataset): + assert dataset.records[1].ball_coordinates == Point3D( + x=50.615, y=35.325, z=0.0 ) + def test_player_coordinates(self, dataset: TrackingDataset): home_player = dataset.metadata.teams[0].players[2] assert dataset.records[0].players_coordinates[home_player] == Point( - x=0.6868899999999999, y=0.6025 + x=68.689, y=39.750 + ) + + def test_correct_normalized_deserialization( + self, raw_data: Path, meta_data_xml: Path + ): + dataset = statsperform.load( + provider_name="sportvu", + pitch_length=105, + pitch_width=68, + meta_data=meta_data_xml, + raw_data=raw_data, + only_alive=False, + coordinates="kloppy", + ) + + assert dataset.records[1].ball_coordinates == Point3D( + x=50.615 / 105, y=1 - 35.325 / 68, z=0.0 ) # Check normalised pitch dimensions @@ -141,3 +210,12 @@ def test_correct_normalized_deserialization( assert pitch_dimensions.x_dim.max == 1.0 assert pitch_dimensions.y_dim.min == 0.0 assert pitch_dimensions.y_dim.max == 1.0 + + # Pitch dimensions are required to transform coordinates + with pytest.raises(ValueError): + statsperform.load( + provider_name="sportvu", + meta_data=meta_data_xml, + raw_data=raw_data, + coordinates="kloppy", + )