impectPy 2.4.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
impectPy/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ # define version attribute
2
+ __version__ = "2.4.4"
3
+
4
+ # import modules
5
+ from .access_token import getAccessToken
6
+ from .iterations import getIterations
7
+ from .matches import getMatches
8
+ from .events import getEvents
9
+ from .matchsums import getPlayerMatchsums, getSquadMatchsums
10
+ from .iteration_averages import getPlayerIterationAverages, getSquadIterationAverages
11
+ from .player_scores import getPlayerMatchScores, getPlayerIterationScores
12
+ from .squad_scores import getSquadMatchScores, getSquadIterationScores
13
+ from .player_profile_scores import getPlayerProfileScores
14
+ from .xml import generateXML
15
+ from .set_pieces import getSetPieces
16
+ from .squad_ratings import getSquadRatings
17
+ from .match_info import getFormations, getSubstitutions, getStartingPositions
18
+ from .config import Config as Config
19
+ from .impect import Impect as Impect
@@ -0,0 +1,38 @@
1
+ # load packages
2
+ import urllib
3
+ import requests
4
+ from impectPy.helpers import RateLimitedAPI
5
+
6
+ ######
7
+ #
8
+ # This function returns an access token for the external API
9
+ #
10
+ ######
11
+
12
+
13
+ # define function
14
+ def getAccessToken(username: str, password: str, session: requests.Session = requests.Session()) -> str:
15
+
16
+ # create an instance of RateLimitedAPI
17
+ connection = RateLimitedAPI(session)
18
+
19
+ return getAccessTokenFromUrl(username, password, connection, "https://login.impect.com/auth/realms/production/protocol/openid-connect/token")
20
+
21
+ def getAccessTokenFromUrl(username: str, password: str, connection: RateLimitedAPI, token_url: str) -> str:
22
+
23
+ # define request parameters
24
+ login = 'client_id=api&grant_type=password&username=' + urllib.parse.quote(
25
+ username) + '&password=' + urllib.parse.quote(password)
26
+
27
+ # define request headers
28
+ connection.session.headers.update({"body": login, "Content-Type": "application/x-www-form-urlencoded"})
29
+
30
+ # request access token
31
+ response = connection.make_api_request(url=token_url, method="POST", data=login)
32
+
33
+ # remove headers again
34
+ connection.session.headers.clear()
35
+
36
+ # get access token from response and return it
37
+ token = response.json()["access_token"]
38
+ return token
impectPy/config.py ADDED
@@ -0,0 +1,4 @@
1
+ class Config(object):
2
+ def __init__(self, host: str = 'https://api.impect.com', oidc_token_endpoint: str = 'https://login.impect.com/auth/realms/production/protocol/openid-connect/token'):
3
+ self.HOST = host
4
+ self.OIDC_TOKEN_ENDPOINT = oidc_token_endpoint
impectPy/events.py ADDED
@@ -0,0 +1,516 @@
1
+ # load packages
2
+ import numpy as np
3
+ import pandas as pd
4
+ import requests
5
+ from impectPy.helpers import RateLimitedAPI
6
+ from .matches import getMatchesFromHost
7
+ from .iterations import getIterationsFromHost
8
+ import re
9
+
10
+ ######
11
+ #
12
+ # This function returns a pandas dataframe that contains all events for a
13
+ # given match
14
+ #
15
+ ######
16
+
17
+
18
+ def getEvents(
19
+ matches: list, token: str, include_kpis: bool = True,
20
+ include_set_pieces: bool = True, session: requests.Session = requests.Session()
21
+ ) -> pd.DataFrame:
22
+
23
+ # create an instance of RateLimitedAPI
24
+ connection = RateLimitedAPI(session)
25
+
26
+ # construct header with access token
27
+ connection.session.headers.update({"Authorization": f"Bearer {token}"})
28
+
29
+ return getEventsFromHost(matches, include_kpis, include_set_pieces, connection, "https://api.impect.com")
30
+
31
+ # define function
32
+ def getEventsFromHost(
33
+ matches: list, include_kpis: bool, include_set_pieces: bool, connection: RateLimitedAPI, host: str
34
+ ) -> pd.DataFrame:
35
+
36
+ # check input for matches argument
37
+ if not isinstance(matches, list):
38
+ raise Exception("Argument 'matches' must be a list of integers.")
39
+
40
+ # get match info
41
+ iterations = pd.concat(
42
+ map(lambda match: connection.make_api_request_limited(
43
+ url=f"{host}/v5/customerapi/matches/{match}",
44
+ method="GET"
45
+ ).process_response(
46
+ endpoint="Iterations"
47
+ ),
48
+ matches),
49
+ ignore_index=True)
50
+
51
+ # filter for matches that are unavailable
52
+ fail_matches = iterations[iterations.lastCalculationDate.isnull()].id.drop_duplicates().to_list()
53
+
54
+ # drop matches that are unavailable from list of matches
55
+ matches = [match for match in matches if match not in fail_matches]
56
+
57
+ # raise exception if no matches remaining or report removed matches
58
+ if len(fail_matches) > 0:
59
+ if len(matches) == 0:
60
+ raise Exception("All supplied matches are unavailable. Execution stopped.")
61
+ else:
62
+ print(f"The following matches are not available yet and were ignored:\n{fail_matches}")
63
+
64
+ # extract iterationIds
65
+ iterations = list(iterations[iterations.lastCalculationDate.notnull()].iterationId.unique())
66
+
67
+ # get match events
68
+ events = pd.concat(
69
+ map(lambda match: connection.make_api_request_limited(
70
+ url=f"{host}/v5/customerapi/matches/{match}/events",
71
+ method="GET"
72
+ ).process_response(
73
+ endpoint="Events"
74
+ ).assign(
75
+ matchId=match
76
+ ),
77
+ matches),
78
+ ignore_index=True)
79
+
80
+ # account for matches without dribbles, duels or opponents tagged
81
+ attributes = [
82
+ "dribbleDistance",
83
+ "dribbleType",
84
+ "dribbleResult",
85
+ "dribblePlayerId",
86
+ "duelDuelType",
87
+ "duelPlayerId",
88
+ "opponentCoordinatesX",
89
+ "opponentCoordinatesY",
90
+ "opponentAdjCoordinatesX",
91
+ "opponentAdjCoordinatesY"
92
+ ]
93
+
94
+ # add attribute if it doesn't exist in df
95
+ for attribute in attributes:
96
+ if attribute not in events.columns:
97
+ events[attribute] = np.nan
98
+
99
+ # get players
100
+ players = pd.concat(
101
+ map(lambda iteration: connection.make_api_request_limited(
102
+ url=f"{host}/v5/customerapi/iterations/{iteration}/players",
103
+ method="GET"
104
+ ).process_response(
105
+ endpoint="Players"
106
+ ),
107
+ iterations),
108
+ ignore_index=True)[["id", "commonname"]].drop_duplicates()
109
+
110
+ # get squads
111
+ squads = pd.concat(
112
+ map(lambda iteration: connection.make_api_request_limited(
113
+ url=f"{host}/v5/customerapi/iterations/{iteration}/squads",
114
+ method="GET"
115
+ ).process_response(
116
+ endpoint="Squads"
117
+ ),
118
+ iterations),
119
+ ignore_index=True)[["id", "name"]].drop_duplicates()
120
+
121
+ # get matches
122
+ matchplan = pd.concat(
123
+ map(lambda iteration: getMatchesFromHost(
124
+ iteration=iteration,
125
+ connection=connection,
126
+ host=host
127
+ ),
128
+ iterations),
129
+ ignore_index=True)
130
+
131
+ # get iterations
132
+ iterations = getIterationsFromHost(connection=connection, host=host)
133
+
134
+ if include_kpis:
135
+ # get event scorings
136
+ scorings = pd.concat(
137
+ map(lambda match: connection.make_api_request_limited(
138
+ url=f"{host}/v5/customerapi/matches/{match}/event-kpis",
139
+ method="GET"
140
+ ).process_response(
141
+ endpoint="Scorings"
142
+ ),
143
+ matches),
144
+ ignore_index=True)
145
+
146
+ # get kpis
147
+ kpis = connection.make_api_request_limited(
148
+ url=f"{host}/v5/customerapi/kpis/event",
149
+ method="GET"
150
+ ).process_response(
151
+ endpoint="EventKPIs"
152
+ )[["id", "name"]]
153
+
154
+ if include_set_pieces:
155
+ # get set piece data
156
+ set_pieces = pd.concat(
157
+ map(lambda match: connection.make_api_request_limited(
158
+ url=f"{host}/v5/customerapi/matches/{match}/set-pieces",
159
+ method="GET"
160
+ ).process_response(
161
+ endpoint="Set-Pieces"
162
+ ),
163
+ matches),
164
+ ignore_index=True
165
+ ).rename(
166
+ columns={"id": "setPieceId"}
167
+ ).explode("setPieceSubPhase", ignore_index=True)
168
+
169
+ # unpack setPieceSubPhase column
170
+ set_pieces = pd.concat(
171
+ [
172
+ set_pieces.drop(columns=["setPieceSubPhase"]),
173
+ pd.json_normalize(set_pieces["setPieceSubPhase"]).add_prefix("setPieceSubPhase.")
174
+ ],
175
+ axis=1
176
+ ).rename(columns=lambda x: re.sub(r"\.(.)", lambda y: y.group(1).upper(), x))
177
+
178
+ # fix potential typing issues
179
+ events.pressingPlayerId = events.pressingPlayerId.astype("Int64")
180
+ events.fouledPlayerId = events.fouledPlayerId.astype("Int64")
181
+ events.passReceiverPlayerId = events.passReceiverPlayerId.astype("Int64")
182
+ events.duelPlayerId = events.duelPlayerId.astype("Int64")
183
+ events.fouledPlayerId = events.fouledPlayerId.astype("Int64")
184
+ if include_set_pieces:
185
+ set_pieces.setPieceSubPhaseMainEventPlayerId = set_pieces.setPieceSubPhaseMainEventPlayerId.astype("Int64")
186
+ set_pieces.setPieceSubPhaseFirstTouchPlayerId = set_pieces.setPieceSubPhaseFirstTouchPlayerId.astype("Int64")
187
+ set_pieces.setPieceSubPhaseSecondTouchPlayerId = set_pieces.setPieceSubPhaseSecondTouchPlayerId.astype("Int64")
188
+
189
+ # start merging dfs
190
+
191
+ # merge events with squads
192
+ events = events.merge(
193
+ squads[["id", "name"]].rename(columns={"id": "squadId", "name": "squadName"}),
194
+ left_on="squadId",
195
+ right_on="squadId",
196
+ how="left",
197
+ suffixes=("", "_home")
198
+ ).merge(
199
+ squads[["id", "name"]].rename(columns={"id": "squadId", "name": "currentAttackingSquadName"}),
200
+ left_on="currentAttackingSquadId",
201
+ right_on="squadId",
202
+ how="left",
203
+ suffixes=("", "_away")
204
+ )
205
+
206
+ # merge events with players
207
+ events = events.merge(
208
+ players[["id", "commonname"]].rename(columns={"id": "playerId", "commonname": "playerName"}),
209
+ left_on="playerId",
210
+ right_on="playerId",
211
+ how="left",
212
+ suffixes=("", "_right")
213
+ ).merge(
214
+ players[["id", "commonname"]].rename(
215
+ columns={"id": "pressingPlayerId", "commonname": "pressingPlayerName"}),
216
+ left_on="pressingPlayerId",
217
+ right_on="pressingPlayerId",
218
+ how="left",
219
+ suffixes=("", "_right")
220
+ ).merge(
221
+ players[["id", "commonname"]].rename(columns={"id": "fouledPlayerId", "commonname": "fouledPlayerName"}),
222
+ left_on="fouledPlayerId",
223
+ right_on="fouledPlayerId",
224
+ how="left",
225
+ suffixes=("", "_right")
226
+ ).merge(
227
+ players[["id", "commonname"]].rename(columns={"id": "duelPlayerId", "commonname": "duelPlayerName"}),
228
+ left_on="duelPlayerId",
229
+ right_on="duelPlayerId",
230
+ how="left",
231
+ suffixes=("", "_right")
232
+ ).merge(
233
+ players[["id", "commonname"]].rename(
234
+ columns={"id": "passReceiverPlayerId", "commonname": "passReceiverPlayerName"}),
235
+ left_on="passReceiverPlayerId",
236
+ right_on="passReceiverPlayerId",
237
+ how="left",
238
+ suffixes=("", "_right")
239
+ ).merge(
240
+ players[["id", "commonname"]].rename(
241
+ columns={"id": "dribbleOpponentPlayerId", "commonname": "dribbleOpponentPlayerName"}),
242
+ left_on="dribblePlayerId",
243
+ right_on="dribbleOpponentPlayerId",
244
+ how="left",
245
+ suffixes=("", "_right")
246
+ )
247
+
248
+ # merge with matches info
249
+ events = events.merge(
250
+ matchplan,
251
+ left_on="matchId",
252
+ right_on="id",
253
+ how="left",
254
+ suffixes=("", "_right")
255
+ )
256
+
257
+ # merge with competition info
258
+ events = events.merge(
259
+ iterations,
260
+ left_on="iterationId",
261
+ right_on="id",
262
+ how="left",
263
+ suffixes=("", "_right")
264
+ )
265
+
266
+ if include_kpis:
267
+ # unnest scorings and full join with kpi list to ensure all kpis are present
268
+ scorings = scorings.merge(kpis, left_on="kpiId", right_on="id", how="outer") \
269
+ .sort_values("kpiId") \
270
+ .drop("kpiId", axis=1) \
271
+ .fillna({"eventId": "", "position": "", "playerId": ""}) \
272
+ .pivot_table(index=["eventId", "position", "playerId"], columns="name", values="value", aggfunc="sum",
273
+ fill_value=None) \
274
+ .reset_index() \
275
+ .loc[lambda df: df["eventId"].notna()]
276
+
277
+ # Replace empty strings with None in the eventId and playerId column
278
+ scorings["eventId"] = scorings["eventId"].mask(scorings["eventId"] == "", None)
279
+ scorings["playerId"] = scorings["playerId"].mask(scorings["playerId"] == "", None)
280
+ events["playerId"] = events["playerId"].mask(events["playerId"] == "", None)
281
+
282
+ # Convert column eventId from float to int
283
+ scorings["eventId"] = scorings["eventId"].astype(pd.Int64Dtype())
284
+ scorings["playerId"] = scorings["playerId"].astype(pd.Int64Dtype())
285
+ events["playerId"] = events["playerId"].astype(pd.Int64Dtype())
286
+
287
+ # merge events and scorings
288
+ events = events.merge(scorings,
289
+ left_on=["playerPosition", "playerId", "id"],
290
+ right_on=["position", "playerId", "eventId"],
291
+ how="left",
292
+ suffixes=("", "_scorings"))
293
+
294
+ if include_set_pieces:
295
+ events = events.merge(
296
+ set_pieces,
297
+ left_on=["setPieceId", "setPieceSubPhaseId"],
298
+ right_on=["setPieceId", "setPieceSubPhaseId"],
299
+ how="left",
300
+ suffixes=("", "_right")
301
+ ).merge(
302
+ players[["id", "commonname"]].rename(
303
+ columns={
304
+ "id": "setPieceSubPhaseMainEventPlayerId",
305
+ "commonname": "setPieceSubPhaseMainEventPlayerName"
306
+ }
307
+ ),
308
+ left_on="setPieceSubPhaseMainEventPlayerId",
309
+ right_on="setPieceSubPhaseMainEventPlayerId",
310
+ how="left",
311
+ suffixes=("", "_right")
312
+ ).merge(
313
+ players[["id", "commonname"]].rename(
314
+ columns={
315
+ "id": "setPieceSubPhasePassReceiverId",
316
+ "commonname": "setPieceSubPhasePassReceiverName"
317
+ }
318
+ ),
319
+ left_on="setPieceSubPhasePassReceiverId",
320
+ right_on="setPieceSubPhasePassReceiverId",
321
+ how="left",
322
+ suffixes=("", "_right")
323
+ ).merge(
324
+ players[["id", "commonname"]].rename(
325
+ columns={
326
+ "id": "setPieceSubPhaseFirstTouchPlayerId",
327
+ "commonname": "setPieceSubPhaseFirstTouchPlayerName"
328
+ }
329
+ ),
330
+ left_on="setPieceSubPhaseFirstTouchPlayerId",
331
+ right_on="setPieceSubPhaseFirstTouchPlayerId",
332
+ how="left",
333
+ suffixes=("", "_right")
334
+ ).merge(
335
+ players[["id", "commonname"]].rename(
336
+ columns={
337
+ "id": "setPieceSubPhaseSecondTouchPlayerId",
338
+ "commonname": "setPieceSubPhaseSecondTouchPlayerName"
339
+ }
340
+ ),
341
+ left_on="setPieceSubPhaseSecondTouchPlayerId",
342
+ right_on="setPieceSubPhaseSecondTouchPlayerId",
343
+ how="left",
344
+ suffixes=("", "_right")
345
+ )
346
+
347
+ # rename some columns
348
+ events = events.rename(columns={
349
+ "currentAttackingSquadId": "attackingSquadId",
350
+ "currentAttackingSquadName": "attackingSquadName",
351
+ "duelDuelType": "duelType",
352
+ "scheduledDate": "dateTime",
353
+ "gameTimeGameTime": "gameTime",
354
+ "gameTimeGameTimeInSec": "gameTimeInSec",
355
+ "eventId": "eventId_scorings",
356
+ "id": "eventId",
357
+ "index": "eventNumber",
358
+ "phaseIndex": "setPiecePhaseIndex",
359
+ "setPieceMainEvent": "setPieceSubPhaseMainEvent",
360
+ })
361
+
362
+ # define desired column order
363
+ event_cols = [
364
+ "matchId",
365
+ "dateTime",
366
+ "competitionId",
367
+ "competitionName",
368
+ "competitionType",
369
+ "iterationId",
370
+ "season",
371
+ "matchDayIndex",
372
+ "matchDayName",
373
+ "homeSquadId",
374
+ "homeSquadName",
375
+ "homeSquadCountryId",
376
+ "homeSquadCountryName",
377
+ "homeSquadType",
378
+ "awaySquadId",
379
+ "awaySquadName",
380
+ "awaySquadCountryId",
381
+ "awaySquadCountryName",
382
+ "awaySquadType",
383
+ "eventId",
384
+ "eventNumber",
385
+ "sequenceIndex",
386
+ "periodId",
387
+ "gameTime",
388
+ "gameTimeInSec",
389
+ "duration",
390
+ "squadId",
391
+ "squadName",
392
+ "attackingSquadId",
393
+ "attackingSquadName",
394
+ "phase",
395
+ "playerId",
396
+ "playerName",
397
+ "playerPosition",
398
+ "playerPositionSide",
399
+ "actionType",
400
+ "action",
401
+ "bodyPart",
402
+ "bodyPartExtended",
403
+ "previousPassHeight",
404
+ "result",
405
+ "startCoordinatesX",
406
+ "startCoordinatesY",
407
+ "startAdjCoordinatesX",
408
+ "startAdjCoordinatesY",
409
+ "startPackingZone",
410
+ "startPitchPosition",
411
+ "startLane",
412
+ "endCoordinatesX",
413
+ "endCoordinatesY",
414
+ "endAdjCoordinatesX",
415
+ "endAdjCoordinatesY",
416
+ "endPackingZone",
417
+ "endPitchPosition",
418
+ "endLane",
419
+ "opponents",
420
+ "pressure",
421
+ "distanceToGoal",
422
+ "pxTTeam",
423
+ "pxTOpponent",
424
+ "pressingPlayerId",
425
+ "pressingPlayerName",
426
+ "distanceToOpponent",
427
+ "opponentCoordinatesX",
428
+ "opponentCoordinatesY",
429
+ "opponentAdjCoordinatesX",
430
+ "opponentAdjCoordinatesY",
431
+ "passReceiverType",
432
+ "passReceiverPlayerId",
433
+ "passReceiverPlayerName",
434
+ "passDistance",
435
+ "passAngle",
436
+ "dribbleDistance",
437
+ "dribbleType",
438
+ "dribbleResult",
439
+ "dribbleOpponentPlayerId",
440
+ "dribbleOpponentPlayerName",
441
+ "shotDistance",
442
+ "shotAngle",
443
+ "shotTargetPointY",
444
+ "shotTargetPointZ",
445
+ "shotWoodwork",
446
+ "shotGkCoordinatesX",
447
+ "shotGkCoordinatesY",
448
+ "shotGkAdjCoordinatesX",
449
+ "shotGkAdjCoordinatesY",
450
+ "shotGkDivePointY",
451
+ "shotGkDivePointZ",
452
+ "duelType",
453
+ "duelPlayerId",
454
+ "duelPlayerName",
455
+ "fouledPlayerId",
456
+ "fouledPlayerName",
457
+ "formationTeam",
458
+ "formationOpponent",
459
+ "inferredSetPiece",
460
+ ]
461
+
462
+ set_piece_cols = [
463
+ "setPieceId",
464
+ "setPiecePhaseIndex",
465
+ "setPieceCategory",
466
+ "adjSetPieceCategory",
467
+ "setPieceExecutionType",
468
+ "setPieceSubPhaseId",
469
+ "setPieceSubPhaseIndex",
470
+ "setPieceSubPhaseStartZone",
471
+ "setPieceSubPhaseCornerEndZone",
472
+ "setPieceSubPhaseCornerType",
473
+ "setPieceSubPhaseFreeKickEndZone",
474
+ "setPieceSubPhaseFreeKickType",
475
+ "setPieceSubPhaseMainEvent",
476
+ "setPieceSubPhaseMainEventPlayerId",
477
+ "setPieceSubPhaseMainEventPlayerName",
478
+ "setPieceSubPhaseMainEventOutcome",
479
+ "setPieceSubPhasePassReceiverId",
480
+ "setPieceSubPhasePassReceiverName",
481
+ "setPieceSubPhaseFirstTouchPlayerId",
482
+ "setPieceSubPhaseFirstTouchPlayerName",
483
+ "setPieceSubPhaseFirstTouchWon",
484
+ "setPieceSubPhaseIndirectHeader",
485
+ "setPieceSubPhaseSecondTouchPlayerId",
486
+ "setPieceSubPhaseSecondTouchPlayerName",
487
+ "setPieceSubPhaseSecondTouchWon",
488
+ ]
489
+
490
+ # add columns that might not exist in previous data versions
491
+ for col in event_cols:
492
+ if col not in events.columns:
493
+ events[col] = np.nan
494
+
495
+ # create order
496
+ order = event_cols
497
+
498
+ if include_set_pieces:
499
+ # add kpis
500
+ order = order + set_piece_cols
501
+
502
+ if include_kpis:
503
+ # get list of kpi columns
504
+ kpi_cols = kpis["name"].tolist()
505
+
506
+ # add kpis
507
+ order = order + kpi_cols
508
+
509
+ # reorder data
510
+ events = events[order]
511
+
512
+ # reorder rows
513
+ events = events.sort_values(["matchId", "eventNumber"])
514
+
515
+ # return events
516
+ return events