gr-analytics 0.3.0__tar.gz → 0.3.2__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.
- {gr_analytics-0.3.0 → gr_analytics-0.3.2}/PKG-INFO +26 -1
- {gr_analytics-0.3.0 → gr_analytics-0.3.2}/README.md +25 -0
- {gr_analytics-0.3.0 → gr_analytics-0.3.2}/gr_analytics/__init__.py +93 -0
- {gr_analytics-0.3.0 → gr_analytics-0.3.2}/gr_analytics/data/driver_data.csv +232 -133
- gr_analytics-0.3.2/gr_analytics/data/race_results.csv +89 -0
- {gr_analytics-0.3.0 → gr_analytics-0.3.2}/gr_analytics.egg-info/PKG-INFO +26 -1
- {gr_analytics-0.3.0 → gr_analytics-0.3.2}/gr_analytics.egg-info/SOURCES.txt +2 -0
- {gr_analytics-0.3.0 → gr_analytics-0.3.2}/pyproject.toml +1 -1
- gr_analytics-0.3.2/tests/test_eight_race_average.py +131 -0
- {gr_analytics-0.3.0 → gr_analytics-0.3.2}/tests/test_scoring.py +17 -9
- {gr_analytics-0.3.0 → gr_analytics-0.3.2}/gr_analytics.egg-info/dependency_links.txt +0 -0
- {gr_analytics-0.3.0 → gr_analytics-0.3.2}/gr_analytics.egg-info/requires.txt +0 -0
- {gr_analytics-0.3.0 → gr_analytics-0.3.2}/gr_analytics.egg-info/top_level.txt +0 -0
- {gr_analytics-0.3.0 → gr_analytics-0.3.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gr_analytics
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Scoring and salary calculation for GridRival fantasy F1
|
|
5
5
|
Author: nce8
|
|
6
6
|
License-Expression: MIT
|
|
@@ -135,6 +135,31 @@ Bundled driver data (`gr_analytics/data/driver_data.csv`) contains starting sala
|
|
|
135
135
|
- `round=0` — pre-season (before Australia 2026)
|
|
136
136
|
- `round=1` — post-Australia 2026
|
|
137
137
|
|
|
138
|
+
## Eight-Race Average
|
|
139
|
+
|
|
140
|
+
GridRival's "8 race average" can be computed instead of entered by hand.
|
|
141
|
+
GridRival seeds the season with 8 slots holding a hard-coded initial
|
|
142
|
+
average (the `round=0` values in driver_data); each race replaces one
|
|
143
|
+
slot with the driver's classified finishing position, and the displayed
|
|
144
|
+
value is the **ceiling** of the slot mean. Race finishing positions live
|
|
145
|
+
in `gr_analytics/data/race_results.csv`.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from gr_analytics import calculate_eight_race_averages, eight_race_average
|
|
149
|
+
|
|
150
|
+
# All drivers, after the latest round in race_results.csv
|
|
151
|
+
calculate_eight_race_averages()
|
|
152
|
+
|
|
153
|
+
# All drivers, after round 2
|
|
154
|
+
calculate_eight_race_averages(through_round=2)
|
|
155
|
+
|
|
156
|
+
# Single driver from scratch: seed 1, finished P6 then P16
|
|
157
|
+
eight_race_average(1, [6, 16])
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This reproduces GridRival's displayed values exactly for all rounds so
|
|
161
|
+
far (verified in `tests/test_eight_race_average.py`).
|
|
162
|
+
|
|
138
163
|
## Lineup Optimisation
|
|
139
164
|
|
|
140
165
|
`optimal_lineup` uses mixed-integer linear programming (via `scipy.optimize.milp`) to find the best 5-driver + 1-constructor lineup within a salary budget.
|
|
@@ -118,6 +118,31 @@ Bundled driver data (`gr_analytics/data/driver_data.csv`) contains starting sala
|
|
|
118
118
|
- `round=0` — pre-season (before Australia 2026)
|
|
119
119
|
- `round=1` — post-Australia 2026
|
|
120
120
|
|
|
121
|
+
## Eight-Race Average
|
|
122
|
+
|
|
123
|
+
GridRival's "8 race average" can be computed instead of entered by hand.
|
|
124
|
+
GridRival seeds the season with 8 slots holding a hard-coded initial
|
|
125
|
+
average (the `round=0` values in driver_data); each race replaces one
|
|
126
|
+
slot with the driver's classified finishing position, and the displayed
|
|
127
|
+
value is the **ceiling** of the slot mean. Race finishing positions live
|
|
128
|
+
in `gr_analytics/data/race_results.csv`.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from gr_analytics import calculate_eight_race_averages, eight_race_average
|
|
132
|
+
|
|
133
|
+
# All drivers, after the latest round in race_results.csv
|
|
134
|
+
calculate_eight_race_averages()
|
|
135
|
+
|
|
136
|
+
# All drivers, after round 2
|
|
137
|
+
calculate_eight_race_averages(through_round=2)
|
|
138
|
+
|
|
139
|
+
# Single driver from scratch: seed 1, finished P6 then P16
|
|
140
|
+
eight_race_average(1, [6, 16])
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
This reproduces GridRival's displayed values exactly for all rounds so
|
|
144
|
+
far (verified in `tests/test_eight_race_average.py`).
|
|
145
|
+
|
|
121
146
|
## Lineup Optimisation
|
|
122
147
|
|
|
123
148
|
`optimal_lineup` uses mixed-integer linear programming (via `scipy.optimize.milp`) to find the best 5-driver + 1-constructor lineup within a salary budget.
|
|
@@ -34,6 +34,16 @@ def driver_data() -> pd.DataFrame:
|
|
|
34
34
|
return pd.read_csv(_DATA_DIR / "driver_data.csv")
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def race_results() -> pd.DataFrame:
|
|
38
|
+
"""
|
|
39
|
+
Load and return race_results.csv as a DataFrame.
|
|
40
|
+
|
|
41
|
+
One row per driver per round with the official classified finishing
|
|
42
|
+
position (DNF/DNS drivers receive their classified position).
|
|
43
|
+
"""
|
|
44
|
+
return pd.read_csv(_DATA_DIR / "race_results.csv")
|
|
45
|
+
|
|
46
|
+
|
|
37
47
|
# ---------------------------------------------------------------------------
|
|
38
48
|
# Lookup tables — Drivers
|
|
39
49
|
# ---------------------------------------------------------------------------
|
|
@@ -330,6 +340,75 @@ def _score_constructors(
|
|
|
330
340
|
# ---------------------------------------------------------------------------
|
|
331
341
|
|
|
332
342
|
|
|
343
|
+
def eight_race_average(initial_average: float, finishing_positions: list) -> int:
|
|
344
|
+
"""
|
|
345
|
+
GridRival's eight-race average after a sequence of races.
|
|
346
|
+
|
|
347
|
+
GridRival seeds each driver's season with 8 "slots" holding a
|
|
348
|
+
hard-coded initial average. Each race replaces one slot with the
|
|
349
|
+
driver's official classified finishing position, so after 8 races the
|
|
350
|
+
initial average has fully rolled off and the value is a true rolling
|
|
351
|
+
average of the last 8 finishes. The displayed value is the ceiling of
|
|
352
|
+
the slot mean (verified against GridRival's displayed values for
|
|
353
|
+
rounds 1-4 of 2026).
|
|
354
|
+
|
|
355
|
+
Parameters
|
|
356
|
+
----------
|
|
357
|
+
initial_average : float
|
|
358
|
+
The hard-coded season-start average (round 0 of driver_data).
|
|
359
|
+
finishing_positions : list of int
|
|
360
|
+
Classified finishing positions in every race so far, oldest first.
|
|
361
|
+
|
|
362
|
+
Returns
|
|
363
|
+
-------
|
|
364
|
+
int
|
|
365
|
+
The eight-race average as GridRival displays it.
|
|
366
|
+
"""
|
|
367
|
+
slots = [initial_average] * 8 + [int(p) for p in finishing_positions]
|
|
368
|
+
return math.ceil(sum(slots[-8:]) / 8)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def calculate_eight_race_averages(through_round: int = None) -> pd.Series:
|
|
372
|
+
"""
|
|
373
|
+
Compute every driver's eight-race average after ``through_round``.
|
|
374
|
+
|
|
375
|
+
Uses the round-0 seeds in driver_data.csv and the finishing positions
|
|
376
|
+
in race_results.csv. Matches the convention of driver_data, where the
|
|
377
|
+
round-N row holds the state *after* race N (so the returned values are
|
|
378
|
+
what GridRival displays going into race N+1).
|
|
379
|
+
|
|
380
|
+
Parameters
|
|
381
|
+
----------
|
|
382
|
+
through_round : int, optional
|
|
383
|
+
Include races 1 through this round. Defaults to the latest round
|
|
384
|
+
in race_results. Pass 0 to get the season-start seeds.
|
|
385
|
+
|
|
386
|
+
Returns
|
|
387
|
+
-------
|
|
388
|
+
Series
|
|
389
|
+
Eight-race average indexed by driver_abbr.
|
|
390
|
+
"""
|
|
391
|
+
dd = driver_data()
|
|
392
|
+
seeds = (
|
|
393
|
+
dd[(dd["type"] == "driver") & (dd["round"] == 0)]
|
|
394
|
+
.set_index("driver_abbr")["eight_race_average"]
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
results = race_results()
|
|
398
|
+
if through_round is None:
|
|
399
|
+
through_round = results["round"].max()
|
|
400
|
+
results = results[results["round"] <= through_round].sort_values("round")
|
|
401
|
+
|
|
402
|
+
averages = {
|
|
403
|
+
abbr: eight_race_average(
|
|
404
|
+
seed,
|
|
405
|
+
results.loc[results["driver_abbr"] == abbr, "finishing_position"],
|
|
406
|
+
)
|
|
407
|
+
for abbr, seed in seeds.items()
|
|
408
|
+
}
|
|
409
|
+
return pd.Series(averages, name="eight_race_average").rename_axis("driver_abbr")
|
|
410
|
+
|
|
411
|
+
|
|
333
412
|
def _validate_scenario(scenario: pd.DataFrame) -> None:
|
|
334
413
|
"""Check that qualifying and race positions are sequential and unique."""
|
|
335
414
|
errors = []
|
|
@@ -722,4 +801,18 @@ def optimal_lineup(
|
|
|
722
801
|
best_total = total
|
|
723
802
|
best_full = full
|
|
724
803
|
|
|
804
|
+
# When balance == 0 the star bonus is zero so the loop picks
|
|
805
|
+
# arbitrarily. Override to star the highest-points eligible driver.
|
|
806
|
+
if balance == 0.0:
|
|
807
|
+
best_full["star"] = 0
|
|
808
|
+
eligible = best_full[best_full["type"] == "driver"]
|
|
809
|
+
if star_salary_cap is not None:
|
|
810
|
+
eligible = eligible[eligible["starting_salary"] <= star_salary_cap]
|
|
811
|
+
if not eligible.empty:
|
|
812
|
+
best_full.loc[eligible["points_earned"].idxmax(), "star"] = 1
|
|
813
|
+
|
|
814
|
+
# Double points_earned for the starred driver so the returned
|
|
815
|
+
# DataFrame reflects actual fantasy points with star doubling.
|
|
816
|
+
best_full.loc[best_full["star"] == 1, "points_earned"] *= 2
|
|
817
|
+
|
|
725
818
|
return best_full.drop(columns=["_locked"])
|
|
@@ -1,133 +1,232 @@
|
|
|
1
|
-
type,driver_abbr,driver_name,driver_team,round,starting_salary,eight_race_average,points_from_last_race
|
|
2
|
-
driver,VER,M. Verstappen,RBR,0,30,1,
|
|
3
|
-
driver,RUS,G. Russell,MER,0,28.7,2,
|
|
4
|
-
driver,NOR,L. Norris,MCL,0,27.4,3,
|
|
5
|
-
driver,PIA,O. Piastri,MCL,0,26.1,4,
|
|
6
|
-
driver,ANT,K. Antonelli,MER,0,24.8,5,
|
|
7
|
-
driver,LEC,C. Leclerc,FER,0,23.5,6,
|
|
8
|
-
driver,ALO,F. Alonso,AMR,0,22.2,7,
|
|
9
|
-
driver,HAM,L. Hamilton,FER,0,20.9,8,
|
|
10
|
-
driver,HAD,I. Hadjar,RBR,0,19.6,9,
|
|
11
|
-
driver,GAS,P. Gasly,ALP,0,18.3,10,
|
|
12
|
-
driver,STR,L. Stroll,AMR,0,17,11,
|
|
13
|
-
driver,SAI,C. Sainz Jr,WIL,0,15.7,12,
|
|
14
|
-
driver,LAW,L. Lawson,RBS,0,14.4,13,
|
|
15
|
-
driver,ALB,A. Albon,WIL,0,13.1,14,
|
|
16
|
-
driver,HUL,N. Hulkenberg,AUD,0,11.8,15,
|
|
17
|
-
driver,BOR,G. Bortoleto,AUD,0,10.5,16,
|
|
18
|
-
driver,BEA,O. Bearman,HAS,0,9.2,17,
|
|
19
|
-
driver,OCO,E. Ocon,HAS,0,7.9,18,
|
|
20
|
-
driver,BOT,V. Bottas,CAD,0,4.7,19,
|
|
21
|
-
driver,PER,S. Perez,CAD,0,4.7,19,
|
|
22
|
-
driver,COL,F. Colapinto,ALP,0,4.7,19,
|
|
23
|
-
driver,LIN,A. Lindblad,RBS,0,4.6,
|
|
24
|
-
team,MER,MER,,0,28.5,1,
|
|
25
|
-
team,MCL,MCL,,0,28.5,2,
|
|
26
|
-
team,RBR,RBR,,0,25,3,
|
|
27
|
-
team,FER,FER,,0,22.5,4,
|
|
28
|
-
team,AMR,AMR,,0,20,5,
|
|
29
|
-
team,WIL,WIL,,0,17.5,6,
|
|
30
|
-
team,RBS,RBS,,0,15,7,
|
|
31
|
-
team,ALP,ALP,,0,12.5,8,
|
|
32
|
-
team,AUD,AUD,,0,10,9,
|
|
33
|
-
team,CAD,CAD,,0,7.5,10,
|
|
34
|
-
team,HAS,HAS,,0,5,11,
|
|
35
|
-
driver,VER,M. Verstappen,RBR,1,28.2,2,
|
|
36
|
-
driver,RUS,G. Russell,MER,1,29.6,2,
|
|
37
|
-
driver,NOR,L. Norris,MCL,1,26.7,4,
|
|
38
|
-
driver,PIA,O. Piastri,MCL,1,24.1,7,
|
|
39
|
-
driver,ANT,K. Antonelli,MER,1,25.9,5,
|
|
40
|
-
driver,LEC,C. Leclerc,FER,1,24.5,6,
|
|
41
|
-
driver,ALO,F. Alonso,AMR,1,20.2,9,
|
|
42
|
-
driver,HAM,L. Hamilton,FER,1,22.1,8,
|
|
43
|
-
driver,HAD,I. Hadjar,RBR,1,17.6,11,
|
|
44
|
-
driver,GAS,P. Gasly,ALP,1,18.3,10,
|
|
45
|
-
driver,STR,L. Stroll,AMR,1,15,12,
|
|
46
|
-
driver,SAI,C. Sainz Jr,WIL,1,13.9,13,
|
|
47
|
-
driver,LAW,L. Lawson,RBS,1,14.5,13,
|
|
48
|
-
driver,ALB,A. Albon,WIL,1,13.9,14,
|
|
49
|
-
driver,HUL,N. Hulkenberg,AUD,1,9.8,16,
|
|
50
|
-
driver,BOR,G. Bortoleto,AUD,1,12.5,
|
|
51
|
-
driver,BEA,O. Bearman,HAS,1,11.2,16,
|
|
52
|
-
driver,OCO,E. Ocon,HAS,1,9.9,18,
|
|
53
|
-
driver,BOT,V. Bottas,CAD,1,4.5,19,
|
|
54
|
-
driver,PER,S. Perez,CAD,1,6.4,19,
|
|
55
|
-
driver,COL,F. Colapinto,ALP,1,6.7,19,
|
|
56
|
-
driver,LIN,A. Lindblad,RBS,1,6.6,18,
|
|
57
|
-
team,MER,MER,,1,28.8,,
|
|
58
|
-
team,MCL,MCL,,1,25.7,,
|
|
59
|
-
team,RBR,RBR,,1,22.4,,
|
|
60
|
-
team,FER,FER,,1,23.7,,
|
|
61
|
-
team,AMR,AMR,,1,17,,
|
|
62
|
-
team,WIL,WIL,,1,16.1,,
|
|
63
|
-
team,RBS,RBS,,1,17.4,,
|
|
64
|
-
team,ALP,ALP,,1,14.2,,
|
|
65
|
-
team,AUD,AUD,,1,9.8,,
|
|
66
|
-
team,CAD,CAD,,1,7.3,,
|
|
67
|
-
team,HAS,HAS,,1,8,,
|
|
68
|
-
driver,RUS,G. Russell,MER,2,29.5,2,171
|
|
69
|
-
driver,ANT,K. Antonelli,MER,2,27.5,5,174
|
|
70
|
-
driver,VER,M. Verstappen,RBR,2,26.2,4,132
|
|
71
|
-
driver,LEC,C. Leclerc,FER,2,24.8,6,164
|
|
72
|
-
driver,NOR,L. Norris,MCL,2,24.7,6,107
|
|
73
|
-
driver,HAM,L. Hamilton,FER,2,24.1,7,169
|
|
74
|
-
driver,PIA,O. Piastri,MCL,2,22.1,8,50
|
|
75
|
-
driver,GAS,P. Gasly,ALP,2,19.8,10,143
|
|
76
|
-
driver,ALO,F. Alonso,AMR,2,18.2,10,78
|
|
77
|
-
driver,HAD,I. Hadjar,RBR,2,18.1,11,118
|
|
78
|
-
driver,LAW,L. Lawson,RBS,2,16.5,13,142
|
|
79
|
-
driver,SAI,C. Sainz Jr,WIL,2,15.9,12,123
|
|
80
|
-
driver,BEA,O. Bearman,HAS,2,13.2,15,186
|
|
81
|
-
driver,STR,L. Stroll,AMR,2,13,13,75
|
|
82
|
-
driver,ALB,A. Albon,WIL,2,11.9,15,68
|
|
83
|
-
driver,HUL,N. Hulkenberg,AUD,2,11.8,16,81
|
|
84
|
-
driver,OCO,E. Ocon,HAS,2,10.7,17,124
|
|
85
|
-
driver,BOR,G. Bortoleto,AUD,2,10.5,16,90
|
|
86
|
-
driver,COL,F. Colapinto,ALP,2,8.7,18,130
|
|
87
|
-
driver,LIN,A. Lindblad,RBS,2,8.6,17,143
|
|
88
|
-
driver,PER,S. Perez,CAD,2,7.3,19,102
|
|
89
|
-
driver,BOT,V. Bottas,CAD,2,6.5,19,94
|
|
90
|
-
team,MER,MER,,2,29.1,,177
|
|
91
|
-
team,FER,FER,,2,24.6,,163
|
|
92
|
-
team,MCL,MCL,,2,22.7,,77
|
|
93
|
-
team,RBR,RBR,,2,21.7,,110
|
|
94
|
-
team,RBS,RBS,,2,17.3,,123
|
|
95
|
-
team,ALP,ALP,,2,16.8,,121
|
|
96
|
-
team,AMR,AMR,,2,15.7,,72
|
|
97
|
-
team,WIL,WIL,,2,13.8,,78
|
|
98
|
-
team,HAS,HAS,,2,11,,125
|
|
99
|
-
team,AUD,AUD,,2,9.7,,80
|
|
100
|
-
team,CAD,CAD,,2,9,,83
|
|
101
|
-
driver,RUS,G. Russell,MER,3,29.5,3,164
|
|
102
|
-
driver,ANT,K. Antonelli,MER,3,28.7,4,173
|
|
103
|
-
driver,LEC,C. Leclerc,FER,3,26.3,5,162
|
|
104
|
-
driver,NOR,L. Norris,MCL,3,24.7,6,118
|
|
105
|
-
driver,VER,M. Verstappen,RBR,3,24.6,5,133
|
|
106
|
-
driver,PIA,O. Piastri,MCL,3,24.1,8,91
|
|
107
|
-
driver,HAM,L. Hamilton,FER,3,23.4,7,158
|
|
108
|
-
driver,GAS,P. Gasly,ALP,3,21.7,10,143
|
|
109
|
-
driver,LAW,L. Lawson,RBS,3,18,12,140
|
|
110
|
-
driver,HAD,I. Hadjar,RBR,3,17.7,11,117
|
|
111
|
-
driver,ALO,F. Alonso,AMR,3,16.2,11,79
|
|
112
|
-
driver,SAI,C. Sainz Jr,WIL,3,14.9,13,115
|
|
113
|
-
driver,HUL,N. Hulkenberg,AUD,3,13.3,15,95
|
|
114
|
-
driver,OCO,E. Ocon,HAS,3,12.7,16,130
|
|
115
|
-
driver,BOR,G. Bortoleto,AUD,3,11.5,16,98
|
|
116
|
-
driver,BEA,O. Bearman,HAS,3,11.2,15,142
|
|
117
|
-
driver,STR,L. Stroll,AMR,3,11,14,69
|
|
118
|
-
driver,ALB,A. Albon,WIL,3,9.9,16,70
|
|
119
|
-
driver,LIN,A. Lindblad,RBS,3,9.7,17,131
|
|
120
|
-
driver,COL,F. Colapinto,ALP,3,9,17,117
|
|
121
|
-
driver,PER,S. Perez,CAD,3,7.5,18,97
|
|
122
|
-
driver,BOT,V. Bottas,CAD,3,6.2,19,87
|
|
123
|
-
team,MER,MER,,3,29.3,,175
|
|
124
|
-
team,FER,FER,,3,24.6,,161
|
|
125
|
-
team,MCL,MCL,,3,23.8,,106
|
|
126
|
-
team,RBR,RBR,,3,21.8,,116
|
|
127
|
-
team,ALP,ALP,,3,17.5,,120
|
|
128
|
-
team,RBS,RBS,,3,17.3,,120
|
|
129
|
-
team,AMR,AMR,,3,12.8,,69
|
|
130
|
-
team,WIL,WIL,,3,12.7,,80
|
|
131
|
-
team,HAS,HAS,,3,11.2,,114
|
|
132
|
-
team,AUD,AUD,,3,10.8,,92
|
|
133
|
-
team,CAD,CAD,,3,8.4,,80
|
|
1
|
+
type,driver_abbr,driver_name,driver_team,round,starting_salary,eight_race_average,points_from_last_race
|
|
2
|
+
driver,VER,M. Verstappen,RBR,0,30,1,
|
|
3
|
+
driver,RUS,G. Russell,MER,0,28.7,2,
|
|
4
|
+
driver,NOR,L. Norris,MCL,0,27.4,3,
|
|
5
|
+
driver,PIA,O. Piastri,MCL,0,26.1,4,
|
|
6
|
+
driver,ANT,K. Antonelli,MER,0,24.8,5,
|
|
7
|
+
driver,LEC,C. Leclerc,FER,0,23.5,6,
|
|
8
|
+
driver,ALO,F. Alonso,AMR,0,22.2,7,
|
|
9
|
+
driver,HAM,L. Hamilton,FER,0,20.9,8,
|
|
10
|
+
driver,HAD,I. Hadjar,RBR,0,19.6,9,
|
|
11
|
+
driver,GAS,P. Gasly,ALP,0,18.3,10,
|
|
12
|
+
driver,STR,L. Stroll,AMR,0,17,11,
|
|
13
|
+
driver,SAI,C. Sainz Jr,WIL,0,15.7,12,
|
|
14
|
+
driver,LAW,L. Lawson,RBS,0,14.4,13,
|
|
15
|
+
driver,ALB,A. Albon,WIL,0,13.1,14,
|
|
16
|
+
driver,HUL,N. Hulkenberg,AUD,0,11.8,15,
|
|
17
|
+
driver,BOR,G. Bortoleto,AUD,0,10.5,16,
|
|
18
|
+
driver,BEA,O. Bearman,HAS,0,9.2,17,
|
|
19
|
+
driver,OCO,E. Ocon,HAS,0,7.9,18,
|
|
20
|
+
driver,BOT,V. Bottas,CAD,0,4.7,19,
|
|
21
|
+
driver,PER,S. Perez,CAD,0,4.7,19,
|
|
22
|
+
driver,COL,F. Colapinto,ALP,0,4.7,19,
|
|
23
|
+
driver,LIN,A. Lindblad,RBS,0,4.6,19,
|
|
24
|
+
team,MER,MER,,0,28.5,1,
|
|
25
|
+
team,MCL,MCL,,0,28.5,2,
|
|
26
|
+
team,RBR,RBR,,0,25,3,
|
|
27
|
+
team,FER,FER,,0,22.5,4,
|
|
28
|
+
team,AMR,AMR,,0,20,5,
|
|
29
|
+
team,WIL,WIL,,0,17.5,6,
|
|
30
|
+
team,RBS,RBS,,0,15,7,
|
|
31
|
+
team,ALP,ALP,,0,12.5,8,
|
|
32
|
+
team,AUD,AUD,,0,10,9,
|
|
33
|
+
team,CAD,CAD,,0,7.5,10,
|
|
34
|
+
team,HAS,HAS,,0,5,11,
|
|
35
|
+
driver,VER,M. Verstappen,RBR,1,28.2,2,
|
|
36
|
+
driver,RUS,G. Russell,MER,1,29.6,2,
|
|
37
|
+
driver,NOR,L. Norris,MCL,1,26.7,4,
|
|
38
|
+
driver,PIA,O. Piastri,MCL,1,24.1,7,
|
|
39
|
+
driver,ANT,K. Antonelli,MER,1,25.9,5,
|
|
40
|
+
driver,LEC,C. Leclerc,FER,1,24.5,6,
|
|
41
|
+
driver,ALO,F. Alonso,AMR,1,20.2,9,
|
|
42
|
+
driver,HAM,L. Hamilton,FER,1,22.1,8,
|
|
43
|
+
driver,HAD,I. Hadjar,RBR,1,17.6,11,
|
|
44
|
+
driver,GAS,P. Gasly,ALP,1,18.3,10,
|
|
45
|
+
driver,STR,L. Stroll,AMR,1,15,12,
|
|
46
|
+
driver,SAI,C. Sainz Jr,WIL,1,13.9,13,
|
|
47
|
+
driver,LAW,L. Lawson,RBS,1,14.5,13,
|
|
48
|
+
driver,ALB,A. Albon,WIL,1,13.9,14,
|
|
49
|
+
driver,HUL,N. Hulkenberg,AUD,1,9.8,16,
|
|
50
|
+
driver,BOR,G. Bortoleto,AUD,1,12.5,16,
|
|
51
|
+
driver,BEA,O. Bearman,HAS,1,11.2,16,
|
|
52
|
+
driver,OCO,E. Ocon,HAS,1,9.9,18,
|
|
53
|
+
driver,BOT,V. Bottas,CAD,1,4.5,19,
|
|
54
|
+
driver,PER,S. Perez,CAD,1,6.4,19,
|
|
55
|
+
driver,COL,F. Colapinto,ALP,1,6.7,19,
|
|
56
|
+
driver,LIN,A. Lindblad,RBS,1,6.6,18,
|
|
57
|
+
team,MER,MER,,1,28.8,,
|
|
58
|
+
team,MCL,MCL,,1,25.7,,
|
|
59
|
+
team,RBR,RBR,,1,22.4,,
|
|
60
|
+
team,FER,FER,,1,23.7,,
|
|
61
|
+
team,AMR,AMR,,1,17,,
|
|
62
|
+
team,WIL,WIL,,1,16.1,,
|
|
63
|
+
team,RBS,RBS,,1,17.4,,
|
|
64
|
+
team,ALP,ALP,,1,14.2,,
|
|
65
|
+
team,AUD,AUD,,1,9.8,,
|
|
66
|
+
team,CAD,CAD,,1,7.3,,
|
|
67
|
+
team,HAS,HAS,,1,8,,
|
|
68
|
+
driver,RUS,G. Russell,MER,2,29.5,2,171
|
|
69
|
+
driver,ANT,K. Antonelli,MER,2,27.5,5,174
|
|
70
|
+
driver,VER,M. Verstappen,RBR,2,26.2,4,132
|
|
71
|
+
driver,LEC,C. Leclerc,FER,2,24.8,6,164
|
|
72
|
+
driver,NOR,L. Norris,MCL,2,24.7,6,107
|
|
73
|
+
driver,HAM,L. Hamilton,FER,2,24.1,7,169
|
|
74
|
+
driver,PIA,O. Piastri,MCL,2,22.1,8,50
|
|
75
|
+
driver,GAS,P. Gasly,ALP,2,19.8,10,143
|
|
76
|
+
driver,ALO,F. Alonso,AMR,2,18.2,10,78
|
|
77
|
+
driver,HAD,I. Hadjar,RBR,2,18.1,11,118
|
|
78
|
+
driver,LAW,L. Lawson,RBS,2,16.5,13,142
|
|
79
|
+
driver,SAI,C. Sainz Jr,WIL,2,15.9,12,123
|
|
80
|
+
driver,BEA,O. Bearman,HAS,2,13.2,15,186
|
|
81
|
+
driver,STR,L. Stroll,AMR,2,13,13,75
|
|
82
|
+
driver,ALB,A. Albon,WIL,2,11.9,15,68
|
|
83
|
+
driver,HUL,N. Hulkenberg,AUD,2,11.8,16,81
|
|
84
|
+
driver,OCO,E. Ocon,HAS,2,10.7,17,124
|
|
85
|
+
driver,BOR,G. Bortoleto,AUD,2,10.5,16,90
|
|
86
|
+
driver,COL,F. Colapinto,ALP,2,8.7,18,130
|
|
87
|
+
driver,LIN,A. Lindblad,RBS,2,8.6,17,143
|
|
88
|
+
driver,PER,S. Perez,CAD,2,7.3,19,102
|
|
89
|
+
driver,BOT,V. Bottas,CAD,2,6.5,19,94
|
|
90
|
+
team,MER,MER,,2,29.1,,177
|
|
91
|
+
team,FER,FER,,2,24.6,,163
|
|
92
|
+
team,MCL,MCL,,2,22.7,,77
|
|
93
|
+
team,RBR,RBR,,2,21.7,,110
|
|
94
|
+
team,RBS,RBS,,2,17.3,,123
|
|
95
|
+
team,ALP,ALP,,2,16.8,,121
|
|
96
|
+
team,AMR,AMR,,2,15.7,,72
|
|
97
|
+
team,WIL,WIL,,2,13.8,,78
|
|
98
|
+
team,HAS,HAS,,2,11,,125
|
|
99
|
+
team,AUD,AUD,,2,9.7,,80
|
|
100
|
+
team,CAD,CAD,,2,9,,83
|
|
101
|
+
driver,RUS,G. Russell,MER,3,29.5,3,164
|
|
102
|
+
driver,ANT,K. Antonelli,MER,3,28.7,4,173
|
|
103
|
+
driver,LEC,C. Leclerc,FER,3,26.3,5,162
|
|
104
|
+
driver,NOR,L. Norris,MCL,3,24.7,6,118
|
|
105
|
+
driver,VER,M. Verstappen,RBR,3,24.6,5,133
|
|
106
|
+
driver,PIA,O. Piastri,MCL,3,24.1,8,91
|
|
107
|
+
driver,HAM,L. Hamilton,FER,3,23.4,7,158
|
|
108
|
+
driver,GAS,P. Gasly,ALP,3,21.7,10,143
|
|
109
|
+
driver,LAW,L. Lawson,RBS,3,18,12,140
|
|
110
|
+
driver,HAD,I. Hadjar,RBR,3,17.7,11,117
|
|
111
|
+
driver,ALO,F. Alonso,AMR,3,16.2,11,79
|
|
112
|
+
driver,SAI,C. Sainz Jr,WIL,3,14.9,13,115
|
|
113
|
+
driver,HUL,N. Hulkenberg,AUD,3,13.3,15,95
|
|
114
|
+
driver,OCO,E. Ocon,HAS,3,12.7,16,130
|
|
115
|
+
driver,BOR,G. Bortoleto,AUD,3,11.5,16,98
|
|
116
|
+
driver,BEA,O. Bearman,HAS,3,11.2,15,142
|
|
117
|
+
driver,STR,L. Stroll,AMR,3,11,14,69
|
|
118
|
+
driver,ALB,A. Albon,WIL,3,9.9,16,70
|
|
119
|
+
driver,LIN,A. Lindblad,RBS,3,9.7,17,131
|
|
120
|
+
driver,COL,F. Colapinto,ALP,3,9,17,117
|
|
121
|
+
driver,PER,S. Perez,CAD,3,7.5,18,97
|
|
122
|
+
driver,BOT,V. Bottas,CAD,3,6.2,19,87
|
|
123
|
+
team,MER,MER,,3,29.3,,175
|
|
124
|
+
team,FER,FER,,3,24.6,,161
|
|
125
|
+
team,MCL,MCL,,3,23.8,,106
|
|
126
|
+
team,RBR,RBR,,3,21.8,,116
|
|
127
|
+
team,ALP,ALP,,3,17.5,,120
|
|
128
|
+
team,RBS,RBS,,3,17.3,,120
|
|
129
|
+
team,AMR,AMR,,3,12.8,,69
|
|
130
|
+
team,WIL,WIL,,3,12.7,,80
|
|
131
|
+
team,HAS,HAS,,3,11.2,,114
|
|
132
|
+
team,AUD,AUD,,3,10.8,,92
|
|
133
|
+
team,CAD,CAD,,3,8.4,,80
|
|
134
|
+
driver,ANT,K. Antonelli,MER,4,28.8,4,176
|
|
135
|
+
driver,RUS,G. Russell,MER,4,28.7,3,165
|
|
136
|
+
driver,NOR,L. Norris,MCL,4,26.7,6,136
|
|
137
|
+
driver,LEC,C. Leclerc,FER,4,25.9,6,161
|
|
138
|
+
driver,PIA,O. Piastri,MCL,4,25.7,8,114
|
|
139
|
+
driver,VER,M. Verstappen,RBR,4,25.3,5,144
|
|
140
|
+
driver,HAM,L. Hamilton,FER,4,23.3,7,157
|
|
141
|
+
driver,GAS,P. Gasly,ALP,4,19.7,11,130
|
|
142
|
+
driver,SAI,C. Sainz Jr,WIL,4,16.4,12,122
|
|
143
|
+
driver,LAW,L. Lawson,RBS,4,16,13,126
|
|
144
|
+
driver,HAD,I. Hadjar,RBR,4,15.7,13,100
|
|
145
|
+
driver,ALO,F. Alonso,AMR,4,15.1,12,85
|
|
146
|
+
driver,OCO,E. Ocon,HAS,4,13.2,15,127
|
|
147
|
+
driver,BEA,O. Bearman,HAS,4,12.9,15,140
|
|
148
|
+
driver,BOR,G. Bortoleto,AUD,4,12.7,15,105
|
|
149
|
+
driver,ALB,A. Albon,WIL,4,11.9,15,87
|
|
150
|
+
driver,HUL,N. Hulkenberg,AUD,4,11.3,16,91
|
|
151
|
+
driver,COL,F. Colapinto,ALP,4,11,16,135
|
|
152
|
+
driver,LIN,A. Lindblad,RBS,4,10.5,16,125
|
|
153
|
+
driver,STR,L. Stroll,AMR,4,10.4,15,74
|
|
154
|
+
driver,PER,S. Perez,CAD,4,8.1,18,98
|
|
155
|
+
driver,BOT,V. Bottas,CAD,4,6,19,85
|
|
156
|
+
team,MER,MER,,4,29.4,,174
|
|
157
|
+
team,MCL,MCL,,4,24.7,,120
|
|
158
|
+
team,FER,FER,,4,24.6,,158
|
|
159
|
+
team,RBR,RBR,,4,20,,111
|
|
160
|
+
team,ALP,ALP,,4,18,,118
|
|
161
|
+
team,RBS,RBS,,4,15.3,,113
|
|
162
|
+
team,WIL,WIL,,4,15,,90
|
|
163
|
+
team,HAS,HAS,,4,12.6,,113
|
|
164
|
+
team,AMR,AMR,,4,11.3,,74
|
|
165
|
+
team,AUD,AUD,,4,11,,92
|
|
166
|
+
team,CAD,CAD,,4,7.3,,80
|
|
167
|
+
driver,ANT,K. Antonelli,MER,5,30.1,,180
|
|
168
|
+
driver,RUS,G. Russell,MER,5,26.7,,156
|
|
169
|
+
driver,VER,M. Verstappen,RBR,5,26.2,,150
|
|
170
|
+
driver,LEC,C. Leclerc,FER,5,25.9,,163
|
|
171
|
+
driver,HAM,L. Hamilton,FER,5,25.3,,163
|
|
172
|
+
driver,NOR,L. Norris,MCL,5,24.7,,133
|
|
173
|
+
driver,PIA,O. Piastri,MCL,5,24.6,,121
|
|
174
|
+
driver,GAS,P. Gasly,ALP,5,19.3,,132
|
|
175
|
+
driver,SAI,C. Sainz Jr,WIL,5,18,,128
|
|
176
|
+
driver,LAW,L. Lawson,RBS,5,18,,135
|
|
177
|
+
driver,HAD,I. Hadjar,RBR,5,17.7,,113
|
|
178
|
+
driver,BEA,O. Bearman,HAS,5,14.5,,140
|
|
179
|
+
driver,ALO,F. Alonso,AMR,5,13.1,,80
|
|
180
|
+
driver,COL,F. Colapinto,ALP,5,13,,145
|
|
181
|
+
driver,HUL,N. Hulkenberg,AUD,5,12.5,,98
|
|
182
|
+
driver,BOR,G. Bortoleto,AUD,5,12.5,,107
|
|
183
|
+
driver,OCO,E. Ocon,HAS,5,12.4,,124
|
|
184
|
+
driver,STR,L. Stroll,AMR,5,9.9,,81
|
|
185
|
+
driver,ALB,A. Albon,WIL,5,9.9,,81
|
|
186
|
+
driver,LIN,A. Lindblad,RBS,5,8.5,,110
|
|
187
|
+
driver,PER,S. Perez,CAD,5,7.4,,96
|
|
188
|
+
driver,BOT,V. Bottas,CAD,5,6.2,,89
|
|
189
|
+
team,MER,MER,,5,28.3,,168
|
|
190
|
+
team,FER,FER,,5,25.9,,158
|
|
191
|
+
team,MCL,MCL,,5,23.5,,120
|
|
192
|
+
team,RBR,RBR,,5,21.8,,120
|
|
193
|
+
team,ALP,ALP,,5,19,,121
|
|
194
|
+
team,WIL,WIL,,5,14.2,,90
|
|
195
|
+
team,RBS,RBS,,5,13.8,,108
|
|
196
|
+
team,HAS,HAS,,5,13,,111
|
|
197
|
+
team,AUD,AUD,,5,12.5,,96
|
|
198
|
+
team,AMR,AMR,,5,9.5,,74
|
|
199
|
+
team,CAD,CAD,,5,7.2,,79
|
|
200
|
+
driver,ANT,K. Antonelli,MER,6,29.9,,179
|
|
201
|
+
driver,HAM,L. Hamilton,FER,6,27,,165
|
|
202
|
+
driver,PIA,O. Piastri,MCL,6,25.3,,130
|
|
203
|
+
driver,RUS,G. Russell,MER,6,24.7,,150
|
|
204
|
+
driver,VER,M. Verstappen,RBR,6,24.2,,139
|
|
205
|
+
driver,LEC,C. Leclerc,FER,6,23.9,,153
|
|
206
|
+
driver,NOR,L. Norris,MCL,6,22.7,,125
|
|
207
|
+
driver,LAW,L. Lawson,RBS,6,20,,140
|
|
208
|
+
driver,GAS,P. Gasly,ALP,6,19.7,,134
|
|
209
|
+
driver,HAD,I. Hadjar,RBR,6,19.7,,126
|
|
210
|
+
driver,SAI,C. Sainz Jr,WIL,6,16,,122
|
|
211
|
+
driver,ALO,F. Alonso,AMR,6,14.7,,90
|
|
212
|
+
driver,OCO,E. Ocon,HAS,6,14.4,,128
|
|
213
|
+
driver,BOR,G. Bortoleto,AUD,6,13.8,,110
|
|
214
|
+
driver,HUL,N. Hulkenberg,AUD,6,12.6,,99
|
|
215
|
+
driver,BEA,O. Bearman,HAS,6,12.5,,127
|
|
216
|
+
driver,COL,F. Colapinto,ALP,6,12.3,,137
|
|
217
|
+
driver,ALB,A. Albon,WIL,6,11.9,,94
|
|
218
|
+
driver,LIN,A. Lindblad,RBS,6,10.5,,121
|
|
219
|
+
driver,PER,S. Perez,CAD,6,8.4,,97
|
|
220
|
+
driver,STR,L. Stroll,AMR,6,8.4,,80
|
|
221
|
+
driver,BOT,V. Bottas,CAD,6,4.8,,83
|
|
222
|
+
team,MER,MER,,6,28.7,,165
|
|
223
|
+
team,FER,FER,,6,26.2,,155
|
|
224
|
+
team,MCL,MCL,,6,22.6,,121
|
|
225
|
+
team,RBR,RBR,,6,21.9,,122
|
|
226
|
+
team,ALP,ALP,,6,18.5,,121
|
|
227
|
+
team,RBS,RBS,,6,16.5,,113
|
|
228
|
+
team,WIL,WIL,,6,14.2,,94
|
|
229
|
+
team,AUD,AUD,,6,12.4,,98
|
|
230
|
+
team,HAS,HAS,,6,12.1,,108
|
|
231
|
+
team,AMR,AMR,,6,8.8,,76
|
|
232
|
+
team,CAD,CAD,,6,6.4,,79
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
round,driver_abbr,finishing_position
|
|
2
|
+
1,RUS,1
|
|
3
|
+
1,ANT,2
|
|
4
|
+
1,LEC,3
|
|
5
|
+
1,HAM,4
|
|
6
|
+
1,NOR,5
|
|
7
|
+
1,VER,6
|
|
8
|
+
1,BEA,7
|
|
9
|
+
1,LIN,8
|
|
10
|
+
1,BOR,9
|
|
11
|
+
1,GAS,10
|
|
12
|
+
1,OCO,11
|
|
13
|
+
1,ALB,12
|
|
14
|
+
1,LAW,13
|
|
15
|
+
1,COL,14
|
|
16
|
+
1,SAI,15
|
|
17
|
+
1,PER,16
|
|
18
|
+
1,STR,17
|
|
19
|
+
1,ALO,18
|
|
20
|
+
1,BOT,19
|
|
21
|
+
1,HAD,20
|
|
22
|
+
1,PIA,21
|
|
23
|
+
1,HUL,22
|
|
24
|
+
2,ANT,1
|
|
25
|
+
2,RUS,2
|
|
26
|
+
2,HAM,3
|
|
27
|
+
2,LEC,4
|
|
28
|
+
2,BEA,5
|
|
29
|
+
2,GAS,6
|
|
30
|
+
2,LAW,7
|
|
31
|
+
2,HAD,8
|
|
32
|
+
2,SAI,9
|
|
33
|
+
2,COL,10
|
|
34
|
+
2,HUL,11
|
|
35
|
+
2,LIN,12
|
|
36
|
+
2,BOT,13
|
|
37
|
+
2,OCO,14
|
|
38
|
+
2,PER,15
|
|
39
|
+
2,VER,16
|
|
40
|
+
2,ALO,17
|
|
41
|
+
2,STR,18
|
|
42
|
+
2,PIA,19
|
|
43
|
+
2,NOR,20
|
|
44
|
+
2,BOR,21
|
|
45
|
+
2,ALB,22
|
|
46
|
+
3,ANT,1
|
|
47
|
+
3,PIA,2
|
|
48
|
+
3,LEC,3
|
|
49
|
+
3,RUS,4
|
|
50
|
+
3,NOR,5
|
|
51
|
+
3,HAM,6
|
|
52
|
+
3,GAS,7
|
|
53
|
+
3,VER,8
|
|
54
|
+
3,LAW,9
|
|
55
|
+
3,OCO,10
|
|
56
|
+
3,HUL,11
|
|
57
|
+
3,HAD,12
|
|
58
|
+
3,BOR,13
|
|
59
|
+
3,LIN,14
|
|
60
|
+
3,SAI,15
|
|
61
|
+
3,COL,16
|
|
62
|
+
3,PER,17
|
|
63
|
+
3,ALO,18
|
|
64
|
+
3,BOT,19
|
|
65
|
+
3,ALB,20
|
|
66
|
+
3,STR,21
|
|
67
|
+
3,BEA,22
|
|
68
|
+
4,ANT,1
|
|
69
|
+
4,NOR,2
|
|
70
|
+
4,PIA,3
|
|
71
|
+
4,RUS,4
|
|
72
|
+
4,VER,5
|
|
73
|
+
4,HAM,6
|
|
74
|
+
4,COL,7
|
|
75
|
+
4,LEC,8
|
|
76
|
+
4,SAI,9
|
|
77
|
+
4,ALB,10
|
|
78
|
+
4,BEA,11
|
|
79
|
+
4,BOR,12
|
|
80
|
+
4,OCO,13
|
|
81
|
+
4,LIN,14
|
|
82
|
+
4,ALO,15
|
|
83
|
+
4,PER,16
|
|
84
|
+
4,STR,17
|
|
85
|
+
4,BOT,18
|
|
86
|
+
4,HUL,19
|
|
87
|
+
4,LAW,20
|
|
88
|
+
4,GAS,21
|
|
89
|
+
4,HAD,22
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gr_analytics
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Scoring and salary calculation for GridRival fantasy F1
|
|
5
5
|
Author: nce8
|
|
6
6
|
License-Expression: MIT
|
|
@@ -135,6 +135,31 @@ Bundled driver data (`gr_analytics/data/driver_data.csv`) contains starting sala
|
|
|
135
135
|
- `round=0` — pre-season (before Australia 2026)
|
|
136
136
|
- `round=1` — post-Australia 2026
|
|
137
137
|
|
|
138
|
+
## Eight-Race Average
|
|
139
|
+
|
|
140
|
+
GridRival's "8 race average" can be computed instead of entered by hand.
|
|
141
|
+
GridRival seeds the season with 8 slots holding a hard-coded initial
|
|
142
|
+
average (the `round=0` values in driver_data); each race replaces one
|
|
143
|
+
slot with the driver's classified finishing position, and the displayed
|
|
144
|
+
value is the **ceiling** of the slot mean. Race finishing positions live
|
|
145
|
+
in `gr_analytics/data/race_results.csv`.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from gr_analytics import calculate_eight_race_averages, eight_race_average
|
|
149
|
+
|
|
150
|
+
# All drivers, after the latest round in race_results.csv
|
|
151
|
+
calculate_eight_race_averages()
|
|
152
|
+
|
|
153
|
+
# All drivers, after round 2
|
|
154
|
+
calculate_eight_race_averages(through_round=2)
|
|
155
|
+
|
|
156
|
+
# Single driver from scratch: seed 1, finished P6 then P16
|
|
157
|
+
eight_race_average(1, [6, 16])
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This reproduces GridRival's displayed values exactly for all rounds so
|
|
161
|
+
far (verified in `tests/test_eight_race_average.py`).
|
|
162
|
+
|
|
138
163
|
## Lineup Optimisation
|
|
139
164
|
|
|
140
165
|
`optimal_lineup` uses mixed-integer linear programming (via `scipy.optimize.milp`) to find the best 5-driver + 1-constructor lineup within a salary budget.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for GridRival eight-race average calculation.
|
|
3
|
+
|
|
4
|
+
Run with: pytest tests/test_eight_race_average.py -v
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from gr_analytics import (
|
|
13
|
+
calculate_eight_race_averages,
|
|
14
|
+
driver_data,
|
|
15
|
+
eight_race_average,
|
|
16
|
+
race_results,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
_TESTS_DIR = Path(__file__).parent
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Pure function tests (hand-calculated)
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestEightRaceAverage:
|
|
28
|
+
def test_no_races_returns_seed(self):
|
|
29
|
+
assert eight_race_average(5, []) == 5
|
|
30
|
+
|
|
31
|
+
def test_one_race_hand_calculated(self):
|
|
32
|
+
# VER, Australia 2026: seed 1, finished P6
|
|
33
|
+
# (7*1 + 6) / 8 = 1.625 -> ceil -> 2
|
|
34
|
+
assert eight_race_average(1, [6]) == 2
|
|
35
|
+
|
|
36
|
+
def test_uses_ceiling_not_rounding(self):
|
|
37
|
+
# NOR, Australia 2026: seed 3, finished P5
|
|
38
|
+
# (7*3 + 5) / 8 = 3.25 -> ceil -> 4 (rounding would give 3)
|
|
39
|
+
assert eight_race_average(3, [5]) == 4
|
|
40
|
+
|
|
41
|
+
def test_exact_integer_unchanged(self):
|
|
42
|
+
# GAS, Australia 2026: seed 10, finished P10
|
|
43
|
+
# (7*10 + 10) / 8 = 10.0 exactly
|
|
44
|
+
assert eight_race_average(10, [10]) == 10
|
|
45
|
+
|
|
46
|
+
def test_two_races_hand_calculated(self):
|
|
47
|
+
# LIN, 2026: seed 19, finished P8 then P12
|
|
48
|
+
# (6*19 + 8 + 12) / 8 = 16.75 -> ceil -> 17
|
|
49
|
+
assert eight_race_average(19, [8, 12]) == 17
|
|
50
|
+
|
|
51
|
+
def test_seed_fully_rolls_off_after_eight_races(self):
|
|
52
|
+
# After 8 races the seed should not matter at all
|
|
53
|
+
positions = [1, 2, 3, 4, 5, 6, 7, 8] # mean 4.5 -> ceil 5
|
|
54
|
+
assert eight_race_average(22, positions) == 5
|
|
55
|
+
assert eight_race_average(1, positions) == 5
|
|
56
|
+
|
|
57
|
+
def test_only_last_eight_races_count(self):
|
|
58
|
+
# First race (P22) falls out of the window on race 9
|
|
59
|
+
positions = [22] + [4] * 8
|
|
60
|
+
assert eight_race_average(10, positions) == 4
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Agreement with GridRival's displayed values (driver_data.csv)
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _rounds_with_recorded_averages():
|
|
69
|
+
"""Rounds whose driver rows have GridRival eight_race_average values.
|
|
70
|
+
|
|
71
|
+
Later rounds may be entered with only salary/points (eight_race_average
|
|
72
|
+
left blank until the corresponding race_results are added), so we only
|
|
73
|
+
check rounds where GridRival's own values exist to compare against.
|
|
74
|
+
"""
|
|
75
|
+
dd = driver_data()
|
|
76
|
+
drivers = dd[dd["type"] == "driver"]
|
|
77
|
+
have_values = drivers.groupby("round")["eight_race_average"].apply(
|
|
78
|
+
lambda s: s.notna().all()
|
|
79
|
+
)
|
|
80
|
+
return sorted(have_values[have_values].index)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class TestAgreementWithGridRival:
|
|
84
|
+
@pytest.mark.parametrize("rnd", _rounds_with_recorded_averages())
|
|
85
|
+
def test_matches_driver_data_sheet(self, rnd):
|
|
86
|
+
"""Computed averages must equal GridRival's values for every round."""
|
|
87
|
+
dd = driver_data()
|
|
88
|
+
sheet = (
|
|
89
|
+
dd[(dd["type"] == "driver") & (dd["round"] == rnd)]
|
|
90
|
+
.set_index("driver_abbr")["eight_race_average"]
|
|
91
|
+
.sort_index()
|
|
92
|
+
)
|
|
93
|
+
computed = calculate_eight_race_averages(through_round=rnd).sort_index()
|
|
94
|
+
|
|
95
|
+
mismatches = sheet[sheet != computed]
|
|
96
|
+
assert mismatches.empty, (
|
|
97
|
+
f"Round {rnd} mismatches (sheet vs computed):\n"
|
|
98
|
+
f"{pd.DataFrame({'sheet': mismatches, 'computed': computed[mismatches.index]})}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def test_default_round_is_latest(self):
|
|
102
|
+
latest = race_results()["round"].max()
|
|
103
|
+
pd.testing.assert_series_equal(
|
|
104
|
+
calculate_eight_race_averages(),
|
|
105
|
+
calculate_eight_race_averages(through_round=latest),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# race_results.csv data integrity
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class TestRaceResultsData:
|
|
115
|
+
def test_positions_complete_each_round(self):
|
|
116
|
+
rr = race_results()
|
|
117
|
+
for rnd, group in rr.groupby("round"):
|
|
118
|
+
positions = sorted(group["finishing_position"])
|
|
119
|
+
assert positions == list(range(1, len(group) + 1)), (
|
|
120
|
+
f"Round {rnd} positions are not a complete 1..n sequence"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def test_round_one_matches_australia_fixture(self):
|
|
124
|
+
"""Race 1 positions must agree with the Australia scenario fixture."""
|
|
125
|
+
australia = pd.read_csv(_TESTS_DIR / "test_australia.csv").set_index(
|
|
126
|
+
"driver_abbr"
|
|
127
|
+
)
|
|
128
|
+
rr = race_results()
|
|
129
|
+
round_one = rr[rr["round"] == 1].set_index("driver_abbr")
|
|
130
|
+
for abbr, expected in australia["finishing_position"].items():
|
|
131
|
+
assert round_one.at[abbr, "finishing_position"] == expected
|
|
@@ -647,17 +647,25 @@ class TestOptimizeForBalance:
|
|
|
647
647
|
assert bal_abbrs == sal_abbrs
|
|
648
648
|
|
|
649
649
|
def test_balance_star_assigned_when_balance_positive(self, balance_pool):
|
|
650
|
-
"""When optimize_for is a positive float, a star driver should be assigned
|
|
650
|
+
"""When optimize_for is a positive float, a star driver should be assigned
|
|
651
|
+
and their points_earned should be doubled."""
|
|
651
652
|
result = optimal_lineup(balance_pool, optimize_for=0.5)
|
|
652
653
|
assert (result["star"] == 1).sum() == 1
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
654
|
+
star_row = result[result["star"] == 1].iloc[0]
|
|
655
|
+
orig_pts = balance_pool.loc[
|
|
656
|
+
balance_pool["driver_abbr"] == star_row["driver_abbr"], "points_earned"
|
|
657
|
+
].iloc[0]
|
|
658
|
+
assert star_row["points_earned"] == orig_pts * 2
|
|
659
|
+
|
|
660
|
+
def test_balance_0_star_assigned_to_highest_pts(self, balance_pool):
|
|
661
|
+
"""When balance=0, the highest-points eligible driver is still starred
|
|
662
|
+
and their points_earned is doubled."""
|
|
663
|
+
result = optimal_lineup(balance_pool, optimize_for=0.0)
|
|
664
|
+
assert (result["star"] == 1).sum() == 1
|
|
665
|
+
star_row = result[result["star"] == 1].iloc[0]
|
|
666
|
+
# D1 has the highest points (200) in the pool
|
|
667
|
+
assert star_row["driver_abbr"] == "D1"
|
|
668
|
+
assert star_row["points_earned"] == 400.0
|
|
661
669
|
|
|
662
670
|
def test_balance_out_of_range_raises(self, balance_pool):
|
|
663
671
|
"""optimize_for outside [0, 1] should raise ValueError."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|