Skip to content

Commit

Permalink
Merge pull request #334 from my-game-plan/feature/position-types
Browse files Browse the repository at this point in the history
Create standardized PositionType class
  • Loading branch information
koenvo authored Oct 25, 2024
2 parents a2112f8 + c648e9f commit 86078dc
Show file tree
Hide file tree
Showing 22 changed files with 537 additions and 401 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/ambv/black
rev: 23.3.0
rev: 22.3.0
hooks:
- id: black
language_version: python3
36 changes: 9 additions & 27 deletions kloppy/domain/models/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
Iterable,
)

from .position import PositionType

from ...utils import deprecated

if sys.version_info >= (3, 8):
Expand All @@ -33,7 +35,6 @@
from .pitch import (
PitchDimensions,
Unit,
Point,
Dimension,
NormalizedPitchDimensions,
MetricPitchDimensions,
Expand All @@ -47,7 +48,6 @@
OrientationError,
InvalidFilterError,
KloppyParameterError,
KloppyError,
)


Expand Down Expand Up @@ -119,20 +119,6 @@ def __str__(self):
return self.value


@dataclass(frozen=True)
class Position:
position_id: str
name: str
coordinates: Optional[Point] = None

def __str__(self):
return self.name

@classmethod
def unknown(cls) -> "Position":
return cls(position_id="", name="Unknown")


@dataclass(frozen=True)
class Player:
"""
Expand All @@ -157,8 +143,8 @@ class Player:

# match specific
starting: bool = False
starting_position: Optional[Position] = None
positions: TimeContainer[Position] = field(
starting_position: Optional[PositionType] = None
positions: TimeContainer[PositionType] = field(
default_factory=TimeContainer, compare=False
)

Expand All @@ -174,7 +160,7 @@ def full_name(self):

@property
@deprecated("starting_position or positions should be used")
def position(self) -> Optional[Position]:
def position(self) -> Optional[PositionType]:
try:
return self.positions.last()
except KeyError:
Expand All @@ -191,7 +177,7 @@ def __eq__(self, other):
return False
return self.player_id == other.player_id

def set_position(self, time: Time, position: Optional[Position]):
def set_position(self, time: Time, position: Optional[PositionType]):
self.positions.set(time, position)


Expand Down Expand Up @@ -235,15 +221,11 @@ def get_player_by_jersey_number(self, jersey_no: int):

return None

def get_player_by_position(self, position_id: Union[int, str], time: Time):
position_id = str(position_id)
def get_player_by_position(self, position: PositionType, time: Time):
for player in self.players:
if player.positions.items:
player_position = player.positions.value_at(time)
if (
player_position
and player_position.position_id == position_id
):
if player_position and player_position == position:
return player

return None
Expand Down Expand Up @@ -1098,7 +1080,7 @@ def _init_player_positions(self):
if player.starting:
player.set_position(
start_of_match,
player.starting_position or Position.unknown(),
player.starting_position or PositionType.unknown(),
)

def _update_formations_and_positions(self):
Expand Down
6 changes: 3 additions & 3 deletions kloppy/domain/models/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
DatasetType,
AttackingDirection,
OrientationError,
Position,
PositionType,
)
from kloppy.utils import (
camelcase_to_snakecase,
Expand Down Expand Up @@ -881,7 +881,7 @@ class SubstitutionEvent(Event):
"""

replacement_player: Player
position: Optional[Position] = None
position: Optional[PositionType] = None

event_type: EventType = EventType.SUBSTITUTION
event_name: str = "substitution"
Expand Down Expand Up @@ -948,7 +948,7 @@ class FormationChangeEvent(Event):
"""

formation_type: FormationType
player_positions: Optional[Dict[Player, Position]] = None
player_positions: Optional[Dict[Player, PositionType]] = None

event_type: EventType = EventType.FORMATION_CHANGE
event_name: str = "formation_change"
Expand Down
92 changes: 92 additions & 0 deletions kloppy/domain/models/position.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from enum import Enum


class PositionType(Enum):
Unknown = ("Unknown", "UNK", None)

Goalkeeper = ("Goalkeeper", "GK", None)

Defender = ("Defender", "DEF", None)
FullBack = ("Full Back", "FB", "Defender")
LeftBack = ("Left Back", "LB", "FullBack")
RightBack = ("Right Back", "RB", "FullBack")
CenterBack = ("Center Back", "CB", "Defender")
LeftCenterBack = ("Left Center Back", "LCB", "CenterBack")
RightCenterBack = ("Right Center Back", "RCB", "CenterBack")

Midfielder = ("Midfielder", "MID", None)
DefensiveMidfield = ("Defensive Midfield", "DM", "Midfielder")
LeftDefensiveMidfield = (
"Left Defensive Midfield",
"LDM",
"DefensiveMidfield",
)
CenterDefensiveMidfield = (
"Center Defensive Midfield",
"CDM",
"DefensiveMidfield",
)
RightDefensiveMidfield = (
"Right Defensive Midfield",
"RDM",
"DefensiveMidfield",
)

CentralMidfield = ("Central Midfield", "CM", "Midfielder")
LeftCentralMidfield = ("Left Central Midfield", "LCM", "CentralMidfield")
CenterMidfield = ("Center Midfield", "CM", "CentralMidfield")
RightCentralMidfield = ("Right Central Midfield", "RCM", "CentralMidfield")

AttackingMidfield = ("Attacking Midfield", "AM", "Midfielder")
LeftAttackingMidfield = (
"Left Attacking Midfield",
"LAM",
"AttackingMidfield",
)
CenterAttackingMidfield = (
"Center Attacking Midfield",
"CAM",
"AttackingMidfield",
)
RightAttackingMidfield = (
"Right Attacking Midfield",
"RAM",
"AttackingMidfield",
)

WideMidfield = ("Wide Midfield", "WM", "Midfielder")
LeftWing = ("Left Wing", "LW", "WideMidfield")
RightWing = ("Right Wing", "RW", "WideMidfield")
LeftMidfield = ("Left Midfield", "LM", "WideMidfield")
RightMidfield = ("Right Midfield", "RM", "WideMidfield")

Attacker = ("Attacker", "ATT", None)
LeftForward = ("Left Forward", "LF", "Attacker")
Striker = ("Striker", "ST", "Attacker")
RightForward = ("Right Forward", "RF", "Attacker")

def __init__(self, long_name, code, parent):
self.long_name = long_name
self.code = code
self._parent = parent

@property
def parent(self):
if self._parent:
return PositionType[self._parent]
return None

def is_subtype_of(self, other):
current = self
while current is not None:
if current == other:
return True
current = current.parent
return False

def __str__(self):
return self.long_name

@classmethod
def unknown(cls) -> "PositionType":
return cls.Unknown
4 changes: 2 additions & 2 deletions kloppy/domain/services/aggregators/minutes_played.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import timedelta
from typing import List, NamedTuple, Union

from kloppy.domain import EventDataset, Player, Position, Time
from kloppy.domain import EventDataset, Player, Time, PositionType
from kloppy.domain.services.aggregators.aggregator import (
EventDatasetAggregator,
)
Expand All @@ -16,7 +16,7 @@ class MinutesPlayed(NamedTuple):

class MinutesPlayedPerPosition(NamedTuple):
player: Player
position: Position
position: PositionType
start_time: Time
end_time: Time
duration: timedelta
Expand Down
39 changes: 27 additions & 12 deletions kloppy/infra/serializers/event/sportec/deserializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,50 @@
Provider,
Metadata,
Player,
Position,
SetPieceQualifier,
SetPieceType,
BodyPartQualifier,
BodyPart,
Qualifier,
CardType,
AttackingDirection,
PositionType,
)
from kloppy.exceptions import DeserializationError
from kloppy.infra.serializers.event.deserializer import EventDataDeserializer
from kloppy.utils import performance_logging


position_types_mapping: Dict[str, PositionType] = {
"TW": PositionType.Goalkeeper,
"IVR": PositionType.RightCenterBack,
"IVL": PositionType.LeftCenterBack,
"STR": PositionType.Striker,
"STL": PositionType.LeftForward,
"STZ": PositionType.Striker,
"ZO": PositionType.CenterAttackingMidfield,
"LV": PositionType.LeftBack,
"RV": PositionType.RightBack,
"DMR": PositionType.RightDefensiveMidfield,
"DRM": PositionType.RightDefensiveMidfield,
"DML": PositionType.LeftDefensiveMidfield,
"DLM": PositionType.LeftDefensiveMidfield,
"ORM": PositionType.RightMidfield,
"OLM": PositionType.LeftMidfield,
"RA": PositionType.RightWing,
"LA": PositionType.LeftWing,
}

logger = logging.getLogger(__name__)


def _team_from_xml_elm(team_elm) -> Team:
team = Team(
team_id=team_elm.attrib["TeamId"],
name=team_elm.attrib["TeamName"],
ground=(
Ground.HOME if team_elm.attrib["Role"] == "home" else Ground.AWAY
),
ground=Ground.HOME
if team_elm.attrib["Role"] == "home"
else Ground.AWAY,
)
team.players = [
Player(
Expand All @@ -53,14 +74,8 @@ def _team_from_xml_elm(team_elm) -> Team:
name=player_elm.attrib["Shortname"],
first_name=player_elm.attrib["FirstName"],
last_name=player_elm.attrib["LastName"],
starting_position=(
Position(
position_id=None,
name=player_elm.attrib["PlayingPosition"],
coordinates=None,
)
if "PlayingPosition" in player_elm.attrib
else None
starting_position=position_types_mapping.get(
player_elm.attrib.get("PlayingPosition"), PositionType.Unknown
),
starting=player_elm.attrib["Starting"] == "true",
)
Expand Down
9 changes: 4 additions & 5 deletions kloppy/infra/serializers/event/statsbomb/deserializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
Orientation,
Period,
Player,
Position,
Provider,
Team,
)
Expand All @@ -21,6 +20,7 @@
from kloppy.utils import performance_logging
from . import specification as SB
from .helpers import parse_freeze_frame, parse_str_ts
from .specification import position_types_mapping

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -179,10 +179,9 @@ def create_teams_and_players(self, raw_events, lineups):

# Create players and teams
player_positions = {
str(player["player"]["id"]): Position(
position_id=str(player["position"]["id"]),
name=player["position"]["name"],
)
str(player["player"]["id"]): position_types_mapping[
player["position"]["id"]
]
for raw_event in starting_xi_events
for player in raw_event["tactics"]["lineup"]
}
Expand Down
5 changes: 4 additions & 1 deletion kloppy/infra/serializers/event/statsbomb/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
Period,
Player,
PlayerData,
PositionType,
)
from kloppy.exceptions import DeserializationError

Expand Down Expand Up @@ -93,7 +94,9 @@ def get_player_from_freeze_frame(player_data, team, i):
elif player_data.get("actor"):
return event.player
elif player_data.get("keeper"):
return team.get_player_by_position(position_id=1, time=event.time)
return team.get_player_by_position(
position=PositionType.Goalkeeper, time=event.time
)
else:
return Player(
player_id=f"T{team.team_id}-E{event.event_id}-{i}",
Expand Down
Loading

0 comments on commit 86078dc

Please sign in to comment.