diff --git a/kloppy/__init__.py b/kloppy/__init__.py
index 8389a052..111b63cc 100644
--- a/kloppy/__init__.py
+++ b/kloppy/__init__.py
@@ -13,4 +13,4 @@
# )
# from .domain.services.state_builder import add_state
-__version__ = "3.15.0"
+__version__ = "3.16.0"
diff --git a/kloppy/domain/models/common.py b/kloppy/domain/models/common.py
index c1830d1b..57e01ebc 100644
--- a/kloppy/domain/models/common.py
+++ b/kloppy/domain/models/common.py
@@ -20,7 +20,7 @@
from .position import PositionType
-from ...utils import deprecated
+from ...utils import deprecated, snake_case
if sys.version_info >= (3, 8):
from typing import Literal
@@ -119,6 +119,46 @@ def __str__(self):
return self.value
+class OfficialType(Enum):
+ """Enumeration for types of officials (referees)."""
+
+ VideoAssistantReferee = "Video Assistant Referee"
+ MainReferee = "Main Referee"
+ AssistantReferee = "Assistant Referee"
+ FourthOfficial = "Fourth Official"
+
+ def __str__(self):
+ return self.value
+
+
+@dataclass(frozen=True)
+class Official:
+ """
+ Represents an official (referee) with optional names and roles.
+ """
+
+ official_id: str
+ name: Optional[str] = None
+ first_name: Optional[str] = None
+ last_name: Optional[str] = None
+ role: Optional[OfficialType] = None
+
+ @property
+ def full_name(self):
+ """
+ Returns the full name of the official, falling back to role-based or ID-based naming.
+ """
+ if self.name:
+ return self.name
+ if self.first_name and self.last_name:
+ return f"{self.first_name} {self.last_name}"
+ if self.last_name:
+ return self.last_name
+ if self.role:
+ return f"{snake_case(str(self.role))}_{self.official_id}"
+ return f"official_{self.official_id}"
+
+
@dataclass(frozen=True)
class Player:
"""
@@ -888,6 +928,85 @@ class DatasetFlag(Flag):
BALL_STATE = 2
+@dataclass
+class Statistic(ABC):
+ name: str = field(init=False)
+
+
+@dataclass
+class ScalarStatistic(Statistic):
+ value: float
+
+
+@dataclass
+class ExpectedGoals(ScalarStatistic):
+ """Expected goals"""
+
+ def __post_init__(self):
+ self.name = "xG"
+
+
+@dataclass
+class PostShotExpectedGoals(ScalarStatistic):
+ """Post-shot expected goals"""
+
+ def __post_init__(self):
+ self.name = "PSxG"
+
+
+@dataclass
+class ActionValue(Statistic):
+ """Action value"""
+
+ name: str
+ action_value_scoring_before: Optional[float] = field(default=None)
+ action_value_scoring_after: Optional[float] = field(default=None)
+ action_value_conceding_before: Optional[float] = field(default=None)
+ action_value_conceding_after: Optional[float] = field(default=None)
+
+ @property
+ def offensive_value(self) -> Optional[float]:
+ return (
+ None
+ if None
+ in (
+ self.action_value_scoring_before,
+ self.action_value_scoring_after,
+ )
+ else self.action_value_scoring_after
+ - self.action_value_scoring_before
+ )
+
+ @property
+ def defensive_value(self) -> Optional[float]:
+ return (
+ None
+ if None
+ in (
+ self.action_value_conceding_before,
+ self.action_value_conceding_after,
+ )
+ else self.action_value_conceding_after
+ - self.action_value_conceding_before
+ )
+
+ @property
+ def value(self) -> Optional[float]:
+ if None in (
+ self.action_value_scoring_before,
+ self.action_value_scoring_after,
+ self.action_value_conceding_before,
+ self.action_value_conceding_after,
+ ):
+ return None
+ return (
+ self.action_value_scoring_after - self.action_value_scoring_before
+ ) - (
+ self.action_value_conceding_after
+ - self.action_value_conceding_before
+ )
+
+
@dataclass
class DataRecord(ABC):
"""
@@ -905,6 +1024,7 @@ class DataRecord(ABC):
next_record: Optional["DataRecord"] = field(init=False)
period: Period
timestamp: timedelta
+ statistics: List[Statistic]
ball_owning_team: Optional[Team]
ball_state: Optional[BallState]
@@ -1016,6 +1136,7 @@ class Metadata:
game_id: Optional[str] = None
home_coach: Optional[str] = None
away_coach: Optional[str] = None
+ officials: Optional[List] = field(default_factory=list)
attributes: Optional[Dict] = field(default_factory=dict, compare=False)
def __post_init__(self):
diff --git a/kloppy/domain/services/event_factory.py b/kloppy/domain/services/event_factory.py
index 7afe01c3..89bc46c7 100644
--- a/kloppy/domain/services/event_factory.py
+++ b/kloppy/domain/services/event_factory.py
@@ -48,6 +48,9 @@ def create_event(event_cls: Type[T], **kwargs) -> T:
if "freeze_frame" not in kwargs:
kwargs["freeze_frame"] = None
+ if "statistics" not in kwargs:
+ kwargs["statistics"] = []
+
all_kwargs = dict(**kwargs, **extra_kwargs)
relevant_kwargs = {
@@ -66,7 +69,9 @@ def create_event(event_cls: Type[T], **kwargs) -> T:
f"The following arguments were skipped: {skipped_kwargs}"
)
- return event_cls(**relevant_kwargs)
+ event = event_cls(**relevant_kwargs)
+
+ return event
class EventFactory:
diff --git a/kloppy/domain/services/frame_factory.py b/kloppy/domain/services/frame_factory.py
new file mode 100644
index 00000000..d53fe41d
--- /dev/null
+++ b/kloppy/domain/services/frame_factory.py
@@ -0,0 +1,36 @@
+import dataclasses
+import warnings
+from dataclasses import fields
+
+from kloppy.domain import Frame
+
+
+def create_frame(**kwargs) -> Frame:
+ """
+ Do the actual construction of a frame.
+
+ This method does a couple of things:
+ 1. Fill in some arguments when not passed
+ 2. Pass only arguments that are accepted by the Frame class.
+ """
+ if "statistics" not in kwargs:
+ kwargs["statistics"] = []
+
+ relevant_kwargs = {
+ field.name: kwargs.get(field.name, field.default)
+ for field in fields(Frame)
+ if field.init
+ and not (
+ field.default == dataclasses.MISSING and field.name not in kwargs
+ )
+ }
+
+ if len(relevant_kwargs) < len(kwargs):
+ skipped_kwargs = set(kwargs.keys()) - set(relevant_kwargs.keys())
+ warnings.warn(
+ f"The following arguments were skipped: {skipped_kwargs}"
+ )
+
+ frame = Frame(**relevant_kwargs)
+
+ return frame
diff --git a/kloppy/domain/services/transformers/dataset.py b/kloppy/domain/services/transformers/dataset.py
index e208e0b5..349d036f 100644
--- a/kloppy/domain/services/transformers/dataset.py
+++ b/kloppy/domain/services/transformers/dataset.py
@@ -220,6 +220,7 @@ def __change_frame_coordinate_system(self, frame: Frame):
for key, player_data in frame.players_data.items()
},
other_data=frame.other_data,
+ statistics=frame.statistics,
)
def __change_frame_dimensions(self, frame: Frame):
@@ -246,6 +247,7 @@ def __change_frame_dimensions(self, frame: Frame):
for key, player_data in frame.players_data.items()
},
other_data=frame.other_data,
+ statistics=frame.statistics,
)
def __change_point_coordinate_system(
@@ -303,6 +305,7 @@ def __flip_frame(self, frame: Frame):
ball_coordinates=self.flip_point(frame.ball_coordinates),
players_data=players_data,
other_data=frame.other_data,
+ statistics=frame.statistics,
)
def transform_event(self, event: Event) -> Event:
diff --git a/kloppy/exceptions.py b/kloppy/exceptions.py
index c6970585..e15a3338 100644
--- a/kloppy/exceptions.py
+++ b/kloppy/exceptions.py
@@ -40,3 +40,7 @@ class UnknownEncoderError(KloppyError):
class KloppyParameterError(KloppyError):
pass
+
+
+class DeserializationWarning(Warning):
+ pass
diff --git a/kloppy/infra/serializers/code/sportscode.py b/kloppy/infra/serializers/code/sportscode.py
index 612bf8ca..c4ccb8c1 100644
--- a/kloppy/infra/serializers/code/sportscode.py
+++ b/kloppy/infra/serializers/code/sportscode.py
@@ -68,6 +68,7 @@ def deserialize(self, inputs: SportsCodeInputs) -> CodeDataset:
labels=parse_labels(instance),
ball_state=None,
ball_owning_team=None,
+ statistics=[],
)
period.end_timestamp = end_timestamp
codes.append(code)
diff --git a/kloppy/infra/serializers/event/datafactory/deserializer.py b/kloppy/infra/serializers/event/datafactory/deserializer.py
index cf3d11eb..44f5df20 100644
--- a/kloppy/infra/serializers/event/datafactory/deserializer.py
+++ b/kloppy/infra/serializers/event/datafactory/deserializer.py
@@ -1,9 +1,8 @@
import json
import logging
-from datetime import timedelta, datetime, timezone
-from dateutil.parser import parse, _parser
from dataclasses import replace
-from typing import Dict, List, Tuple, Union, IO, NamedTuple
+from datetime import datetime, timedelta, timezone
+from typing import IO, Dict, List, NamedTuple, Tuple, Union
from kloppy.domain import (
AttackingDirection,
@@ -41,7 +40,6 @@
from kloppy.infra.serializers.event.deserializer import EventDataDeserializer
from kloppy.utils import Readable, performance_logging
-
logger = logging.getLogger(__name__)
@@ -435,7 +433,7 @@ def deserialize(self, inputs: DatafactoryInputs) -> EventDataset:
+ status_update["time"]
+ match["stadiumGMT"],
"%Y%m%d%H:%M:%S%z",
- ).astimezone(timezone.utc)
+ )
half = status_update["t"]["half"]
if status_update["type"] == DF_EVENT_TYPE_STATUS_MATCH_START:
half = 1
@@ -458,8 +456,10 @@ def deserialize(self, inputs: DatafactoryInputs) -> EventDataset:
date = match["date"]
if date:
# TODO: scheduledStart and stadiumGMT should probably be used here too
- date = parse(date).astimezone(timezone.utc)
- except _parser.ParserError:
+ date = datetime.strptime(date, "%Y%m%d").replace(
+ tzinfo=timezone.utc
+ )
+ except ValueError:
date = None
game_week = match.get("week", None)
if game_week:
diff --git a/kloppy/infra/serializers/event/sportec/deserializer.py b/kloppy/infra/serializers/event/sportec/deserializer.py
index 14895206..b240db49 100644
--- a/kloppy/infra/serializers/event/sportec/deserializer.py
+++ b/kloppy/infra/serializers/event/sportec/deserializer.py
@@ -2,7 +2,6 @@
from typing import Dict, List, NamedTuple, IO
from datetime import timedelta, datetime, timezone
import logging
-from dateutil.parser import parse
from lxml import objectify
from kloppy.domain import (
@@ -29,6 +28,8 @@
CardType,
AttackingDirection,
PositionType,
+ Official,
+ OfficialType,
)
from kloppy.exceptions import DeserializationError
from kloppy.infra.serializers.event.deserializer import EventDataDeserializer
@@ -55,6 +56,14 @@
"LA": PositionType.LeftWing,
}
+referee_types_mapping: Dict[str, OfficialType] = {
+ "referee": OfficialType.MainReferee,
+ "firstAssistant": OfficialType.AssistantReferee,
+ "videoReferee": OfficialType.VideoAssistantReferee,
+ "secondAssistant": OfficialType.AssistantReferee,
+ "fourthOfficial": OfficialType.FourthOfficial,
+}
+
logger = logging.getLogger(__name__)
@@ -102,6 +111,7 @@ class SportecMetadata(NamedTuple):
fps: int
home_coach: str
away_coach: str
+ officials: List[Official]
def sportec_metadata_from_xml_elm(match_root) -> SportecMetadata:
@@ -213,6 +223,31 @@ def sportec_metadata_from_xml_elm(match_root) -> SportecMetadata:
]
)
+ if hasattr(match_root, "MatchInformation") and hasattr(
+ match_root.MatchInformation, "Referees"
+ ):
+ officials = []
+ referee_path = objectify.ObjectPath(
+ "PutDataRequest.MatchInformation.Referees"
+ )
+ referee_elms = referee_path.find(match_root).iterchildren(
+ tag="Referee"
+ )
+
+ for referee in referee_elms:
+ ref_attrib = referee.attrib
+ officials.append(
+ Official(
+ official_id=ref_attrib["PersonId"],
+ name=ref_attrib["Shortname"],
+ first_name=ref_attrib["FirstName"],
+ last_name=ref_attrib["LastName"],
+ role=referee_types_mapping[ref_attrib["Role"]],
+ )
+ )
+ else:
+ officials = []
+
return SportecMetadata(
score=score,
teams=teams,
@@ -222,6 +257,7 @@ def sportec_metadata_from_xml_elm(match_root) -> SportecMetadata:
fps=SPORTEC_FPS,
home_coach=home_coach,
away_coach=away_coach,
+ officials=officials,
)
@@ -277,7 +313,7 @@ def _event_chain_from_xml_elm(event_elm):
def _parse_datetime(dt_str: str) -> datetime:
- return parse(dt_str).astimezone(timezone.utc)
+ return datetime.fromisoformat(dt_str)
def _get_event_qualifiers(event_chain: Dict) -> List[Qualifier]:
@@ -432,9 +468,9 @@ def deserialize(self, inputs: SportecEventDataInputs) -> EventDataset:
event_root = objectify.fromstring(inputs.event_data.read())
with performance_logging("parse data", logger=logger):
- date = parse(
+ date = datetime.fromisoformat(
match_root.MatchInformation.General.attrib["KickoffTime"]
- ).astimezone(timezone.utc)
+ )
game_week = match_root.MatchInformation.General.attrib["MatchDay"]
game_id = match_root.MatchInformation.General.attrib["MatchId"]
@@ -673,6 +709,7 @@ def deserialize(self, inputs: SportecEventDataInputs) -> EventDataset:
game_id=game_id,
home_coach=home_coach,
away_coach=away_coach,
+ officials=sportec_metadata.officials,
)
return EventDataset(
diff --git a/kloppy/infra/serializers/event/statsbomb/helpers.py b/kloppy/infra/serializers/event/statsbomb/helpers.py
index fc3a9ec3..5f16016f 100644
--- a/kloppy/infra/serializers/event/statsbomb/helpers.py
+++ b/kloppy/infra/serializers/event/statsbomb/helpers.py
@@ -11,7 +11,10 @@
Player,
PlayerData,
PositionType,
+ ActionValue,
+ Provider,
)
+from kloppy.domain.services.frame_factory import create_frame
from kloppy.exceptions import DeserializationError
@@ -21,6 +24,25 @@ def parse_str_ts(timestamp: str) -> float:
return timedelta(seconds=int(h) * 3600 + int(m) * 60 + float(s))
+def parse_obv_values(raw_event: dict) -> Optional[ActionValue]:
+ game_state_values_data = {}
+ obv_mapping = {
+ "obv_for_before": "action_value_scoring_before",
+ "obv_against_before": "action_value_conceding_before",
+ "obv_for_after": "action_value_scoring_after",
+ "obv_against_after": "action_value_conceding_after",
+ }
+ for sb_name, kloppy_name in obv_mapping.items():
+ obv_value = raw_event.get(sb_name)
+ if obv_value is not None:
+ game_state_values_data[kloppy_name] = obv_value
+
+ if game_state_values_data:
+ game_state_value = ActionValue(name="OBV", **game_state_values_data)
+
+ return game_state_value
+
+
def get_team_by_id(team_id: int, teams: List[Team]) -> Team:
"""Get a team by its id."""
if str(team_id) == teams[0].team_id:
@@ -129,7 +151,7 @@ def get_player_from_freeze_frame(player_data, team, i):
+ event.timestamp.total_seconds() * FREEZE_FRAME_FPS
)
- return Frame(
+ frame = create_frame(
frame_id=frame_id,
ball_coordinates=Point3D(
x=event.coordinates.x, y=event.coordinates.y, z=0
@@ -141,3 +163,5 @@ def get_player_from_freeze_frame(player_data, team, i):
ball_owning_team=event.ball_owning_team,
other_data={"visible_area": visible_area},
)
+
+ return frame
diff --git a/kloppy/infra/serializers/event/statsbomb/specification.py b/kloppy/infra/serializers/event/statsbomb/specification.py
index 30bf2df2..f6fa2995 100644
--- a/kloppy/infra/serializers/event/statsbomb/specification.py
+++ b/kloppy/infra/serializers/event/statsbomb/specification.py
@@ -27,6 +27,8 @@
FormationType,
PositionType,
CounterAttackQualifier,
+ ExpectedGoals,
+ PostShotExpectedGoals,
)
from kloppy.exceptions import DeserializationError
from kloppy.infra.serializers.event.statsbomb.helpers import (
@@ -34,6 +36,7 @@
get_team_by_id,
get_period_by_id,
parse_coordinates,
+ parse_obv_values,
)
@@ -293,6 +296,7 @@ def deserialize(self, event_factory: EventFactory) -> List[Event]:
return events
def _parse_generic_kwargs(self) -> Dict:
+ game_state_value = parse_obv_values(self.raw_event)
return {
"period": self.period,
"timestamp": parse_str_ts(self.raw_event["timestamp"]),
@@ -311,6 +315,7 @@ def _parse_generic_kwargs(self) -> Dict:
),
"related_event_ids": self.raw_event.get("related_events", []),
"raw_event": self.raw_event,
+ "statistics": [game_state_value] if game_state_value else [],
}
def _create_aerial_won_event(
@@ -548,6 +553,16 @@ def _create_events(
EVENT_TYPE.SHOT, shot_dict
) + _get_body_part_qualifiers(shot_dict)
+ for statistic_cls, prop_name in {
+ ExpectedGoals: "statsbomb_xg",
+ PostShotExpectedGoals: "shot_execution_xg",
+ }.items():
+ value = shot_dict.get(prop_name, None)
+ if value is not None:
+ generic_event_kwargs["statistics"].append(
+ statistic_cls(value=value)
+ )
+
shot_event = event_factory.build_shot(
result=result,
qualifiers=qualifiers,
diff --git a/kloppy/infra/serializers/event/statsperform/deserializer.py b/kloppy/infra/serializers/event/statsperform/deserializer.py
index f603717a..6271e0d2 100644
--- a/kloppy/infra/serializers/event/statsperform/deserializer.py
+++ b/kloppy/infra/serializers/event/statsperform/deserializer.py
@@ -1,9 +1,10 @@
-import pytz
import math
from typing import Dict, List, NamedTuple, IO, Optional
import logging
from datetime import datetime, timedelta
+import pytz
+
from kloppy.domain import (
EventDataset,
Team,
@@ -34,6 +35,8 @@
GoalkeeperActionType,
CounterAttackQualifier,
PositionType,
+ ExpectedGoals,
+ PostShotExpectedGoals,
)
from kloppy.exceptions import DeserializationError
from kloppy.infra.serializers.event.deserializer import EventDataDeserializer
@@ -151,6 +154,9 @@
EVENT_QUALIFIER_FORMATION_PLAYER_IDS = 30
EVENT_QUALIFIER_FORMATION_PLAYER_POSITIONS = 131
+EVENT_QUALIFIER_XG = 321
+EVENT_QUALIFIER_POST_SHOT_XG = 322
+
event_type_names = {
1: "pass",
2: "offside pass",
@@ -402,13 +408,27 @@ def _parse_shot(raw_event: OptaEvent) -> Dict:
y=100 - result_coordinates.y,
)
- return dict(
+ event_info = dict(
coordinates=coordinates,
result=result,
result_coordinates=result_coordinates,
qualifiers=qualifiers,
)
+ statistics = []
+ for event_qualifier, statistic in zip(
+ [EVENT_QUALIFIER_XG, EVENT_QUALIFIER_POST_SHOT_XG],
+ [ExpectedGoals, PostShotExpectedGoals],
+ ):
+ xg_value = raw_event.qualifiers.get(event_qualifier)
+ if xg_value:
+ statistics.append(statistic(value=float(xg_value)))
+
+ if statistics:
+ event_info["statistics"] = statistics
+
+ return event_info
+
def _parse_goalkeeper_events(raw_event: OptaEvent) -> Dict:
qualifiers = _get_event_qualifiers(raw_event.qualifiers)
@@ -724,6 +744,8 @@ def deserialize(self, inputs: StatsPerformInputs) -> EventDataset:
f"Set end of period {period.id} to {raw_event.timestamp}"
)
period.end_timestamp = raw_event.timestamp
+ elif raw_event.type_id == EVENT_TYPE_PLAYER_ON:
+ continue
else:
if not period.start_timestamp:
# not started yet
@@ -793,11 +815,18 @@ def deserialize(self, inputs: StatsPerformInputs) -> EventDataset:
):
if raw_event.type_id == EVENT_TYPE_SHOT_GOAL:
if 374 in raw_event.qualifiers:
+ # Qualifier 374 specifies the actual time of the shot for all goal events
+ # It uses London timezone for both MA3 and F24 feeds
+ naive_datetime = datetime.strptime(
+ raw_event.qualifiers[374],
+ "%Y-%m-%d %H:%M:%S.%f",
+ )
+ timezone = pytz.timezone("Europe/London")
+ aware_datetime = timezone.localize(
+ naive_datetime
+ )
generic_event_kwargs["timestamp"] = (
- datetime.strptime(
- raw_event.qualifiers[374],
- "%Y-%m-%d %H:%M:%S.%f",
- ).replace(tzinfo=pytz.utc)
+ aware_datetime.astimezone(pytz.utc)
- period.start_timestamp
)
shot_event_kwargs = _parse_shot(raw_event)
diff --git a/kloppy/infra/serializers/event/statsperform/parsers/base.py b/kloppy/infra/serializers/event/statsperform/parsers/base.py
index 3fee98b9..2a7ca7cc 100644
--- a/kloppy/infra/serializers/event/statsperform/parsers/base.py
+++ b/kloppy/infra/serializers/event/statsperform/parsers/base.py
@@ -61,7 +61,7 @@ def extract_score(self) -> Optional[Score]:
"""Return the score of the game."""
return None
- def extract_date(self) -> Optional[str]:
+ def extract_date(self) -> Optional[datetime]:
"""Return the date of the game."""
return None
diff --git a/kloppy/infra/serializers/event/statsperform/parsers/f24_xml.py b/kloppy/infra/serializers/event/statsperform/parsers/f24_xml.py
index f32dbd95..e8cb1ffb 100644
--- a/kloppy/infra/serializers/event/statsperform/parsers/f24_xml.py
+++ b/kloppy/infra/serializers/event/statsperform/parsers/f24_xml.py
@@ -1,10 +1,11 @@
"""XML parser for Opta F24 feeds."""
-import pytz
-from datetime import datetime, timezone
+
+from datetime import datetime
from typing import List, Optional
-from dateutil.parser import parse
-from .base import OptaXMLParser, OptaEvent
+import pytz
+
+from .base import OptaEvent, OptaXMLParser
def _parse_f24_datetime(dt_str: str) -> datetime:
@@ -15,9 +16,10 @@ def zero_pad_milliseconds(timestamp):
return ".".join(parts[:-1] + ["{:03d}".format(int(parts[-1]))])
dt_str = zero_pad_milliseconds(dt_str)
- return datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%S.%f").replace(
- tzinfo=pytz.utc
- )
+ naive_datetime = datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%S.%f")
+ timezone = pytz.timezone("Europe/London")
+ aware_datetime = timezone.localize(naive_datetime)
+ return aware_datetime.astimezone(pytz.utc)
class F24XMLParser(OptaXMLParser):
@@ -54,11 +56,16 @@ def extract_events(self) -> List[OptaEvent]:
for event in game_elm.iterchildren("Event")
]
- def extract_date(self) -> Optional[str]:
+ def extract_date(self) -> Optional[datetime]:
"""Return the date of the game."""
game_elm = self.root.find("Game")
if game_elm and "game_date" in game_elm.attrib:
- return parse(game_elm.attrib["game_date"]).astimezone(timezone.utc)
+ naive_datetime = datetime.strptime(
+ game_elm.attrib["game_date"], "%Y-%m-%dT%H:%M:%S"
+ )
+ timezone = pytz.timezone("Europe/London")
+ aware_datetime = timezone.localize(naive_datetime)
+ return aware_datetime.astimezone(pytz.utc)
else:
return None
diff --git a/kloppy/infra/serializers/event/statsperform/parsers/ma1_json.py b/kloppy/infra/serializers/event/statsperform/parsers/ma1_json.py
index c9aa3974..8c1bf6e2 100644
--- a/kloppy/infra/serializers/event/statsperform/parsers/ma1_json.py
+++ b/kloppy/infra/serializers/event/statsperform/parsers/ma1_json.py
@@ -1,10 +1,11 @@
"""JSON parser for Stats Perform MA1 feeds."""
-import pytz
+
from datetime import datetime, timezone
-from typing import Any, Optional, List, Tuple, Dict
+from typing import Any, Dict, List, Optional, Tuple
-from kloppy.domain import Period, Score, Team, Ground, Player
+from kloppy.domain import Ground, Period, Player, Score, Team
from kloppy.exceptions import DeserializationError
+
from .base import OptaJSONParser
@@ -30,12 +31,12 @@ def extract_periods(self) -> List[Period]:
id=period["id"],
start_timestamp=datetime.strptime(
period_start_raw, "%Y-%m-%dT%H:%M:%SZ"
- ).replace(tzinfo=pytz.utc)
+ ).replace(tzinfo=timezone.utc)
if period_start_raw
else None,
end_timestamp=datetime.strptime(
period_end_raw, "%Y-%m-%dT%H:%M:%SZ"
- ).replace(tzinfo=pytz.utc)
+ ).replace(tzinfo=timezone.utc)
if period_end_raw
else None,
)
@@ -95,12 +96,12 @@ def extract_lineups(self) -> Tuple[Team, Team]:
raise DeserializationError("Lineup incomplete")
return home_team, away_team
- def extract_date(self) -> Optional[str]:
+ def extract_date(self) -> Optional[datetime]:
"""Return the date of the game."""
if "matchInfo" in self.root and "date" in self.root["matchInfo"]:
return datetime.strptime(
self.root["matchInfo"]["date"], "%Y-%m-%dZ"
- ).astimezone(timezone.utc)
+ ).replace(tzinfo=timezone.utc)
else:
return None
diff --git a/kloppy/infra/serializers/event/statsperform/parsers/ma1_xml.py b/kloppy/infra/serializers/event/statsperform/parsers/ma1_xml.py
index 5b7bda49..92058877 100644
--- a/kloppy/infra/serializers/event/statsperform/parsers/ma1_xml.py
+++ b/kloppy/infra/serializers/event/statsperform/parsers/ma1_xml.py
@@ -1,6 +1,5 @@
"""XML parser for Stats Perform MA1 feeds."""
-import pytz
-from datetime import datetime
+from datetime import datetime, timezone
from typing import Any, Optional, List, Dict, Tuple
from kloppy.domain import Period, Score, Team, Ground, Player
@@ -22,10 +21,10 @@ def extract_periods(self) -> List[Period]:
id=int(period.get("id")),
start_timestamp=datetime.strptime(
period.get("start"), "%Y-%m-%dT%H:%M:%SZ"
- ).replace(tzinfo=pytz.utc),
+ ).replace(tzinfo=timezone.utc),
end_timestamp=datetime.strptime(
period.get("end"), "%Y-%m-%dT%H:%M:%SZ"
- ).replace(tzinfo=pytz.utc),
+ ).replace(tzinfo=timezone.utc),
)
)
return parsed_periods
diff --git a/kloppy/infra/serializers/event/statsperform/parsers/ma3_json.py b/kloppy/infra/serializers/event/statsperform/parsers/ma3_json.py
index 59494bfa..a91cc148 100644
--- a/kloppy/infra/serializers/event/statsperform/parsers/ma3_json.py
+++ b/kloppy/infra/serializers/event/statsperform/parsers/ma3_json.py
@@ -1,6 +1,5 @@
"""JSON parser for Stats Perform MA3 feeds."""
-import pytz
-from datetime import datetime
+from datetime import datetime, timezone
from typing import List
from .base import OptaJSONParser, OptaEvent
@@ -9,12 +8,12 @@
def _parse_ma3_datetime(dt_str: str) -> datetime:
try:
return datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%S.%fZ").replace(
- tzinfo=pytz.utc
+ tzinfo=timezone.utc
)
except ValueError:
return datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%SZ").replace(
- tzinfo=pytz.utc
+ tzinfo=timezone.utc
)
diff --git a/kloppy/infra/serializers/event/statsperform/parsers/ma3_xml.py b/kloppy/infra/serializers/event/statsperform/parsers/ma3_xml.py
index 148b4d79..823f8313 100644
--- a/kloppy/infra/serializers/event/statsperform/parsers/ma3_xml.py
+++ b/kloppy/infra/serializers/event/statsperform/parsers/ma3_xml.py
@@ -1,6 +1,5 @@
"""XML parser for Stats Perform MA3 feeds."""
-import pytz
-from datetime import datetime
+from datetime import datetime, timezone
from typing import List
from .base import OptaXMLParser, OptaEvent
@@ -9,11 +8,11 @@
def _parse_ma3_datetime(dt_str: str) -> datetime:
try:
return datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%S.%fZ").replace(
- tzinfo=pytz.utc
+ tzinfo=timezone.utc
)
except ValueError:
return datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%SZ").replace(
- tzinfo=pytz.utc
+ tzinfo=timezone.utc
)
diff --git a/kloppy/infra/serializers/event/wyscout/deserializer_v3.py b/kloppy/infra/serializers/event/wyscout/deserializer_v3.py
index 1fa4cb2c..1e05ea53 100644
--- a/kloppy/infra/serializers/event/wyscout/deserializer_v3.py
+++ b/kloppy/infra/serializers/event/wyscout/deserializer_v3.py
@@ -1,13 +1,12 @@
import bisect
import json
import logging
+import warnings
from dataclasses import replace
-from datetime import timedelta, timezone
+from datetime import datetime, timedelta, timezone
from enum import Enum
from typing import Dict, List, Optional
-from dateutil.parser import parse
-
from kloppy.domain import (
BodyPart,
BodyPartQualifier,
@@ -40,8 +39,12 @@
TakeOnResult,
Team,
BallState,
+ FormationType,
+ CarryResult,
+ ExpectedGoals,
+ PostShotExpectedGoals,
)
-from kloppy.exceptions import DeserializationError
+from kloppy.exceptions import DeserializationError, DeserializationWarning
from kloppy.utils import performance_logging
from ..deserializer import EventDataDeserializer
@@ -272,10 +275,20 @@ def _parse_shot(raw_event: Dict) -> Dict:
elif raw_event["shot"]["bodyPart"] == "right_foot":
qualifiers.append(BodyPartQualifier(value=BodyPart.RIGHT_FOOT))
+ statistics = []
+ for statistic_cls, prop_name in {
+ ExpectedGoals: "xg",
+ PostShotExpectedGoals: "postShotXg",
+ }.items():
+ value = raw_event["shot"].get(prop_name, None)
+ if value is not None:
+ statistics.append(statistic_cls(value=value))
+
return {
"result": result,
"result_coordinates": _create_shot_result_coordinates(raw_event),
"qualifiers": qualifiers,
+ "statistics": statistics,
}
@@ -755,7 +768,9 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset:
)
date = raw_events["match"].get("dateutc")
if date:
- date = parse(date).astimezone(timezone.utc)
+ date = datetime.strptime(date, "%Y-%m-%d %H:%M:%S").replace(
+ tzinfo=timezone.utc
+ )
game_week = raw_events["match"].get("gameweek")
if game_week:
game_week = str(game_week)
@@ -826,6 +841,19 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset:
str(raw_event["possession"]["team"]["id"])
]
+ if player_id == INVALID_PLAYER:
+ player = None
+ elif player_id not in players[team_id]:
+ player = None
+ warnings.warn(
+ f"Event {raw_event['id']} was performed by player {player_id} and team {team_id}, "
+ f"but the player does not appear to be part of that team's lineup. "
+ f"Handled by setting the event's player to None.",
+ DeserializationWarning,
+ )
+ else:
+ player = players[team_id][player_id]
+
generic_event_args = {
"event_id": raw_event["id"],
"raw_event": raw_event,
@@ -838,11 +866,7 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset:
else None
),
"team": team,
- "player": (
- players[team_id][player_id]
- if player_id != INVALID_PLAYER
- else None
- ),
+ "player": player,
"ball_owning_team": ball_owning_team,
"ball_state": None,
"period": periods[-1],
diff --git a/kloppy/infra/serializers/tracking/metrica_csv.py b/kloppy/infra/serializers/tracking/metrica_csv.py
index e20c1172..629f4962 100644
--- a/kloppy/infra/serializers/tracking/metrica_csv.py
+++ b/kloppy/infra/serializers/tracking/metrica_csv.py
@@ -20,6 +20,7 @@
Player,
PlayerData,
)
+from kloppy.domain.services.frame_factory import create_frame
from kloppy.infra.serializers.tracking.deserializer import (
TrackingDataDeserializer,
)
@@ -188,7 +189,7 @@ def deserialize(
**away_partial_frame.players_data,
}
- frame = Frame(
+ frame = create_frame(
frame_id=frame_id,
timestamp=timedelta(seconds=frame_id / frame_rate)
- period.start_timestamp,
diff --git a/kloppy/infra/serializers/tracking/metrica_epts/deserializer.py b/kloppy/infra/serializers/tracking/metrica_epts/deserializer.py
index b81a6d45..d83fd186 100644
--- a/kloppy/infra/serializers/tracking/metrica_epts/deserializer.py
+++ b/kloppy/infra/serializers/tracking/metrica_epts/deserializer.py
@@ -11,6 +11,7 @@
PlayerData,
DatasetTransformer,
)
+from kloppy.domain.services.frame_factory import create_frame
from kloppy.utils import performance_logging
from .metadata import load_metadata, EPTSMetadata
@@ -73,7 +74,7 @@ def _frame_from_row(
other_data=other_data,
)
- frame = Frame(
+ frame = create_frame(
frame_id=row["frame_id"],
timestamp=timestamp,
ball_owning_team=None,
diff --git a/kloppy/infra/serializers/tracking/secondspectrum.py b/kloppy/infra/serializers/tracking/secondspectrum.py
index ca4441e8..94b0f4dc 100644
--- a/kloppy/infra/serializers/tracking/secondspectrum.py
+++ b/kloppy/infra/serializers/tracking/secondspectrum.py
@@ -25,6 +25,7 @@
PlayerData,
Score,
)
+from kloppy.domain.services.frame_factory import create_frame
from kloppy.utils import Readable, performance_logging
@@ -96,7 +97,7 @@ def _frame_from_framedata(cls, teams, period, frame_data):
coordinates=Point(float(x), float(y)), speed=speed
)
- return Frame(
+ frame = create_frame(
frame_id=frame_id,
timestamp=frame_timestamp,
ball_coordinates=ball_coordinates,
@@ -108,6 +109,8 @@ def _frame_from_framedata(cls, teams, period, frame_data):
other_data={},
)
+ return frame
+
@staticmethod
def __validate_inputs(inputs: Dict[str, Readable]):
if "xml_metadata" not in inputs:
diff --git a/kloppy/infra/serializers/tracking/skillcorner.py b/kloppy/infra/serializers/tracking/skillcorner.py
index b5cc0306..acce0966 100644
--- a/kloppy/infra/serializers/tracking/skillcorner.py
+++ b/kloppy/infra/serializers/tracking/skillcorner.py
@@ -1,15 +1,12 @@
+import json
import logging
-from datetime import timedelta, timezone
-from dateutil.parser import parse
import warnings
-from typing import NamedTuple, IO, Optional, Union, Dict
-from collections import Counter
-import numpy as np
-import json
+from collections import Counter, defaultdict
+from datetime import datetime, timedelta, timezone
from pathlib import Path
+from typing import IO, Dict, NamedTuple, Optional, Union
from kloppy.domain import (
- attacking_direction_from_frame,
AttackingDirection,
DatasetFlag,
Frame,
@@ -18,6 +15,7 @@
Orientation,
Period,
Player,
+ PlayerData,
Point,
Point3D,
PositionType,
@@ -25,8 +23,9 @@
Score,
Team,
TrackingDataset,
- PlayerData,
+ attacking_direction_from_frame,
)
+from kloppy.domain.services.frame_factory import create_frame
from kloppy.infra.serializers.tracking.deserializer import (
TrackingDataDeserializer,
)
@@ -133,15 +132,18 @@ def _get_frame_data(
track_id = frame_record.get("track_id", None)
group_name = frame_record.get("group_name", None)
- if trackable_object == ball_id:
- group_name = "ball"
+ if trackable_object == ball_id or group_name == "balls":
+ group_name = "balls"
z = frame_record.get("z")
if z is not None:
z = float(z)
ball_coordinates = Point3D(x=float(x), y=float(y), z=z)
continue
- elif trackable_object in referee_dict.keys():
+ elif (
+ trackable_object in referee_dict.keys()
+ or group_name == "referee"
+ ):
group_name = "referee"
continue # Skip Referee Coords
@@ -173,7 +175,7 @@ def _get_frame_data(
players_data[player] = PlayerData(coordinates=Point(x, y))
- return Frame(
+ frame = create_frame(
frame_id=frame_id,
timestamp=frame_time,
ball_coordinates=ball_coordinates,
@@ -184,6 +186,8 @@ def _get_frame_data(
other_data={},
)
+ return frame
+
@classmethod
def _timestamp_from_timestring(cls, timestring):
parts = timestring.split(":")
@@ -207,22 +211,21 @@ def _get_skillcorner_attacking_directions(cls, frames, periods):
x-coords might not reflect the attacking direction.
"""
attacking_directions = {}
- frame_period_ids = np.array([_frame.period.id for _frame in frames])
- frame_attacking_directions = np.array(
- [
- attacking_direction_from_frame(frame)
- if len(frame.players_data) > 0
- else AttackingDirection.NOT_SET
- for frame in frames
- ]
- )
+ # Group attacking directions by period ID
+ period_direction_map = defaultdict(list)
+ for frame in frames:
+ if len(frame.players_data) > 0:
+ direction = attacking_direction_from_frame(frame)
+ else:
+ direction = AttackingDirection.NOT_SET
+ period_direction_map[frame.period.id].append(direction)
+
+ # Determine the most common attacking direction for each period
for period_id in periods.keys():
- if period_id in frame_period_ids:
- count = Counter(
- frame_attacking_directions[frame_period_ids == period_id]
- )
- attacking_directions[period_id] = count.most_common()[0][0]
+ if period_id in period_direction_map:
+ count = Counter(period_direction_map[period_id])
+ attacking_directions[period_id] = count.most_common(1)[0][0]
else:
attacking_directions[period_id] = AttackingDirection.NOT_SET
@@ -252,28 +255,33 @@ def __get_periods(cls, tracking):
"""gets the Periods contained in the tracking data"""
periods = {}
- _periods = np.array([f["period"] for f in tracking])
- unique_periods = set(_periods)
- unique_periods = [
- period for period in unique_periods if period is not None
- ]
+ # Extract unique periods while filtering out None values
+ unique_periods = {
+ frame["period"]
+ for frame in tracking
+ if frame["period"] is not None
+ }
for period in unique_periods:
+ # Filter frames that belong to the current period and have valid "time"
_frames = [
frame
for frame in tracking
if frame["period"] == period and frame["time"] is not None
]
- periods[period] = Period(
- id=period,
- start_timestamp=timedelta(
- seconds=_frames[0]["frame"] / frame_rate
- ),
- end_timestamp=timedelta(
- seconds=_frames[-1]["frame"] / frame_rate
- ),
- )
+ # Ensure _frames is not empty before accessing the first and last elements
+ if _frames:
+ periods[period] = Period(
+ id=period,
+ start_timestamp=timedelta(
+ seconds=_frames[0]["frame"] / frame_rate
+ ),
+ end_timestamp=timedelta(
+ seconds=_frames[-1]["frame"] / frame_rate
+ ),
+ )
+
return periods
@classmethod
@@ -367,7 +375,9 @@ def deserialize(self, inputs: SkillCornerInputs) -> TrackingDataset:
date = metadata.get("date_time")
if date:
- date = parse(date).astimezone(timezone.utc)
+ date = datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ").replace(
+ tzinfo=timezone.utc
+ )
game_id = metadata.get("id")
if game_id:
diff --git a/kloppy/infra/serializers/tracking/sportec/deserializer.py b/kloppy/infra/serializers/tracking/sportec/deserializer.py
index 3f418375..b4e50906 100644
--- a/kloppy/infra/serializers/tracking/sportec/deserializer.py
+++ b/kloppy/infra/serializers/tracking/sportec/deserializer.py
@@ -2,8 +2,7 @@
import warnings
from collections import defaultdict
from typing import NamedTuple, Optional, Union, IO
-from datetime import timedelta, timezone
-from dateutil.parser import parse
+from datetime import datetime, timedelta
from lxml import objectify
@@ -22,6 +21,7 @@
Provider,
PlayerData,
)
+from kloppy.domain.services.frame_factory import create_frame
from kloppy.utils import performance_logging
@@ -122,6 +122,7 @@ def deserialize(
with performance_logging("parse metadata", logger=logger):
sportec_metadata = sportec_metadata_from_xml_elm(match_root)
teams = home_team, away_team = sportec_metadata.teams
+
periods = sportec_metadata.periods
transformer = self.get_transformer(
pitch_length=sportec_metadata.x_max,
@@ -130,10 +131,16 @@ def deserialize(
home_coach = sportec_metadata.home_coach
away_coach = sportec_metadata.away_coach
+ official_ids = []
+ if sportec_metadata.officials:
+ official_ids = [
+ x.official_id for x in sportec_metadata.officials
+ ]
+
with performance_logging("parse raw data", logger=logger):
- date = parse(
+ date = datetime.fromisoformat(
match_root.MatchInformation.General.attrib["KickoffTime"]
- ).astimezone(timezone.utc)
+ )
game_week = match_root.MatchInformation.General.attrib["MatchDay"]
game_id = match_root.MatchInformation.General.attrib["MatchId"]
@@ -156,6 +163,7 @@ def _iter():
for i, (frame_id, frame_data) in enumerate(
sorted(raw_frames.items())
):
+
if "ball" not in frame_data:
# Frames without ball data are corrupt.
continue
@@ -165,7 +173,7 @@ def _iter():
continue
if i % sample == 0:
- yield Frame(
+ yield create_frame(
frame_id=frame_id,
timestamp=timedelta(
seconds=(
@@ -193,6 +201,7 @@ def _iter():
)
for player_id, raw_player_data in frame_data.items()
if player_id != "ball"
+ and player_id not in official_ids
},
other_data={},
ball_coordinates=Point3D(
@@ -242,6 +251,7 @@ def _iter():
game_id=game_id,
home_coach=home_coach,
away_coach=away_coach,
+ officials=sportec_metadata.officials,
)
return TrackingDataset(
diff --git a/kloppy/infra/serializers/tracking/statsperform.py b/kloppy/infra/serializers/tracking/statsperform.py
index 1cf413d5..21a5a4c3 100644
--- a/kloppy/infra/serializers/tracking/statsperform.py
+++ b/kloppy/infra/serializers/tracking/statsperform.py
@@ -19,6 +19,7 @@
TrackingDataset,
attacking_direction_from_frame,
)
+from kloppy.domain.services.frame_factory import create_frame
from kloppy.exceptions import DeserializationError
from kloppy.utils import performance_logging
from kloppy.infra.serializers.event.statsperform.parsers import get_parser
@@ -129,7 +130,7 @@ def _frame_from_framedata(cls, teams_list, period, frame_data):
players_data[player] = PlayerData(coordinates=Point(x, y))
- return Frame(
+ frame = create_frame(
frame_id=frame_id,
timestamp=frame_timestamp,
ball_coordinates=ball_coordinates,
@@ -140,6 +141,8 @@ def _frame_from_framedata(cls, teams_list, period, frame_data):
other_data={},
)
+ return frame
+
def deserialize(self, inputs: StatsPerformInputs) -> TrackingDataset:
with performance_logging("Loading meta data", logger=logger):
meta_data_parser = get_parser(inputs.meta_data, "MA1")
diff --git a/kloppy/infra/serializers/tracking/tracab/tracab_dat.py b/kloppy/infra/serializers/tracking/tracab/tracab_dat.py
index 831370cb..72ddbdf6 100644
--- a/kloppy/infra/serializers/tracking/tracab/tracab_dat.py
+++ b/kloppy/infra/serializers/tracking/tracab/tracab_dat.py
@@ -1,9 +1,8 @@
import logging
-from datetime import timedelta, timezone
+from datetime import datetime, timedelta, timezone
import warnings
from typing import Dict, Optional, Union
import html
-from dateutil.parser import parse
from lxml import objectify
@@ -26,6 +25,7 @@
PlayerData,
PositionType,
)
+from kloppy.domain.services.frame_factory import create_frame
from kloppy.exceptions import DeserializationError
from kloppy.utils import Readable, performance_logging
@@ -114,7 +114,7 @@ def _frame_from_line(cls, teams, period, line, frame_rate):
else:
raise DeserializationError(f"Unknown ball state: {ball_state}")
- return Frame(
+ frame = create_frame(
frame_id=frame_id,
timestamp=timedelta(seconds=frame_id / frame_rate)
- period.start_timestamp,
@@ -128,6 +128,8 @@ def _frame_from_line(cls, teams, period, line, frame_rate):
other_data={},
)
+ return frame
+
@staticmethod
def __validate_inputs(inputs: Dict[str, Readable]):
if "metadata" not in inputs:
@@ -184,9 +186,9 @@ def deserialize(self, inputs: TRACABInputs) -> TrackingDataset:
pitch_size_height = float(
match.attrib["fPitchYSizeMeters"].replace(",", ".")
)
- date = parse(meta_data.match.attrib["dtDate"]).astimezone(
- timezone.utc
- )
+ date = datetime.strptime(
+ meta_data.match.attrib["dtDate"], "%Y-%m-%d %H:%M:%S"
+ ).replace(tzinfo=timezone.utc)
game_id = meta_data.match.attrib["iId"]
for period in match.iterchildren(tag="period"):
@@ -205,7 +207,9 @@ def deserialize(self, inputs: TRACABInputs) -> TrackingDataset:
)
)
elif hasattr(meta_data, "Phase1StartFrame"):
- date = parse(str(meta_data["Kickoff"]))
+ date = datetime.strptime(
+ str(meta_data["Kickoff"]), "%Y-%m-%d %H:%M:%S"
+ ).replace(tzinfo=timezone.utc)
game_id = str(meta_data["GameID"])
id_suffix = "ID"
player_item = "item"
diff --git a/kloppy/infra/serializers/tracking/tracab/tracab_json.py b/kloppy/infra/serializers/tracking/tracab/tracab_json.py
index a5f52958..921e4df5 100644
--- a/kloppy/infra/serializers/tracking/tracab/tracab_json.py
+++ b/kloppy/infra/serializers/tracking/tracab/tracab_json.py
@@ -24,6 +24,7 @@
attacking_direction_from_frame,
)
from kloppy.domain.models import PositionType
+from kloppy.domain.services.frame_factory import create_frame
from kloppy.exceptions import DeserializationError
from kloppy.utils import Readable, performance_logging
@@ -105,7 +106,7 @@ def _create_frame(cls, teams, period, raw_frame, frame_rate):
f"Unknown ball state: {raw_ball_position['BallStatus']}"
)
- return Frame(
+ frame = create_frame(
frame_id=frame_id,
timestamp=timedelta(seconds=frame_id / frame_rate)
- period.start_timestamp,
@@ -118,6 +119,8 @@ def _create_frame(cls, teams, period, raw_frame, frame_rate):
other_data={},
)
+ return frame
+
@staticmethod
def __validate_inputs(inputs: Dict[str, Readable]):
if "metadata" not in inputs:
diff --git a/kloppy/tests/files/opta_f24.xml b/kloppy/tests/files/opta_f24.xml
index 02622eb8..47c46c0b 100644
--- a/kloppy/tests/files/opta_f24.xml
+++ b/kloppy/tests/files/opta_f24.xml
@@ -389,6 +389,8 @@
+
+
diff --git a/kloppy/tests/files/sportec_positional_w_referee.xml b/kloppy/tests/files/sportec_positional_w_referee.xml
new file mode 100644
index 00000000..d9f12d8f
--- /dev/null
+++ b/kloppy/tests/files/sportec_positional_w_referee.xml
@@ -0,0 +1,671 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/kloppy/tests/issues/issue_60/test_issue_60.py b/kloppy/tests/issues/issue_60/test_issue_60.py
index 5687973e..4d14b972 100644
--- a/kloppy/tests/issues/issue_60/test_issue_60.py
+++ b/kloppy/tests/issues/issue_60/test_issue_60.py
@@ -16,7 +16,7 @@ def test_deleted_event_opta(self):
assert deleted_event_id not in df["event_id"].to_list()
# OPTA F24 file: Pass -> Deleted Event -> Tackle
- assert event_dataset.events[16].event_name == "pass"
+ assert event_dataset.events[15].event_name == "pass"
assert (
- event_dataset.events[17].event_name == "duel"
+ event_dataset.events[16].event_name == "duel"
) # Deleted Event is filter out
diff --git a/kloppy/tests/prs/pr_358/test_pr_358.py b/kloppy/tests/prs/pr_358/test_pr_358.py
new file mode 100644
index 00000000..e2c1ab82
--- /dev/null
+++ b/kloppy/tests/prs/pr_358/test_pr_358.py
@@ -0,0 +1,19 @@
+import pytest
+
+from kloppy import wyscout
+from kloppy.exceptions import DeserializationWarning
+
+
+def test_ignore_unknown_player(base_dir):
+ with pytest.warns(
+ DeserializationWarning,
+ match="the player does not appear to be part of that team's lineup",
+ ):
+ dataset = wyscout.load(
+ event_data=base_dir / "prs" / "pr_358" / "wyscout_events_v3.json",
+ coordinates="wyscout",
+ )
+
+ assert len(dataset.events) == 2
+
+ assert dataset.events[1].player is None
diff --git a/kloppy/tests/prs/pr_358/wyscout_events_v3.json b/kloppy/tests/prs/pr_358/wyscout_events_v3.json
new file mode 100644
index 00000000..ce350053
--- /dev/null
+++ b/kloppy/tests/prs/pr_358/wyscout_events_v3.json
@@ -0,0 +1,3086 @@
+{
+ "meta": [],
+ "events": [
+ {
+ "id": 1927028854,
+ "matchId": 5154199,
+ "matchPeriod": "1H",
+ "minute": 0,
+ "second": 3,
+ "matchTimestamp": "00:00:03.211",
+ "videoTimestamp": "4.211804",
+ "relatedEventId": 1927028859,
+ "type": {
+ "primary": "pass",
+ "secondary": [
+ "back_pass",
+ "short_or_medium_pass"
+ ]
+ },
+ "location": {
+ "x": 51,
+ "y": 51
+ },
+ "team": {
+ "id": 3164,
+ "name": "Sampdoria",
+ "formation": "4-5-1"
+ },
+ "opponentTeam": {
+ "id": 3159,
+ "name": "Juventus",
+ "formation": "3-4-2-1"
+ },
+ "player": {
+ "id": 239298,
+ "name": "F. Bonazzoli",
+ "position": "CF"
+ },
+ "pass": {
+ "accurate": true,
+ "angle": 180,
+ "height": null,
+ "length": 18,
+ "recipient": {
+ "id": 21006,
+ "name": "L. Tonelli",
+ "position": "RCB"
+ },
+ "endLocation": {
+ "x": 34,
+ "y": 52
+ }
+ },
+ "shot": null,
+ "groundDuel": null,
+ "aerialDuel": null,
+ "infraction": null,
+ "carry": null,
+ "possession": {
+ "id": 1927028854,
+ "duration": "8.693585",
+ "types": [
+ "attack"
+ ],
+ "eventsNumber": 7,
+ "eventIndex": 0,
+ "startLocation": {
+ "x": 51,
+ "y": 51
+ },
+ "endLocation": {
+ "x": 86,
+ "y": 98
+ },
+ "team": {
+ "id": 3164,
+ "name": "Sampdoria",
+ "formation": "4-5-1"
+ },
+ "attack": {
+ "withShot": false,
+ "withShotOnGoal": false,
+ "withGoal": false,
+ "flank": "right",
+ "xg": 0
+ }
+ }
+ },
+ {
+ "id": 1927028859,
+ "matchId": 5154199,
+ "matchPeriod": "1H",
+ "minute": 0,
+ "second": 4,
+ "matchTimestamp": "00:00:04.227",
+ "videoTimestamp": "5.227381",
+ "relatedEventId": 1927028860,
+ "type": {
+ "primary": "pass",
+ "secondary": [
+ "short_or_medium_pass"
+ ]
+ },
+ "location": {
+ "x": 34,
+ "y": 52
+ },
+ "team": {
+ "id": 3164,
+ "name": "Sampdoria",
+ "formation": "4-5-1"
+ },
+ "opponentTeam": {
+ "id": 3159,
+ "name": "Juventus",
+ "formation": "3-4-2-1"
+ },
+ "player": {
+ "id": 1,
+ "name": "H. Vanaken",
+ "position": "CAM"
+ },
+ "pass": {
+ "accurate": true,
+ "angle": 124,
+ "height": null,
+ "length": 3,
+ "recipient": {
+ "id": 215503,
+ "name": "O. Colley",
+ "position": "LCB"
+ },
+ "endLocation": {
+ "x": 32,
+ "y": 56
+ }
+ },
+ "shot": null,
+ "groundDuel": null,
+ "aerialDuel": null,
+ "infraction": null,
+ "carry": null,
+ "possession": {
+ "id": 1927028854,
+ "duration": "8.693585",
+ "types": [
+ "attack"
+ ],
+ "eventsNumber": 7,
+ "eventIndex": 1,
+ "startLocation": {
+ "x": 51,
+ "y": 51
+ },
+ "endLocation": {
+ "x": 86,
+ "y": 98
+ },
+ "team": {
+ "id": 3164,
+ "name": "Sampdoria",
+ "formation": "4-5-1"
+ },
+ "attack": {
+ "withShot": false,
+ "withShotOnGoal": false,
+ "withGoal": false,
+ "flank": "right",
+ "xg": 0
+ }
+ }
+ }
+ ],
+ "match": {
+ "wyId": 5154199,
+ "gsmId": 5154199,
+ "label": "Juventus - Sampdoria, 3 - 0",
+ "date": "September 20, 2020 at 8:45:00 PM GMT+2",
+ "dateutc": "2020-09-20 18:45:00",
+ "status": "Played",
+ "duration": "Regular",
+ "winner": 3159,
+ "competitionId": 524,
+ "seasonId": 186353,
+ "roundId": 4422711,
+ "gameweek": 1,
+ "teamsData": {
+ "3159": {
+ "teamId": 3159,
+ "side": "home",
+ "score": 3,
+ "scoreHT": 1,
+ "scoreET": 0,
+ "scoreP": 0,
+ "coachId": 679990,
+ "hasFormation": 1,
+ "formation": {
+ "lineup": [
+ {
+ "playerId": 489124,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "54",
+ "redCards": "0",
+ "shirtNumber": 38,
+ "assists": "0"
+ },
+ {
+ "playerId": 20751,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 16,
+ "assists": "0"
+ },
+ {
+ "playerId": 472363,
+ "goals": "1",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 44,
+ "assists": "0"
+ },
+ {
+ "playerId": 20461,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 3,
+ "assists": "0"
+ },
+ {
+ "playerId": 209117,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 25,
+ "assists": "0"
+ },
+ {
+ "playerId": 7870,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 8,
+ "assists": "1"
+ },
+ {
+ "playerId": 3322,
+ "goals": "1",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 7,
+ "assists": "0"
+ },
+ {
+ "playerId": 70083,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 13,
+ "assists": "0"
+ },
+ {
+ "playerId": 20459,
+ "goals": "1",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 19,
+ "assists": "0"
+ },
+ {
+ "playerId": 471348,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 14,
+ "assists": "0"
+ },
+ {
+ "playerId": 7849,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 1,
+ "assists": "0"
+ }
+ ],
+ "bench": [
+ {
+ "playerId": 255256,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 5,
+ "assists": "0"
+ },
+ {
+ "playerId": 21128,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 31,
+ "assists": "0"
+ },
+ {
+ "playerId": 134427,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 24,
+ "assists": "0"
+ },
+ {
+ "playerId": 20455,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 77,
+ "assists": "0"
+ },
+ {
+ "playerId": 485096,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 40,
+ "assists": "0"
+ },
+ {
+ "playerId": 472395,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 41,
+ "assists": "0"
+ },
+ {
+ "playerId": 496673,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 39,
+ "assists": "0"
+ },
+ {
+ "playerId": 105334,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 11,
+ "assists": "0"
+ },
+ {
+ "playerId": 20395,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 2,
+ "assists": "0"
+ },
+ {
+ "playerId": 345695,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 28,
+ "assists": "0"
+ },
+ {
+ "playerId": 361807,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 30,
+ "assists": "0"
+ }
+ ],
+ "substitutions": [
+ {
+ "minute": 67,
+ "playerIn": 20395,
+ "playerOut": 489124,
+ "assists": "0"
+ },
+ {
+ "minute": 78,
+ "playerIn": 361807,
+ "playerOut": 20751,
+ "assists": "0"
+ },
+ {
+ "minute": 82,
+ "playerIn": 105334,
+ "playerOut": 472363,
+ "assists": "0"
+ },
+ {
+ "minute": 83,
+ "playerIn": 345695,
+ "playerOut": 20461,
+ "assists": "0"
+ }
+ ]
+ }
+ },
+ "3164": {
+ "teamId": 3164,
+ "side": "away",
+ "score": 0,
+ "scoreHT": 0,
+ "scoreET": 0,
+ "scoreP": 0,
+ "coachId": 20508,
+ "hasFormation": 1,
+ "formation": {
+ "lineup": [
+ {
+ "playerId": 415809,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 12,
+ "assists": "0"
+ },
+ {
+ "playerId": 21006,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "4",
+ "redCards": "0",
+ "shirtNumber": 21,
+ "assists": "0"
+ },
+ {
+ "playerId": 449978,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 26,
+ "assists": "0"
+ },
+ {
+ "playerId": 239298,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 9,
+ "assists": "0"
+ },
+ {
+ "playerId": 237057,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 18,
+ "assists": "0"
+ },
+ {
+ "playerId": 20876,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 6,
+ "assists": "0"
+ },
+ {
+ "playerId": 99833,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 24,
+ "assists": "0"
+ },
+ {
+ "playerId": 257027,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 1,
+ "assists": "0"
+ },
+ {
+ "playerId": 354547,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 14,
+ "assists": "0"
+ },
+ {
+ "playerId": 215503,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 15,
+ "assists": "0"
+ },
+ {
+ "playerId": 222964,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 3,
+ "assists": "0"
+ }
+ ],
+ "bench": [
+ {
+ "playerId": 263433,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 25,
+ "assists": "0"
+ },
+ {
+ "playerId": 372408,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 20,
+ "assists": "0"
+ },
+ {
+ "playerId": 391740,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 5,
+ "assists": "0"
+ },
+ {
+ "playerId": 134429,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 28,
+ "assists": "0"
+ },
+ {
+ "playerId": 21331,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 30,
+ "assists": "0"
+ },
+ {
+ "playerId": 434154,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 4,
+ "assists": "0"
+ },
+ {
+ "playerId": 21004,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 19,
+ "assists": "0"
+ },
+ {
+ "playerId": 20479,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 27,
+ "assists": "0"
+ },
+ {
+ "playerId": 20689,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 11,
+ "assists": "0"
+ },
+ {
+ "playerId": 449472,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 38,
+ "assists": "0"
+ },
+ {
+ "playerId": 703,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 22,
+ "assists": "0"
+ },
+ {
+ "playerId": 20446,
+ "goals": "0",
+ "ownGoals": "0",
+ "yellowCards": "0",
+ "redCards": "0",
+ "shirtNumber": 8,
+ "assists": "0"
+ }
+ ],
+ "substitutions": [
+ {
+ "minute": 46,
+ "playerIn": 20479,
+ "playerOut": 415809,
+ "assists": "0"
+ },
+ {
+ "minute": 46,
+ "playerIn": 20689,
+ "playerOut": 21006,
+ "assists": "0"
+ },
+ {
+ "minute": 46,
+ "playerIn": 703,
+ "playerOut": 449978,
+ "assists": "0"
+ },
+ {
+ "minute": 70,
+ "playerIn": 449472,
+ "playerOut": 239298,
+ "assists": "0"
+ },
+ {
+ "minute": 70,
+ "playerIn": 20446,
+ "playerOut": 237057,
+ "assists": "0"
+ }
+ ]
+ }
+ }
+ },
+ "venue": null,
+ "referees": [
+ {
+ "refereeId": 396780,
+ "role": "referee"
+ },
+ {
+ "refereeId": 378959,
+ "role": "firstAssistant"
+ },
+ {
+ "refereeId": 420139,
+ "role": "secondAssistant"
+ },
+ {
+ "refereeId": 393581,
+ "role": "fourthOfficial"
+ },
+ {
+ "refereeId": 0,
+ "role": "firstAdditionalAssistant"
+ },
+ {
+ "refereeId": 0,
+ "role": "secondAdditionalAssistant"
+ }
+ ],
+ "hasDataAvailable": true
+ },
+ "formations": {
+ "3159": {
+ "1H": {
+ "0": {
+ "3-4-2-1": {
+ "id": 2589485,
+ "scheme": "3-4-2-1",
+ "matchPeriod": "1H",
+ "startSec": 0,
+ "endSec": 3987,
+ "players": [
+ {
+ "209117": {
+ "playerId": 209117,
+ "position": "rcmf"
+ }
+ },
+ {
+ "489124": {
+ "playerId": 489124,
+ "position": "lwb"
+ }
+ },
+ {
+ "20461": {
+ "playerId": 20461,
+ "position": "lcb3"
+ }
+ },
+ {
+ "20459": {
+ "playerId": 20459,
+ "position": "cb"
+ }
+ },
+ {
+ "471348": {
+ "playerId": 471348,
+ "position": "lcmf"
+ }
+ },
+ {
+ "70083": {
+ "playerId": 70083,
+ "position": "rcb3"
+ }
+ },
+ {
+ "7849": {
+ "playerId": 7849,
+ "position": "gk"
+ }
+ },
+ {
+ "20751": {
+ "playerId": 20751,
+ "position": "rwb"
+ }
+ },
+ {
+ "3322": {
+ "playerId": 3322,
+ "position": "cf"
+ }
+ },
+ {
+ "7870": {
+ "playerId": 7870,
+ "position": "amf"
+ }
+ },
+ {
+ "472363": {
+ "playerId": 472363,
+ "position": "amf"
+ }
+ }
+ ]
+ }
+ }
+ },
+ "2H": {
+ "1278": {
+ "3-4-2-1": {
+ "id": 2589662,
+ "scheme": "3-4-2-1",
+ "matchPeriod": "2H",
+ "startSec": 1278,
+ "endSec": 1951,
+ "players": [
+ {
+ "7870": {
+ "playerId": 7870,
+ "position": "amf"
+ }
+ },
+ {
+ "472363": {
+ "playerId": 472363,
+ "position": "amf"
+ }
+ },
+ {
+ "20395": {
+ "playerId": 20395,
+ "position": "rwb"
+ }
+ },
+ {
+ "209117": {
+ "playerId": 209117,
+ "position": "rcmf"
+ }
+ },
+ {
+ "20461": {
+ "playerId": 20461,
+ "position": "lcb3"
+ }
+ },
+ {
+ "20459": {
+ "playerId": 20459,
+ "position": "cb"
+ }
+ },
+ {
+ "471348": {
+ "playerId": 471348,
+ "position": "lcmf"
+ }
+ },
+ {
+ "70083": {
+ "playerId": 70083,
+ "position": "rcb3"
+ }
+ },
+ {
+ "7849": {
+ "playerId": 7849,
+ "position": "gk"
+ }
+ },
+ {
+ "20751": {
+ "playerId": 20751,
+ "position": "lwb"
+ }
+ },
+ {
+ "3322": {
+ "playerId": 3322,
+ "position": "cf"
+ }
+ }
+ ]
+ }
+ },
+ "1951": {
+ "3-4-2-1": {
+ "id": 2589672,
+ "scheme": "3-4-2-1",
+ "matchPeriod": "2H",
+ "startSec": 1951,
+ "endSec": 2192,
+ "players": [
+ {
+ "70083": {
+ "playerId": 70083,
+ "position": "rcb3"
+ }
+ },
+ {
+ "7849": {
+ "playerId": 7849,
+ "position": "gk"
+ }
+ },
+ {
+ "3322": {
+ "playerId": 3322,
+ "position": "cf"
+ }
+ },
+ {
+ "7870": {
+ "playerId": 7870,
+ "position": "amf"
+ }
+ },
+ {
+ "361807": {
+ "playerId": 361807,
+ "position": "rcmf"
+ }
+ },
+ {
+ "472363": {
+ "playerId": 472363,
+ "position": "amf"
+ }
+ },
+ {
+ "20395": {
+ "playerId": 20395,
+ "position": "lwb"
+ }
+ },
+ {
+ "209117": {
+ "playerId": 209117,
+ "position": "lcmf"
+ }
+ },
+ {
+ "20461": {
+ "playerId": 20461,
+ "position": "lcb3"
+ }
+ },
+ {
+ "20459": {
+ "playerId": 20459,
+ "position": "cb"
+ }
+ },
+ {
+ "471348": {
+ "playerId": 471348,
+ "position": "rwb"
+ }
+ }
+ ]
+ }
+ },
+ "2192": {
+ "3-4-2-1": {
+ "id": 2589677,
+ "scheme": "3-4-2-1",
+ "matchPeriod": "2H",
+ "startSec": 2192,
+ "endSec": 2824,
+ "players": [
+ {
+ "20395": {
+ "playerId": 20395,
+ "position": "lwb"
+ }
+ },
+ {
+ "209117": {
+ "playerId": 209117,
+ "position": "rcmf"
+ }
+ },
+ {
+ "105334": {
+ "playerId": 105334,
+ "position": "amf"
+ }
+ },
+ {
+ "20459": {
+ "playerId": 20459,
+ "position": "cb"
+ }
+ },
+ {
+ "471348": {
+ "playerId": 471348,
+ "position": "rwb"
+ }
+ },
+ {
+ "70083": {
+ "playerId": 70083,
+ "position": "rcb3"
+ }
+ },
+ {
+ "7849": {
+ "playerId": 7849,
+ "position": "gk"
+ }
+ },
+ {
+ "3322": {
+ "playerId": 3322,
+ "position": "cf"
+ }
+ },
+ {
+ "7870": {
+ "playerId": 7870,
+ "position": "amf"
+ }
+ },
+ {
+ "345695": {
+ "playerId": 345695,
+ "position": "lcb3"
+ }
+ },
+ {
+ "361807": {
+ "playerId": 361807,
+ "position": "lcmf"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "3164": {
+ "1H": {
+ "0": {
+ "4-5-1": {
+ "id": 2589548,
+ "scheme": "4-5-1",
+ "matchPeriod": "1H",
+ "startSec": 0,
+ "endSec": 2713,
+ "players": [
+ {
+ "237057": {
+ "playerId": 237057,
+ "position": "rcmf3"
+ }
+ },
+ {
+ "20876": {
+ "playerId": 20876,
+ "position": "dmf"
+ }
+ },
+ {
+ "354547": {
+ "playerId": 354547,
+ "position": "lcmf3"
+ }
+ },
+ {
+ "415809": {
+ "playerId": 415809,
+ "position": "rw"
+ }
+ },
+ {
+ "215503": {
+ "playerId": 215503,
+ "position": "lcb"
+ }
+ },
+ {
+ "257027": {
+ "playerId": 257027,
+ "position": "gk"
+ }
+ },
+ {
+ "99833": {
+ "playerId": 99833,
+ "position": "rb"
+ }
+ },
+ {
+ "449978": {
+ "playerId": 449978,
+ "position": "lw"
+ }
+ },
+ {
+ "222964": {
+ "playerId": 222964,
+ "position": "lb"
+ }
+ },
+ {
+ "239298": {
+ "playerId": 239298,
+ "position": "cf"
+ }
+ },
+ {
+ "21006": {
+ "playerId": 21006,
+ "position": "rcb"
+ }
+ }
+ ]
+ }
+ }
+ },
+ "2H": {
+ "4": {
+ "4-3-1-2": {
+ "id": 2589654,
+ "scheme": "4-3-1-2",
+ "matchPeriod": "2H",
+ "startSec": 4,
+ "endSec": 1461,
+ "players": [
+ {
+ "215503": {
+ "playerId": 215503,
+ "position": "lcb"
+ }
+ },
+ {
+ "257027": {
+ "playerId": 257027,
+ "position": "gk"
+ }
+ },
+ {
+ "99833": {
+ "playerId": 99833,
+ "position": "rb"
+ }
+ },
+ {
+ "222964": {
+ "playerId": 222964,
+ "position": "lb"
+ }
+ },
+ {
+ "239298": {
+ "playerId": 239298,
+ "position": "cf"
+ }
+ },
+ {
+ "237057": {
+ "playerId": 237057,
+ "position": "rcmf3"
+ }
+ },
+ {
+ "703": {
+ "playerId": 703,
+ "position": "rcb"
+ }
+ },
+ {
+ "20876": {
+ "playerId": 20876,
+ "position": "dmf"
+ }
+ },
+ {
+ "354547": {
+ "playerId": 354547,
+ "position": "lcmf3"
+ }
+ },
+ {
+ "20479": {
+ "playerId": 20479,
+ "position": "ss"
+ }
+ },
+ {
+ "20689": {
+ "playerId": 20689,
+ "position": "amf"
+ }
+ }
+ ]
+ }
+ },
+ "1461": {
+ "4-4-2": {
+ "id": 2589686,
+ "scheme": "4-4-2",
+ "matchPeriod": "2H",
+ "startSec": 1461,
+ "endSec": 2824,
+ "players": [
+ {
+ "703": {
+ "playerId": 703,
+ "position": "rcb"
+ }
+ },
+ {
+ "20876": {
+ "playerId": 20876,
+ "position": "lcmf"
+ }
+ },
+ {
+ "449472": {
+ "playerId": 449472,
+ "position": "rw"
+ }
+ },
+ {
+ "354547": {
+ "playerId": 354547,
+ "position": "lw"
+ }
+ },
+ {
+ "20479": {
+ "playerId": 20479,
+ "position": "cf"
+ }
+ },
+ {
+ "20689": {
+ "playerId": 20689,
+ "position": "ss"
+ }
+ },
+ {
+ "215503": {
+ "playerId": 215503,
+ "position": "lcb"
+ }
+ },
+ {
+ "257027": {
+ "playerId": 257027,
+ "position": "gk"
+ }
+ },
+ {
+ "99833": {
+ "playerId": 99833,
+ "position": "rb"
+ }
+ },
+ {
+ "20446": {
+ "playerId": 20446,
+ "position": "rcmf"
+ }
+ },
+ {
+ "222964": {
+ "playerId": 222964,
+ "position": "lb"
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "substitutions": {
+ "3159": {
+ "2H": {
+ "1278": {
+ "in": [
+ {
+ "playerId": 20395
+ }
+ ],
+ "out": [
+ {
+ "playerId": 489124
+ }
+ ]
+ },
+ "1951": {
+ "in": [
+ {
+ "playerId": 361807
+ }
+ ],
+ "out": [
+ {
+ "playerId": 20751
+ }
+ ]
+ },
+ "2192": {
+ "in": [
+ {
+ "playerId": 105334
+ },
+ {
+ "playerId": 345695
+ }
+ ],
+ "out": [
+ {
+ "playerId": 472363
+ },
+ {
+ "playerId": 20461
+ }
+ ]
+ }
+ }
+ },
+ "3164": {
+ "2H": {
+ "4": {
+ "in": [
+ {
+ "playerId": 703
+ },
+ {
+ "playerId": 20479
+ },
+ {
+ "playerId": 20689
+ }
+ ],
+ "out": [
+ {
+ "playerId": 415809
+ },
+ {
+ "playerId": 449978
+ },
+ {
+ "playerId": 21006
+ }
+ ]
+ },
+ "1461": {
+ "in": [
+ {
+ "playerId": 449472
+ },
+ {
+ "playerId": 20446
+ }
+ ],
+ "out": [
+ {
+ "playerId": 239298
+ },
+ {
+ "playerId": 237057
+ }
+ ]
+ }
+ }
+ }
+ },
+ "teams": {
+ "3159": {
+ "team": {
+ "wyId": 3159,
+ "gsmId": 1242,
+ "name": "Juventus",
+ "officialName": "Juventus FC",
+ "city": "Torino",
+ "area": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "type": "club",
+ "category": "default",
+ "gender": "male",
+ "children": [
+ {
+ "name": "Juventus U20",
+ "wyId": 76395
+ },
+ {
+ "name": "Juventus U14",
+ "wyId": 65171
+ },
+ {
+ "name": "Juventus Next Gen",
+ "wyId": 63044
+ },
+ {
+ "name": "Juventus U16",
+ "wyId": 32081
+ },
+ {
+ "name": "Juventus U15",
+ "wyId": 20450
+ },
+ {
+ "name": "Juventus U19",
+ "wyId": 26540
+ },
+ {
+ "name": "Juventus U17",
+ "wyId": 32859
+ }
+ ],
+ "imageDataURL": "https://cdn5.wyscout.com/photos/team/public/80_120x120.png"
+ }
+ },
+ "3164": {
+ "team": {
+ "wyId": 3164,
+ "gsmId": 1247,
+ "name": "Sampdoria",
+ "officialName": "UC Sampdoria",
+ "city": "Genova",
+ "area": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "type": "club",
+ "category": "default",
+ "gender": "male",
+ "children": [
+ {
+ "name": "Sampdoria U20",
+ "wyId": 76399
+ },
+ {
+ "name": "Sampdoria U14",
+ "wyId": 65449
+ },
+ {
+ "name": "Sampdoria U18",
+ "wyId": 65358
+ },
+ {
+ "name": "Sampdoria U16",
+ "wyId": 36488
+ },
+ {
+ "name": "Sampdoria U17",
+ "wyId": 22302
+ },
+ {
+ "name": "Sampdoria U15",
+ "wyId": 21663
+ },
+ {
+ "name": "Sampdoria U19",
+ "wyId": 3710
+ }
+ ],
+ "imageDataURL": "https://cdn5.wyscout.com/photos/team/public/88_120x120.png"
+ }
+ }
+ },
+ "players": {
+ "3159": [
+ {
+ "player": {
+ "wyId": 489124,
+ "gsmId": 472023,
+ "shortName": "G. Frabotta",
+ "firstName": "Gianluca",
+ "middleName": "",
+ "lastName": "Frabotta",
+ "height": 187,
+ "weight": 70,
+ "birthDate": "1999-06-24",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Defender",
+ "code2": "DF",
+ "code3": "DEF"
+ },
+ "foot": "left",
+ "currentTeamId": 1627,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g472023_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 20751,
+ "gsmId": 61414,
+ "shortName": "J. Cuadrado",
+ "firstName": "Juan Guillermo",
+ "middleName": "",
+ "lastName": "Cuadrado Bello",
+ "height": 176,
+ "weight": 72,
+ "birthDate": "1988-05-26",
+ "birthArea": {
+ "id": 170,
+ "alpha2code": "CO",
+ "alpha3code": "COL",
+ "name": "Colombia"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Midfielder",
+ "code2": "MD",
+ "code3": "MID"
+ },
+ "foot": "right",
+ "currentTeamId": null,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/24886_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 472363,
+ "gsmId": 460405,
+ "shortName": "D. Kulusevski",
+ "firstName": "Dejan",
+ "middleName": "",
+ "lastName": "Kulusevski",
+ "height": 186,
+ "weight": 75,
+ "birthDate": "2000-04-25",
+ "birthArea": {
+ "id": 752,
+ "alpha2code": "SE",
+ "alpha3code": "SWE",
+ "name": "Sweden"
+ },
+ "passportArea": {
+ "id": 807,
+ "alpha2code": "MK",
+ "alpha3code": "MKD",
+ "name": "Macedonia FYR"
+ },
+ "role": {
+ "name": "Forward",
+ "code2": "FW",
+ "code3": "FWD"
+ },
+ "foot": "left",
+ "currentTeamId": 1624,
+ "currentNationalTeamId": 7047,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g460405_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 20461,
+ "gsmId": 17684,
+ "shortName": "G. Chiellini",
+ "firstName": "Giorgio",
+ "middleName": "",
+ "lastName": "Chiellini",
+ "height": 187,
+ "weight": 86,
+ "birthDate": "1984-08-14",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Defender",
+ "code2": "DF",
+ "code3": "DEF"
+ },
+ "foot": "left",
+ "currentTeamId": null,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "retired",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/445_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 209117,
+ "gsmId": 242768,
+ "shortName": "A. Rabiot",
+ "firstName": "Adrien",
+ "middleName": "",
+ "lastName": "Rabiot",
+ "height": 188,
+ "weight": 80,
+ "birthDate": "1995-04-03",
+ "birthArea": {
+ "id": 250,
+ "alpha2code": "FR",
+ "alpha3code": "FRA",
+ "name": "France"
+ },
+ "passportArea": {
+ "id": 250,
+ "alpha2code": "FR",
+ "alpha3code": "FRA",
+ "name": "France"
+ },
+ "role": {
+ "name": "Midfielder",
+ "code2": "MD",
+ "code3": "MID"
+ },
+ "foot": "left",
+ "currentTeamId": null,
+ "currentNationalTeamId": 4418,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/80854_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 7870,
+ "gsmId": 66532,
+ "shortName": "A. Ramsey",
+ "firstName": "Aaron",
+ "middleName": "",
+ "lastName": "Ramsey",
+ "height": 183,
+ "weight": 76,
+ "birthDate": "1990-12-26",
+ "birthArea": {
+ "id": 1013,
+ "alpha2code": "WA",
+ "alpha3code": "XWA",
+ "name": "Wales"
+ },
+ "passportArea": {
+ "id": 1013,
+ "alpha2code": "WA",
+ "alpha3code": "XWA",
+ "name": "Wales"
+ },
+ "role": {
+ "name": "Midfielder",
+ "code2": "MD",
+ "code3": "MID"
+ },
+ "foot": "right",
+ "currentTeamId": 10529,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/12398_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 3322,
+ "gsmId": 382,
+ "shortName": "Cristiano Ronaldo",
+ "firstName": "Cristiano Ronaldo",
+ "middleName": "",
+ "lastName": "dos Santos Aveiro",
+ "height": 187,
+ "weight": 83,
+ "birthDate": "1985-02-05",
+ "birthArea": {
+ "id": 620,
+ "alpha2code": "PT",
+ "alpha3code": "PRT",
+ "name": "Portugal"
+ },
+ "passportArea": {
+ "id": 620,
+ "alpha2code": "PT",
+ "alpha3code": "PRT",
+ "name": "Portugal"
+ },
+ "role": {
+ "name": "Forward",
+ "code2": "FW",
+ "code3": "FWD"
+ },
+ "foot": "right",
+ "currentTeamId": 16470,
+ "currentNationalTeamId": 9905,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/5045_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 70083,
+ "gsmId": 75848,
+ "shortName": "Danilo",
+ "firstName": "Danilo Luiz",
+ "middleName": "",
+ "lastName": "da Silva",
+ "height": 184,
+ "weight": 78,
+ "birthDate": "1991-07-15",
+ "birthArea": {
+ "id": 76,
+ "alpha2code": "BR",
+ "alpha3code": "BRA",
+ "name": "Brazil"
+ },
+ "passportArea": {
+ "id": 76,
+ "alpha2code": "BR",
+ "alpha3code": "BRA",
+ "name": "Brazil"
+ },
+ "role": {
+ "name": "Defender",
+ "code2": "DF",
+ "code3": "DEF"
+ },
+ "foot": "right",
+ "currentTeamId": 3159,
+ "currentNationalTeamId": 6380,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/37089_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 20459,
+ "gsmId": 3979,
+ "shortName": "L. Bonucci",
+ "firstName": "Leonardo",
+ "middleName": "",
+ "lastName": "Bonucci",
+ "height": 189,
+ "weight": 85,
+ "birthDate": "1987-05-01",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Defender",
+ "code2": "DF",
+ "code3": "DEF"
+ },
+ "foot": "right",
+ "currentTeamId": null,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "retired",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/13748_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 471348,
+ "gsmId": 459693,
+ "shortName": "W. McKennie",
+ "firstName": "Weston ",
+ "middleName": "",
+ "lastName": "McKennie",
+ "height": 185,
+ "weight": 84,
+ "birthDate": "1998-08-28",
+ "birthArea": {
+ "id": 840,
+ "alpha2code": "US",
+ "alpha3code": "USA",
+ "name": "United States"
+ },
+ "passportArea": {
+ "id": 840,
+ "alpha2code": "US",
+ "alpha3code": "USA",
+ "name": "United States"
+ },
+ "role": {
+ "name": "Midfielder",
+ "code2": "MD",
+ "code3": "MID"
+ },
+ "foot": "right",
+ "currentTeamId": 3159,
+ "currentNationalTeamId": 8134,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g459693_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 7849,
+ "gsmId": 16085,
+ "shortName": "W. Szczęsny",
+ "firstName": "Wojciech ",
+ "middleName": "",
+ "lastName": "Szczęsny",
+ "height": 195,
+ "weight": 90,
+ "birthDate": "1990-04-18",
+ "birthArea": {
+ "id": 616,
+ "alpha2code": "PL",
+ "alpha3code": "POL",
+ "name": "Poland"
+ },
+ "passportArea": {
+ "id": 616,
+ "alpha2code": "PL",
+ "alpha3code": "POL",
+ "name": "Poland"
+ },
+ "role": {
+ "name": "Goalkeeper",
+ "code2": "GK",
+ "code3": "GKP"
+ },
+ "foot": "right",
+ "currentTeamId": null,
+ "currentNationalTeamId": 13869,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/19133_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 255256,
+ "gsmId": 288085,
+ "shortName": "Arthur",
+ "firstName": "Arthur Henrique",
+ "middleName": "",
+ "lastName": "Ramos de Oliveira Melo",
+ "height": 171,
+ "weight": 73,
+ "birthDate": "1996-08-12",
+ "birthArea": {
+ "id": 76,
+ "alpha2code": "BR",
+ "alpha3code": "BRA",
+ "name": "Brazil"
+ },
+ "passportArea": {
+ "id": 76,
+ "alpha2code": "BR",
+ "alpha3code": "BRA",
+ "name": "Brazil"
+ },
+ "role": {
+ "name": "Midfielder",
+ "code2": "MD",
+ "code3": "MID"
+ },
+ "foot": "right",
+ "currentTeamId": 3159,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g288085_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 21128,
+ "gsmId": 95779,
+ "shortName": "C. Pinsoglio",
+ "firstName": "Carlo",
+ "middleName": "",
+ "lastName": "Pinsoglio",
+ "height": 194,
+ "weight": 85,
+ "birthDate": "1990-03-16",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Goalkeeper",
+ "code2": "GK",
+ "code3": "GKP"
+ },
+ "foot": "left",
+ "currentTeamId": 3159,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/37632_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 134427,
+ "gsmId": 162676,
+ "shortName": "D. Rugani",
+ "firstName": "Daniele",
+ "middleName": "",
+ "lastName": "Rugani",
+ "height": 190,
+ "weight": 84,
+ "birthDate": "1994-07-29",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Defender",
+ "code2": "DF",
+ "code3": "DEF"
+ },
+ "foot": "right",
+ "currentTeamId": 3159,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/79321_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 20455,
+ "gsmId": 53,
+ "shortName": "G. Buffon",
+ "firstName": "Gianluigi",
+ "middleName": "",
+ "lastName": "Buffon",
+ "height": 192,
+ "weight": 92,
+ "birthDate": "1978-01-28",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Goalkeeper",
+ "code2": "GK",
+ "code3": "GKP"
+ },
+ "foot": "right",
+ "currentTeamId": null,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "retired",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/442_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 485096,
+ "gsmId": 469983,
+ "shortName": "G. Vrioni",
+ "firstName": "Giacomo",
+ "middleName": "",
+ "lastName": "Vrioni",
+ "height": 188,
+ "weight": 79,
+ "birthDate": "1998-10-15",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 8,
+ "alpha2code": "AL",
+ "alpha3code": "ALB",
+ "name": "Albania"
+ },
+ "role": {
+ "name": "Forward",
+ "code2": "FW",
+ "code3": "FWD"
+ },
+ "foot": "left",
+ "currentTeamId": 7854,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g469983_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 472395,
+ "gsmId": 460439,
+ "shortName": "H. Nicolussi Caviglia",
+ "firstName": "Hans",
+ "middleName": "",
+ "lastName": "Nicolussi Caviglia",
+ "height": 184,
+ "weight": 79,
+ "birthDate": "2000-06-18",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Midfielder",
+ "code2": "MD",
+ "code3": "MID"
+ },
+ "foot": "right",
+ "currentTeamId": 3159,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g460439_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 496673,
+ "gsmId": 477620,
+ "shortName": "M. Portanova",
+ "firstName": "Manolo",
+ "middleName": "",
+ "lastName": "Portanova",
+ "height": 183,
+ "weight": 77,
+ "birthDate": "2000-06-02",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Midfielder",
+ "code2": "MD",
+ "code3": "MID"
+ },
+ "foot": "right",
+ "currentTeamId": 3211,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g477620_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 105334,
+ "gsmId": 65287,
+ "shortName": "Douglas Costa",
+ "firstName": "Douglas ",
+ "middleName": "",
+ "lastName": "Costa de Souza",
+ "height": 172,
+ "weight": 70,
+ "birthDate": "1990-09-14",
+ "birthArea": {
+ "id": 76,
+ "alpha2code": "BR",
+ "alpha3code": "BRA",
+ "name": "Brazil"
+ },
+ "passportArea": {
+ "id": 76,
+ "alpha2code": "BR",
+ "alpha3code": "BRA",
+ "name": "Brazil"
+ },
+ "role": {
+ "name": "Forward",
+ "code2": "FW",
+ "code3": "FWD"
+ },
+ "foot": "left",
+ "currentTeamId": null,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/28399_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 20395,
+ "gsmId": 158411,
+ "shortName": "M. De Sciglio",
+ "firstName": "Mattia",
+ "middleName": "",
+ "lastName": "De Sciglio",
+ "height": 182,
+ "weight": 78,
+ "birthDate": "1992-10-20",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Defender",
+ "code2": "DF",
+ "code3": "DEF"
+ },
+ "foot": "right",
+ "currentTeamId": 3159,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/41462_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 345695,
+ "gsmId": 374637,
+ "shortName": "M. Demiral",
+ "firstName": "Merih",
+ "middleName": "",
+ "lastName": "Demiral",
+ "height": 192,
+ "weight": 86,
+ "birthDate": "1998-03-05",
+ "birthArea": {
+ "id": 792,
+ "alpha2code": "TR",
+ "alpha3code": "TUR",
+ "name": "Turkey"
+ },
+ "passportArea": {
+ "id": 792,
+ "alpha2code": "TR",
+ "alpha3code": "TUR",
+ "name": "Turkey"
+ },
+ "role": {
+ "name": "Defender",
+ "code2": "DF",
+ "code3": "DEF"
+ },
+ "foot": "right",
+ "currentTeamId": 16465,
+ "currentNationalTeamId": 4687,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g374637_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 361807,
+ "gsmId": 396178,
+ "shortName": "R. Bentancur",
+ "firstName": "Rodrigo",
+ "middleName": "",
+ "lastName": "Bentancur Colmán",
+ "height": 187,
+ "weight": 73,
+ "birthDate": "1997-06-25",
+ "birthArea": {
+ "id": 858,
+ "alpha2code": "UY",
+ "alpha3code": "URY",
+ "name": "Uruguay"
+ },
+ "passportArea": {
+ "id": 858,
+ "alpha2code": "UY",
+ "alpha3code": "URY",
+ "name": "Uruguay"
+ },
+ "role": {
+ "name": "Midfielder",
+ "code2": "MD",
+ "code3": "MID"
+ },
+ "foot": "right",
+ "currentTeamId": 1624,
+ "currentNationalTeamId": 15670,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g396178_100x130.png"
+ }
+ }
+ ],
+ "3164": [
+ {
+ "player": {
+ "wyId": 415809,
+ "gsmId": 433613,
+ "shortName": "F. Depaoli",
+ "firstName": "Fabio",
+ "middleName": "",
+ "lastName": "Depaoli",
+ "height": 182,
+ "weight": 71,
+ "birthDate": "1997-04-24",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Defender",
+ "code2": "DF",
+ "code3": "DEF"
+ },
+ "foot": "right",
+ "currentTeamId": 3164,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g433613_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 21006,
+ "gsmId": 145358,
+ "shortName": "L. Tonelli",
+ "firstName": "Lorenzo",
+ "middleName": "",
+ "lastName": "Tonelli",
+ "height": 183,
+ "weight": 75,
+ "birthDate": "1990-01-17",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Defender",
+ "code2": "DF",
+ "code3": "DEF"
+ },
+ "foot": "right",
+ "currentTeamId": null,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/37978_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 449978,
+ "gsmId": 454740,
+ "shortName": "M. Léris",
+ "firstName": "Mehdi",
+ "middleName": "",
+ "lastName": "Léris",
+ "height": 186,
+ "weight": 78,
+ "birthDate": "1998-05-23",
+ "birthArea": {
+ "id": 250,
+ "alpha2code": "FR",
+ "alpha3code": "FRA",
+ "name": "France"
+ },
+ "passportArea": {
+ "id": 12,
+ "alpha2code": "DZ",
+ "alpha3code": "DZA",
+ "name": "Algeria"
+ },
+ "role": {
+ "name": "Midfielder",
+ "code2": "MD",
+ "code3": "MID"
+ },
+ "foot": "right",
+ "currentTeamId": 3207,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g454740_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 239298,
+ "gsmId": 273833,
+ "shortName": "F. Bonazzoli",
+ "firstName": "Federico",
+ "middleName": "",
+ "lastName": "Bonazzoli",
+ "height": 182,
+ "weight": 74,
+ "birthDate": "1997-05-21",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Forward",
+ "code2": "FW",
+ "code3": "FWD"
+ },
+ "foot": "left",
+ "currentTeamId": 3235,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g273833_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 237057,
+ "gsmId": 270648,
+ "shortName": "M. Thorsby",
+ "firstName": "Morten",
+ "middleName": "",
+ "lastName": "Thorsby",
+ "height": 188,
+ "weight": 83,
+ "birthDate": "1996-05-05",
+ "birthArea": {
+ "id": 578,
+ "alpha2code": "NO",
+ "alpha3code": "NOR",
+ "name": "Norway"
+ },
+ "passportArea": {
+ "id": 578,
+ "alpha2code": "NO",
+ "alpha3code": "NOR",
+ "name": "Norway"
+ },
+ "role": {
+ "name": "Midfielder",
+ "code2": "MD",
+ "code3": "MID"
+ },
+ "foot": "right",
+ "currentTeamId": 3193,
+ "currentNationalTeamId": 7336,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g270648_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 20876,
+ "gsmId": 21090,
+ "shortName": "A. Ekdal",
+ "firstName": "Albin",
+ "middleName": "",
+ "lastName": "Ekdal",
+ "height": 188,
+ "weight": 82,
+ "birthDate": "1989-07-28",
+ "birthArea": {
+ "id": 752,
+ "alpha2code": "SE",
+ "alpha3code": "SWE",
+ "name": "Sweden"
+ },
+ "passportArea": {
+ "id": 752,
+ "alpha2code": "SE",
+ "alpha3code": "SWE",
+ "name": "Sweden"
+ },
+ "role": {
+ "name": "Midfielder",
+ "code2": "MD",
+ "code3": "MID"
+ },
+ "foot": "right",
+ "currentTeamId": 6713,
+ "currentNationalTeamId": 7047,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/9700_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 99833,
+ "gsmId": 123106,
+ "shortName": "B. Bereszyński",
+ "firstName": "Bartosz",
+ "middleName": "",
+ "lastName": "Bereszyński",
+ "height": 183,
+ "weight": 77,
+ "birthDate": "1992-07-12",
+ "birthArea": {
+ "id": 616,
+ "alpha2code": "PL",
+ "alpha3code": "POL",
+ "name": "Poland"
+ },
+ "passportArea": {
+ "id": 616,
+ "alpha2code": "PL",
+ "alpha3code": "POL",
+ "name": "Poland"
+ },
+ "role": {
+ "name": "Defender",
+ "code2": "DF",
+ "code3": "DEF"
+ },
+ "foot": "right",
+ "currentTeamId": 3164,
+ "currentNationalTeamId": 13869,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/51565_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 257027,
+ "gsmId": 289808,
+ "shortName": "E. Audero",
+ "firstName": "Emil",
+ "middleName": "",
+ "lastName": "Audero Mulyadi",
+ "height": 192,
+ "weight": 83,
+ "birthDate": "1997-01-18",
+ "birthArea": {
+ "id": 360,
+ "alpha2code": "ID",
+ "alpha3code": "IDN",
+ "name": "Indonesia"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Goalkeeper",
+ "code2": "GK",
+ "code3": "GKP"
+ },
+ "foot": "right",
+ "currentTeamId": 3200,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g289808_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 354547,
+ "gsmId": 391087,
+ "shortName": "J. Jankto",
+ "firstName": "Jakub",
+ "middleName": "",
+ "lastName": "Jankto",
+ "height": 184,
+ "weight": 74,
+ "birthDate": "1996-01-19",
+ "birthArea": {
+ "id": 203,
+ "alpha2code": "CZ",
+ "alpha3code": "CZE",
+ "name": "Czech Republic"
+ },
+ "passportArea": {
+ "id": 203,
+ "alpha2code": "CZ",
+ "alpha3code": "CZE",
+ "name": "Czech Republic"
+ },
+ "role": {
+ "name": "Midfielder",
+ "code2": "MD",
+ "code3": "MID"
+ },
+ "foot": "left",
+ "currentTeamId": 3173,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g391087_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 215503,
+ "gsmId": 246644,
+ "shortName": "O. Colley",
+ "firstName": "Omar",
+ "middleName": "",
+ "lastName": "Colley",
+ "height": 191,
+ "weight": 90,
+ "birthDate": "1992-10-24",
+ "birthArea": {
+ "id": 270,
+ "alpha2code": "GM",
+ "alpha3code": "GMB",
+ "name": "Gambia"
+ },
+ "passportArea": {
+ "id": 270,
+ "alpha2code": "GM",
+ "alpha3code": "GMB",
+ "name": "Gambia"
+ },
+ "role": {
+ "name": "Defender",
+ "code2": "DF",
+ "code3": "DEF"
+ },
+ "foot": "left",
+ "currentTeamId": 4489,
+ "currentNationalTeamId": 18728,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/68504_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 222964,
+ "gsmId": 256802,
+ "shortName": "T. Augello",
+ "firstName": "Tommaso",
+ "middleName": "",
+ "lastName": "Augello",
+ "height": 180,
+ "weight": 70,
+ "birthDate": "1994-08-30",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Defender",
+ "code2": "DF",
+ "code3": "DEF"
+ },
+ "foot": "left",
+ "currentTeamId": 3173,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g256802_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 263433,
+ "gsmId": 296232,
+ "shortName": "A. Ferrari",
+ "firstName": "Alex",
+ "middleName": "",
+ "lastName": "Ferrari",
+ "height": 191,
+ "weight": 80,
+ "birthDate": "1994-07-01",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Defender",
+ "code2": "DF",
+ "code3": "DEF"
+ },
+ "foot": "right",
+ "currentTeamId": 3164,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/77028_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 372408,
+ "gsmId": 402008,
+ "shortName": "A. La Gumina",
+ "firstName": "Antonino",
+ "middleName": "",
+ "lastName": "La Gumina",
+ "height": 182,
+ "weight": 72,
+ "birthDate": "1996-03-06",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Forward",
+ "code2": "FW",
+ "code3": "FWD"
+ },
+ "foot": "right",
+ "currentTeamId": 3164,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g402008_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 391740,
+ "gsmId": 414054,
+ "shortName": "J. Chabot",
+ "firstName": "Jeffrey Julian",
+ "middleName": "",
+ "lastName": "Chabot",
+ "height": 195,
+ "weight": 85,
+ "birthDate": "1998-02-12",
+ "birthArea": {
+ "id": 276,
+ "alpha2code": "DE",
+ "alpha3code": "DEU",
+ "name": "Germany"
+ },
+ "passportArea": {
+ "id": 250,
+ "alpha2code": "FR",
+ "alpha3code": "FRA",
+ "name": "France"
+ },
+ "role": {
+ "name": "Defender",
+ "code2": "DF",
+ "code3": "DEF"
+ },
+ "foot": "left",
+ "currentTeamId": 2445,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g414054_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 134429,
+ "gsmId": 180575,
+ "shortName": "L. Capezzi",
+ "firstName": "Leonardo",
+ "middleName": "",
+ "lastName": "Capezzi",
+ "height": 178,
+ "weight": 70,
+ "birthDate": "1995-03-28",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Midfielder",
+ "code2": "MD",
+ "code3": "MID"
+ },
+ "foot": "right",
+ "currentTeamId": 3242,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g180575_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 21331,
+ "gsmId": 91443,
+ "shortName": "N. Ravaglia",
+ "firstName": "Nicola",
+ "middleName": "",
+ "lastName": "Ravaglia",
+ "height": 184,
+ "weight": 78,
+ "birthDate": "1988-12-12",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Goalkeeper",
+ "code2": "GK",
+ "code3": "GKP"
+ },
+ "foot": "right",
+ "currentTeamId": 3164,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/19312_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 434154,
+ "gsmId": 444205,
+ "shortName": "R. Vieira",
+ "firstName": "Ronaldo",
+ "middleName": "",
+ "lastName": "Vieira Nan",
+ "height": 178,
+ "weight": 70,
+ "birthDate": "1998-07-19",
+ "birthArea": {
+ "id": 624,
+ "alpha2code": "GW",
+ "alpha3code": "GNB",
+ "name": "Guinea-Bissau"
+ },
+ "passportArea": {
+ "id": 826,
+ "alpha2code": "EN",
+ "alpha3code": "XEN",
+ "name": "England"
+ },
+ "role": {
+ "name": "Midfielder",
+ "code2": "MD",
+ "code3": "MID"
+ },
+ "foot": "right",
+ "currentTeamId": 3164,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g444205_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 21004,
+ "gsmId": 71499,
+ "shortName": "V. Regini",
+ "firstName": "Vasco",
+ "middleName": "",
+ "lastName": "Regini",
+ "height": 185,
+ "weight": 79,
+ "birthDate": "1990-09-09",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 674,
+ "alpha2code": "SM",
+ "alpha3code": "SMR",
+ "name": "San Marino"
+ },
+ "role": {
+ "name": "Defender",
+ "code2": "DF",
+ "code3": "DEF"
+ },
+ "foot": "left",
+ "currentTeamId": null,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/13288_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 20479,
+ "gsmId": 4164,
+ "shortName": "F. Quagliarella",
+ "firstName": "Fabio",
+ "middleName": "",
+ "lastName": "Quagliarella",
+ "height": 180,
+ "weight": 0,
+ "birthDate": "1983-01-31",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Forward",
+ "code2": "FW",
+ "code3": "FWD"
+ },
+ "foot": "right",
+ "currentTeamId": null,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "retired",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/28_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 20689,
+ "gsmId": 91722,
+ "shortName": "G. Ramírez",
+ "firstName": "Gastón Exequiel",
+ "middleName": "",
+ "lastName": "Ramírez Pereyra",
+ "height": 178,
+ "weight": 83,
+ "birthDate": "1990-12-02",
+ "birthArea": {
+ "id": 858,
+ "alpha2code": "UY",
+ "alpha3code": "URY",
+ "name": "Uruguay"
+ },
+ "passportArea": {
+ "id": 858,
+ "alpha2code": "UY",
+ "alpha3code": "URY",
+ "name": "Uruguay"
+ },
+ "role": {
+ "name": "Midfielder",
+ "code2": "MD",
+ "code3": "MID"
+ },
+ "foot": "left",
+ "currentTeamId": 15633,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/36136_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 449472,
+ "gsmId": -45021,
+ "shortName": "M. Damsgaard",
+ "firstName": "Mikkel",
+ "middleName": "",
+ "lastName": "Damsgaard",
+ "height": 180,
+ "weight": 71,
+ "birthDate": "2000-07-03",
+ "birthArea": {
+ "id": 208,
+ "alpha2code": "DK",
+ "alpha3code": "DNK",
+ "name": "Denmark"
+ },
+ "passportArea": {
+ "id": 208,
+ "alpha2code": "DK",
+ "alpha3code": "DNK",
+ "name": "Denmark"
+ },
+ "role": {
+ "name": "Midfielder",
+ "code2": "MD",
+ "code3": "MID"
+ },
+ "foot": "right",
+ "currentTeamId": 1669,
+ "currentNationalTeamId": 7712,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/g-45021_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 703,
+ "gsmId": 5803,
+ "shortName": "M. Yoshida",
+ "firstName": "Maya",
+ "middleName": "",
+ "lastName": "Yoshida",
+ "height": 189,
+ "weight": 87,
+ "birthDate": "1988-08-24",
+ "birthArea": {
+ "id": 392,
+ "alpha2code": "JP",
+ "alpha3code": "JPN",
+ "name": "Japan"
+ },
+ "passportArea": {
+ "id": 392,
+ "alpha2code": "JP",
+ "alpha3code": "JPN",
+ "name": "Japan"
+ },
+ "role": {
+ "name": "Defender",
+ "code2": "DF",
+ "code3": "DEF"
+ },
+ "foot": "right",
+ "currentTeamId": 7847,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/37158_100x130.png"
+ }
+ },
+ {
+ "player": {
+ "wyId": 20446,
+ "gsmId": 162670,
+ "shortName": "V. Verre",
+ "firstName": "Valerio",
+ "middleName": "",
+ "lastName": "Verre",
+ "height": 181,
+ "weight": 70,
+ "birthDate": "1994-01-11",
+ "birthArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "passportArea": {
+ "id": 380,
+ "alpha2code": "IT",
+ "alpha3code": "ITA",
+ "name": "Italy"
+ },
+ "role": {
+ "name": "Midfielder",
+ "code2": "MD",
+ "code3": "MID"
+ },
+ "foot": "both",
+ "currentTeamId": 3171,
+ "currentNationalTeamId": null,
+ "gender": "male",
+ "status": "active",
+ "imageDataURL": "https://cdn5.wyscout.com/photos/players/public/56950_100x130.png"
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/kloppy/tests/test_adapter.py b/kloppy/tests/test_adapter.py
index e6ff29c7..10409958 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) == 40
+ assert len(dataset.events) == 39
diff --git a/kloppy/tests/test_helpers.py b/kloppy/tests/test_helpers.py
index 01f9c0e7..3aeff45e 100644
--- a/kloppy/tests/test_helpers.py
+++ b/kloppy/tests/test_helpers.py
@@ -25,6 +25,7 @@
Team,
TrackingDataset,
)
+from kloppy.domain.services.frame_factory import create_frame
class TestHelpers:
@@ -68,7 +69,7 @@ def _get_tracking_dataset(self):
tracking_data = TrackingDataset(
metadata=metadata,
records=[
- Frame(
+ create_frame(
frame_id=1,
timestamp=0.1,
ball_owning_team=teams[0],
@@ -78,7 +79,7 @@ def _get_tracking_dataset(self):
other_data=None,
ball_coordinates=Point3D(x=100, y=-50, z=0),
),
- Frame(
+ create_frame(
frame_id=2,
timestamp=0.2,
ball_owning_team=teams[1],
diff --git a/kloppy/tests/test_opta.py b/kloppy/tests/test_opta.py
index b38db5fa..7cb974f7 100644
--- a/kloppy/tests/test_opta.py
+++ b/kloppy/tests/test_opta.py
@@ -61,11 +61,11 @@ def test_parse_f24_datetime():
"""Test if the F24 datetime is correctly parsed"""
# timestamps have millisecond precision
assert _parse_f24_datetime("2018-09-23T15:02:13.608") == datetime(
- 2018, 9, 23, 15, 2, 13, 608000, tzinfo=timezone.utc
+ 2018, 9, 23, 14, 2, 13, 608000, tzinfo=timezone.utc
)
# milliseconds are not left-padded
assert _parse_f24_datetime("2018-09-23T15:02:14.39") == datetime(
- 2018, 9, 23, 15, 2, 14, 39000, tzinfo=timezone.utc
+ 2018, 9, 23, 14, 2, 14, 39000, tzinfo=timezone.utc
)
@@ -325,7 +325,7 @@ def test_correct_deserialization(self, dataset: EventDataset):
)
def test_timestamp_goal(self, dataset: EventDataset):
- """Check timestamp from qualifier in case of goal"""
+ """Check timestamp from qualifier 374 in case of goal"""
goal = dataset.get_event_by_id("2318695229")
assert goal.timestamp == (
_parse_f24_datetime("2018-09-23T16:07:48.525") # event timestamp
@@ -414,6 +414,27 @@ def test_own_goal(self, dataset: EventDataset):
assert own_goal.result_coordinates == Point3D(0.0, 100 - 45.6, 1.9)
assert own_goal.ball_state == BallState.DEAD
+ def test_goal(self, dataset: EventDataset):
+ """Test if goals are correctly deserialized"""
+ goal = dataset.get_event_by_id("2614247749")
+ assert goal.result == ShotResult.GOAL
+ assert (
+ next(
+ statistic
+ for statistic in goal.statistics
+ if statistic.name == "xG"
+ ).value
+ == 0.9780699610710144
+ )
+ assert (
+ next(
+ statistic
+ for statistic in goal.statistics
+ if statistic.name == "PSxG"
+ ).value
+ == 0.98
+ )
+
class TestOptaDuelEvent:
"""Tests related to deserializing duel events"""
diff --git a/kloppy/tests/test_sportec.py b/kloppy/tests/test_sportec.py
index 1c11bb78..ac8ad2de 100644
--- a/kloppy/tests/test_sportec.py
+++ b/kloppy/tests/test_sportec.py
@@ -16,6 +16,8 @@
BallState,
Point3D,
PositionType,
+ OfficialType,
+ Official,
)
from kloppy import sportec
@@ -119,6 +121,10 @@ class TestSportecTrackingData:
def raw_data(self, base_dir) -> str:
return base_dir / "files/sportec_positional.xml"
+ @pytest.fixture
+ def raw_data_referee(self, base_dir) -> str:
+ return base_dir / "files/sportec_positional_w_referee.xml"
+
@pytest.fixture
def meta_data(self, base_dir) -> str:
return base_dir / "files/sportec_meta.xml"
@@ -145,6 +151,7 @@ def test_load_metadata(self, raw_data: Path, meta_data: Path):
assert dataset.metadata.periods[1].end_timestamp == timedelta(
seconds=4000 + 2996.68
)
+ assert len(dataset.metadata.officials) == 4
def test_load_frames(self, raw_data: Path, meta_data: Path):
dataset = sportec.load_tracking(
@@ -238,3 +245,52 @@ def test_enriched_metadata(self, raw_data: Path, meta_data: Path):
if away_coach:
assert isinstance(away_coach, str)
assert away_coach == "M. Rose"
+
+ def test_referees(self, raw_data_referee: Path, meta_data: Path):
+ dataset = sportec.load_tracking(
+ raw_data=raw_data_referee,
+ meta_data=meta_data,
+ coordinates="sportec",
+ only_alive=True,
+ )
+ assert len(dataset.metadata.officials) == 4
+
+ assert (
+ Official(
+ official_id="42",
+ name="Pierluigi Collina",
+ role=OfficialType.MainReferee,
+ ).role.value
+ == "Main Referee"
+ )
+
+ assert (
+ Official(
+ official_id="42",
+ name="Pierluigi Collina",
+ role=OfficialType.MainReferee,
+ ).full_name
+ == "Pierluigi Collina"
+ )
+ assert (
+ Official(
+ official_id="42",
+ first_name="Pierluigi",
+ last_name="Collina",
+ role=OfficialType.MainReferee,
+ ).full_name
+ == "Pierluigi Collina"
+ )
+ assert (
+ Official(
+ official_id="42",
+ last_name="Collina",
+ role=OfficialType.MainReferee,
+ ).full_name
+ == "Collina"
+ )
+ assert (
+ Official(official_id="42", role=OfficialType.MainReferee).full_name
+ == "main_referee_42"
+ )
+ assert Official(official_id="42").full_name == "official_42"
diff --git a/kloppy/tests/test_statsbomb.py b/kloppy/tests/test_statsbomb.py
index 9995bd4b..d3f3d1f5 100644
--- a/kloppy/tests/test_statsbomb.py
+++ b/kloppy/tests/test_statsbomb.py
@@ -717,6 +717,15 @@ def test_open_play(self, dataset: EventDataset):
)
# An open play shot should not have a set piece qualifier
assert shot.get_qualifier_value(SetPieceQualifier) is None
+ # A shot event should have a xG value
+ assert (
+ next(
+ statistic
+ for statistic in shot.statistics
+ if statistic.name == "xG"
+ ).value
+ == 0.12441643
+ )
def test_free_kick(self, dataset: EventDataset):
"""It should add set piece qualifiers to free kick shots"""
diff --git a/kloppy/tests/test_statsperform.py b/kloppy/tests/test_statsperform.py
index f1c772d1..9e8e9070 100644
--- a/kloppy/tests/test_statsperform.py
+++ b/kloppy/tests/test_statsperform.py
@@ -177,7 +177,7 @@ def test_deserialize_all(self, event_dataset: EventDataset):
pitch_length=None,
pitch_width=None,
)
- assert len(event_dataset.records) == 1652
+ assert len(event_dataset.records) == 1643
substitution_events = event_dataset.find_all("substitution")
assert len(substitution_events) == 9
diff --git a/kloppy/utils.py b/kloppy/utils.py
index b0858398..68d36af2 100644
--- a/kloppy/utils.py
+++ b/kloppy/utils.py
@@ -169,3 +169,8 @@ def __get__(self, instance, owner):
stacklevel=2,
)
return self.value
+
+
+def snake_case(s: str) -> str:
+ """Convert a string to snake_case."""
+ return re.sub(r"[\s\-]+", "_", s.strip()).lower()
diff --git a/mkdocs.yml b/mkdocs.yml
index 7c8b8ea2..1ae00900 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,17 +1,14 @@
-site_name: kloppy 3.15.0
+site_name: kloppy 3.16.0
site_url: https://kloppy.pysport.org
repo_url: https://github.com/PySport/kloppy
repo_name: 'GitHub'
edit_uri: blob/master/docs/
extra_css: [style.css]
-
# TODO: set-up Google Analytics project to track
-google_analytics: null
-
+google_analytics: null
theme:
name: material
custom_dir: docs/overrides
-
nav:
- Home: index.md
- Open-data: open-data.md
@@ -28,30 +25,29 @@ nav:
- TRACAB: getting-started/tracab.ipynb
- Wyscout: getting-started/wyscout.ipynb
- Examples:
- - Event Data: examples/event_data.ipynb
- - Tracking Data: examples/tracking_data.ipynb
- - Broadcast Tracking Data: examples/broadcast_tracking_data.ipynb
- - Code data: examples/code_data.ipynb
- - State: examples/state.ipynb
- - Navigating: examples/navigating.ipynb
- - Plotting: examples/plotting.ipynb
- - Config: examples/config.ipynb
- - Adapters: examples/adapter.ipynb
-# - API Reference:
-# - Domain:
-# - Common: api/domain/common.md
-# - Pitch: api/domain/pitch.md
-# - Tracking: api/domain/tracking.md
-# - Event: api/domain/event.md
+ - Event Data: examples/event_data.ipynb
+ - Tracking Data: examples/tracking_data.ipynb
+ - Broadcast Tracking Data: examples/broadcast_tracking_data.ipynb
+ - Code data: examples/code_data.ipynb
+ - State: examples/state.ipynb
+ - Navigating: examples/navigating.ipynb
+ - Plotting: examples/plotting.ipynb
+ - Config: examples/config.ipynb
+ - Adapters: examples/adapter.ipynb
+ # - API Reference:
+ # - Domain:
+ # - Common: api/domain/common.md
+ # - Pitch: api/domain/pitch.md
+ # - Tracking: api/domain/tracking.md
+ # - Event: api/domain/event.md
- Providers: 'providers.md'
- Other:
- - Issues: 'issues.md'
- - Contributing: 'contributing.md'
- - Sponsors: 'sponsors.md'
- - About: 'about.md'
- - Changelog: 'changelog.md'
- - License: 'license.md'
-
+ - Issues: 'issues.md'
+ - Contributing: 'contributing.md'
+ - Sponsors: 'sponsors.md'
+ - About: 'about.md'
+ - Changelog: 'changelog.md'
+ - License: 'license.md'
plugins:
- mkdocs-jupyter:
include_source: True
@@ -69,7 +65,6 @@ plugins:
- exclude:
glob:
- presentations/*
-
markdown_extensions:
- pymdownx.highlight:
use_pygments: true
diff --git a/setup.py b/setup.py
index 6d78f9c5..a2ed9746 100644
--- a/setup.py
+++ b/setup.py
@@ -42,7 +42,6 @@ def setup_package():
"requests>=2.0.0,<3",
"pytz>=2020.1",
'typing_extensions;python_version<"3.11"',
- "python-dateutil>=2.8.1,<3",
"sortedcontainers>=2",
],
extras_require={