diff --git a/kloppy/domain/models/event.py b/kloppy/domain/models/event.py
index f1f48a5d..e41bca24 100644
--- a/kloppy/domain/models/event.py
+++ b/kloppy/domain/models/event.py
@@ -185,6 +185,7 @@ class EventType(Enum):
PLAYER_ON (EventType):
PLAYER_OFF (EventType):
RECOVERY (EventType):
+ MISCONTROL (EventType):
BALL_OUT (EventType):
FOUL_COMMITTED (EventType):
FORMATION_CHANGE (EventType):
@@ -203,6 +204,7 @@ class EventType(Enum):
PLAYER_ON = "PLAYER_ON"
PLAYER_OFF = "PLAYER_OFF"
RECOVERY = "RECOVERY"
+ MISCONTROL = "MISCONTROL"
BALL_OUT = "BALL_OUT"
FOUL_COMMITTED = "FOUL_COMMITTED"
FORMATION_CHANGE = "FORMATION_CHANGE"
@@ -864,6 +866,20 @@ class BallOutEvent(Event):
event_name: str = "ball_out"
+@dataclass(repr=False)
+@docstring_inherit_attributes(Event)
+class MiscontrolEvent(Event):
+ """
+ MiscontrolEvent
+ Attributes:
+ event_type (EventType): `EventType.MISCONTROL` (See [`EventType`][kloppy.domain.models.event.EventType])
+ event_name (str): "miscontrol"
+ """
+
+ event_type: EventType = EventType.MISCONTROL
+ event_name: str = "miscontrol"
+
+
@dataclass(repr=False)
@docstring_inherit_attributes(Event)
class FoulCommittedEvent(Event):
@@ -976,6 +992,7 @@ def generic_record_converter(event: Event):
"FormationChangeEvent",
"EventDataset",
"RecoveryEvent",
+ "MiscontrolEvent",
"FoulCommittedEvent",
"BallOutEvent",
"SetPieceType",
diff --git a/kloppy/domain/services/event_factory.py b/kloppy/domain/services/event_factory.py
index 0f550ba5..cb1af4b9 100644
--- a/kloppy/domain/services/event_factory.py
+++ b/kloppy/domain/services/event_factory.py
@@ -9,6 +9,7 @@
GenericEvent,
TakeOnEvent,
RecoveryEvent,
+ MiscontrolEvent,
CarryEvent,
DuelEvent,
ClearanceEvent,
@@ -78,6 +79,9 @@ def build_generic(self, **kwargs) -> GenericEvent:
def build_recovery(self, **kwargs) -> RecoveryEvent:
return create_event(RecoveryEvent, **kwargs)
+ def build_miscontrol(self, **kwargs) -> MiscontrolEvent:
+ return create_event(MiscontrolEvent, **kwargs)
+
def build_take_on(self, **kwargs) -> TakeOnEvent:
return create_event(TakeOnEvent, **kwargs)
diff --git a/kloppy/infra/serializers/event/opta/deserializer.py b/kloppy/infra/serializers/event/opta/deserializer.py
index 38e235ec..6418c89c 100644
--- a/kloppy/infra/serializers/event/opta/deserializer.py
+++ b/kloppy/infra/serializers/event/opta/deserializer.py
@@ -72,6 +72,7 @@
EVENT_TYPE_CARD = 17
EVENT_TYPE_RECOVERY = 49
EVENT_TYPE_FORMATION_CHANGE = 40
+EVENT_TYPE_BALL_TOUCH = 61
BALL_OUT_EVENTS = [EVENT_TYPE_BALL_OUT, EVENT_TYPE_CORNER_AWARDED]
DUEL_EVENTS = [EVENT_TYPE_TACKLE, EVENT_TYPE_AERIAL, EVENT_TYPE_50_50]
@@ -85,6 +86,7 @@
EVENT_TYPE_SHOT_SAVED,
EVENT_TYPE_SHOT_GOAL,
EVENT_TYPE_RECOVERY,
+ EVENT_TYPE_BALL_TOUCH,
)
EVENT_QUALIFIER_GOAL_KICK = 124
@@ -716,6 +718,12 @@ def deserialize(self, inputs: OptaInputs) -> EventDataset:
**duel_event_kwargs,
**generic_event_kwargs,
)
+ elif (type_id == EVENT_TYPE_BALL_TOUCH) & (outcome == 0):
+ event = self.event_factory.build_miscontrol(
+ result=None,
+ qualifiers=None,
+ **generic_event_kwargs,
+ )
elif (type_id == EVENT_TYPE_FOUL_COMMITTED) and (
outcome == 0
):
diff --git a/kloppy/infra/serializers/event/statsbomb/deserializer.py b/kloppy/infra/serializers/event/statsbomb/deserializer.py
index 017b583c..31d9604e 100644
--- a/kloppy/infra/serializers/event/statsbomb/deserializer.py
+++ b/kloppy/infra/serializers/event/statsbomb/deserializer.py
@@ -52,6 +52,7 @@
SB_EVENT_TYPE_SHOT = 16
SB_EVENT_TYPE_PASS = 30
SB_EVENT_TYPE_50_50 = 33
+SB_EVENT_TYPE_MISCONTROL = 38
SB_EVENT_TYPE_CARRY = 43
SB_EVENT_TYPE_HALF_START = 18
@@ -794,6 +795,13 @@ def deserialize(self, inputs: StatsBombInputs) -> EventDataset:
**generic_event_kwargs,
)
new_events.append(clearance_event)
+ elif event_type == SB_EVENT_TYPE_MISCONTROL:
+ miscontrol_event = self.event_factory.build_miscontrol(
+ result=None,
+ qualifiers=None,
+ **generic_event_kwargs,
+ )
+ new_events.append(miscontrol_event)
# For dribble and carry the definitions
# are flipped between StatsBomb and kloppy
elif event_type == SB_EVENT_TYPE_DRIBBLE:
diff --git a/kloppy/infra/serializers/event/wyscout/deserializer_v2.py b/kloppy/infra/serializers/event/wyscout/deserializer_v2.py
index 9b3cdcb8..89908e6f 100644
--- a/kloppy/infra/serializers/event/wyscout/deserializer_v2.py
+++ b/kloppy/infra/serializers/event/wyscout/deserializer_v2.py
@@ -421,6 +421,18 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset:
**clearance_event_args,
**generic_event_args,
)
+ elif (
+ raw_event["subEventId"]
+ == wyscout_events.OTHERS_ON_BALL.TOUCH
+ ) & (_has_tag(raw_event, wyscout_tags.MISSED_BALL)):
+ miscontrol_event_args = {
+ "result": None,
+ "qualifiers": _generic_qualifiers(raw_event),
+ }
+ event = self.event_factory.build_miscontrol(
+ **miscontrol_event_args,
+ **generic_event_args,
+ )
else:
recovery_event_args = _parse_recovery(raw_event)
event = self.event_factory.build_recovery(
diff --git a/kloppy/infra/serializers/event/wyscout/deserializer_v3.py b/kloppy/infra/serializers/event/wyscout/deserializer_v3.py
index e5cea060..34cbe4ae 100644
--- a/kloppy/infra/serializers/event/wyscout/deserializer_v3.py
+++ b/kloppy/infra/serializers/event/wyscout/deserializer_v3.py
@@ -71,13 +71,6 @@ def _parse_team(raw_events, wyId: str, ground: Ground) -> Team:
return team
-def _has_tag(raw_event, tag_id) -> bool:
- for tag in raw_event["tags"]:
- if tag["id"] == tag_id:
- return True
- return False
-
-
def _generic_qualifiers(raw_event: Dict) -> List[Qualifier]:
qualifiers: List[Qualifier] = []
diff --git a/kloppy/tests/files/opta_f24.xml b/kloppy/tests/files/opta_f24.xml
index 4abc3e83..95a54799 100644
--- a/kloppy/tests/files/opta_f24.xml
+++ b/kloppy/tests/files/opta_f24.xml
@@ -264,5 +264,9 @@
+
+
+
+
diff --git a/kloppy/tests/test_adapter.py b/kloppy/tests/test_adapter.py
index 7eaa3e7a..c0935f0f 100644
--- a/kloppy/tests/test_adapter.py
+++ b/kloppy/tests/test_adapter.py
@@ -57,4 +57,4 @@ def read_to_stream(self, url: str, output: BinaryIO):
# Asserts borrowed from `test_opta.py`
assert dataset.metadata.provider == Provider.OPTA
assert dataset.dataset_type == DatasetType.EVENT
- assert len(dataset.events) == 23
+ assert len(dataset.events) == 24
diff --git a/kloppy/tests/test_opta.py b/kloppy/tests/test_opta.py
index 51d4a36f..45f43b8f 100644
--- a/kloppy/tests/test_opta.py
+++ b/kloppy/tests/test_opta.py
@@ -40,7 +40,7 @@ def test_correct_deserialization(self, f7_data: str, f24_data: str):
)
assert dataset.metadata.provider == Provider.OPTA
assert dataset.dataset_type == DatasetType.EVENT
- assert len(dataset.events) == 23
+ assert len(dataset.events) == 24
assert len(dataset.metadata.periods) == 2
assert (
dataset.events[10].ball_owning_team == dataset.metadata.teams[1]
@@ -113,6 +113,9 @@ def test_correct_deserialization(self, f7_data: str, f24_data: str):
assert dataset.events[18].result.value == "OWN_GOAL" # 2318697001
# Check OFFSIDE pass has end_coordinates
assert dataset.events[20].receiver_coordinates.x == 89.3 # 2360555167
+ assert (
+ dataset.events[23].event_type == EventType.MISCONTROL
+ ) # 250913217
# Check counterattack
assert (
diff --git a/kloppy/tests/test_statsbomb.py b/kloppy/tests/test_statsbomb.py
index 93b85355..88ad5446 100644
--- a/kloppy/tests/test_statsbomb.py
+++ b/kloppy/tests/test_statsbomb.py
@@ -160,6 +160,7 @@ def test_correct_deserialization(
== DuelType.GROUND
)
assert dataset.events[272].event_type == EventType.CLEARANCE
+ assert dataset.events[68].event_type == EventType.MISCONTROL
def test_correct_normalized_deserialization(
self, lineup_data: Path, event_data: Path
diff --git a/kloppy/tests/test_wyscout.py b/kloppy/tests/test_wyscout.py
index 493f8ada..61e2069f 100644
--- a/kloppy/tests/test_wyscout.py
+++ b/kloppy/tests/test_wyscout.py
@@ -62,6 +62,7 @@ def test_correct_v2_deserialization(self, event_v2_data: Path):
data_version="V2",
)
assert dataset.records[2].coordinates == Point(29.0, 6.0)
+ assert dataset.events[11].event_type == EventType.MISCONTROL
assert dataset.events[136].event_type == EventType.CLEARANCE
assert (