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.
- {spforge-0.8.20/spforge.egg-info → spforge-0.8.25}/PKG-INFO +1 -1
- {spforge-0.8.20 → spforge-0.8.25}/pyproject.toml +1 -1
- {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/_base.py +2 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/_base.py +6 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/_player_rating.py +120 -43
- {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/_team_rating.py +23 -20
- {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/player_performance_predictor.py +1 -1
- {spforge-0.8.20 → spforge-0.8.25/spforge.egg-info}/PKG-INFO +1 -1
- {spforge-0.8.20 → spforge-0.8.25}/tests/feature_generator/test_rolling_window.py +36 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/ratings/test_player_rating_generator.py +429 -118
- {spforge-0.8.20 → spforge-0.8.25}/tests/ratings/test_team_rating_generator.py +153 -11
- {spforge-0.8.20 → spforge-0.8.25}/LICENSE +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/MANIFEST.in +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/README.md +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/examples/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/examples/game_level_example.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/examples/lol/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/examples/lol/data/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/examples/lol/data/subsample_lol_data.parquet +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/examples/lol/data/utils.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/examples/lol/pipeline_transformer_example.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/examples/nba/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/examples/nba/cross_validation_example.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/examples/nba/data/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/examples/nba/data/game_player_subsample.parquet +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/examples/nba/data/utils.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/examples/nba/feature_engineering_example.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/examples/nba/game_winner_example.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/examples/nba/predictor_transformers_example.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/setup.cfg +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/autopipeline.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/base_feature_generator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/cross_validator/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/cross_validator/_base.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/cross_validator/cross_validator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/data_structures.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/distributions/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/distributions/_negative_binomial_estimator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/distributions/_normal_distribution_predictor.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/distributions/_student_t_distribution_estimator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/estimator/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/estimator/_conditional_estimator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/estimator/_frequency_bucketing_classifier.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/estimator/_granularity_estimator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/estimator/_group_by_estimator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/estimator/_ordinal_classifier.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/estimator/_sklearn_enhancer_estimator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/_lag.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/_net_over_predicted.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/_regressor_feature_generator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/_rolling_against_opponent.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/_rolling_mean_binary.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/_rolling_mean_days.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/_rolling_window.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/feature_generator/_utils.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/features_generator_pipeline.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/hyperparameter_tuning/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/hyperparameter_tuning/_default_search_spaces.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/hyperparameter_tuning/_tuner.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/performance_transformers/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/performance_transformers/_performance_manager.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/performance_transformers/_performances_transformers.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/enums.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/league_identifier.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/league_start_rating_optimizer.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/start_rating_generator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/team_performance_predictor.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/team_start_rating_generator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/ratings/utils.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/scorer/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/scorer/_score.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/transformers/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/transformers/_base.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/transformers/_net_over_predicted.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/transformers/_operator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/transformers/_other_transformer.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/transformers/_predictor.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/transformers/_simple_transformer.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/transformers/_team_ratio_predictor.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge/utils.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge.egg-info/SOURCES.txt +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge.egg-info/dependency_links.txt +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge.egg-info/requires.txt +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/spforge.egg-info/top_level.txt +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/cross_validator/test_cross_validator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/distributions/test_distribution.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/end_to_end/test_estimator_hyperparameter_tuning.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/end_to_end/test_league_start_rating_optimizer.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/end_to_end/test_lol_player_kills.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/end_to_end/test_nba_player_points.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/end_to_end/test_nba_player_ratings_hyperparameter_tuning.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/end_to_end/test_nba_prediction_consistency.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/estimator/test_sklearn_estimator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/feature_generator/test_lag.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/feature_generator/test_regressor_feature_generator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/feature_generator/test_rolling_against_opponent.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/feature_generator/test_rolling_mean_binary.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/feature_generator/test_rolling_mean_days.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/hyperparameter_tuning/test_estimator_tuner.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/hyperparameter_tuning/test_rating_tuner.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/performance_transformers/test_performance_manager.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/performance_transformers/test_performances_transformers.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/ratings/test_player_rating_no_mutation.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/ratings/test_ratings_property.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/ratings/test_utils_scaled_weights.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/scorer/test_score.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/scorer/test_score_aggregation_granularity.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/test_autopipeline.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/test_autopipeline_context.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/test_feature_generator_pipeline.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/transformers/test_estimator_transformer_context.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/transformers/test_net_over_predicted.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/transformers/test_other_transformer.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/transformers/test_predictor_transformer.py +0 -0
- {spforge-0.8.20 → spforge-0.8.25}/tests/transformers/test_simple_transformer.py +0 -0
- {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.
|
|
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.
|
|
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,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 =
|
|
438
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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(
|
|
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
|
|
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
|
|
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=
|
|
1317
|
+
id=team2, players=t2_pre, rating_value=t2_def_rating
|
|
1241
1318
|
),
|
|
1242
1319
|
team_rating=PreMatchTeamRating(
|
|
1243
|
-
id=team1, players=
|
|
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=
|
|
1334
|
+
id=team2, players=t2_pre, rating_value=t2_off_rating
|
|
1258
1335
|
),
|
|
1259
1336
|
team_rating=PreMatchTeamRating(
|
|
1260
|
-
id=team1, players=
|
|
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=
|
|
1362
|
+
id=team1, players=t1_pre, rating_value=t1_def_rating
|
|
1286
1363
|
),
|
|
1287
1364
|
team_rating=PreMatchTeamRating(
|
|
1288
|
-
id=team2, players=
|
|
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=
|
|
1379
|
+
id=team1, players=t1_pre, rating_value=t1_off_rating
|
|
1303
1380
|
),
|
|
1304
1381
|
team_rating=PreMatchTeamRating(
|
|
1305
|
-
id=team2, players=
|
|
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
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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.
|
|
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.
|
|
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)
|