spforge 0.8.20__tar.gz → 0.8.25__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.

Potentially problematic release.


This version of spforge might be problematic. Click here for more details.

Files changed (119) hide show
  1. {spforge-0.8.20/spforge.egg-info → spforge-0.8.25}/PKG-INFO +1 -1
  2. {spforge-0.8.20 → spforge-0.8.25}/pyproject.toml +1 -1
  3. {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/_base.py +2 -0
  4. {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/_base.py +6 -0
  5. {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/_player_rating.py +120 -43
  6. {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/_team_rating.py +23 -20
  7. {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/player_performance_predictor.py +1 -1
  8. {spforge-0.8.20 → spforge-0.8.25/spforge.egg-info}/PKG-INFO +1 -1
  9. {spforge-0.8.20 → spforge-0.8.25}/tests/feature_generator/test_rolling_window.py +36 -0
  10. {spforge-0.8.20 → spforge-0.8.25}/tests/ratings/test_player_rating_generator.py +429 -118
  11. {spforge-0.8.20 → spforge-0.8.25}/tests/ratings/test_team_rating_generator.py +153 -11
  12. {spforge-0.8.20 → spforge-0.8.25}/LICENSE +0 -0
  13. {spforge-0.8.20 → spforge-0.8.25}/MANIFEST.in +0 -0
  14. {spforge-0.8.20 → spforge-0.8.25}/README.md +0 -0
  15. {spforge-0.8.20 → spforge-0.8.25}/examples/__init__.py +0 -0
  16. {spforge-0.8.20 → spforge-0.8.25}/examples/game_level_example.py +0 -0
  17. {spforge-0.8.20 → spforge-0.8.25}/examples/lol/__init__.py +0 -0
  18. {spforge-0.8.20 → spforge-0.8.25}/examples/lol/data/__init__.py +0 -0
  19. {spforge-0.8.20 → spforge-0.8.25}/examples/lol/data/subsample_lol_data.parquet +0 -0
  20. {spforge-0.8.20 → spforge-0.8.25}/examples/lol/data/utils.py +0 -0
  21. {spforge-0.8.20 → spforge-0.8.25}/examples/lol/pipeline_transformer_example.py +0 -0
  22. {spforge-0.8.20 → spforge-0.8.25}/examples/nba/__init__.py +0 -0
  23. {spforge-0.8.20 → spforge-0.8.25}/examples/nba/cross_validation_example.py +0 -0
  24. {spforge-0.8.20 → spforge-0.8.25}/examples/nba/data/__init__.py +0 -0
  25. {spforge-0.8.20 → spforge-0.8.25}/examples/nba/data/game_player_subsample.parquet +0 -0
  26. {spforge-0.8.20 → spforge-0.8.25}/examples/nba/data/utils.py +0 -0
  27. {spforge-0.8.20 → spforge-0.8.25}/examples/nba/feature_engineering_example.py +0 -0
  28. {spforge-0.8.20 → spforge-0.8.25}/examples/nba/game_winner_example.py +0 -0
  29. {spforge-0.8.20 → spforge-0.8.25}/examples/nba/predictor_transformers_example.py +0 -0
  30. {spforge-0.8.20 → spforge-0.8.25}/setup.cfg +0 -0
  31. {spforge-0.8.20 → spforge-0.8.25}/spforge/__init__.py +0 -0
  32. {spforge-0.8.20 → spforge-0.8.25}/spforge/autopipeline.py +0 -0
  33. {spforge-0.8.20 → spforge-0.8.25}/spforge/base_feature_generator.py +0 -0
  34. {spforge-0.8.20 → spforge-0.8.25}/spforge/cross_validator/__init__.py +0 -0
  35. {spforge-0.8.20 → spforge-0.8.25}/spforge/cross_validator/_base.py +0 -0
  36. {spforge-0.8.20 → spforge-0.8.25}/spforge/cross_validator/cross_validator.py +0 -0
  37. {spforge-0.8.20 → spforge-0.8.25}/spforge/data_structures.py +0 -0
  38. {spforge-0.8.20 → spforge-0.8.25}/spforge/distributions/__init__.py +0 -0
  39. {spforge-0.8.20 → spforge-0.8.25}/spforge/distributions/_negative_binomial_estimator.py +0 -0
  40. {spforge-0.8.20 → spforge-0.8.25}/spforge/distributions/_normal_distribution_predictor.py +0 -0
  41. {spforge-0.8.20 → spforge-0.8.25}/spforge/distributions/_student_t_distribution_estimator.py +0 -0
  42. {spforge-0.8.20 → spforge-0.8.25}/spforge/estimator/__init__.py +0 -0
  43. {spforge-0.8.20 → spforge-0.8.25}/spforge/estimator/_conditional_estimator.py +0 -0
  44. {spforge-0.8.20 → spforge-0.8.25}/spforge/estimator/_frequency_bucketing_classifier.py +0 -0
  45. {spforge-0.8.20 → spforge-0.8.25}/spforge/estimator/_granularity_estimator.py +0 -0
  46. {spforge-0.8.20 → spforge-0.8.25}/spforge/estimator/_group_by_estimator.py +0 -0
  47. {spforge-0.8.20 → spforge-0.8.25}/spforge/estimator/_ordinal_classifier.py +0 -0
  48. {spforge-0.8.20 → spforge-0.8.25}/spforge/estimator/_sklearn_enhancer_estimator.py +0 -0
  49. {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/__init__.py +0 -0
  50. {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/_lag.py +0 -0
  51. {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/_net_over_predicted.py +0 -0
  52. {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/_regressor_feature_generator.py +0 -0
  53. {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/_rolling_against_opponent.py +0 -0
  54. {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/_rolling_mean_binary.py +0 -0
  55. {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/_rolling_mean_days.py +0 -0
  56. {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/_rolling_window.py +0 -0
  57. {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/_utils.py +0 -0
  58. {spforge-0.8.20 → spforge-0.8.25}/spforge/features_generator_pipeline.py +0 -0
  59. {spforge-0.8.20 → spforge-0.8.25}/spforge/hyperparameter_tuning/__init__.py +0 -0
  60. {spforge-0.8.20 → spforge-0.8.25}/spforge/hyperparameter_tuning/_default_search_spaces.py +0 -0
  61. {spforge-0.8.20 → spforge-0.8.25}/spforge/hyperparameter_tuning/_tuner.py +0 -0
  62. {spforge-0.8.20 → spforge-0.8.25}/spforge/performance_transformers/__init__.py +0 -0
  63. {spforge-0.8.20 → spforge-0.8.25}/spforge/performance_transformers/_performance_manager.py +0 -0
  64. {spforge-0.8.20 → spforge-0.8.25}/spforge/performance_transformers/_performances_transformers.py +0 -0
  65. {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/__init__.py +0 -0
  66. {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/enums.py +0 -0
  67. {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/league_identifier.py +0 -0
  68. {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/league_start_rating_optimizer.py +0 -0
  69. {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/start_rating_generator.py +0 -0
  70. {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/team_performance_predictor.py +0 -0
  71. {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/team_start_rating_generator.py +0 -0
  72. {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/utils.py +0 -0
  73. {spforge-0.8.20 → spforge-0.8.25}/spforge/scorer/__init__.py +0 -0
  74. {spforge-0.8.20 → spforge-0.8.25}/spforge/scorer/_score.py +0 -0
  75. {spforge-0.8.20 → spforge-0.8.25}/spforge/transformers/__init__.py +0 -0
  76. {spforge-0.8.20 → spforge-0.8.25}/spforge/transformers/_base.py +0 -0
  77. {spforge-0.8.20 → spforge-0.8.25}/spforge/transformers/_net_over_predicted.py +0 -0
  78. {spforge-0.8.20 → spforge-0.8.25}/spforge/transformers/_operator.py +0 -0
  79. {spforge-0.8.20 → spforge-0.8.25}/spforge/transformers/_other_transformer.py +0 -0
  80. {spforge-0.8.20 → spforge-0.8.25}/spforge/transformers/_predictor.py +0 -0
  81. {spforge-0.8.20 → spforge-0.8.25}/spforge/transformers/_simple_transformer.py +0 -0
  82. {spforge-0.8.20 → spforge-0.8.25}/spforge/transformers/_team_ratio_predictor.py +0 -0
  83. {spforge-0.8.20 → spforge-0.8.25}/spforge/utils.py +0 -0
  84. {spforge-0.8.20 → spforge-0.8.25}/spforge.egg-info/SOURCES.txt +0 -0
  85. {spforge-0.8.20 → spforge-0.8.25}/spforge.egg-info/dependency_links.txt +0 -0
  86. {spforge-0.8.20 → spforge-0.8.25}/spforge.egg-info/requires.txt +0 -0
  87. {spforge-0.8.20 → spforge-0.8.25}/spforge.egg-info/top_level.txt +0 -0
  88. {spforge-0.8.20 → spforge-0.8.25}/tests/cross_validator/test_cross_validator.py +0 -0
  89. {spforge-0.8.20 → spforge-0.8.25}/tests/distributions/test_distribution.py +0 -0
  90. {spforge-0.8.20 → spforge-0.8.25}/tests/end_to_end/test_estimator_hyperparameter_tuning.py +0 -0
  91. {spforge-0.8.20 → spforge-0.8.25}/tests/end_to_end/test_league_start_rating_optimizer.py +0 -0
  92. {spforge-0.8.20 → spforge-0.8.25}/tests/end_to_end/test_lol_player_kills.py +0 -0
  93. {spforge-0.8.20 → spforge-0.8.25}/tests/end_to_end/test_nba_player_points.py +0 -0
  94. {spforge-0.8.20 → spforge-0.8.25}/tests/end_to_end/test_nba_player_ratings_hyperparameter_tuning.py +0 -0
  95. {spforge-0.8.20 → spforge-0.8.25}/tests/end_to_end/test_nba_prediction_consistency.py +0 -0
  96. {spforge-0.8.20 → spforge-0.8.25}/tests/estimator/test_sklearn_estimator.py +0 -0
  97. {spforge-0.8.20 → spforge-0.8.25}/tests/feature_generator/test_lag.py +0 -0
  98. {spforge-0.8.20 → spforge-0.8.25}/tests/feature_generator/test_regressor_feature_generator.py +0 -0
  99. {spforge-0.8.20 → spforge-0.8.25}/tests/feature_generator/test_rolling_against_opponent.py +0 -0
  100. {spforge-0.8.20 → spforge-0.8.25}/tests/feature_generator/test_rolling_mean_binary.py +0 -0
  101. {spforge-0.8.20 → spforge-0.8.25}/tests/feature_generator/test_rolling_mean_days.py +0 -0
  102. {spforge-0.8.20 → spforge-0.8.25}/tests/hyperparameter_tuning/test_estimator_tuner.py +0 -0
  103. {spforge-0.8.20 → spforge-0.8.25}/tests/hyperparameter_tuning/test_rating_tuner.py +0 -0
  104. {spforge-0.8.20 → spforge-0.8.25}/tests/performance_transformers/test_performance_manager.py +0 -0
  105. {spforge-0.8.20 → spforge-0.8.25}/tests/performance_transformers/test_performances_transformers.py +0 -0
  106. {spforge-0.8.20 → spforge-0.8.25}/tests/ratings/test_player_rating_no_mutation.py +0 -0
  107. {spforge-0.8.20 → spforge-0.8.25}/tests/ratings/test_ratings_property.py +0 -0
  108. {spforge-0.8.20 → spforge-0.8.25}/tests/ratings/test_utils_scaled_weights.py +0 -0
  109. {spforge-0.8.20 → spforge-0.8.25}/tests/scorer/test_score.py +0 -0
  110. {spforge-0.8.20 → spforge-0.8.25}/tests/scorer/test_score_aggregation_granularity.py +0 -0
  111. {spforge-0.8.20 → spforge-0.8.25}/tests/test_autopipeline.py +0 -0
  112. {spforge-0.8.20 → spforge-0.8.25}/tests/test_autopipeline_context.py +0 -0
  113. {spforge-0.8.20 → spforge-0.8.25}/tests/test_feature_generator_pipeline.py +0 -0
  114. {spforge-0.8.20 → spforge-0.8.25}/tests/transformers/test_estimator_transformer_context.py +0 -0
  115. {spforge-0.8.20 → spforge-0.8.25}/tests/transformers/test_net_over_predicted.py +0 -0
  116. {spforge-0.8.20 → spforge-0.8.25}/tests/transformers/test_other_transformer.py +0 -0
  117. {spforge-0.8.20 → spforge-0.8.25}/tests/transformers/test_predictor_transformer.py +0 -0
  118. {spforge-0.8.20 → spforge-0.8.25}/tests/transformers/test_simple_transformer.py +0 -0
  119. {spforge-0.8.20 → spforge-0.8.25}/tests/transformers/test_team_ratio_predictor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spforge
3
- Version: 0.8.20
3
+ Version: 0.8.25
4
4
  Summary: A flexible framework for generating features, ratings, and building machine learning or other models for training and inference on sports data.
5
5
  Author-email: Mathias Holmstrøm <mathiasholmstom@gmail.com>
6
6
  License: See LICENSE file
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "spforge"
7
- version = "0.8.20"
7
+ version = "0.8.25"
8
8
  description = "A flexible framework for generating features, ratings, and building machine learning or other models for training and inference on sports data."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -176,6 +176,8 @@ class LagGenerator(FeatureGenerator):
176
176
  if additional_cols:
177
177
  cols.extend(additional_cols)
178
178
 
179
+ cols = list(dict.fromkeys(cols))
180
+
179
181
  if self._df is None:
180
182
  self._df = df.select(cols)
181
183
  else:
@@ -176,6 +176,9 @@ class RatingGenerator(FeatureGenerator):
176
176
  @to_polars
177
177
  @nw.narwhalify
178
178
  def transform(self, df: IntoFrameT) -> IntoFrameT:
179
+ if self.performance_manager and self.performance_manager.ori_performance_column in df.columns:
180
+ df = nw.from_native(self.performance_manager.transform(df))
181
+
179
182
  pl_df: pl.DataFrame
180
183
  pl_df = df.to_native() if df.implementation.is_polars() else df.to_polars().to_native()
181
184
  return self._historical_transform(pl_df)
@@ -188,6 +191,9 @@ class RatingGenerator(FeatureGenerator):
188
191
  - use existing ratings to compute pre-match ratings/features
189
192
  - do NOT update ratings
190
193
  """
194
+ if self.performance_manager and self.performance_manager.ori_performance_column in df.columns:
195
+ df = nw.from_native(self.performance_manager.transform(df))
196
+
191
197
  pl_df: pl.DataFrame
192
198
  pl_df = df.to_native() if df.implementation.is_polars() else df.to_polars().to_native()
193
199
  return self._future_transform(pl_df)
@@ -330,7 +330,21 @@ class PlayerRatingGenerator(RatingGenerator):
330
330
  df = df.drop(cols_to_drop)
331
331
  return df
332
332
 
333
+ def _validate_playing_time_columns(self, df: pl.DataFrame) -> None:
334
+ cn = self.column_names
335
+ if cn.team_players_playing_time and cn.team_players_playing_time not in df.columns:
336
+ raise ValueError(
337
+ f"team_players_playing_time column '{cn.team_players_playing_time}' "
338
+ f"not found in DataFrame. Available columns: {list(df.columns)}"
339
+ )
340
+ if cn.opponent_players_playing_time and cn.opponent_players_playing_time not in df.columns:
341
+ raise ValueError(
342
+ f"opponent_players_playing_time column '{cn.opponent_players_playing_time}' "
343
+ f"not found in DataFrame. Available columns: {list(df.columns)}"
344
+ )
345
+
333
346
  def _historical_transform(self, df: pl.DataFrame) -> pl.DataFrame:
347
+ self._validate_playing_time_columns(df)
334
348
  df = self._scale_participation_weight_columns(df)
335
349
  match_df = self._create_match_df(df)
336
350
  ratings = self._calculate_ratings(match_df)
@@ -359,6 +373,7 @@ class PlayerRatingGenerator(RatingGenerator):
359
373
  return self._remove_internal_scaled_columns(result)
360
374
 
361
375
  def _future_transform(self, df: pl.DataFrame) -> pl.DataFrame:
376
+ self._validate_playing_time_columns(df)
362
377
  df = self._scale_participation_weight_columns(df)
363
378
  match_df = self._create_match_df(df)
364
379
  ratings = self._calculate_future_ratings(match_df)
@@ -433,9 +448,16 @@ class PlayerRatingGenerator(RatingGenerator):
433
448
  team1_off_perf = self._team_off_perf_from_collection(c1)
434
449
  team2_off_perf = self._team_off_perf_from_collection(c2)
435
450
 
451
+ team1_def_perf: float | None = None
452
+ team2_def_perf: float | None = None
453
+
436
454
  if self.use_off_def_split:
437
- team1_def_perf = 1.0 - team2_off_perf
438
- team2_def_perf = 1.0 - team1_off_perf
455
+ team1_def_perf = (
456
+ 1.0 - team2_off_perf if team2_off_perf is not None else None
457
+ )
458
+ team2_def_perf = (
459
+ 1.0 - team1_off_perf if team1_off_perf is not None else None
460
+ )
439
461
  else:
440
462
  team1_def_perf = team1_off_perf
441
463
  team2_def_perf = team2_off_perf
@@ -459,10 +481,14 @@ class PlayerRatingGenerator(RatingGenerator):
459
481
  pred_off = self._performance_predictor.predict_performance(
460
482
  player_rating=pre_player,
461
483
  opponent_team_rating=PreMatchTeamRating(
462
- id=team2, players=[], rating_value=team2_def_rating
484
+ id=team2,
485
+ players=c2.pre_match_player_ratings,
486
+ rating_value=team2_def_rating,
463
487
  ),
464
488
  team_rating=PreMatchTeamRating(
465
- id=team1, players=[], rating_value=team1_off_rating
489
+ id=team1,
490
+ players=c1.pre_match_player_ratings,
491
+ rating_value=team1_off_rating,
466
492
  ),
467
493
  )
468
494
 
@@ -477,33 +503,39 @@ class PlayerRatingGenerator(RatingGenerator):
477
503
  other=getattr(pre_player, "other", None),
478
504
  ),
479
505
  opponent_team_rating=PreMatchTeamRating(
480
- id=team2, players=[], rating_value=team2_off_rating
506
+ id=team2,
507
+ players=c2.pre_match_player_ratings,
508
+ rating_value=team2_off_rating,
481
509
  ),
482
510
  team_rating=PreMatchTeamRating(
483
- id=team1, players=[], rating_value=team1_def_rating
511
+ id=team1,
512
+ players=c1.pre_match_player_ratings,
513
+ rating_value=team1_def_rating,
484
514
  ),
485
515
  )
486
516
 
487
517
  perf_value = pre_player.match_performance.performance_value
488
518
  if perf_value is None:
489
519
  off_change = 0.0
490
- def_change = 0.0
491
520
  else:
492
521
  off_perf = float(perf_value)
493
- def_perf = float(team1_def_perf)
494
-
495
- if not self.use_off_def_split:
496
- pred_def = pred_off
497
- def_perf = off_perf
498
-
499
522
  mult_off = self._applied_multiplier_off(off_state)
500
- mult_def = self._applied_multiplier_def(def_state)
501
-
502
523
  off_change = (
503
524
  (off_perf - float(pred_off))
504
525
  * mult_off
505
526
  * float(pre_player.match_performance.participation_weight)
506
527
  )
528
+
529
+ if perf_value is None or team1_def_perf is None:
530
+ def_change = 0.0
531
+ else:
532
+ def_perf = float(team1_def_perf)
533
+
534
+ if not self.use_off_def_split:
535
+ pred_def = pred_off
536
+ def_perf = float(perf_value)
537
+
538
+ mult_def = self._applied_multiplier_def(def_state)
507
539
  def_change = (
508
540
  (def_perf - float(pred_def))
509
541
  * mult_def
@@ -542,10 +574,14 @@ class PlayerRatingGenerator(RatingGenerator):
542
574
  pred_off = self._performance_predictor.predict_performance(
543
575
  player_rating=pre_player,
544
576
  opponent_team_rating=PreMatchTeamRating(
545
- id=team1, players=[], rating_value=team1_def_rating
577
+ id=team1,
578
+ players=c1.pre_match_player_ratings,
579
+ rating_value=team1_def_rating,
546
580
  ),
547
581
  team_rating=PreMatchTeamRating(
548
- id=team2, players=[], rating_value=team2_off_rating
582
+ id=team2,
583
+ players=c2.pre_match_player_ratings,
584
+ rating_value=team2_off_rating,
549
585
  ),
550
586
  )
551
587
 
@@ -560,43 +596,49 @@ class PlayerRatingGenerator(RatingGenerator):
560
596
  other=getattr(pre_player, "other", None),
561
597
  ),
562
598
  opponent_team_rating=PreMatchTeamRating(
563
- id=team1, players=[], rating_value=team1_off_rating
599
+ id=team1,
600
+ players=c1.pre_match_player_ratings,
601
+ rating_value=team1_off_rating,
564
602
  ),
565
603
  team_rating=PreMatchTeamRating(
566
- id=team2, players=[], rating_value=team2_def_rating
604
+ id=team2,
605
+ players=c2.pre_match_player_ratings,
606
+ rating_value=team2_def_rating,
567
607
  ),
568
608
  )
569
609
 
570
610
  perf_value = pre_player.match_performance.performance_value
571
611
  if perf_value is None:
572
612
  off_change = 0.0
573
- def_change = 0.0
574
613
  else:
575
614
  off_perf = float(perf_value)
576
- def_perf = float(team2_def_perf)
577
-
578
- if not self.use_off_def_split:
579
- pred_def = pred_off
580
- def_perf = off_perf
581
-
582
615
  mult_off = self._applied_multiplier_off(off_state)
583
- mult_def = self._applied_multiplier_def(def_state)
584
-
585
616
  off_change = (
586
617
  (off_perf - float(pred_off))
587
618
  * mult_off
588
619
  * float(pre_player.match_performance.participation_weight)
589
620
  )
621
+
622
+ if perf_value is None or team2_def_perf is None:
623
+ def_change = 0.0
624
+ else:
625
+ def_perf = float(team2_def_perf)
626
+
627
+ if not self.use_off_def_split:
628
+ pred_def = pred_off
629
+ def_perf = float(perf_value)
630
+
631
+ mult_def = self._applied_multiplier_def(def_state)
590
632
  def_change = (
591
633
  (def_perf - float(pred_def))
592
634
  * mult_def
593
635
  * float(pre_player.match_performance.participation_weight)
594
636
  )
595
637
 
596
- if math.isnan(off_change) or math.isnan(def_change):
597
- raise ValueError(
598
- f"NaN player rating change for player_id={pid}, match_id={r[cn.match_id]}"
599
- )
638
+ if math.isnan(off_change) or math.isnan(def_change):
639
+ raise ValueError(
640
+ f"NaN player rating change for player_id={pid}, match_id={r[cn.match_id]}"
641
+ )
600
642
 
601
643
  player_updates.append(
602
644
  (
@@ -870,6 +912,12 @@ class PlayerRatingGenerator(RatingGenerator):
870
912
  if cn.league and cn.league in df.columns:
871
913
  player_stat_cols.append(cn.league)
872
914
 
915
+ if cn.team_players_playing_time and cn.team_players_playing_time in df.columns:
916
+ player_stat_cols.append(cn.team_players_playing_time)
917
+
918
+ if cn.opponent_players_playing_time and cn.opponent_players_playing_time in df.columns:
919
+ player_stat_cols.append(cn.opponent_players_playing_time)
920
+
873
921
  df = df.with_columns(pl.struct(player_stat_cols).alias(PLAYER_STATS))
874
922
 
875
923
  group_cols = [cn.match_id, cn.team_id, cn.start_date]
@@ -946,10 +994,24 @@ class PlayerRatingGenerator(RatingGenerator):
946
994
  else None
947
995
  )
948
996
 
997
+ team_playing_time = None
998
+ opponent_playing_time = None
999
+ if cn.team_players_playing_time:
1000
+ raw_value = team_player.get(cn.team_players_playing_time)
1001
+ if raw_value is not None:
1002
+ team_playing_time = raw_value
1003
+
1004
+ if cn.opponent_players_playing_time:
1005
+ raw_value = team_player.get(cn.opponent_players_playing_time)
1006
+ if raw_value is not None:
1007
+ opponent_playing_time = raw_value
1008
+
949
1009
  mp = MatchPerformance(
950
1010
  performance_value=perf_val,
951
1011
  projected_participation_weight=projected_participation_weight,
952
1012
  participation_weight=participation_weight,
1013
+ team_players_playing_time=team_playing_time,
1014
+ opponent_players_playing_time=opponent_playing_time,
953
1015
  )
954
1016
 
955
1017
  if player_id in self._player_off_ratings and player_id in self._player_def_ratings:
@@ -1031,12 +1093,14 @@ class PlayerRatingGenerator(RatingGenerator):
1031
1093
 
1032
1094
  return pre_match_player_ratings, pre_match_player_off_values
1033
1095
 
1034
- def _team_off_perf_from_collection(self, c: PreMatchPlayersCollection) -> float:
1096
+ def _team_off_perf_from_collection(
1097
+ self, c: PreMatchPlayersCollection
1098
+ ) -> float | None:
1035
1099
  # observed offense perf = weighted mean of player performance_value using participation_weight if present
1036
1100
  # skip players with null performance
1037
1101
  cn = self.column_names
1038
1102
  if not c.pre_match_player_ratings:
1039
- return 0.0
1103
+ return None
1040
1104
  wsum = 0.0
1041
1105
  psum = 0.0
1042
1106
  for pre in c.pre_match_player_ratings:
@@ -1050,7 +1114,7 @@ class PlayerRatingGenerator(RatingGenerator):
1050
1114
  )
1051
1115
  psum += float(perf_val) * w
1052
1116
  wsum += w
1053
- return psum / wsum if wsum else 0.0
1117
+ return psum / wsum if wsum else None
1054
1118
 
1055
1119
  def _team_off_def_rating_from_collection(
1056
1120
  self, c: PreMatchPlayersCollection
@@ -1181,10 +1245,23 @@ class PlayerRatingGenerator(RatingGenerator):
1181
1245
  ppw = pw
1182
1246
  proj_w.append(float(ppw))
1183
1247
 
1248
+ team_playing_time = None
1249
+ opponent_playing_time = None
1250
+ if cn.team_players_playing_time:
1251
+ raw_value = tp.get(cn.team_players_playing_time)
1252
+ if raw_value is not None:
1253
+ team_playing_time = raw_value
1254
+ if cn.opponent_players_playing_time:
1255
+ raw_value = tp.get(cn.opponent_players_playing_time)
1256
+ if raw_value is not None:
1257
+ opponent_playing_time = raw_value
1258
+
1184
1259
  mp = MatchPerformance(
1185
1260
  performance_value=get_perf_value(tp),
1186
1261
  projected_participation_weight=ppw,
1187
1262
  participation_weight=pw,
1263
+ team_players_playing_time=team_playing_time,
1264
+ opponent_players_playing_time=opponent_playing_time,
1188
1265
  )
1189
1266
 
1190
1267
  ensure_new_player(pid, day_number, mp, league, position, pre_list) # noqa: B023
@@ -1237,10 +1314,10 @@ class PlayerRatingGenerator(RatingGenerator):
1237
1314
  pred_off = self._performance_predictor.predict_performance(
1238
1315
  player_rating=pre,
1239
1316
  opponent_team_rating=PreMatchTeamRating(
1240
- id=team2, players=[], rating_value=t2_def_rating
1317
+ id=team2, players=t2_pre, rating_value=t2_def_rating
1241
1318
  ),
1242
1319
  team_rating=PreMatchTeamRating(
1243
- id=team1, players=[], rating_value=t1_off_rating
1320
+ id=team1, players=t1_pre, rating_value=t1_off_rating
1244
1321
  ),
1245
1322
  )
1246
1323
 
@@ -1254,10 +1331,10 @@ class PlayerRatingGenerator(RatingGenerator):
1254
1331
  position=pre.position,
1255
1332
  ),
1256
1333
  opponent_team_rating=PreMatchTeamRating(
1257
- id=team2, players=[], rating_value=t2_off_rating
1334
+ id=team2, players=t2_pre, rating_value=t2_off_rating
1258
1335
  ),
1259
1336
  team_rating=PreMatchTeamRating(
1260
- id=team1, players=[], rating_value=t1_def_rating
1337
+ id=team1, players=t1_pre, rating_value=t1_def_rating
1261
1338
  ),
1262
1339
  )
1263
1340
 
@@ -1282,10 +1359,10 @@ class PlayerRatingGenerator(RatingGenerator):
1282
1359
  pred_off = self._performance_predictor.predict_performance(
1283
1360
  player_rating=pre,
1284
1361
  opponent_team_rating=PreMatchTeamRating(
1285
- id=team1, players=[], rating_value=t1_def_rating
1362
+ id=team1, players=t1_pre, rating_value=t1_def_rating
1286
1363
  ),
1287
1364
  team_rating=PreMatchTeamRating(
1288
- id=team2, players=[], rating_value=t2_off_rating
1365
+ id=team2, players=t2_pre, rating_value=t2_off_rating
1289
1366
  ),
1290
1367
  )
1291
1368
 
@@ -1299,10 +1376,10 @@ class PlayerRatingGenerator(RatingGenerator):
1299
1376
  position=pre.position,
1300
1377
  ),
1301
1378
  opponent_team_rating=PreMatchTeamRating(
1302
- id=team1, players=[], rating_value=t1_off_rating
1379
+ id=team1, players=t1_pre, rating_value=t1_off_rating
1303
1380
  ),
1304
1381
  team_rating=PreMatchTeamRating(
1305
- id=team2, players=[], rating_value=t2_def_rating
1382
+ id=team2, players=t2_pre, rating_value=t2_def_rating
1306
1383
  ),
1307
1384
  )
1308
1385
 
@@ -326,16 +326,7 @@ class TeamRatingGenerator(RatingGenerator):
326
326
  opp_off_pre = float(o_off.rating_value)
327
327
  opp_def_pre = float(o_def.rating_value)
328
328
 
329
- off_perf = (
330
- float(r[self.performance_column])
331
- if r.get(self.performance_column) is not None
332
- else 0.0
333
- )
334
- opp_off_perf = float(r[perf_opp_col]) if r.get(perf_opp_col) is not None else 0.0
335
- if self.use_off_def_split:
336
- def_perf = 1.0 - opp_off_perf
337
- else:
338
- def_perf = off_perf
329
+ off_perf_raw = r.get(self.performance_column)
339
330
 
340
331
  pred_off = self._performance_predictor.predict_performance(
341
332
  rating_value=s_off.rating_value, opponent_team_rating_value=o_def.rating_value
@@ -346,16 +337,28 @@ class TeamRatingGenerator(RatingGenerator):
346
337
  if not self.use_off_def_split:
347
338
  pred_def = pred_off
348
339
 
349
- mult_off = self._applied_multiplier(s_off, self.rating_change_multiplier_offense)
350
- mult_def = self._applied_multiplier(s_def, self.rating_change_multiplier_defense)
351
-
352
- off_change = (off_perf - pred_off) * mult_off
353
- def_change = (def_perf - pred_def) * mult_def
354
-
355
- if math.isnan(off_change) or math.isnan(def_change):
356
- raise ValueError(
357
- f"NaN rating change for team_id={team_id}, match_id={r[cn.match_id]}"
358
- )
340
+ # Null performance means no rating change
341
+ if off_perf_raw is None:
342
+ off_change = 0.0
343
+ def_change = 0.0
344
+ else:
345
+ off_perf = float(off_perf_raw)
346
+ opp_off_perf = float(r[perf_opp_col]) if r.get(perf_opp_col) is not None else 0.0
347
+ if self.use_off_def_split:
348
+ def_perf = 1.0 - opp_off_perf
349
+ else:
350
+ def_perf = off_perf
351
+
352
+ mult_off = self._applied_multiplier(s_off, self.rating_change_multiplier_offense)
353
+ mult_def = self._applied_multiplier(s_def, self.rating_change_multiplier_defense)
354
+
355
+ off_change = (off_perf - pred_off) * mult_off
356
+ def_change = (def_perf - pred_def) * mult_def
357
+
358
+ if math.isnan(off_change) or math.isnan(def_change):
359
+ raise ValueError(
360
+ f"NaN rating change for team_id={team_id}, match_id={r[cn.match_id]}"
361
+ )
359
362
 
360
363
  rows.append(
361
364
  {
@@ -133,7 +133,7 @@ class RatingPlayerDifferencePerformancePredictor(PlayerPerformancePredictor):
133
133
  team_rating_value = team_rating.rating_value
134
134
 
135
135
  if player_rating.match_performance.opponent_players_playing_time and isinstance(
136
- player_rating.match_performance.team_players_playing_time, dict
136
+ player_rating.match_performance.opponent_players_playing_time, dict
137
137
  ):
138
138
  weight_opp_rating = 0
139
139
  sum_playing_time = 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spforge
3
- Version: 0.8.20
3
+ Version: 0.8.25
4
4
  Summary: A flexible framework for generating features, ratings, and building machine learning or other models for training and inference on sports data.
5
5
  Author-email: Mathias Holmstrøm <mathiasholmstom@gmail.com>
6
6
  License: See LICENSE file
@@ -684,3 +684,39 @@ def test_rolling_mean_historical_transform_higher_granularity(column_names, use_
684
684
  }
685
685
  )
686
686
  pd.testing.assert_frame_equal(transformed_df, expected_df, check_like=True, check_dtype=False)
687
+
688
+
689
+ @pytest.mark.parametrize("df", [pd.DataFrame, pl.DataFrame])
690
+ def test_rolling_window__feature_also_used_as_column_names_field(df):
691
+ column_names = ColumnNames(
692
+ match_id="game_id",
693
+ player_id="player_id",
694
+ team_id="team_id",
695
+ start_date="game_date",
696
+ participation_weight="three_pointers_attempted",
697
+ )
698
+ data = df(
699
+ {
700
+ "game_id": [1, 1, 2, 2],
701
+ "player_id": ["a", "b", "a", "b"],
702
+ "team_id": [1, 2, 1, 2],
703
+ "game_date": [
704
+ pd.to_datetime("2023-01-01"),
705
+ pd.to_datetime("2023-01-01"),
706
+ pd.to_datetime("2023-01-02"),
707
+ pd.to_datetime("2023-01-02"),
708
+ ],
709
+ "three_pointers_attempted": [5.0, 3.0, 7.0, 4.0],
710
+ }
711
+ )
712
+
713
+ transformer = RollingWindowTransformer(
714
+ features=["three_pointers_attempted"],
715
+ window=20,
716
+ granularity=["player_id"],
717
+ )
718
+
719
+ transformed_df = transformer.fit_transform(data, column_names=column_names)
720
+
721
+ assert transformer.features_out[0] in transformed_df.columns
722
+ assert len(transformed_df) == len(data)