55from datetime import datetime , timedelta
66from enum import Enum , Flag
77from typing import (
8+ Any ,
9+ Callable ,
810 Dict ,
11+ Generic ,
12+ Iterable ,
913 List ,
14+ Literal ,
15+ NewType ,
1016 Optional ,
11- Callable ,
12- Union ,
13- Any ,
1417 TypeVar ,
15- Generic ,
16- NewType ,
18+ Union ,
1719 overload ,
18- Iterable ,
1920)
2021
21- from .position import PositionType
22-
2322from ...utils import deprecated , snake_case
24-
25- if sys .version_info >= (3 , 8 ):
26- from typing import Literal
27- else :
28- from typing_extensions import Literal
23+ from .position import PositionType
2924
3025if sys .version_info >= (3 , 11 ):
3126 from typing import Self
3227else :
3328 from typing_extensions import Self
3429
30+ from ...exceptions import (
31+ InvalidFilterError ,
32+ KloppyParameterError ,
33+ OrientationError ,
34+ )
35+ from .formation import FormationType
3536from .pitch import (
36- PitchDimensions ,
37- Unit ,
3837 Dimension ,
39- NormalizedPitchDimensions ,
40- MetricPitchDimensions ,
4138 ImperialPitchDimensions ,
39+ MetricPitchDimensions ,
40+ NormalizedPitchDimensions ,
4241 OptaPitchDimensions ,
42+ PitchDimensions ,
43+ Unit ,
4344 WyscoutPitchDimensions ,
4445)
45- from .formation import FormationType
46- from .time import Time , Period , TimeContainer
47- from ...exceptions import (
48- OrientationError ,
49- InvalidFilterError ,
50- KloppyParameterError ,
51- )
46+ from .time import Period , Time , TimeContainer
5247
5348
5449@dataclass
@@ -264,7 +259,10 @@ def get_player_by_jersey_number(self, jersey_no: int):
264259 def get_player_by_position (self , position : PositionType , time : Time ):
265260 for player in self .players :
266261 if player .positions .items :
267- player_position = player .positions .value_at (time )
262+ try :
263+ player_position = player .positions .value_at (time )
264+ except KeyError : # player that is subbed in later
265+ continue
268266 if player_position and player_position == position :
269267 return player
270268
@@ -928,6 +926,85 @@ class DatasetFlag(Flag):
928926 BALL_STATE = 2
929927
930928
929+ @dataclass
930+ class Statistic (ABC ):
931+ name : str = field (init = False )
932+
933+
934+ @dataclass
935+ class ScalarStatistic (Statistic ):
936+ value : float
937+
938+
939+ @dataclass
940+ class ExpectedGoals (ScalarStatistic ):
941+ """Expected goals"""
942+
943+ def __post_init__ (self ):
944+ self .name = "xG"
945+
946+
947+ @dataclass
948+ class PostShotExpectedGoals (ScalarStatistic ):
949+ """Post-shot expected goals"""
950+
951+ def __post_init__ (self ):
952+ self .name = "PSxG"
953+
954+
955+ @dataclass
956+ class ActionValue (Statistic ):
957+ """Action value"""
958+
959+ name : str
960+ action_value_scoring_before : Optional [float ] = field (default = None )
961+ action_value_scoring_after : Optional [float ] = field (default = None )
962+ action_value_conceding_before : Optional [float ] = field (default = None )
963+ action_value_conceding_after : Optional [float ] = field (default = None )
964+
965+ @property
966+ def offensive_value (self ) -> Optional [float ]:
967+ return (
968+ None
969+ if None
970+ in (
971+ self .action_value_scoring_before ,
972+ self .action_value_scoring_after ,
973+ )
974+ else self .action_value_scoring_after
975+ - self .action_value_scoring_before
976+ )
977+
978+ @property
979+ def defensive_value (self ) -> Optional [float ]:
980+ return (
981+ None
982+ if None
983+ in (
984+ self .action_value_conceding_before ,
985+ self .action_value_conceding_after ,
986+ )
987+ else self .action_value_conceding_after
988+ - self .action_value_conceding_before
989+ )
990+
991+ @property
992+ def value (self ) -> Optional [float ]:
993+ if None in (
994+ self .action_value_scoring_before ,
995+ self .action_value_scoring_after ,
996+ self .action_value_conceding_before ,
997+ self .action_value_conceding_after ,
998+ ):
999+ return None
1000+ return (
1001+ self .action_value_scoring_after - self .action_value_scoring_before
1002+ ) - (
1003+ self .action_value_conceding_after
1004+ - self .action_value_conceding_before
1005+ )
1006+
1007+
9311008@dataclass
9321009class DataRecord (ABC ):
9331010 """
@@ -945,6 +1022,7 @@ class DataRecord(ABC):
9451022 next_record : Optional ["DataRecord" ] = field (init = False )
9461023 period : Period
9471024 timestamp : timedelta
1025+ statistics : List [Statistic ]
9481026 ball_owning_team : Optional [Team ]
9491027 ball_state : Optional [BallState ]
9501028
@@ -1118,6 +1196,7 @@ def _init_player_positions(self):
11181196 start_of_match = self .metadata .periods [0 ].start_time
11191197 for team in self .metadata .teams :
11201198 for player in team .players :
1199+ player .positions .reset ()
11211200 if player .starting :
11221201 player .set_position (
11231202 start_of_match ,
0 commit comments