Skip to content

Commit 2af3859

Browse files
authored
feat(Signality): add support for Signality tracking data (#356)
1 parent b0828d3 commit 2af3859

File tree

11 files changed

+4033
-34
lines changed

11 files changed

+4033
-34
lines changed

kloppy/_providers/signality.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from typing import Optional, Iterable
2+
3+
from kloppy.domain import TrackingDataset, CoordinateSystem
4+
from kloppy.infra.serializers.tracking.signality import (
5+
SignalityDeserializer,
6+
SignalityInputs,
7+
)
8+
from kloppy.io import FileLike, open_as_file, expand_inputs
9+
10+
11+
def load(
12+
meta_data: FileLike,
13+
raw_data_feeds: Iterable[FileLike],
14+
venue_information: FileLike,
15+
sample_rate: Optional[float] = None,
16+
limit: Optional[int] = None,
17+
coordinates: Optional[str] = None,
18+
) -> TrackingDataset:
19+
"""
20+
Load and deserialize tracking data from multiple input files.
21+
22+
Args:
23+
meta_data (FileLike): json feed containing metadata information of the game.
24+
raw_data_feeds (Iterable[FileLike]): json feeds containing raw tracking data feeds.
25+
venue_information (FileLike): json feed containing venue information where the game was played.
26+
sample_rate (Optional[float]): Sampling rate to be applied during deserialization (default: None).
27+
limit (Optional[int]): Limit on the number of frames to process (default: None).
28+
coordinates (Optional[str]): Coordinate system to use for deserialization (default: None).
29+
30+
Returns:
31+
TrackingDataset: A deserialized tracking dataset object.
32+
"""
33+
34+
raw_data_feeds = expand_inputs(raw_data_feeds)
35+
36+
deserializer = SignalityDeserializer(
37+
sample_rate=sample_rate,
38+
limit=limit,
39+
coordinate_system=coordinates,
40+
)
41+
42+
with open_as_file(meta_data) as meta_data_fp, open_as_file(
43+
venue_information
44+
) as venue_information_fp:
45+
return deserializer.deserialize(
46+
inputs=SignalityInputs(
47+
meta_data=meta_data_fp,
48+
venue_information=venue_information_fp,
49+
raw_data_feeds=raw_data_feeds,
50+
)
51+
)

kloppy/domain/models/common.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ class Provider(Enum):
112112
STATSPERFORM = "statsperform"
113113
HAWKEYE = "hawkeye"
114114
SPORTVU = "sportvu"
115+
SIGNALITY = "signality"
115116
OTHER = "other"
116117

117118
def __str__(self):
@@ -866,6 +867,43 @@ def pitch_dimensions(self) -> PitchDimensions:
866867
)
867868

868869

870+
class SignalityCoordinateSystem(ProviderCoordinateSystem):
871+
@property
872+
def provider(self) -> Provider:
873+
return Provider.SIGNALITY
874+
875+
@property
876+
def origin(self) -> Origin:
877+
return Origin.CENTER
878+
879+
@property
880+
def vertical_orientation(self) -> VerticalOrientation:
881+
return VerticalOrientation.BOTTOM_TO_TOP
882+
883+
@property
884+
def pitch_dimensions(self) -> PitchDimensions:
885+
if self._pitch_length is not None and self._pitch_width is not None:
886+
return MetricPitchDimensions(
887+
x_dim=Dimension(
888+
-1 * self._pitch_length / 2, self._pitch_length / 2
889+
),
890+
y_dim=Dimension(
891+
-1 * self._pitch_width / 2, self._pitch_width / 2
892+
),
893+
pitch_length=self._pitch_length,
894+
pitch_width=self._pitch_width,
895+
standardized=False,
896+
)
897+
else:
898+
return MetricPitchDimensions(
899+
x_dim=Dimension(None, None),
900+
y_dim=Dimension(None, None),
901+
pitch_length=None,
902+
pitch_width=None,
903+
standardized=False,
904+
)
905+
906+
869907
class DatafactoryCoordinateSystem(ProviderCoordinateSystem):
870908
@property
871909
def provider(self) -> Provider:
@@ -1012,6 +1050,7 @@ def build_coordinate_system(
10121050
Provider.SECONDSPECTRUM: SecondSpectrumCoordinateSystem,
10131051
Provider.HAWKEYE: HawkEyeCoordinateSystem,
10141052
Provider.SPORTVU: SportVUCoordinateSystem,
1053+
Provider.SIGNALITY: SignalityCoordinateSystem,
10151054
}
10161055

10171056
if provider in coordinate_systems:

kloppy/domain/services/__init__.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from typing import List
1+
from collections import defaultdict, Counter
2+
from typing import List, Dict
23

3-
from kloppy.domain import AttackingDirection, Frame, Ground
4+
from kloppy.domain import AttackingDirection, Frame, Ground, Period
45

56
from .event_factory import EventFactory, create_event
67
from .transformers import DatasetTransformer, DatasetTransformerBuilder
@@ -37,10 +38,42 @@ def attacking_direction_from_frame(frame: Frame) -> AttackingDirection:
3738
return AttackingDirection.RTL
3839

3940

41+
def attacking_directions_from_multi_frames(
42+
frames: List[Frame], periods: List[Period]
43+
) -> Dict[int, AttackingDirection]:
44+
"""
45+
with only partial tracking data we cannot rely on a single frame to
46+
infer the attacking directions as a simple average of only some players
47+
x-coords might not reflect the attacking direction.
48+
"""
49+
attacking_directions = {}
50+
51+
# Group attacking directions by period ID
52+
period_direction_map = defaultdict(list)
53+
for frame in frames:
54+
if len(frame.players_data) > 0:
55+
direction = attacking_direction_from_frame(frame)
56+
else:
57+
direction = AttackingDirection.NOT_SET
58+
period_direction_map[frame.period.id].append(direction)
59+
60+
# Determine the most common attacking direction for each period
61+
for period in periods:
62+
period_id = period.id
63+
if period_id in period_direction_map:
64+
count = Counter(period_direction_map[period_id])
65+
attacking_directions[period_id] = count.most_common(1)[0][0]
66+
else:
67+
attacking_directions[period_id] = AttackingDirection.NOT_SET
68+
69+
return attacking_directions
70+
71+
4072
__all__ = [
4173
"DatasetTransformer",
4274
"DatasetTransformerBuilder",
4375
"EventFactory",
4476
"create_event",
4577
"attacking_direction_from_frame",
78+
"attacking_directions_from_multi_frames",
4679
]

0 commit comments

Comments
 (0)