Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create standardized PositionType class #334

Merged
merged 7 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading