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

Fix FoulComitted + Card for Wyscout v2 #261

Merged
merged 9 commits into from
Dec 29, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
134 changes: 95 additions & 39 deletions kloppy/infra/serializers/event/wyscout/deserializer_v2.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import json
import logging
from typing import Dict, List, Tuple, NamedTuple, IO
from typing import Dict, List, NamedTuple, IO

from kloppy.domain import (
BodyPart,
BodyPartQualifier,
CardQualifier,
CardType,
CounterAttackQualifier,
Dimension,
DuelResult,
DuelQualifier,
DuelType,
EventDataset,
EventType,
GoalkeeperQualifier,
GoalkeeperActionType,
Ground,
Expand All @@ -29,7 +30,6 @@
SetPieceQualifier,
SetPieceType,
ShotResult,
TakeOnResult,
Team,
)
from kloppy.utils import performance_logging
Expand Down Expand Up @@ -208,6 +208,14 @@ def _parse_goalkeeper_save(raw_event) -> List[Qualifier]:

def _parse_foul(raw_event: Dict) -> Dict:
qualifiers = _generic_qualifiers(raw_event)

if _has_tag(raw_event, wyscout_tags.RED_CARD):
qualifiers.append(CardQualifier(value=CardType.RED))
elif _has_tag(raw_event, wyscout_tags.YELLOW_CARD):
qualifiers.append(CardQualifier(value=CardType.FIRST_YELLOW))
elif _has_tag(raw_event, wyscout_tags.SECOND_YELLOW_CARD):
qualifiers.append(CardQualifier(value=CardType.SECOND_YELLOW))

return {
"result": None,
"qualifiers": qualifiers,
Expand Down Expand Up @@ -412,7 +420,7 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset:
)

generic_event_args = {
"event_id": raw_event["id"],
"event_id": str(raw_event["id"]),
"raw_event": raw_event,
"coordinates": Point(
x=float(raw_event["positions"][0]["x"]),
Expand All @@ -428,39 +436,54 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset:
"timestamp": raw_event["eventSec"],
}

event = None
new_events = []
if raw_event["eventId"] == wyscout_events.SHOT.EVENT:
shot_event_args = _parse_shot(raw_event, next_event)
event = self.event_factory.build_shot(
shot_event = self.event_factory.build_shot(
**shot_event_args, **generic_event_args
)
new_events.append(shot_event)
elif raw_event["eventId"] == wyscout_events.PASS.EVENT:
pass_event_args = _parse_pass(raw_event, next_event)
event = self.event_factory.build_pass(
pass_event = self.event_factory.build_pass(
**pass_event_args, **generic_event_args
)
new_events.append(pass_event)
elif raw_event["eventId"] == wyscout_events.FOUL.EVENT:
foul_event_args = _parse_foul(raw_event)
event = self.event_factory.build_foul_committed(
foul_event = self.event_factory.build_foul_committed(
**foul_event_args, **generic_event_args
)
new_events.append(foul_event)
if any(
(_has_tag(raw_event, tag) for tag in wyscout_tags.CARD)
):
card_event_args = _parse_card(raw_event)
event = self.event_factory.build_card(
**card_event_args, **generic_event_args
card_event_id = (
f"card-{generic_event_args['event_id']}"
)
card_event = self.event_factory.build_card(
**card_event_args,
**{
**generic_event_args,
"event_id": card_event_id,
},
)
new_events.append(card_event)
elif raw_event["eventId"] == wyscout_events.INTERRUPTION.EVENT:
ball_out_event_args = _parse_ball_out(raw_event)
event = self.event_factory.build_ball_out(
ball_out_event = self.event_factory.build_ball_out(
**ball_out_event_args, **generic_event_args
)
new_events.append(ball_out_event)
elif raw_event["eventId"] == wyscout_events.SAVE.EVENT:
goalkeeper_save_args = _parse_goalkeeper_save(raw_event)
event = self.event_factory.build_goalkeeper_event(
**goalkeeper_save_args, **generic_event_args
goalkeeper_save_event = (
self.event_factory.build_goalkeeper_event(
**goalkeeper_save_args, **generic_event_args
)
)
new_events.append(goalkeeper_save_event)
elif raw_event["eventId"] == wyscout_events.FREE_KICK.EVENT:
set_piece_event_args = _parse_set_piece(
raw_event, next_event
Expand All @@ -469,16 +492,18 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset:
raw_event["subEventId"]
in wyscout_events.FREE_KICK.PASS_TYPES
):
event = self.event_factory.build_pass(
fk_pass_event = self.event_factory.build_pass(
**set_piece_event_args, **generic_event_args
)
new_events.append(fk_pass_event)
elif (
raw_event["subEventId"]
in wyscout_events.FREE_KICK.SHOT_TYPES
):
event = self.event_factory.build_shot(
fk_shot_event = self.event_factory.build_shot(
**set_piece_event_args, **generic_event_args
)
new_events.append(fk_shot_event)

elif (
raw_event["eventId"] == wyscout_events.OTHERS_ON_BALL.EVENT
Expand All @@ -488,10 +513,11 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset:
== wyscout_events.OTHERS_ON_BALL.CLEARANCE
):
clearance_event_args = _parse_clearance(raw_event)
event = self.event_factory.build_clearance(
clearance_event = self.event_factory.build_clearance(
**clearance_event_args,
**generic_event_args,
)
new_events.append(clearance_event)
elif (
raw_event["subEventId"]
== wyscout_events.OTHERS_ON_BALL.TOUCH
Expand All @@ -500,56 +526,86 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset:
"result": None,
"qualifiers": _generic_qualifiers(raw_event),
}
event = self.event_factory.build_miscontrol(
miscontrol_event = self.event_factory.build_miscontrol(
**miscontrol_event_args,
**generic_event_args,
)
new_events.append(miscontrol_event)
else:
recovery_event_args = _parse_recovery(raw_event)
event = self.event_factory.build_recovery(
recovery_event = self.event_factory.build_recovery(
**recovery_event_args, **generic_event_args
)
new_events.append(recovery_event)
elif raw_event["eventId"] == wyscout_events.DUEL.EVENT:
duel_event_args = _parse_duel(raw_event)
event = self.event_factory.build_duel(
duel_event = self.event_factory.build_duel(
**duel_event_args, **generic_event_args
)
new_events.append(duel_event)
elif raw_event["eventId"] not in [
wyscout_events.SAVE.EVENT,
wyscout_events.OFFSIDE.EVENT,
]:
# The events SAVE and OFFSIDE are already merged with PASS and SHOT events
qualifiers = _generic_qualifiers(raw_event)
event = self.event_factory.build_generic(
generic_event = self.event_factory.build_generic(
result=None,
qualifiers=qualifiers,
**generic_event_args,
)
new_events.append(generic_event)

# Since Interception is not an event in wyscout v2 but a tag for pass, touch and duel. Therefore,
# we convert those duels and touch events to an interception. And insert interception before passes.
# Wyscout v2 does not have a seperate event type for
probberechts marked this conversation as resolved.
Show resolved Hide resolved
# interceptions. Interceptions are recored by adding a tag to
probberechts marked this conversation as resolved.
Show resolved Hide resolved
# the next pass, touch or duel. Therefore, we convert events
# with this tag to an interception.
if _has_tag(raw_event, wyscout_tags.INTERCEPTION):
interception_event_args = _parse_interception(
raw_event, next_event
)
interception_event = self.event_factory.build_interception(
**interception_event_args,
**generic_event_args,
)

if event.event_type.name == "DUEL":
# when DuelEvent is interception, we need to overwrite this and the previous DuelEvent
events = events[:-1]
event = interception_event
elif event.event_name in ["recovery", "miscontrol"]:
event = interception_event
elif event.event_name in ["pass", "clearance"]:
events.append(
transformer.transform_event(interception_event)
)

if event and self.should_include_event(event):
events.append(transformer.transform_event(event))
for i, new_event in enumerate(list(new_events)):
if new_event.event_type == EventType.DUEL:
# when DuelEvent is interception, we need to
# overwrite this and the previous DuelEvent
events = events[:-1]
new_events[
i
] = self.event_factory.build_interception(
**interception_event_args,
**generic_event_args,
)
elif new_event.event_type in [
EventType.RECOVERY,
EventType.MISCONTROL,
]:
# replace touch events
new_events[
i
] = self.event_factory.build_interception(
**interception_event_args,
**generic_event_args,
)
elif new_event.event_type in [
EventType.PASS,
EventType.CLEARANCE,
]:
# insert an interception event before interception passes
generic_event_args[
"event_id"
] = f"interception-{generic_event_args['event_id']}"
interception_event = (
self.event_factory.build_interception(
**interception_event_args,
**generic_event_args,
)
)
new_events.insert(i, interception_event)

for new_event in new_events:
if self.should_include_event(new_event):
events.append(transformer.transform_event(new_event))

metadata = Metadata(
teams=[home_team, away_team],
Expand Down
2 changes: 1 addition & 1 deletion kloppy/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
import pytest


@pytest.fixture
@pytest.fixture(scope="session")
def base_dir() -> Path:
return Path(__file__).parent
8 changes: 4 additions & 4 deletions kloppy/tests/files/wyscout_events_v3.json
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@
"videoTimestamp": "8.148438"
},
{
"id": 663291840,
"id": 663291841,
"type": {
"primary": "duel",
"secondary": [
Expand Down Expand Up @@ -662,7 +662,7 @@
"videoTimestamp": "8.148438"
},
{
"id": 663291840,
"id": 663291842,
"type": {
"primary": "duel",
"secondary": [
Expand Down Expand Up @@ -724,7 +724,7 @@
"videoTimestamp": "8.148438"
},
{
"id": 663291842,
"id": 663291843,
"minute": 0,
"matchId": 2852835,
"matchPeriod": "1H",
Expand Down Expand Up @@ -4148,4 +4148,4 @@
}
},
"meta": []
}
}
Loading