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.
Files changed (24) hide show
  1. {rustat-python-api-0.7.9/rustat_python_api.egg-info → rustat-python-api-0.7.11}/PKG-INFO +1 -1
  2. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/pitch_control.py +75 -34
  3. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11/rustat_python_api.egg-info}/PKG-INFO +1 -1
  4. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/setup.py +1 -1
  5. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/LICENSE +0 -0
  6. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/README.md +0 -0
  7. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/pyproject.toml +0 -0
  8. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/__init__.py +0 -0
  9. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/config.py +0 -0
  10. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/kernels/__init__.py +0 -0
  11. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/kernels/maha.py +0 -0
  12. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/matching/__init__.py +0 -0
  13. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/matching/dataloader.py +0 -0
  14. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/matching/pc_adder.py +0 -0
  15. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/matching/tr_adder.py +0 -0
  16. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/models_api.py +0 -0
  17. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/parser.py +0 -0
  18. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/processing.py +0 -0
  19. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api/urls.py +0 -0
  20. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api.egg-info/SOURCES.txt +0 -0
  21. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api.egg-info/dependency_links.txt +0 -0
  22. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api.egg-info/requires.txt +0 -0
  23. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/rustat_python_api.egg-info/top_level.txt +0 -0
  24. {rustat-python-api-0.7.9 → rustat-python-api-0.7.11}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rustat-python-api
3
- Version: 0.7.9
3
+ Version: 0.7.11
4
4
  Summary: A Python wrapper for RuStat API
5
5
  Home-page: https://github.com/dailydaniel/rustat-python-api
6
6
  Author: Daniel Zholkovsky
@@ -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
- for pid, grp in side_df.groupby("player_id"):
151
- locs_out[half][pid] = grp[["pos_x", "pos_y"]].values
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
- sxy = (pos_tp1 - pos_t) / dt_secs[:, None, None] # (F,P,2)
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, :] - pos_t, dim=-1) # (F,P)
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
- (*pos_t.shape[:-1], 2, 2), device=device, dtype=dtype
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
- mu = pos_t + 0.5 * sxy # (F,P,2)
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 padded], axis=1), device=device, dtype=dtype
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 padded], axis=1),
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
- if np.isfinite(self.locs_home[half][k][tp, :]).all():
738
- plt.scatter(
739
- self.locs_home[half][k][tp, 0],
740
- self.locs_home[half][k][tp, 1],
741
- color="darkgrey",
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
- if np.isfinite(self.locs_away[half][k][tp, :]).all():
747
- plt.scatter(
748
- self.locs_away[half][k][tp, 0],
749
- self.locs_away[half][k][tp, 1],
750
- color="black",
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rustat-python-api
3
- Version: 0.7.9
3
+ Version: 0.7.11
4
4
  Summary: A Python wrapper for RuStat API
5
5
  Home-page: https://github.com/dailydaniel/rustat-python-api
6
6
  Author: Daniel Zholkovsky
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="rustat-python-api",
5
- version="0.7.9",
5
+ version="0.7.11",
6
6
  description="A Python wrapper for RuStat API",
7
7
  long_description=open("README.md").read(),
8
8
  long_description_content_type="text/markdown",