diff --git a/kloppy/domain/services/state_builder/builders/score.py b/kloppy/domain/services/state_builder/builders/score.py index a482fc4f..9ac3c1ab 100644 --- a/kloppy/domain/services/state_builder/builders/score.py +++ b/kloppy/domain/services/state_builder/builders/score.py @@ -27,4 +27,9 @@ def reduce_after(self, state: Score, event: Event) -> Score: state = replace(state, home=state.home + 1) else: state = replace(state, away=state.away + 1) + elif event.result == ShotResult.OWN_GOAL: + if event.team.ground == Ground.HOME: + state = replace(state, away=state.away + 1) + else: + state = replace(state, home=state.home + 1) return state diff --git a/kloppy/infra/serializers/event/statsbomb/deserializer.py b/kloppy/infra/serializers/event/statsbomb/deserializer.py index cf0b5b35..97ddde18 100644 --- a/kloppy/infra/serializers/event/statsbomb/deserializer.py +++ b/kloppy/infra/serializers/event/statsbomb/deserializer.py @@ -53,6 +53,8 @@ SB_EVENT_TYPE_CLEARANCE = 9 SB_EVENT_TYPE_DRIBBLE = 14 SB_EVENT_TYPE_SHOT = 16 +SB_EVENT_TYPE_OWN_GOAL_AGAINST = 20 +SB_EVENT_TYPE_OWN_GOAL_FOR = 25 SB_EVENT_TYPE_GOALKEEPER_EVENT = 23 SB_EVENT_TYPE_PASS = 30 SB_EVENT_TYPE_50_50 = 33 @@ -856,6 +858,15 @@ def deserialize(self, inputs: StatsBombInputs) -> EventDataset: **generic_event_kwargs, ) new_events.append(shot_event) + elif event_type == SB_EVENT_TYPE_OWN_GOAL_AGAINST: + shot_event = self.event_factory.build_shot( + result=ShotResult.OWN_GOAL, + qualifiers=[], + **generic_event_kwargs, + ) + new_events.append(shot_event) + elif event_type == SB_EVENT_TYPE_OWN_GOAL_FOR: + pass elif event_type == SB_EVENT_TYPE_CLEARANCE: clearance_event_kwargs = _parse_clearance( raw_event=raw_event, events=events @@ -1063,7 +1074,7 @@ def deserialize(self, inputs: StatsBombInputs) -> EventDataset: # Last step is to add freeze_frame information for event in events: if event.event_type == EventType.SHOT: - if "freeze_frame" in event.raw_event["shot"]: + if "freeze_frame" in event.raw_event.get("shot", {}): event.freeze_frame = transformer.transform_frame( _parse_freeze_frame( freeze_frame=event.raw_event["shot"][ diff --git a/kloppy/tests/files/statsbomb_event.json b/kloppy/tests/files/statsbomb_event.json index af949022..f2c08486 100644 --- a/kloppy/tests/files/statsbomb_event.json +++ b/kloppy/tests/files/statsbomb_event.json @@ -170249,6 +170249,82 @@ "aerial_won" : true } }, { + "id" : "f942c5b5-df4b-4ee4-9e90-ed5f5", + "index" : 4005, + "period" : 2, + "timestamp" : "00:44:51.456", + "minute" : 44, + "second" : 51, + "type" : { + "id" : 25, + "name" : "Own Goal For" + }, + "possession" : 101, + "possession_team" : { + "id" : 206, + "name" : "Deportivo Alavés" + }, + "play_pattern" : { + "id" : 2, + "name" : "From Corner" + }, + "obv_for_after" : null, + "obv_for_before" : null, + "obv_for_net" : null, + "obv_against_after" : null, + "obv_against_before" : null, + "obv_against_net" : null, + "obv_total_net" : null, + "team" : { + "id" : 206, + "name" : "Deportivo Alavés" + }, + "location" : [ 115.7, 39.9 ], + "duration" : 0.0, + "related_events" : [ "89dd4f4b-0a70-48d8-a0e7-ac4c" ] + }, { + "id" : "89dd4f4b-0a70-48d8-a0e7-ac4c", + "index" : 4006, + "period" : 2, + "timestamp" : "00:44:51.456", + "minute" : 44, + "second" : 51, + "type" : { + "id" : 20, + "name" : "Own Goal Against" + }, + "possession" : 101, + "possession_team" : { + "id" : 206, + "name" : "Deportivo Alavés" + }, + "play_pattern" : { + "id" : 2, + "name" : "From Corner" + }, + "obv_for_after" : null, + "obv_for_before" : null, + "obv_for_net" : null, + "obv_against_after" : null, + "obv_against_before" : null, + "obv_against_net" : null, + "obv_total_net" : null, + "team" : { + "id" : 217, + "name" : "Barcelona" + }, + "player" : { + "id" : 6629, + "name" : "Fernando Pacheco Flores" + }, + "position" : { + "id" : 5, + "name" : "Left Center Back" + }, + "location" : [ 4.4, 40.2 ], + "duration" : 0.0, + "related_events" : [ "f942c5b5-df4b-4ee4-9e90-ed5f5" ] + }, { "id" : "e1cc4d5e-ba55-4b6b-88cc-dae13311c1d9", "index" : 4001, "period" : 2, @@ -170388,4 +170464,4 @@ } } } -] \ No newline at end of file +] diff --git a/kloppy/tests/test_helpers.py b/kloppy/tests/test_helpers.py index 2e5a8822..cb08989b 100644 --- a/kloppy/tests/test_helpers.py +++ b/kloppy/tests/test_helpers.py @@ -376,7 +376,7 @@ def test_event_dataset_to_polars(self, base_dir): import polars as pl c = df.select(pl.col("event_id").count())[0, 0] - assert c == 4041 + assert c == 4042 def test_tracking_dataset_to_polars(self): """ diff --git a/kloppy/tests/test_state_builder.py b/kloppy/tests/test_state_builder.py index e9ef412c..7311f89c 100644 --- a/kloppy/tests/test_state_builder.py +++ b/kloppy/tests/test_state_builder.py @@ -32,7 +32,8 @@ def test_score_state_builder(self, base_dir): "0-0": 2909, "1-0": 717, "2-0": 405, - "3-0": 10, + "3-0": 7, + "3-1": 4, } def test_sequence_state_builder(self, base_dir): @@ -93,7 +94,7 @@ def test_formation_state_builder(self, base_dir): # inspect FormationChangeEvent usage and formation state_builder assert events_per_formation_change["4-1-4-1"] == 3085 - assert events_per_formation_change["4-4-2"] == 956 + assert events_per_formation_change["4-4-2"] == 957 assert dataset.metadata.teams[0].starting_formation == FormationType( "4-4-2" diff --git a/kloppy/tests/test_statsbomb.py b/kloppy/tests/test_statsbomb.py index a94f4aa0..bb01c8fe 100644 --- a/kloppy/tests/test_statsbomb.py +++ b/kloppy/tests/test_statsbomb.py @@ -15,8 +15,8 @@ Point, Provider, FormationType, - Frame, Position, + ShotResult, ) from kloppy import statsbomb @@ -55,7 +55,7 @@ def test_correct_deserialization( assert dataset.metadata.provider == Provider.STATSBOMB assert dataset.dataset_type == DatasetType.EVENT - assert len(dataset.events) == 4041 + assert len(dataset.events) == 4042 assert len(dataset.metadata.periods) == 2 assert ( dataset.metadata.orientation == Orientation.ACTION_EXECUTING_TEAM @@ -170,12 +170,12 @@ def test_correct_deserialization( ) assert ( - dataset.events[4039].get_qualifier_value(GoalkeeperQualifier) + dataset.events[4040].get_qualifier_value(GoalkeeperQualifier) == GoalkeeperActionType.SMOTHER ) assert ( - dataset.events[4040].get_qualifier_value(GoalkeeperQualifier) + dataset.events[4041].get_qualifier_value(GoalkeeperQualifier) == GoalkeeperActionType.PUNCH ) @@ -254,6 +254,36 @@ def test_foul_committed(self, lineup_data: Path, event_data: Path): assert len(dataset.events) == 23 + def test_own_goal(self, lineup_data: Path, event_data: Path): + """ + Test own goal events. + + The StatsBomb "Own Goal For" (id = 25) and one "Own Goal Against" (id = 20) events + should be converted to a single shot event with ShotResult.OWN_GOAL. + """ + dataset = statsbomb.load( + lineup_data=lineup_data, + event_data=event_data, + ) + + # The Own Goal For event should be removed + own_goal_for_event = [ + event + for event in dataset.events + if event.event_id == "f942c5b5-df4b-4ee4-9e90-ed5f5" + ] + assert len(own_goal_for_event) == 0 + + # The Own Goal Against event should be converted to a shot event + own_goal_against_event = [ + event + for event in dataset.events + if event.event_id == "89dd4f4b-0a70-48d8-a0e7-ac4c" + ] + assert len(own_goal_against_event) == 1 + assert own_goal_against_event[0].event_type == EventType.SHOT + assert own_goal_against_event[0].result == ShotResult.OWN_GOAL + def test_related_events(self, lineup_data: Path, event_data: Path): dataset = statsbomb.load( lineup_data=lineup_data, event_data=event_data diff --git a/kloppy/tests/test_to_records.py b/kloppy/tests/test_to_records.py index 232825c6..c23496d7 100644 --- a/kloppy/tests/test_to_records.py +++ b/kloppy/tests/test_to_records.py @@ -29,7 +29,7 @@ def dataset(self, event_data: Path, lineup_data: Path) -> EventDataset: def test_default_columns(self, dataset: EventDataset): records = dataset.to_records() - assert len(records) == 4041 + assert len(records) == 4042 assert list(records[0].keys()) == [ "event_id", "event_type",