spforge 0.8.29__tar.gz → 0.8.31__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.
- {spforge-0.8.29/spforge.egg-info → spforge-0.8.31}/PKG-INFO +1 -1
- {spforge-0.8.29 → spforge-0.8.31}/pyproject.toml +1 -1
- {spforge-0.8.29 → spforge-0.8.31}/spforge/data_structures.py +4 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/ratings/_player_rating.py +116 -7
- {spforge-0.8.29 → spforge-0.8.31/spforge.egg-info}/PKG-INFO +1 -1
- {spforge-0.8.29 → spforge-0.8.31}/tests/ratings/test_player_rating_generator.py +164 -0
- {spforge-0.8.29 → spforge-0.8.31}/LICENSE +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/MANIFEST.in +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/README.md +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/examples/__init__.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/examples/game_level_example.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/examples/lol/__init__.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/examples/lol/data/__init__.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/examples/lol/data/subsample_lol_data.parquet +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/examples/lol/data/utils.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/examples/lol/pipeline_transformer_example.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/examples/nba/__init__.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/examples/nba/cross_validation_example.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/examples/nba/data/__init__.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/examples/nba/data/game_player_subsample.parquet +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/examples/nba/data/utils.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/examples/nba/feature_engineering_example.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/examples/nba/game_winner_example.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/examples/nba/predictor_transformers_example.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/setup.cfg +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/__init__.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/autopipeline.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/base_feature_generator.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/cross_validator/__init__.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/cross_validator/_base.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/cross_validator/cross_validator.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/distributions/__init__.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/distributions/_negative_binomial_estimator.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/distributions/_normal_distribution_predictor.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/distributions/_student_t_distribution_estimator.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/estimator/__init__.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/estimator/_conditional_estimator.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/estimator/_frequency_bucketing_classifier.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/estimator/_granularity_estimator.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/estimator/_group_by_estimator.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/estimator/_ordinal_classifier.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/estimator/_sklearn_enhancer_estimator.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/feature_generator/__init__.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/feature_generator/_base.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/feature_generator/_lag.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/feature_generator/_net_over_predicted.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/feature_generator/_regressor_feature_generator.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/feature_generator/_rolling_against_opponent.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/feature_generator/_rolling_mean_binary.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/feature_generator/_rolling_mean_days.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/feature_generator/_rolling_window.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/feature_generator/_utils.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/features_generator_pipeline.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/hyperparameter_tuning/__init__.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/hyperparameter_tuning/_default_search_spaces.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/hyperparameter_tuning/_tuner.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/performance_transformers/__init__.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/performance_transformers/_performance_manager.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/performance_transformers/_performances_transformers.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/ratings/__init__.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/ratings/_base.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/ratings/_team_rating.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/ratings/enums.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/ratings/league_identifier.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/ratings/league_start_rating_optimizer.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/ratings/player_performance_predictor.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/ratings/start_rating_generator.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/ratings/team_performance_predictor.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/ratings/team_start_rating_generator.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/ratings/utils.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/scorer/__init__.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/scorer/_score.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/transformers/__init__.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/transformers/_base.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/transformers/_net_over_predicted.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/transformers/_operator.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/transformers/_other_transformer.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/transformers/_predictor.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/transformers/_simple_transformer.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/transformers/_team_ratio_predictor.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge/utils.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge.egg-info/SOURCES.txt +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge.egg-info/dependency_links.txt +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge.egg-info/requires.txt +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/spforge.egg-info/top_level.txt +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/cross_validator/test_cross_validator.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/distributions/test_distribution.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/end_to_end/test_estimator_hyperparameter_tuning.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/end_to_end/test_league_start_rating_optimizer.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/end_to_end/test_lol_player_kills.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/end_to_end/test_nba_player_points.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/end_to_end/test_nba_player_ratings_hyperparameter_tuning.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/end_to_end/test_nba_prediction_consistency.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/estimator/test_sklearn_estimator.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/feature_generator/test_lag.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/feature_generator/test_regressor_feature_generator.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/feature_generator/test_rolling_against_opponent.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/feature_generator/test_rolling_mean_binary.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/feature_generator/test_rolling_mean_days.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/feature_generator/test_rolling_window.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/hyperparameter_tuning/test_estimator_tuner.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/hyperparameter_tuning/test_rating_tuner.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/performance_transformers/test_performance_manager.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/performance_transformers/test_performances_transformers.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/ratings/test_player_rating_no_mutation.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/ratings/test_ratings_property.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/ratings/test_team_rating_generator.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/ratings/test_utils_scaled_weights.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/scorer/test_score.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/scorer/test_score_aggregation_granularity.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/scorer/test_scorer_name.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/test_autopipeline.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/test_autopipeline_context.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/test_feature_generator_pipeline.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/transformers/test_estimator_transformer_context.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/transformers/test_net_over_predicted.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/transformers/test_other_transformer.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/transformers/test_predictor_transformer.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/tests/transformers/test_simple_transformer.py +0 -0
- {spforge-0.8.29 → spforge-0.8.31}/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.
|
|
3
|
+
Version: 0.8.31
|
|
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.
|
|
7
|
+
version = "0.8.31"
|
|
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"
|
|
@@ -12,6 +12,8 @@ class ColumnNames:
|
|
|
12
12
|
position: str | None = None
|
|
13
13
|
participation_weight: str | None = None
|
|
14
14
|
projected_participation_weight: str | None = None
|
|
15
|
+
defense_participation_weight: str | None = None
|
|
16
|
+
projected_defense_participation_weight: str | None = None
|
|
15
17
|
update_match_id: str | None = None
|
|
16
18
|
parent_team_id: str | None = None
|
|
17
19
|
team_players_playing_time: str | None = None
|
|
@@ -81,6 +83,8 @@ class MatchPerformance:
|
|
|
81
83
|
performance_value: float | None
|
|
82
84
|
participation_weight: float | None
|
|
83
85
|
projected_participation_weight: float
|
|
86
|
+
defense_participation_weight: float | None = None
|
|
87
|
+
projected_defense_participation_weight: float | None = None
|
|
84
88
|
team_players_playing_time: dict[str, float] | None = None
|
|
85
89
|
opponent_players_playing_time: dict[str, float] | None = None
|
|
86
90
|
|
|
@@ -39,6 +39,8 @@ from spforge.feature_generator._utils import to_polars
|
|
|
39
39
|
PLAYER_STATS = "__PLAYER_STATS"
|
|
40
40
|
_SCALED_PW = "__scaled_participation_weight__"
|
|
41
41
|
_SCALED_PPW = "__scaled_projected_participation_weight__"
|
|
42
|
+
_SCALED_DPW = "__scaled_defense_participation_weight__"
|
|
43
|
+
_SCALED_PDPW = "__scaled_projected_defense_participation_weight__"
|
|
42
44
|
|
|
43
45
|
|
|
44
46
|
class PlayerRatingGenerator(RatingGenerator):
|
|
@@ -186,6 +188,8 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
186
188
|
self.auto_scale_participation_weights = bool(auto_scale_participation_weights)
|
|
187
189
|
self._participation_weight_max: float | None = None
|
|
188
190
|
self._projected_participation_weight_max: float | None = None
|
|
191
|
+
self._defense_participation_weight_max: float | None = None
|
|
192
|
+
self._projected_defense_participation_weight_max: float | None = None
|
|
189
193
|
|
|
190
194
|
self._player_off_ratings: dict[str, PlayerRating] = {}
|
|
191
195
|
self._player_def_ratings: dict[str, PlayerRating] = {}
|
|
@@ -233,8 +237,11 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
233
237
|
eps = 1e-6
|
|
234
238
|
return min_val < -eps or max_val > (1.0 + eps)
|
|
235
239
|
|
|
236
|
-
if
|
|
237
|
-
cn.
|
|
240
|
+
if (
|
|
241
|
+
_out_of_bounds(cn.participation_weight)
|
|
242
|
+
or _out_of_bounds(cn.projected_participation_weight)
|
|
243
|
+
or _out_of_bounds(cn.defense_participation_weight)
|
|
244
|
+
or _out_of_bounds(cn.projected_defense_participation_weight)
|
|
238
245
|
):
|
|
239
246
|
self.scale_participation_weights = True
|
|
240
247
|
logging.warning(
|
|
@@ -289,6 +296,25 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
289
296
|
elif self._participation_weight_max is not None:
|
|
290
297
|
self._projected_participation_weight_max = self._participation_weight_max
|
|
291
298
|
|
|
299
|
+
if cn.defense_participation_weight and cn.defense_participation_weight in df.columns:
|
|
300
|
+
q_val = pl_df[cn.defense_participation_weight].quantile(0.99, "linear")
|
|
301
|
+
if q_val is not None:
|
|
302
|
+
self._defense_participation_weight_max = float(q_val)
|
|
303
|
+
elif self._participation_weight_max is not None:
|
|
304
|
+
self._defense_participation_weight_max = self._participation_weight_max
|
|
305
|
+
|
|
306
|
+
if (
|
|
307
|
+
cn.projected_defense_participation_weight
|
|
308
|
+
and cn.projected_defense_participation_weight in df.columns
|
|
309
|
+
):
|
|
310
|
+
q_val = pl_df[cn.projected_defense_participation_weight].quantile(0.99, "linear")
|
|
311
|
+
if q_val is not None:
|
|
312
|
+
self._projected_defense_participation_weight_max = float(q_val)
|
|
313
|
+
elif self._defense_participation_weight_max is not None:
|
|
314
|
+
self._projected_defense_participation_weight_max = self._defense_participation_weight_max
|
|
315
|
+
elif self._projected_participation_weight_max is not None:
|
|
316
|
+
self._projected_defense_participation_weight_max = self._projected_participation_weight_max
|
|
317
|
+
|
|
292
318
|
def _scale_participation_weight_columns(self, df: pl.DataFrame) -> pl.DataFrame:
|
|
293
319
|
"""Create internal scaled participation weight columns without mutating originals."""
|
|
294
320
|
if not self.scale_participation_weights:
|
|
@@ -321,6 +347,32 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
321
347
|
.alias(_SCALED_PPW)
|
|
322
348
|
)
|
|
323
349
|
|
|
350
|
+
if (
|
|
351
|
+
cn.defense_participation_weight
|
|
352
|
+
and cn.defense_participation_weight in df.columns
|
|
353
|
+
and self._defense_participation_weight_max is not None
|
|
354
|
+
and self._defense_participation_weight_max > 0
|
|
355
|
+
):
|
|
356
|
+
denom = float(self._defense_participation_weight_max)
|
|
357
|
+
df = df.with_columns(
|
|
358
|
+
(pl.col(cn.defense_participation_weight) / denom)
|
|
359
|
+
.clip(0.0, 1.0)
|
|
360
|
+
.alias(_SCALED_DPW)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
if (
|
|
364
|
+
cn.projected_defense_participation_weight
|
|
365
|
+
and cn.projected_defense_participation_weight in df.columns
|
|
366
|
+
and self._projected_defense_participation_weight_max is not None
|
|
367
|
+
and self._projected_defense_participation_weight_max > 0
|
|
368
|
+
):
|
|
369
|
+
denom = float(self._projected_defense_participation_weight_max)
|
|
370
|
+
df = df.with_columns(
|
|
371
|
+
(pl.col(cn.projected_defense_participation_weight) / denom)
|
|
372
|
+
.clip(0.0, 1.0)
|
|
373
|
+
.alias(_SCALED_PDPW)
|
|
374
|
+
)
|
|
375
|
+
|
|
324
376
|
return df
|
|
325
377
|
|
|
326
378
|
def _get_participation_weight_col(self) -> str:
|
|
@@ -339,7 +391,9 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
339
391
|
|
|
340
392
|
def _remove_internal_scaled_columns(self, df: pl.DataFrame) -> pl.DataFrame:
|
|
341
393
|
"""Remove internal scaled columns before returning."""
|
|
342
|
-
cols_to_drop = [
|
|
394
|
+
cols_to_drop = [
|
|
395
|
+
c for c in [_SCALED_PW, _SCALED_PPW, _SCALED_DPW, _SCALED_PDPW] if c in df.columns
|
|
396
|
+
]
|
|
343
397
|
if cols_to_drop:
|
|
344
398
|
df = df.drop(cols_to_drop)
|
|
345
399
|
return df
|
|
@@ -541,7 +595,7 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
541
595
|
* float(pre_player.match_performance.participation_weight)
|
|
542
596
|
)
|
|
543
597
|
|
|
544
|
-
if
|
|
598
|
+
if team1_def_perf is None:
|
|
545
599
|
def_change = 0.0
|
|
546
600
|
else:
|
|
547
601
|
def_perf = float(team1_def_perf)
|
|
@@ -554,7 +608,7 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
554
608
|
def_change = (
|
|
555
609
|
(def_perf - float(pred_def))
|
|
556
610
|
* mult_def
|
|
557
|
-
* float(pre_player.match_performance.
|
|
611
|
+
* float(pre_player.match_performance.defense_participation_weight)
|
|
558
612
|
)
|
|
559
613
|
|
|
560
614
|
if math.isnan(off_change) or math.isnan(def_change):
|
|
@@ -635,7 +689,7 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
635
689
|
* float(pre_player.match_performance.participation_weight)
|
|
636
690
|
)
|
|
637
691
|
|
|
638
|
-
if
|
|
692
|
+
if team2_def_perf is None:
|
|
639
693
|
def_change = 0.0
|
|
640
694
|
else:
|
|
641
695
|
def_perf = float(team2_def_perf)
|
|
@@ -648,7 +702,7 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
648
702
|
def_change = (
|
|
649
703
|
(def_perf - float(pred_def))
|
|
650
704
|
* mult_def
|
|
651
|
-
* float(pre_player.match_performance.
|
|
705
|
+
* float(pre_player.match_performance.defense_participation_weight)
|
|
652
706
|
)
|
|
653
707
|
|
|
654
708
|
if math.isnan(off_change) or math.isnan(def_change):
|
|
@@ -922,6 +976,19 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
922
976
|
if _SCALED_PPW in df.columns:
|
|
923
977
|
player_stat_cols.append(_SCALED_PPW)
|
|
924
978
|
|
|
979
|
+
if cn.defense_participation_weight and cn.defense_participation_weight in df.columns:
|
|
980
|
+
player_stat_cols.append(cn.defense_participation_weight)
|
|
981
|
+
if _SCALED_DPW in df.columns:
|
|
982
|
+
player_stat_cols.append(_SCALED_DPW)
|
|
983
|
+
|
|
984
|
+
if (
|
|
985
|
+
cn.projected_defense_participation_weight
|
|
986
|
+
and cn.projected_defense_participation_weight in df.columns
|
|
987
|
+
):
|
|
988
|
+
player_stat_cols.append(cn.projected_defense_participation_weight)
|
|
989
|
+
if _SCALED_PDPW in df.columns:
|
|
990
|
+
player_stat_cols.append(_SCALED_PDPW)
|
|
991
|
+
|
|
925
992
|
if cn.position and cn.position in df.columns:
|
|
926
993
|
player_stat_cols.append(cn.position)
|
|
927
994
|
|
|
@@ -1041,6 +1108,28 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
1041
1108
|
projected_participation_weight = participation_weight
|
|
1042
1109
|
projected_participation_weights.append(projected_participation_weight)
|
|
1043
1110
|
|
|
1111
|
+
# Use scaled defense participation weight if available, otherwise default to participation_weight
|
|
1112
|
+
if _SCALED_DPW in team_player:
|
|
1113
|
+
defense_participation_weight = team_player.get(_SCALED_DPW, participation_weight)
|
|
1114
|
+
elif cn.defense_participation_weight:
|
|
1115
|
+
defense_participation_weight = team_player.get(
|
|
1116
|
+
cn.defense_participation_weight, participation_weight
|
|
1117
|
+
)
|
|
1118
|
+
else:
|
|
1119
|
+
defense_participation_weight = participation_weight
|
|
1120
|
+
|
|
1121
|
+
# Use scaled projected defense participation weight if available
|
|
1122
|
+
if _SCALED_PDPW in team_player:
|
|
1123
|
+
projected_defense_participation_weight = team_player.get(
|
|
1124
|
+
_SCALED_PDPW, defense_participation_weight
|
|
1125
|
+
)
|
|
1126
|
+
elif cn.projected_defense_participation_weight:
|
|
1127
|
+
projected_defense_participation_weight = team_player.get(
|
|
1128
|
+
cn.projected_defense_participation_weight, defense_participation_weight
|
|
1129
|
+
)
|
|
1130
|
+
else:
|
|
1131
|
+
projected_defense_participation_weight = defense_participation_weight
|
|
1132
|
+
|
|
1044
1133
|
perf_val = (
|
|
1045
1134
|
float(team_player[self.performance_column])
|
|
1046
1135
|
if (
|
|
@@ -1061,6 +1150,8 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
1061
1150
|
performance_value=perf_val,
|
|
1062
1151
|
projected_participation_weight=projected_participation_weight,
|
|
1063
1152
|
participation_weight=participation_weight,
|
|
1153
|
+
defense_participation_weight=defense_participation_weight,
|
|
1154
|
+
projected_defense_participation_weight=projected_defense_participation_weight,
|
|
1064
1155
|
team_players_playing_time=team_playing_time,
|
|
1065
1156
|
opponent_players_playing_time=opponent_playing_time,
|
|
1066
1157
|
)
|
|
@@ -1296,6 +1387,22 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
1296
1387
|
ppw = pw
|
|
1297
1388
|
proj_w.append(float(ppw))
|
|
1298
1389
|
|
|
1390
|
+
# Use scaled defense participation weight if available
|
|
1391
|
+
if _SCALED_DPW in tp:
|
|
1392
|
+
dpw = tp.get(_SCALED_DPW, pw)
|
|
1393
|
+
elif cn.defense_participation_weight:
|
|
1394
|
+
dpw = tp.get(cn.defense_participation_weight, pw)
|
|
1395
|
+
else:
|
|
1396
|
+
dpw = pw
|
|
1397
|
+
|
|
1398
|
+
# Use scaled projected defense participation weight if available
|
|
1399
|
+
if _SCALED_PDPW in tp:
|
|
1400
|
+
pdpw = tp.get(_SCALED_PDPW, dpw)
|
|
1401
|
+
elif cn.projected_defense_participation_weight:
|
|
1402
|
+
pdpw = tp.get(cn.projected_defense_participation_weight, dpw)
|
|
1403
|
+
else:
|
|
1404
|
+
pdpw = dpw
|
|
1405
|
+
|
|
1299
1406
|
team_playing_time = self._get_players_playing_time(
|
|
1300
1407
|
tp, cn.team_players_playing_time
|
|
1301
1408
|
)
|
|
@@ -1307,6 +1414,8 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
1307
1414
|
performance_value=get_perf_value(tp),
|
|
1308
1415
|
projected_participation_weight=ppw,
|
|
1309
1416
|
participation_weight=pw,
|
|
1417
|
+
defense_participation_weight=dpw,
|
|
1418
|
+
projected_defense_participation_weight=pdpw,
|
|
1310
1419
|
team_players_playing_time=team_playing_time,
|
|
1311
1420
|
opponent_players_playing_time=opponent_playing_time,
|
|
1312
1421
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spforge
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.31
|
|
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
|
|
@@ -2133,6 +2133,59 @@ def test_fit_transform_when_all_players_have_null_performance_then_no_rating_cha
|
|
|
2133
2133
|
)
|
|
2134
2134
|
|
|
2135
2135
|
|
|
2136
|
+
def test_null_individual_perf_still_updates_def_rating(base_cn):
|
|
2137
|
+
"""
|
|
2138
|
+
Regression test: Players with null individual performance should still get DEF updates.
|
|
2139
|
+
|
|
2140
|
+
Bug: Line 598 had `if perf_value is None or team1_def_perf is None: def_change = 0.0`
|
|
2141
|
+
This incorrectly skipped DEF updates when player had null individual performance.
|
|
2142
|
+
|
|
2143
|
+
Fix: Changed to `if team1_def_perf is None: def_change = 0.0`
|
|
2144
|
+
Defense is team-level, so null individual perf should NOT block DEF updates.
|
|
2145
|
+
|
|
2146
|
+
Test creates scenario where P1 has null perf but team defense is known (poor).
|
|
2147
|
+
Verifies P1's DEF rating decreases (proving defensive update logic ran).
|
|
2148
|
+
"""
|
|
2149
|
+
# Match 1: Balanced to establish baseline ratings
|
|
2150
|
+
# Match 2: P1 null perf, but T2 dominates offense (0.9) so T1 defense is poor (0.1)
|
|
2151
|
+
df = pl.DataFrame(
|
|
2152
|
+
{
|
|
2153
|
+
"pid": ["P1", "P2", "P3", "P4", "P1", "P2", "P3", "P4"],
|
|
2154
|
+
"tid": ["T1", "T1", "T2", "T2", "T1", "T1", "T2", "T2"],
|
|
2155
|
+
"mid": ["M1", "M1", "M1", "M1", "M2", "M2", "M2", "M2"],
|
|
2156
|
+
"dt": ["2024-01-01"] * 4 + ["2024-01-02"] * 4,
|
|
2157
|
+
"perf": [0.5, 0.5, 0.5, 0.5, None, 0.1, 0.9, 0.9],
|
|
2158
|
+
"pw": [1.0] * 8,
|
|
2159
|
+
}
|
|
2160
|
+
)
|
|
2161
|
+
|
|
2162
|
+
gen = PlayerRatingGenerator(
|
|
2163
|
+
performance_column="perf",
|
|
2164
|
+
column_names=base_cn,
|
|
2165
|
+
use_off_def_split=True,
|
|
2166
|
+
rating_change_multiplier_offense=50.0, # High multipliers to ensure visible changes
|
|
2167
|
+
rating_change_multiplier_defense=50.0,
|
|
2168
|
+
start_rating_value=1000.0,
|
|
2169
|
+
)
|
|
2170
|
+
gen.fit_transform(df)
|
|
2171
|
+
|
|
2172
|
+
# P1 had null perf in M2, so OFF rating should be unchanged from baseline
|
|
2173
|
+
p1_off = gen._player_off_ratings["P1"].rating_value
|
|
2174
|
+
assert p1_off == 1000.0, f"P1 OFF should be 1000 (null perf), got {p1_off}"
|
|
2175
|
+
|
|
2176
|
+
# P1's DEF rating MUST decrease because T1's defense was poor (0.1) in M2
|
|
2177
|
+
# Team defense = 1.0 - opponent offense = 1.0 - 0.9 = 0.1 (much worse than expected 0.5)
|
|
2178
|
+
p1_def = gen._player_def_ratings["P1"].rating_value
|
|
2179
|
+
assert p1_def < 1000.0, (
|
|
2180
|
+
f"P1 DEF should decrease (team defended poorly), but got {p1_def}. "
|
|
2181
|
+
f"Bug: defensive update was incorrectly skipped for null individual performance."
|
|
2182
|
+
)
|
|
2183
|
+
|
|
2184
|
+
# Sanity check: P2 had valid perf (0.1) so OFF should change too
|
|
2185
|
+
p2_off = gen._player_off_ratings["P2"].rating_value
|
|
2186
|
+
assert p2_off != 1000.0, f"P2 OFF should change (valid perf 0.1), got {p2_off}"
|
|
2187
|
+
|
|
2188
|
+
|
|
2136
2189
|
# --- team_players_playing_time Tests ---
|
|
2137
2190
|
|
|
2138
2191
|
|
|
@@ -2710,3 +2763,114 @@ def test_ignore_opponent_predictor_reference_rating_set_correctly(base_cn):
|
|
|
2710
2763
|
assert gen5._performance_predictor._reference_rating == 1200.0, (
|
|
2711
2764
|
f"Expected hardcoded start rating 1200.0 to take precedence, got {gen5._performance_predictor._reference_rating}"
|
|
2712
2765
|
)
|
|
2766
|
+
|
|
2767
|
+
|
|
2768
|
+
def test_separate_offense_defense_participation_weights(base_cn):
|
|
2769
|
+
"""Test that offense and defense use separate participation weights.
|
|
2770
|
+
|
|
2771
|
+
When participation_weight represents offensive activity (e.g., shots attempted),
|
|
2772
|
+
using it for both offense and defense updates creates bias. This test verifies
|
|
2773
|
+
that defense_participation_weight is used for defensive rating updates.
|
|
2774
|
+
"""
|
|
2775
|
+
from dataclasses import replace
|
|
2776
|
+
|
|
2777
|
+
cn = replace(
|
|
2778
|
+
base_cn,
|
|
2779
|
+
participation_weight="shots_attempted",
|
|
2780
|
+
defense_participation_weight="minutes",
|
|
2781
|
+
)
|
|
2782
|
+
|
|
2783
|
+
# Create a scenario where a high-volume shooter (many shots) faces a low-volume shooter
|
|
2784
|
+
# The high-volume shooter should have larger offensive updates but equal defensive updates
|
|
2785
|
+
df = pl.DataFrame(
|
|
2786
|
+
{
|
|
2787
|
+
"pid": ["P1", "P2", "P3", "P4"],
|
|
2788
|
+
"tid": ["T1", "T1", "T2", "T2"],
|
|
2789
|
+
"mid": ["M1", "M1", "M1", "M1"],
|
|
2790
|
+
"dt": ["2024-01-01"] * 4,
|
|
2791
|
+
"perf": [0.6, 0.4, 0.5, 0.5], # Varying performance values
|
|
2792
|
+
"shots_attempted": [10.0, 10.0, 10.0, 10.0], # Same offensive activity
|
|
2793
|
+
"minutes": [30.0, 30.0, 30.0, 30.0], # Same defensive activity
|
|
2794
|
+
}
|
|
2795
|
+
)
|
|
2796
|
+
|
|
2797
|
+
gen = PlayerRatingGenerator(
|
|
2798
|
+
performance_column="perf",
|
|
2799
|
+
column_names=cn,
|
|
2800
|
+
auto_scale_performance=True,
|
|
2801
|
+
rating_change_multiplier_offense=50,
|
|
2802
|
+
rating_change_multiplier_defense=50,
|
|
2803
|
+
)
|
|
2804
|
+
|
|
2805
|
+
result = gen.fit_transform(df)
|
|
2806
|
+
|
|
2807
|
+
# Verify that the defense_participation_weight column is present in the data
|
|
2808
|
+
assert "minutes" in df.columns
|
|
2809
|
+
|
|
2810
|
+
# All players performed equally (0.5) with equal participation weights,
|
|
2811
|
+
# so ratings should be symmetric
|
|
2812
|
+
assert "P1" in gen._player_off_ratings
|
|
2813
|
+
assert "P1" in gen._player_def_ratings
|
|
2814
|
+
|
|
2815
|
+
# Now test with different participation weights for offense vs defense
|
|
2816
|
+
df2 = pl.DataFrame(
|
|
2817
|
+
{
|
|
2818
|
+
"pid": ["P1", "P2", "P3", "P4"],
|
|
2819
|
+
"tid": ["T1", "T1", "T2", "T2"],
|
|
2820
|
+
"mid": ["M2", "M2", "M2", "M2"],
|
|
2821
|
+
"dt": ["2024-01-02"] * 4,
|
|
2822
|
+
"perf": [0.6, 0.4, 0.5, 0.5],
|
|
2823
|
+
"shots_attempted": [20.0, 5.0, 10.0, 10.0], # P1 shoots much more
|
|
2824
|
+
"minutes": [30.0, 30.0, 30.0, 30.0], # But all play same minutes
|
|
2825
|
+
}
|
|
2826
|
+
)
|
|
2827
|
+
|
|
2828
|
+
result2 = gen.fit_transform(df2)
|
|
2829
|
+
|
|
2830
|
+
# P1 should have larger offensive rating changes due to high shots_attempted
|
|
2831
|
+
# but equal defensive rating changes due to equal minutes played
|
|
2832
|
+
p1_off = gen._player_off_ratings["P1"]
|
|
2833
|
+
p2_off = gen._player_off_ratings["P2"]
|
|
2834
|
+
p1_def = gen._player_def_ratings["P1"]
|
|
2835
|
+
p2_def = gen._player_def_ratings["P2"]
|
|
2836
|
+
|
|
2837
|
+
# Both players have same games_played count for defense
|
|
2838
|
+
assert p1_def.games_played == p2_def.games_played
|
|
2839
|
+
|
|
2840
|
+
# Verify that ratings were updated
|
|
2841
|
+
assert p1_off.games_played > 0
|
|
2842
|
+
assert p2_off.games_played > 0
|
|
2843
|
+
|
|
2844
|
+
|
|
2845
|
+
@pytest.mark.parametrize("library", ["polars", "pandas"])
|
|
2846
|
+
def test_defense_participation_weight_backwards_compatibility(base_cn, library):
|
|
2847
|
+
"""Test that when defense_participation_weight is not set, it defaults to participation_weight."""
|
|
2848
|
+
import pandas as pd
|
|
2849
|
+
|
|
2850
|
+
df_data = {
|
|
2851
|
+
"pid": ["P1", "P2", "P3", "P4"],
|
|
2852
|
+
"tid": ["T1", "T1", "T2", "T2"],
|
|
2853
|
+
"mid": ["M1", "M1", "M1", "M1"],
|
|
2854
|
+
"dt": ["2024-01-01"] * 4,
|
|
2855
|
+
"perf": [0.6, 0.4, 0.5, 0.5],
|
|
2856
|
+
"pw": [1.0, 0.5, 0.8, 0.8],
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
if library == "polars":
|
|
2860
|
+
df = pl.DataFrame(df_data)
|
|
2861
|
+
else:
|
|
2862
|
+
df = pd.DataFrame(df_data)
|
|
2863
|
+
|
|
2864
|
+
# When defense_participation_weight is None, it should default to participation_weight
|
|
2865
|
+
gen = PlayerRatingGenerator(
|
|
2866
|
+
performance_column="perf",
|
|
2867
|
+
column_names=base_cn,
|
|
2868
|
+
auto_scale_performance=True,
|
|
2869
|
+
)
|
|
2870
|
+
|
|
2871
|
+
result = gen.fit_transform(df)
|
|
2872
|
+
|
|
2873
|
+
# Should work without errors
|
|
2874
|
+
assert result is not None
|
|
2875
|
+
assert len(gen._player_off_ratings) > 0
|
|
2876
|
+
assert len(gen._player_def_ratings) > 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spforge-0.8.29 → spforge-0.8.31}/spforge/distributions/_student_t_distribution_estimator.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spforge-0.8.29 → spforge-0.8.31}/spforge/performance_transformers/_performances_transformers.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spforge-0.8.29 → spforge-0.8.31}/tests/end_to_end/test_nba_player_ratings_hyperparameter_tuning.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spforge-0.8.29 → spforge-0.8.31}/tests/feature_generator/test_regressor_feature_generator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spforge-0.8.29 → spforge-0.8.31}/tests/performance_transformers/test_performance_manager.py
RENAMED
|
File without changes
|
{spforge-0.8.29 → spforge-0.8.31}/tests/performance_transformers/test_performances_transformers.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|