rustat-python-api 0.7.9__tar.gz → 0.7.11__tar.gz
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.
- {rustat-python-api-0.7.9/rustat_python_api.egg-info → rustat-python-api-0.7.11}/PKG-INFO +1 -1
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/pitch_control.py +75 -34
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11/rustat_python_api.egg-info}/PKG-INFO +1 -1
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/setup.py +1 -1
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/LICENSE +0 -0
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/README.md +0 -0
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/pyproject.toml +0 -0
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/__init__.py +0 -0
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/config.py +0 -0
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/kernels/__init__.py +0 -0
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/kernels/maha.py +0 -0
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/matching/__init__.py +0 -0
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/matching/dataloader.py +0 -0
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/matching/pc_adder.py +0 -0
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/matching/tr_adder.py +0 -0
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/models_api.py +0 -0
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/parser.py +0 -0
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/processing.py +0 -0
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/urls.py +0 -0
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api.egg-info/SOURCES.txt +0 -0
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api.egg-info/dependency_links.txt +0 -0
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api.egg-info/requires.txt +0 -0
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api.egg-info/top_level.txt +0 -0
- {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/setup.cfg +0 -0
|
@@ -137,7 +137,8 @@ class PitchControl:
|
|
|
137
137
|
"""Vectorised construction of player location dictionaries.
|
|
138
138
|
|
|
139
139
|
Returns (locs_home, locs_away) where each is
|
|
140
|
-
{half: {player_id: np.ndarray(T,2)}}.
|
|
140
|
+
{half: {player_id: np.ndarray(T,2)}} where T is the full length of the half.
|
|
141
|
+
Players not on field at time t have NaN coordinates at index t.
|
|
141
142
|
"""
|
|
142
143
|
locs_home = {1: {}, 2: {}}
|
|
143
144
|
locs_away = {1: {}, 2: {}}
|
|
@@ -145,10 +146,30 @@ class PitchControl:
|
|
|
145
146
|
# Work per half to keep order and avoid extra boolean checks.
|
|
146
147
|
for half in (1, 2):
|
|
147
148
|
half_df = tracking[tracking["half"] == half]
|
|
149
|
+
|
|
150
|
+
# Get all unique timestamps for this half (sorted)
|
|
151
|
+
all_timestamps = np.sort(half_df["second"].unique())
|
|
152
|
+
|
|
148
153
|
for side, locs_out in [("left", locs_home), ("right", locs_away)]:
|
|
149
154
|
side_df = half_df[half_df["side_1h"] == side]
|
|
150
|
-
|
|
151
|
-
|
|
155
|
+
|
|
156
|
+
for pid in side_df["player_id"].unique():
|
|
157
|
+
# Get data for this specific player
|
|
158
|
+
player_data = side_df[side_df["player_id"] == pid][
|
|
159
|
+
["second", "pos_x", "pos_y"]
|
|
160
|
+
].copy()
|
|
161
|
+
|
|
162
|
+
# Create full timeline DataFrame with all timestamps
|
|
163
|
+
full_timeline = pd.DataFrame({"second": all_timestamps})
|
|
164
|
+
|
|
165
|
+
# Left join: timestamps without player data will have NaN coordinates
|
|
166
|
+
player_data_full = full_timeline.merge(
|
|
167
|
+
player_data, on="second", how="left"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Store only coordinates (NaN where player is not on field)
|
|
171
|
+
locs_out[half][pid] = player_data_full[["pos_x", "pos_y"]].values
|
|
172
|
+
|
|
152
173
|
return locs_home, locs_away
|
|
153
174
|
|
|
154
175
|
@staticmethod
|
|
@@ -385,7 +406,21 @@ class PitchControl:
|
|
|
385
406
|
|
|
386
407
|
device = locs.device
|
|
387
408
|
|
|
388
|
-
|
|
409
|
+
# Create validity mask for players (not NaN at both t and t+1)
|
|
410
|
+
valid_mask = torch.isfinite(pos_t).all(dim=-1) & torch.isfinite(pos_tp1).all(
|
|
411
|
+
dim=-1
|
|
412
|
+
) # (F,P)
|
|
413
|
+
|
|
414
|
+
# Replace NaN with zeros to avoid NaN propagation in calculations
|
|
415
|
+
# (these values will be masked out later)
|
|
416
|
+
pos_t_clean = torch.where(
|
|
417
|
+
valid_mask.unsqueeze(-1), pos_t, torch.zeros_like(pos_t)
|
|
418
|
+
)
|
|
419
|
+
pos_tp1_clean = torch.where(
|
|
420
|
+
valid_mask.unsqueeze(-1), pos_tp1, torch.zeros_like(pos_tp1)
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
sxy = (pos_tp1_clean - pos_t_clean) / dt_secs[:, None, None] # (F,P,2)
|
|
389
424
|
speed = torch.linalg.norm(sxy, dim=-1) # (F,P)
|
|
390
425
|
norm_sxy = speed.clamp(min=1e-6)
|
|
391
426
|
|
|
@@ -402,7 +437,7 @@ class PitchControl:
|
|
|
402
437
|
|
|
403
438
|
Srat = (speed / 13) ** 2 # (F,P)
|
|
404
439
|
|
|
405
|
-
Ri = torch.linalg.norm(ball_pos[:, None, :] -
|
|
440
|
+
Ri = torch.linalg.norm(ball_pos[:, None, :] - pos_t_clean, dim=-1) # (F,P)
|
|
406
441
|
Ri = torch.minimum(
|
|
407
442
|
4 + Ri**3 / (18**3 / 6), torch.tensor(10.0, device=device, dtype=dtype)
|
|
408
443
|
)
|
|
@@ -411,7 +446,7 @@ class PitchControl:
|
|
|
411
446
|
S22 = (1 - Srat) * Ri / 2
|
|
412
447
|
|
|
413
448
|
S = torch.zeros(
|
|
414
|
-
(*
|
|
449
|
+
(*pos_t_clean.shape[:-1], 2, 2), device=device, dtype=dtype
|
|
415
450
|
) # (F,P,2,2)
|
|
416
451
|
S[..., 0, 0] = S11
|
|
417
452
|
S[..., 1, 1] = S22
|
|
@@ -426,7 +461,15 @@ class PitchControl:
|
|
|
426
461
|
else:
|
|
427
462
|
Sigma_inv = torch.linalg.inv(Sigma + eye)
|
|
428
463
|
|
|
429
|
-
|
|
464
|
+
# Mask out invalid players by zeroing their Sigma_inv
|
|
465
|
+
# This ensures their influence contribution is exactly 0
|
|
466
|
+
Sigma_inv = torch.where(
|
|
467
|
+
valid_mask.unsqueeze(-1).unsqueeze(-1), # (F,P,1,1)
|
|
468
|
+
Sigma_inv,
|
|
469
|
+
torch.zeros_like(Sigma_inv),
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
mu = pos_t_clean + 0.5 * sxy # (F,P,2)
|
|
430
473
|
|
|
431
474
|
diff = locs.view(1, 1, -1, 2) # (1,1,N,2)
|
|
432
475
|
diff = diff - mu.unsqueeze(2) # (F,P,N,2)
|
|
@@ -446,25 +489,17 @@ class PitchControl:
|
|
|
446
489
|
def _stack_team_frames(
|
|
447
490
|
players: list[np.ndarray], frames: np.ndarray, device: str, dtype: torch.dtype
|
|
448
491
|
):
|
|
449
|
-
"""Stack positions for given frames into torch tensors (pos_t, pos_tp1).
|
|
450
|
-
# Ensure every player's trajectory is long enough; if not, pad by repeating
|
|
451
|
-
# the last available coordinate so that indexing `frames` and `frames+1` is safe.
|
|
452
|
-
max_needed = frames[-1] + 1 # we access idx and idx+1
|
|
453
|
-
|
|
454
|
-
padded = []
|
|
455
|
-
for p in players:
|
|
456
|
-
if len(p) <= max_needed:
|
|
457
|
-
pad_len = max_needed + 1 - len(p)
|
|
458
|
-
if pad_len > 0:
|
|
459
|
-
last = p[-1][None, :]
|
|
460
|
-
p = np.vstack([p, np.repeat(last, pad_len, axis=0)])
|
|
461
|
-
padded.append(p)
|
|
492
|
+
"""Stack positions for given frames into torch tensors (pos_t, pos_tp1).
|
|
462
493
|
|
|
494
|
+
After build_player_locs alignment, all player arrays have the same length
|
|
495
|
+
(full timeline), so no padding is needed. Players not on field have NaN coordinates.
|
|
496
|
+
"""
|
|
497
|
+
# All arrays are now aligned, just stack them directly
|
|
463
498
|
pos_t = torch.tensor(
|
|
464
|
-
np.stack([p[frames] for p in
|
|
499
|
+
np.stack([p[frames] for p in players], axis=1), device=device, dtype=dtype
|
|
465
500
|
) # (F,P,2)
|
|
466
501
|
pos_tp1 = torch.tensor(
|
|
467
|
-
np.stack([p[frames + 1] for p in
|
|
502
|
+
np.stack([p[frames + 1] for p in players], axis=1),
|
|
468
503
|
device=device,
|
|
469
504
|
dtype=dtype,
|
|
470
505
|
)
|
|
@@ -734,21 +769,27 @@ class PitchControl:
|
|
|
734
769
|
|
|
735
770
|
for k in self.locs_home[half].keys():
|
|
736
771
|
# if len(self.locs_home[half][k]) >= tp:
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
772
|
+
try:
|
|
773
|
+
if np.isfinite(self.locs_home[half][k][tp, :]).all():
|
|
774
|
+
plt.scatter(
|
|
775
|
+
self.locs_home[half][k][tp, 0],
|
|
776
|
+
self.locs_home[half][k][tp, 1],
|
|
777
|
+
color="darkgrey",
|
|
778
|
+
)
|
|
779
|
+
except Exception as e:
|
|
780
|
+
print(f"No data for player {k}: {e}")
|
|
743
781
|
|
|
744
782
|
for k in self.locs_away[half].keys():
|
|
745
783
|
# if len(self.locs_away[half][k]) >= tp:
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
784
|
+
try:
|
|
785
|
+
if np.isfinite(self.locs_away[half][k][tp, :]).all():
|
|
786
|
+
plt.scatter(
|
|
787
|
+
self.locs_away[half][k][tp, 0],
|
|
788
|
+
self.locs_away[half][k][tp, 1],
|
|
789
|
+
color="black",
|
|
790
|
+
)
|
|
791
|
+
except Exception as e:
|
|
792
|
+
print(f"No data for player {k}: {e}")
|
|
752
793
|
|
|
753
794
|
plt.scatter(
|
|
754
795
|
self.locs_ball[half][tp, 0], self.locs_ball[half][tp, 1], color="red"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/matching/dataloader.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api.egg-info/requires.txt
RENAMED
|
File without changes
|
{rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|