Skip to content

Commit

Permalink
Merge pull request #204 from my-game-plan/feature/add_duel_events
Browse files Browse the repository at this point in the history
Add DuelEvent
  • Loading branch information
koenvo committed Jul 14, 2023
2 parents e4c892b + c7facb3 commit ec18671
Show file tree
Hide file tree
Showing 33 changed files with 764 additions and 95 deletions.
1 change: 0 additions & 1 deletion kloppy/_providers/datafactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ def load(
event_factory=event_factory or get_config("event_factory"),
)
with open_as_file(event_data) as event_data_fp:

return deserializer.deserialize(
inputs=DatafactoryInputs(event_data=event_data_fp),
)
1 change: 0 additions & 1 deletion kloppy/_providers/opta.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ def load(
with open_as_file(f7_data) as f7_data_fp, open_as_file(
f24_data
) as f24_data_fp:

return deserializer.deserialize(
inputs=OptaInputs(f7_data=f7_data_fp, f24_data=f24_data_fp),
)
1 change: 0 additions & 1 deletion kloppy/_providers/statsbomb.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ def load(
) as lineup_data_fp, open_as_file(
Source.create(three_sixty_data, optional=True)
) as three_sixty_data_fp:

return deserializer.deserialize(
inputs=StatsBombInputs(
event_data=event_data_fp,
Expand Down
4 changes: 0 additions & 4 deletions kloppy/domain/models/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,6 @@ def vertical_orientation(self) -> VerticalOrientation:

@property
def pitch_dimensions(self) -> PitchDimensions:

if self.length is not None and self.width is not None:
return PitchDimensions(
x_dim=Dimension(0, 1),
Expand Down Expand Up @@ -656,7 +655,6 @@ def pitch_dimensions(self) -> PitchDimensions:


def build_coordinate_system(provider: Provider, **kwargs):

if provider == Provider.TRACAB:
return TracabCoordinateSystem(normalized=False, **kwargs)

Expand Down Expand Up @@ -966,7 +964,6 @@ def to_records(
as_list: bool = True,
**named_columns: "Column",
) -> Union[List[Dict[str, Any]], Iterable[Dict[str, Any]]]:

from ..services.transformers.data_record import get_transformer_cls

transformer = get_transformer_cls(self.dataset_type)(
Expand All @@ -984,7 +981,6 @@ def to_dict(
orient: Literal["list"] = "list",
**named_columns: "Column",
) -> Dict[str, List[Any]]:

if orient == "list":
from ..services.transformers.data_record import get_transformer_cls

Expand Down
87 changes: 87 additions & 0 deletions kloppy/domain/models/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,28 @@ def is_success(self):
return self == self.COMPLETE


class DuelResult(ResultType):
"""
DuelResult
Attributes:
WON (DuelResult): When winning the duel (player touching the ball first)
LOST (DuelResult): When losing the duel (opponent touches the ball first)
NEUTRAL (DuelResult): When neither player wins duel [Mainly for WyScout v2]
"""

WON = "WON"
LOST = "LOST"
NEUTRAL = "NEUTRAL"

@property
def is_success(self):
"""
Returns if the duel was won
"""
return self == self.WON


class CardType(Enum):
"""
CardType
Expand All @@ -157,6 +179,7 @@ class EventType(Enum):
TAKE_ON (EventType):
CARRY (EventType):
CLEARANCE (EventType):
DUEL (EventType):
SUBSTITUTION (EventType):
CARD (EventType):
PLAYER_ON (EventType):
Expand All @@ -174,6 +197,7 @@ class EventType(Enum):
TAKE_ON = "TAKE_ON"
CARRY = "CARRY"
CLEARANCE = "CLEARANCE"
DUEL = "DUEL"
SUBSTITUTION = "SUBSTITUTION"
CARD = "CARD"
PLAYER_ON = "PLAYER_ON"
Expand Down Expand Up @@ -356,6 +380,28 @@ class GoalkeeperActionQualifier(EnumQualifier):
value: GoalkeeperAction


class DuelType(Enum):
"""
DuelType
Attributes:
AERIAL (DuelType): A duel when the ball is in the air and loose.
GROUND (DuelType): A duel when the ball is on the ground.
LOOSE_BALL (DuelType): When the ball is not under the control of any particular player or team.
SLIDING_TACKLE (DuelType): A duel where the player slides on the ground to kick the ball away from an opponent.
"""

AERIAL = "AERIAL"
GROUND = "GROUND"
LOOSE_BALL = "LOOSE_BALL"
SLIDING_TACKLE = "SLIDING_TACKLE"


@dataclass
class DuelQualifier(EnumQualifier):
value: DuelType


@dataclass
class CounterAttackQualifier(BoolQualifier):
pass
Expand Down Expand Up @@ -425,6 +471,27 @@ def get_qualifier_value(self, qualifier_type: Type[Qualifier]):
return qualifier.value
return None

def get_qualifier_values(self, qualifier_type: Type[Qualifier]):
"""
Returns all Qualifiers of a certain type, or None if qualifier is not present.
Arguments:
qualifier_type: one of the following QualifierTypes: [`SetPieceQualifier`][kloppy.domain.models.event.SetPieceQualifier]
[`BodyPartQualifier`][kloppy.domain.models.event.BodyPartQualifier] [`PassQualifier`][kloppy.domain.models.event.PassQualifier]
Examples:
>>> from kloppy.domain import SetPieceQualifier
>>> pass_event.get_qualifier_value(SetPieceQualifier)
<SetPieceType.GOAL_KICK: 'GOAL_KICK'>
"""
qualifiers = []
if self.qualifiers:
for qualifier in self.qualifiers:
if isinstance(qualifier, qualifier_type):
qualifiers.append(qualifier)

return qualifiers

def get_related_events(self) -> List["Event"]:
if not self.dataset:
raise OrphanedRecordError()
Expand Down Expand Up @@ -667,6 +734,22 @@ class ClearanceEvent(Event):
event_name: str = "clearance"


@dataclass(repr=False)
@docstring_inherit_attributes(Event)
class DuelEvent(Event):
"""
DuelEvent
Attributes:
event_type (EventType): `EventType.DUEL` (See [`EventType`][kloppy.domain.models.event.EventType])
event_name (str): `"duel"`
"""

event_type: EventType = EventType.DUEL
event_name: str = "duel"


@dataclass(repr=False)
@docstring_inherit_attributes(Event)
class SubstitutionEvent(Event):
Expand Down Expand Up @@ -905,4 +988,8 @@ def generic_record_converter(event: Event):
"GoalkeeperAction",
"GoalkeeperActionQualifier",
"CounterAttackQualifier",
"DuelEvent",
"DuelType",
"DuelQualifier",
"DuelResult",
]
4 changes: 4 additions & 0 deletions kloppy/domain/services/event_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
TakeOnEvent,
RecoveryEvent,
CarryEvent,
DuelEvent,
ClearanceEvent,
FormationChangeEvent,
BallOutEvent,
Expand Down Expand Up @@ -86,6 +87,9 @@ def build_carry(self, **kwargs) -> CarryEvent:
def build_clearance(self, **kwargs) -> ClearanceEvent:
return create_event(ClearanceEvent, **kwargs)

def build_duel(self, **kwargs) -> DuelEvent:
return create_event(DuelEvent, **kwargs)

def build_formation_change(self, **kwargs) -> FormationChangeEvent:
return create_event(FormationChangeEvent, **kwargs)

Expand Down
1 change: 0 additions & 1 deletion kloppy/domain/services/state_builder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ def add_state(dataset: EventDataset, *builder_keys: List[str]) -> EventDataset:

events = []
for event in dataset.events:

state = {
builder_key: builder.reduce_before(state[builder_key], event)
for builder_key, builder in builders.items()
Expand Down
1 change: 0 additions & 1 deletion kloppy/domain/services/state_builder/builders/sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ def reduce_before(self, state: Sequence, event: Event) -> Sequence:
return state

def reduce_after(self, state: Sequence, event: Event) -> Sequence:

if isinstance(event, CLOSE_SEQUENCE):
state = replace(
state, sequence_id=state.sequence_id + 1, team=None
Expand Down
1 change: 0 additions & 1 deletion kloppy/domain/services/transformers/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,6 @@ def __call__(self, frame: Frame) -> Dict[str, Any]:
else None,
)
for player, player_data in frame.players_data.items():

row.update(
{
f"{player.player_id}_x": player_data.coordinates.x
Expand Down
1 change: 0 additions & 1 deletion kloppy/domain/services/transformers/data_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ def __init__(
**named_columns: Union[str, Callable[[T], Any]],
):
if not columns and not named_columns:

converter = self.default_transformer()
else:
default = self.default_transformer()
Expand Down
13 changes: 0 additions & 13 deletions kloppy/domain/services/transformers/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ def __init__(
to_pitch_dimensions: Optional[PitchDimensions] = None,
to_orientation: Optional[Orientation] = None,
):

if (
from_pitch_dimensions
and from_coordinate_system
Expand Down Expand Up @@ -90,7 +89,6 @@ def _needs_pitch_dimensions_change(self):
def change_point_dimensions(
self, point: Union[Point, Point3D, None]
) -> Union[Point, Point3D, None]:

if point is None:
return None

Expand All @@ -108,7 +106,6 @@ def change_point_dimensions(
def flip_point(
self, point: Union[Point, Point3D, None]
) -> Union[Point, Point3D, None]:

if not point:
return None

Expand Down Expand Up @@ -160,7 +157,6 @@ def __needs_flip(
return flip

def transform_frame(self, frame: Frame) -> Frame:

# Change coordinate system
if self._needs_coordinate_system_change:
frame = self.__change_frame_coordinate_system(frame)
Expand All @@ -178,7 +174,6 @@ def transform_frame(self, frame: Frame) -> Frame:
return frame

def __change_frame_coordinate_system(self, frame: Frame):

return Frame(
# doesn't change
timestamp=frame.timestamp,
Expand Down Expand Up @@ -206,7 +201,6 @@ def __change_frame_coordinate_system(self, frame: Frame):
)

def __change_frame_dimensions(self, frame: Frame):

return Frame(
# doesn't change
timestamp=frame.timestamp,
Expand Down Expand Up @@ -235,7 +229,6 @@ def __change_frame_dimensions(self, frame: Frame):
def __change_point_coordinate_system(
self, point: Union[Point, Point3D, None]
) -> Union[Point, Point3D, None]:

if not point:
return None

Expand All @@ -258,7 +251,6 @@ def __change_point_coordinate_system(
return Point(x=x, y=y)

def __flip_frame(self, frame: Frame):

players_data = {}
for player, data in frame.players_data.items():
players_data[player] = PlayerData(
Expand All @@ -282,7 +274,6 @@ def __flip_frame(self, frame: Frame):
)

def transform_event(self, event: Event) -> Event:

# Change coordinate system
if self._needs_coordinate_system_change:
event = self.__change_event_coordinate_system(event)
Expand All @@ -304,7 +295,6 @@ def transform_event(self, event: Event) -> Event:
return event

def __change_event_coordinate_system(self, event: Event):

position_changes = {
field.name: self.__change_point_coordinate_system(
getattr(event, field.name)
Expand All @@ -317,7 +307,6 @@ def __change_event_coordinate_system(self, event: Event):
return replace(event, **position_changes)

def __change_event_dimensions(self, event: Event):

position_changes = {
field.name: self.change_point_dimensions(
getattr(event, field.name)
Expand All @@ -330,7 +319,6 @@ def __change_event_dimensions(self, event: Event):
return replace(event, **position_changes)

def __flip_event(self, event: Event):

position_changes = {
field.name: self.flip_point(getattr(event, field.name))
for field in fields(event)
Expand All @@ -351,7 +339,6 @@ def transform_dataset(
to_orientation: Optional[Orientation] = None,
to_coordinate_system: Optional[CoordinateSystem] = None,
) -> Dataset:

if (
to_pitch_dimensions is None
and to_orientation is None
Expand Down
1 change: 0 additions & 1 deletion kloppy/infra/serializers/event/datafactory/deserializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,6 @@ def provider(self) -> Provider:
return Provider.DATAFACTORY

def deserialize(self, inputs: DatafactoryInputs) -> EventDataset:

transformer = self.get_transformer(length=2, width=2)

with performance_logging("load data", logger=logger):
Expand Down
5 changes: 0 additions & 5 deletions kloppy/infra/serializers/event/metrica/json_deserializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ def _parse_subtypes(event: dict) -> List:
def _parse_pass(
event: Dict, previous_event: Dict, subtypes: List, team: Team
) -> Dict:

event_type_id = event["type"]["id"]

if event_type_id == MS_PASS_OUTCOME_COMPLETE:
Expand Down Expand Up @@ -157,7 +156,6 @@ def _parse_pass(
def _get_event_qualifiers(
event: Dict, previous_event: Dict, subtypes: List
) -> List[Qualifier]:

qualifiers = []

qualifiers.extend(_get_event_setpiece_qualifiers(previous_event, subtypes))
Expand All @@ -169,7 +167,6 @@ def _get_event_qualifiers(
def _get_event_setpiece_qualifiers(
previous_event: Dict, subtypes: List
) -> List[Qualifier]:

qualifiers = []
previous_event_type_id = previous_event["type"]["id"]
if previous_event_type_id == MS_SET_PIECE:
Expand All @@ -193,7 +190,6 @@ def _get_event_setpiece_qualifiers(


def _get_event_bodypart_qualifiers(subtypes: List) -> List[Qualifier]:

qualifiers = []
if subtypes and MS_BODY_PART_HEAD in subtypes:
qualifiers.append(BodyPartQualifier(value=BodyPart.HEAD))
Expand Down Expand Up @@ -274,7 +270,6 @@ def deserialize(self, inputs: MetricaJsonEventDataInputs) -> EventDataset:
with performance_logging("parse data", logger=logger):
events = []
for i, raw_event in enumerate(raw_events["data"]):

if raw_event["team"]["id"] == metadata.teams[0].team_id:
team = metadata.teams[0]
elif raw_event["team"]["id"] == metadata.teams[1].team_id:
Expand Down
Loading

0 comments on commit ec18671

Please sign in to comment.