Skip to content

Commit

Permalink
fix(statsperform): bugfixes and cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
probberechts committed Apr 13, 2024
1 parent 4af1f76 commit 49ac769
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 190 deletions.
7 changes: 3 additions & 4 deletions kloppy/_providers/opta.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ def load(
coordinate_system=coordinates,
event_factory=event_factory or get_config("event_factory"),
)
with (
open_as_file(f7_data) as f7_data_fp,
open_as_file(f24_data) as f24_data_fp,
):
with open_as_file(f7_data) as f7_data_fp, open_as_file(
f24_data
) as f24_data_fp:
return deserializer.deserialize(
inputs=StatsPerformInputs(
meta_data=f7_data_fp,
Expand Down
91 changes: 52 additions & 39 deletions kloppy/_providers/statsperform.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
"""Functions to load Stats Perform data."""
from typing import List, Optional

from kloppy.config import get_config
from kloppy.domain import (
EventDataset,
EventFactory,
TrackingDataset,
EventType,
Provider,
)
from kloppy.infra.serializers.tracking.statsperform import (
StatsPerformDeserializer as StatsPerformTrackingDeserializer,
StatsPerformInputs as StatsPerformTrackingInputs,
TrackingDataset,
)
from kloppy.infra.serializers.event.statsperform import (
StatsPerformDeserializer as StatsPerformEventDeserializer,
StatsPerformInputs as StatsPerformEventInputs,
StatsPerformDeserializer as StatsPerformEventDeserializer,
)
from kloppy.infra.serializers.tracking.statsperform import (
StatsPerformInputs as StatsPerformTrackingInputs,
StatsPerformDeserializer as StatsPerformTrackingDeserializer,
)
from kloppy.config import get_config
from kloppy.io import FileLike, open_as_file
from kloppy.utils import deprecated

Expand All @@ -24,32 +25,24 @@
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",
tracking_system: 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 = StatsPerformTrackingDeserializer(
provider=Provider[provider_name.upper()],
provider=Provider[tracking_system.upper()],
sample_rate=sample_rate,
limit=limit,
coordinate_system=coordinates,
only_alive=only_alive,
)
with (
open_as_file(meta_data) as meta_data_fp,
open_as_file(raw_data) as raw_data_fp,
):
with open_as_file(meta_data) as meta_data_fp, open_as_file(
raw_data
) as raw_data_fp:
return deserializer.deserialize(
inputs=StatsPerformTrackingInputs(
meta_data=meta_data_fp,
Expand All @@ -63,69 +56,89 @@ def load(
def load_event(
ma1_data: FileLike,
ma3_data: FileLike,
pitch_length: Optional[float] = None,
pitch_width: Optional[float] = None,
event_types: Optional[List[str | EventType]] = None,
coordinates: Optional[str] = None,
event_factory: Optional[EventFactory] = None,
) -> EventDataset:
"""
Load Stats Perform event data into a [`EventDataset`][kloppy.domain.models.event.EventDataset]
"""Load Stats Perform event data.
Args:
ma1_data: MA1 json or xml feed containing the lineup information
ma3_data: MA3 json or xml feed containing the events
event_types:
coordinates:
event_factory:
pitch_length: length of the pitch (in meters)
pitch_width: width of the pitch (in meters)
event_types: list of event types to load
coordinates: coordinate system to use
event_factory: a custom event factory
Returns:
EventDataset: the loaded event data
"""
deserializer = StatsPerformEventDeserializer(
event_types=event_types,
coordinate_system=coordinates,
event_factory=event_factory or get_config("event_factory"),
event_factory=event_factory or get_config("event_factory"), # type: ignore
)
with (
open_as_file(ma1_data) as ma1_data_fp,
open_as_file(ma3_data) as ma3_data_fp,
):
with open_as_file(ma1_data) as ma1_data_fp, open_as_file(
ma3_data
) as ma3_data_fp:
return deserializer.deserialize(
inputs=StatsPerformEventInputs(
meta_data=ma1_data_fp,
meta_feed="MA1",
event_data=ma3_data_fp,
event_feed="MA3",
pitch_length=pitch_length,
pitch_width=pitch_width,
),
)


def load_tracking(
ma1_data: FileLike,
ma25_data: FileLike,
tracking_system: 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:
"""
Load Stats Perform tracking data into a [`TrackingDataset`][kloppy.domain.models.tracking.TrackingDataset]
Load Stats Perform tracking data.
Args:
ma1_data: json or xml feed containing the lineup information
ma25_data: txt file containing the tracking data
event_types:
coordinates:
event_factory:
tracking_system: system that generated the tracking data
pitch_length: length of the pitch (in meters)
pitch_width: width of the pitch (in meters)
sample_rate: sample the data at a specific rate
limit: limit the number of frames loaded
coordinates: coordinate system to use
only_alive: only include frames in which the game is not paused
Returns:
TrackingDataset: the loaded tracking data
"""
deserializer = StatsPerformTrackingDeserializer(
provider=Provider[tracking_system.upper()],
sample_rate=sample_rate,
limit=limit,
coordinate_system=coordinates,
only_alive=only_alive,
)
with (
open_as_file(ma1_data) as ma1_data_fp,
open_as_file(ma25_data) as ma25_data_fp,
):
with open_as_file(ma1_data) as ma1_data_fp, open_as_file(
ma25_data
) as ma25_data_fp:
return deserializer.deserialize(
inputs=StatsPerformTrackingInputs(
meta_data=ma1_data_fp, raw_data=ma25_data_fp
meta_data=ma1_data_fp,
raw_data=ma25_data_fp,
pitch_length=pitch_length,
pitch_width=pitch_width,
)
)
110 changes: 81 additions & 29 deletions kloppy/domain/models/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,13 +556,26 @@ def vertical_orientation(self) -> VerticalOrientation:

@property
def pitch_dimensions(self) -> PitchDimensions:
return MetricPitchDimensions(
x_dim=Dimension(-1 * self.pitch_length / 2, self.pitch_length / 2),
y_dim=Dimension(-1 * self.pitch_width / 2, self.pitch_width / 2),
pitch_length=self.pitch_length,
pitch_width=self.pitch_width,
standardized=False,
).convert(to_unit=Unit.CENTIMETERS)
if self.pitch_length is not None and self.pitch_width is not None:
return MetricPitchDimensions(
x_dim=Dimension(
-1 * self.pitch_length / 2, self.pitch_length / 2
),
y_dim=Dimension(
-1 * self.pitch_width / 2, self.pitch_width / 2
),
pitch_length=self.pitch_length,
pitch_width=self.pitch_width,
standardized=False,
).convert(to_unit=Unit.CENTIMETERS)
else:
return MetricPitchDimensions(
x_dim=Dimension(None, None),
y_dim=Dimension(None, None),
pitch_length=None,
pitch_width=None,
standardized=False,
).convert(to_unit=Unit.CENTIMETERS)


@dataclass
Expand All @@ -581,13 +594,26 @@ def vertical_orientation(self) -> VerticalOrientation:

@property
def pitch_dimensions(self) -> PitchDimensions:
return MetricPitchDimensions(
x_dim=Dimension(-1 * self.pitch_length / 2, self.pitch_length / 2),
y_dim=Dimension(-1 * self.pitch_width / 2, self.pitch_width / 2),
pitch_length=self.pitch_length,
pitch_width=self.pitch_width,
standardized=False,
)
if self.pitch_length is not None and self.pitch_width is not None:
return MetricPitchDimensions(
x_dim=Dimension(
-1 * self.pitch_length / 2, self.pitch_length / 2
),
y_dim=Dimension(
-1 * self.pitch_width / 2, self.pitch_width / 2
),
pitch_length=self.pitch_length,
pitch_width=self.pitch_width,
standardized=False,
)
else:
return MetricPitchDimensions(
x_dim=Dimension(None, None),
y_dim=Dimension(None, None),
pitch_length=None,
pitch_width=None,
standardized=False,
)


@dataclass
Expand Down Expand Up @@ -652,13 +678,26 @@ def vertical_orientation(self) -> VerticalOrientation:

@property
def pitch_dimensions(self) -> PitchDimensions:
return MetricPitchDimensions(
x_dim=Dimension(-self.pitch_length / 2, self.pitch_length / 2),
y_dim=Dimension(-self.pitch_width / 2, self.pitch_width / 2),
pitch_length=self.pitch_length,
pitch_width=self.pitch_width,
standardized=False,
)
if self.pitch_length is not None and self.pitch_width is not None:
return MetricPitchDimensions(
x_dim=Dimension(
-1 * self.pitch_length / 2, self.pitch_length / 2
),
y_dim=Dimension(
-1 * self.pitch_width / 2, self.pitch_width / 2
),
pitch_length=self.pitch_length,
pitch_width=self.pitch_width,
standardized=False,
)
else:
return MetricPitchDimensions(
x_dim=Dimension(None, None),
y_dim=Dimension(None, None),
pitch_length=None,
pitch_width=None,
standardized=False,
)


@dataclass
Expand Down Expand Up @@ -722,13 +761,26 @@ def vertical_orientation(self) -> VerticalOrientation:

@property
def pitch_dimensions(self) -> PitchDimensions:
return MetricPitchDimensions(
x_dim=Dimension(-1 * self.pitch_length / 2, self.pitch_length / 2),
y_dim=Dimension(-1 * self.pitch_width / 2, self.pitch_width / 2),
pitch_length=self.pitch_length,
pitch_width=self.pitch_width,
standardized=False,
)
if self.pitch_length is not None and self.pitch_width is not None:
return MetricPitchDimensions(
x_dim=Dimension(
-1 * self.pitch_length / 2, self.pitch_length / 2
),
y_dim=Dimension(
-1 * self.pitch_width / 2, self.pitch_width / 2
),
pitch_length=self.pitch_length,
pitch_width=self.pitch_width,
standardized=False,
)
else:
return MetricPitchDimensions(
x_dim=Dimension(None, None),
y_dim=Dimension(None, None),
pitch_length=None,
pitch_width=None,
standardized=False,
)


@dataclass
Expand Down Expand Up @@ -769,7 +821,7 @@ def pitch_dimensions(self) -> PitchDimensions:
class SportVUCoordinateSystem(CoordinateSystem):
@property
def provider(self) -> Provider:
return Provider.SportVU
return Provider.SPORTVU

@property
def origin(self) -> Origin:
Expand Down
14 changes: 5 additions & 9 deletions kloppy/domain/models/pitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from math import sqrt
from typing import Optional

from kloppy.exceptions import KloppyError
from kloppy.exceptions import MissingDimensionError

DEFAULT_PITCH_LENGTH = 105.0
DEFAULT_PITCH_WIDTH = 68.0
Expand Down Expand Up @@ -100,17 +100,13 @@ class Dimension:
def to_base(self, value: float) -> float:
"""Map a value from this dimension to [0, 1]."""
if self.min is None or self.max is None:
raise KloppyError(
"The pitch boundaries need to be fully specified to convert coordinates."
)
raise MissingDimensionError()
return (value - self.min) / (self.max - self.min)

def from_base(self, value: float) -> float:
"""Map a value from [0, 1] to this dimension."""
if self.min is None or self.max is None:
raise KloppyError(
"The pitch boundaries need to be fully specified to convert coordinates."
)
raise MissingDimensionError()
return value * (self.max - self.min) + self.min


Expand Down Expand Up @@ -292,7 +288,7 @@ def to_metric_base(
or self.y_dim.min is None
or self.y_dim.max is None
):
raise KloppyError(
raise MissingDimensionError(
"The pitch boundaries need to be fully specified to convert coordinates."
)

Expand Down Expand Up @@ -406,7 +402,7 @@ def from_metric_base(
or self.y_dim.min is None
or self.y_dim.max is None
):
raise KloppyError(
raise MissingDimensionError(
"The pitch boundaries need to be fully specified to convert coordinates."
)

Expand Down
Loading

0 comments on commit 49ac769

Please sign in to comment.