gr-analytics 0.3.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gr_analytics
3
- Version: 0.3.1
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 = []
@@ -1,166 +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,20,
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,6,
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
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.1
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.
@@ -7,4 +7,6 @@ gr_analytics.egg-info/dependency_links.txt
7
7
  gr_analytics.egg-info/requires.txt
8
8
  gr_analytics.egg-info/top_level.txt
9
9
  gr_analytics/data/driver_data.csv
10
+ gr_analytics/data/race_results.csv
11
+ tests/test_eight_race_average.py
10
12
  tests/test_scoring.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gr_analytics"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  description = "Scoring and salary calculation for GridRival fantasy F1"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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
File without changes