gr-analytics 0.3.2__tar.gz → 0.4.0__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.2
3
+ Version: 0.4.0
4
4
  Summary: Scoring and salary calculation for GridRival fantasy F1
5
5
  Author: nce8
6
6
  License-Expression: MIT
@@ -14,6 +14,8 @@ Description-Content-Type: text/markdown
14
14
  Requires-Dist: pandas
15
15
  Requires-Dist: scipy
16
16
  Requires-Dist: numpy
17
+ Provides-Extra: fastf1
18
+ Requires-Dist: fastf1; extra == "fastf1"
17
19
 
18
20
  # gr_analytics
19
21
 
@@ -39,7 +41,8 @@ scenario = pd.read_csv("my_race.csv")
39
41
  # Score the event (defaults to the latest round in driver_data)
40
42
  result = score_event(scenario)
41
43
 
42
- # Or score a specific round
44
+ # Or score a specific race: pass the round one less than the race
45
+ # (round=1 is the post-race-1 state, so this scores race 2). See "Driver Data".
43
46
  result = score_event(scenario, round=1)
44
47
 
45
48
  # Score your specific team selection
@@ -135,19 +138,31 @@ Bundled driver data (`gr_analytics/data/driver_data.csv`) contains starting sala
135
138
  - `round=0` — pre-season (before Australia 2026)
136
139
  - `round=1` — post-Australia 2026
137
140
 
141
+ Each `round=N` row holds the state **after** race N: the post-race salaries,
142
+ the finishing position from race N, and the 8-race average including race N.
143
+
144
+ **Round semantics for `score_event`:** the `round` argument is that
145
+ post-race state, so `score_event(scenario, round=N)` scores the race that
146
+ *follows* it. Pass the round **one less** than the race you're scoring:
147
+ `round=0` scores race 1 (off the pre-season seeds), `round=1` scores race 2,
148
+ and so on. Improvement points therefore compare each finish to the 8-race
149
+ average *going into* that race (`calculate_eight_race_averages(through_round=round)`).
150
+
138
151
  ## Eight-Race Average
139
152
 
140
153
  GridRival's "8 race average" can be computed instead of entered by hand.
141
154
  GridRival seeds the season with 8 slots holding a hard-coded initial
142
155
  average (the `round=0` values in driver_data); each race replaces one
143
156
  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`.
157
+ value is the **ceiling** of the slot mean. Each driver's finishing
158
+ position per race is recorded in the `finishing_position` column of
159
+ `driver_data.csv` (the `round=N` row holds the finish from race N), so
160
+ the average is derived entirely from `driver_data`.
146
161
 
147
162
  ```python
148
163
  from gr_analytics import calculate_eight_race_averages, eight_race_average
149
164
 
150
- # All drivers, after the latest round in race_results.csv
165
+ # All drivers, after the latest round with recorded finishing positions
151
166
  calculate_eight_race_averages()
152
167
 
153
168
  # All drivers, after round 2
@@ -160,6 +175,12 @@ eight_race_average(1, [6, 16])
160
175
  This reproduces GridRival's displayed values exactly for all rounds so
161
176
  far (verified in `tests/test_eight_race_average.py`).
162
177
 
178
+ `score_event` uses this computed average for improvement points (the value
179
+ *going into* the race, i.e. `calculate_eight_race_averages(through_round=round)`)
180
+ rather than the stored `eight_race_average` column. That column is kept only as
181
+ GridRival's transcribed ground-truth/test oracle and may be blank for recent
182
+ rounds; scoring no longer depends on it.
183
+
163
184
  ## Lineup Optimisation
164
185
 
165
186
  `optimal_lineup` uses mixed-integer linear programming (via `scipy.optimize.milp`) to find the best 5-driver + 1-constructor lineup within a salary budget.
@@ -22,7 +22,8 @@ scenario = pd.read_csv("my_race.csv")
22
22
  # Score the event (defaults to the latest round in driver_data)
23
23
  result = score_event(scenario)
24
24
 
25
- # Or score a specific round
25
+ # Or score a specific race: pass the round one less than the race
26
+ # (round=1 is the post-race-1 state, so this scores race 2). See "Driver Data".
26
27
  result = score_event(scenario, round=1)
27
28
 
28
29
  # Score your specific team selection
@@ -118,19 +119,31 @@ Bundled driver data (`gr_analytics/data/driver_data.csv`) contains starting sala
118
119
  - `round=0` — pre-season (before Australia 2026)
119
120
  - `round=1` — post-Australia 2026
120
121
 
122
+ Each `round=N` row holds the state **after** race N: the post-race salaries,
123
+ the finishing position from race N, and the 8-race average including race N.
124
+
125
+ **Round semantics for `score_event`:** the `round` argument is that
126
+ post-race state, so `score_event(scenario, round=N)` scores the race that
127
+ *follows* it. Pass the round **one less** than the race you're scoring:
128
+ `round=0` scores race 1 (off the pre-season seeds), `round=1` scores race 2,
129
+ and so on. Improvement points therefore compare each finish to the 8-race
130
+ average *going into* that race (`calculate_eight_race_averages(through_round=round)`).
131
+
121
132
  ## Eight-Race Average
122
133
 
123
134
  GridRival's "8 race average" can be computed instead of entered by hand.
124
135
  GridRival seeds the season with 8 slots holding a hard-coded initial
125
136
  average (the `round=0` values in driver_data); each race replaces one
126
137
  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`.
138
+ value is the **ceiling** of the slot mean. Each driver's finishing
139
+ position per race is recorded in the `finishing_position` column of
140
+ `driver_data.csv` (the `round=N` row holds the finish from race N), so
141
+ the average is derived entirely from `driver_data`.
129
142
 
130
143
  ```python
131
144
  from gr_analytics import calculate_eight_race_averages, eight_race_average
132
145
 
133
- # All drivers, after the latest round in race_results.csv
146
+ # All drivers, after the latest round with recorded finishing positions
134
147
  calculate_eight_race_averages()
135
148
 
136
149
  # All drivers, after round 2
@@ -143,6 +156,12 @@ eight_race_average(1, [6, 16])
143
156
  This reproduces GridRival's displayed values exactly for all rounds so
144
157
  far (verified in `tests/test_eight_race_average.py`).
145
158
 
159
+ `score_event` uses this computed average for improvement points (the value
160
+ *going into* the race, i.e. `calculate_eight_race_averages(through_round=round)`)
161
+ rather than the stored `eight_race_average` column. That column is kept only as
162
+ GridRival's transcribed ground-truth/test oracle and may be blank for recent
163
+ rounds; scoring no longer depends on it.
164
+
146
165
  ## Lineup Optimisation
147
166
 
148
167
  `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,16 +34,6 @@ 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
-
47
37
  # ---------------------------------------------------------------------------
48
38
  # Lookup tables — Drivers
49
39
  # ---------------------------------------------------------------------------
@@ -372,16 +362,19 @@ def calculate_eight_race_averages(through_round: int = None) -> pd.Series:
372
362
  """
373
363
  Compute every driver's eight-race average after ``through_round``.
374
364
 
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).
365
+ Reads everything from driver_data.csv: the round-0 ``eight_race_average``
366
+ seeds and the per-race ``finishing_position`` recorded on each driver's
367
+ round row (the round-N row holds the finishing position from race N,
368
+ parallel to ``points_from_last_race``). Matches the convention of
369
+ driver_data, where the round-N row holds the state *after* race N (so
370
+ the returned values are what GridRival displays going into race N+1).
379
371
 
380
372
  Parameters
381
373
  ----------
382
374
  through_round : int, optional
383
375
  Include races 1 through this round. Defaults to the latest round
384
- in race_results. Pass 0 to get the season-start seeds.
376
+ whose finishing positions are recorded in driver_data. Pass 0 to
377
+ get the season-start seeds.
385
378
 
386
379
  Returns
387
380
  -------
@@ -389,20 +382,20 @@ def calculate_eight_race_averages(through_round: int = None) -> pd.Series:
389
382
  Eight-race average indexed by driver_abbr.
390
383
  """
391
384
  dd = driver_data()
392
- seeds = (
393
- dd[(dd["type"] == "driver") & (dd["round"] == 0)]
394
- .set_index("driver_abbr")["eight_race_average"]
395
- )
385
+ drivers = dd[dd["type"] == "driver"]
386
+ seeds = drivers[drivers["round"] == 0].set_index("driver_abbr")[
387
+ "eight_race_average"
388
+ ]
396
389
 
397
- results = race_results()
390
+ finishes = drivers[(drivers["round"] >= 1) & drivers["finishing_position"].notna()]
398
391
  if through_round is None:
399
- through_round = results["round"].max()
400
- results = results[results["round"] <= through_round].sort_values("round")
392
+ through_round = int(finishes["round"].max())
393
+ finishes = finishes[finishes["round"] <= through_round].sort_values("round")
401
394
 
402
395
  averages = {
403
396
  abbr: eight_race_average(
404
397
  seed,
405
- results.loc[results["driver_abbr"] == abbr, "finishing_position"],
398
+ finishes.loc[finishes["driver_abbr"] == abbr, "finishing_position"],
406
399
  )
407
400
  for abbr, seed in seeds.items()
408
401
  }
@@ -531,6 +524,11 @@ def score_event(scenario: pd.DataFrame, round: int = None) -> pd.DataFrame:
531
524
  round = dd["round"].max()
532
525
  dd = dd[dd["round"] == round].copy()
533
526
 
527
+ # `finishing_position` stored in driver_data is the historical race result
528
+ # (used only for the eight-race average); the race being scored is supplied
529
+ # by `scenario`, so drop the stored column to avoid a merge collision.
530
+ dd = dd.drop(columns=["finishing_position"], errors="ignore")
531
+
534
532
  drivers_dd = dd[dd["type"] == "driver"].copy()
535
533
  teams_dd = dd[dd["type"] == "team"].copy()
536
534
 
@@ -562,6 +560,15 @@ def score_event(scenario: pd.DataFrame, round: int = None) -> pd.DataFrame:
562
560
  how="left",
563
561
  ).rename(columns={"race_position": "finishing_position"})
564
562
 
563
+ # Improvement points compare each driver's finish to GridRival's eight-race
564
+ # average *going into* this race — i.e. the average as of the end of `round`,
565
+ # the state the scenario is scored against (round=0 scores race 1 off the
566
+ # season-start seeds). Compute it rather than trust the stored
567
+ # eight_race_average column, which is left blank for rounds whose GridRival
568
+ # value hasn't been transcribed.
569
+ avgs = calculate_eight_race_averages(through_round=round)
570
+ drivers_merged["eight_race_average"] = drivers_merged["driver_abbr"].map(avgs)
571
+
565
572
  df = pd.concat([drivers_merged, teams_dd], ignore_index=True)
566
573
 
567
574
  drivers = df[df["type"] == "driver"].copy()
@@ -0,0 +1,265 @@
1
+ type,driver_abbr,driver_name,driver_team,round,starting_salary,eight_race_average,points_from_last_race,finishing_position
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,,6
36
+ driver,RUS,G. Russell,MER,1,29.6,2,,1
37
+ driver,NOR,L. Norris,MCL,1,26.7,4,,5
38
+ driver,PIA,O. Piastri,MCL,1,24.1,7,,21
39
+ driver,ANT,K. Antonelli,MER,1,25.9,5,,2
40
+ driver,LEC,C. Leclerc,FER,1,24.5,6,,3
41
+ driver,ALO,F. Alonso,AMR,1,20.2,9,,18
42
+ driver,HAM,L. Hamilton,FER,1,22.1,8,,4
43
+ driver,HAD,I. Hadjar,RBR,1,17.6,11,,20
44
+ driver,GAS,P. Gasly,ALP,1,18.3,10,,10
45
+ driver,STR,L. Stroll,AMR,1,15,12,,17
46
+ driver,SAI,C. Sainz Jr,WIL,1,13.9,13,,15
47
+ driver,LAW,L. Lawson,RBS,1,14.5,13,,13
48
+ driver,ALB,A. Albon,WIL,1,13.9,14,,12
49
+ driver,HUL,N. Hulkenberg,AUD,1,9.8,16,,22
50
+ driver,BOR,G. Bortoleto,AUD,1,12.5,16,,9
51
+ driver,BEA,O. Bearman,HAS,1,11.2,16,,7
52
+ driver,OCO,E. Ocon,HAS,1,9.9,18,,11
53
+ driver,BOT,V. Bottas,CAD,1,4.5,19,,19
54
+ driver,PER,S. Perez,CAD,1,6.4,19,,16
55
+ driver,COL,F. Colapinto,ALP,1,6.7,19,,14
56
+ driver,LIN,A. Lindblad,RBS,1,6.6,18,,8
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,2
69
+ driver,ANT,K. Antonelli,MER,2,27.5,5,174,1
70
+ driver,VER,M. Verstappen,RBR,2,26.2,4,132,16
71
+ driver,LEC,C. Leclerc,FER,2,24.8,6,164,4
72
+ driver,NOR,L. Norris,MCL,2,24.7,6,107,20
73
+ driver,HAM,L. Hamilton,FER,2,24.1,7,169,3
74
+ driver,PIA,O. Piastri,MCL,2,22.1,8,50,19
75
+ driver,GAS,P. Gasly,ALP,2,19.8,10,143,6
76
+ driver,ALO,F. Alonso,AMR,2,18.2,10,78,17
77
+ driver,HAD,I. Hadjar,RBR,2,18.1,11,118,8
78
+ driver,LAW,L. Lawson,RBS,2,16.5,13,142,7
79
+ driver,SAI,C. Sainz Jr,WIL,2,15.9,12,123,9
80
+ driver,BEA,O. Bearman,HAS,2,13.2,15,186,5
81
+ driver,STR,L. Stroll,AMR,2,13,13,75,18
82
+ driver,ALB,A. Albon,WIL,2,11.9,15,68,22
83
+ driver,HUL,N. Hulkenberg,AUD,2,11.8,16,81,11
84
+ driver,OCO,E. Ocon,HAS,2,10.7,17,124,14
85
+ driver,BOR,G. Bortoleto,AUD,2,10.5,16,90,21
86
+ driver,COL,F. Colapinto,ALP,2,8.7,18,130,10
87
+ driver,LIN,A. Lindblad,RBS,2,8.6,17,143,12
88
+ driver,PER,S. Perez,CAD,2,7.3,19,102,15
89
+ driver,BOT,V. Bottas,CAD,2,6.5,19,94,13
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,4
102
+ driver,ANT,K. Antonelli,MER,3,28.7,4,173,1
103
+ driver,LEC,C. Leclerc,FER,3,26.3,5,162,3
104
+ driver,NOR,L. Norris,MCL,3,24.7,6,118,5
105
+ driver,VER,M. Verstappen,RBR,3,24.6,5,133,8
106
+ driver,PIA,O. Piastri,MCL,3,24.1,8,91,2
107
+ driver,HAM,L. Hamilton,FER,3,23.4,7,158,6
108
+ driver,GAS,P. Gasly,ALP,3,21.7,10,143,7
109
+ driver,LAW,L. Lawson,RBS,3,18,12,140,9
110
+ driver,HAD,I. Hadjar,RBR,3,17.7,11,117,12
111
+ driver,ALO,F. Alonso,AMR,3,16.2,11,79,18
112
+ driver,SAI,C. Sainz Jr,WIL,3,14.9,13,115,15
113
+ driver,HUL,N. Hulkenberg,AUD,3,13.3,15,95,11
114
+ driver,OCO,E. Ocon,HAS,3,12.7,16,130,10
115
+ driver,BOR,G. Bortoleto,AUD,3,11.5,16,98,13
116
+ driver,BEA,O. Bearman,HAS,3,11.2,15,142,22
117
+ driver,STR,L. Stroll,AMR,3,11,14,69,21
118
+ driver,ALB,A. Albon,WIL,3,9.9,16,70,20
119
+ driver,LIN,A. Lindblad,RBS,3,9.7,17,131,14
120
+ driver,COL,F. Colapinto,ALP,3,9,17,117,16
121
+ driver,PER,S. Perez,CAD,3,7.5,18,97,17
122
+ driver,BOT,V. Bottas,CAD,3,6.2,19,87,19
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,1
135
+ driver,RUS,G. Russell,MER,4,28.7,3,165,4
136
+ driver,NOR,L. Norris,MCL,4,26.7,6,136,2
137
+ driver,LEC,C. Leclerc,FER,4,25.9,6,161,8
138
+ driver,PIA,O. Piastri,MCL,4,25.7,8,114,3
139
+ driver,VER,M. Verstappen,RBR,4,25.3,5,144,5
140
+ driver,HAM,L. Hamilton,FER,4,23.3,7,157,6
141
+ driver,GAS,P. Gasly,ALP,4,19.7,11,130,21
142
+ driver,SAI,C. Sainz Jr,WIL,4,16.4,12,122,9
143
+ driver,LAW,L. Lawson,RBS,4,16,13,126,20
144
+ driver,HAD,I. Hadjar,RBR,4,15.7,13,100,22
145
+ driver,ALO,F. Alonso,AMR,4,15.1,12,85,15
146
+ driver,OCO,E. Ocon,HAS,4,13.2,15,127,13
147
+ driver,BEA,O. Bearman,HAS,4,12.9,15,140,11
148
+ driver,BOR,G. Bortoleto,AUD,4,12.7,15,105,12
149
+ driver,ALB,A. Albon,WIL,4,11.9,15,87,10
150
+ driver,HUL,N. Hulkenberg,AUD,4,11.3,16,91,19
151
+ driver,COL,F. Colapinto,ALP,4,11,16,135,7
152
+ driver,LIN,A. Lindblad,RBS,4,10.5,16,125,14
153
+ driver,STR,L. Stroll,AMR,4,10.4,15,74,17
154
+ driver,PER,S. Perez,CAD,4,8.1,18,98,16
155
+ driver,BOT,V. Bottas,CAD,4,6,19,85,18
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,1
168
+ driver,RUS,G. Russell,MER,5,26.7,,156,19
169
+ driver,VER,M. Verstappen,RBR,5,26.2,,150,3
170
+ driver,LEC,C. Leclerc,FER,5,25.9,,163,4
171
+ driver,HAM,L. Hamilton,FER,5,25.3,,163,2
172
+ driver,NOR,L. Norris,MCL,5,24.7,,133,18
173
+ driver,PIA,O. Piastri,MCL,5,24.6,,121,11
174
+ driver,GAS,P. Gasly,ALP,5,19.3,,132,8
175
+ driver,SAI,C. Sainz Jr,WIL,5,18,,128,9
176
+ driver,LAW,L. Lawson,RBS,5,18,,135,7
177
+ driver,HAD,I. Hadjar,RBR,5,17.7,,113,5
178
+ driver,BEA,O. Bearman,HAS,5,14.5,,140,10
179
+ driver,ALO,F. Alonso,AMR,5,13.1,,80,20
180
+ driver,COL,F. Colapinto,ALP,5,13,,145,6
181
+ driver,HUL,N. Hulkenberg,AUD,5,12.5,,98,12
182
+ driver,BOR,G. Bortoleto,AUD,5,12.5,,107,13
183
+ driver,OCO,E. Ocon,HAS,5,12.4,,124,14
184
+ driver,STR,L. Stroll,AMR,5,9.9,,81,15
185
+ driver,ALB,A. Albon,WIL,5,9.9,,81,21
186
+ driver,LIN,A. Lindblad,RBS,5,8.5,,110,22
187
+ driver,PER,S. Perez,CAD,5,7.4,,96,17
188
+ driver,BOT,V. Bottas,CAD,5,6.2,,89,16
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,1
201
+ driver,HAM,L. Hamilton,FER,6,27,,165,2
202
+ driver,PIA,O. Piastri,MCL,6,25.3,,130,5
203
+ driver,RUS,G. Russell,MER,6,24.7,,150,12
204
+ driver,VER,M. Verstappen,RBR,6,24.2,,139,22
205
+ driver,LEC,C. Leclerc,FER,6,23.9,,153,17
206
+ driver,NOR,L. Norris,MCL,6,22.7,,125,19
207
+ driver,LAW,L. Lawson,RBS,6,20,,140,6
208
+ driver,GAS,P. Gasly,ALP,6,19.7,,134,3
209
+ driver,HAD,I. Hadjar,RBR,6,19.7,,126,4
210
+ driver,SAI,C. Sainz Jr,WIL,6,16,,122,16
211
+ driver,ALO,F. Alonso,AMR,6,14.7,,90,10
212
+ driver,OCO,E. Ocon,HAS,6,14.4,,128,9
213
+ driver,BOR,G. Bortoleto,AUD,6,13.8,,110,11
214
+ driver,HUL,N. Hulkenberg,AUD,6,12.6,,99,13
215
+ driver,BEA,O. Bearman,HAS,6,12.5,,127,20
216
+ driver,COL,F. Colapinto,ALP,6,12.3,,137,14
217
+ driver,ALB,A. Albon,WIL,6,11.9,,94,8
218
+ driver,LIN,A. Lindblad,RBS,6,10.5,,121,7
219
+ driver,PER,S. Perez,CAD,6,8.4,,97,15
220
+ driver,STR,L. Stroll,AMR,6,8.4,,80,18
221
+ driver,BOT,V. Bottas,CAD,6,4.8,,83,21
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,
233
+ driver,HAM,L. Hamilton,FER,7,28.7,,168,1
234
+ driver,ANT,K. Antonelli,MER,7,27.9,,169,16
235
+ driver,RUS,G. Russell,MER,7,26.6,,154,2
236
+ driver,PIA,O. Piastri,MCL,7,25.8,,132,5
237
+ driver,VER,M. Verstappen,RBR,7,25.4,,142,4
238
+ driver,NOR,L. Norris,MCL,7,24.7,,132,3
239
+ driver,LEC,C. Leclerc,FER,7,21.9,,146,15
240
+ driver,HAD,I. Hadjar,RBR,7,21.2,,129,6
241
+ driver,GAS,P. Gasly,ALP,7,20.8,,135,7
242
+ driver,LAW,L. Lawson,RBS,7,20.3,,139,8
243
+ driver,SAI,C. Sainz Jr,WIL,7,16.1,,121,12
244
+ driver,BOR,G. Bortoleto,AUD,7,14.8,,112,11
245
+ driver,OCO,E. Ocon,HAS,7,14.1,,125,13
246
+ driver,COL,F. Colapinto,ALP,7,14.1,,135,10
247
+ driver,ALO,F. Alonso,AMR,7,12.7,,88,19
248
+ driver,LIN,A. Lindblad,RBS,7,12.5,,123,9
249
+ driver,BEA,O. Bearman,HAS,7,11.5,,121,17
250
+ driver,HUL,N. Hulkenberg,AUD,7,11.2,,96,20
251
+ driver,ALB,A. Albon,WIL,7,10.3,,91,18
252
+ driver,PER,S. Perez,CAD,7,9.2,,99,14
253
+ driver,STR,L. Stroll,AMR,7,6.4,,75,22
254
+ driver,BOT,V. Bottas,CAD,7,4.1,,78,21
255
+ team,MER,MER,,7,27.8,,162,
256
+ team,FER,FER,,7,25.2,,153,
257
+ team,MCL,MCL,,7,24.4,,126,
258
+ team,RBR,RBR,,7,23.2,,126,
259
+ team,ALP,ALP,,7,18.2,,122,
260
+ team,RBS,RBS,,7,17.2,,116,
261
+ team,WIL,WIL,,7,13,,94,
262
+ team,AUD,AUD,,7,12.9,,99,
263
+ team,HAS,HAS,,7,12.1,,106,
264
+ team,AMR,AMR,,7,7.6,,74,
265
+ team,CAD,CAD,,7,6.4,,78,
@@ -0,0 +1,119 @@
1
+ """
2
+ Fetch official F1 classified finishing positions via FastF1.
3
+
4
+ These feed the ``finishing_position`` column of ``driver_data.csv`` (the
5
+ round-N row holds each driver's finishing position in race N), which
6
+ ``calculate_eight_race_averages()`` rolls into GridRival's eight-race average.
7
+
8
+ FastF1 reads the official F1 timing data, so ``Position`` is the *classified*
9
+ finishing position — DNF/DNS drivers keep the position they are classified in,
10
+ matching GridRival's convention. Driver codes are FastF1's three-letter
11
+ ``Abbreviation`` values, which are the FIA codes used as ``driver_abbr`` in
12
+ driver_data.csv (VER, RUS, ...).
13
+
14
+ ``fastf1`` is an optional dependency (``pip install fastf1``) and is imported
15
+ lazily, so importing :mod:`gr_analytics` never requires it. Fetching a session
16
+ needs network access; results are cached so repeat calls are offline-fast.
17
+
18
+ Examples
19
+ --------
20
+ >>> from gr_analytics.get_positions import race_finishing_positions
21
+ >>> race_finishing_positions(2026, 1) # round 1 (season opener)
22
+ driver_abbr
23
+ RUS 1
24
+ ANT 2
25
+ ...
26
+ Name: finishing_position, dtype: int64
27
+
28
+ >>> # Long-format rows for new rounds, shaped like driver_data's per-race data
29
+ >>> from gr_analytics.get_positions import finishing_positions_frame
30
+ >>> finishing_positions_frame(2026, [5, 6])
31
+ """
32
+
33
+ from pathlib import Path
34
+
35
+ import pandas as pd
36
+
37
+ # Cache outside the repo so fetched timing data isn't committed or shipped.
38
+ _DEFAULT_CACHE = Path.home() / ".cache" / "fastf1"
39
+
40
+
41
+ def _enable_cache(cache_dir=None) -> None:
42
+ """Point FastF1 at a cache directory, creating it if needed."""
43
+ import fastf1
44
+
45
+ cache_dir = Path(cache_dir) if cache_dir is not None else _DEFAULT_CACHE
46
+ cache_dir.mkdir(parents=True, exist_ok=True)
47
+ fastf1.Cache.enable_cache(str(cache_dir))
48
+
49
+
50
+ def race_finishing_positions(year: int, rnd: int, cache_dir=None) -> pd.Series:
51
+ """
52
+ Classified finishing positions for a single race.
53
+
54
+ Parameters
55
+ ----------
56
+ year : int
57
+ Season, e.g. ``2026``.
58
+ rnd : int
59
+ Championship round number (1 = season opener).
60
+ cache_dir : path-like, optional
61
+ FastF1 cache location. Defaults to ``~/.cache/fastf1``.
62
+
63
+ Returns
64
+ -------
65
+ pandas.Series
66
+ Finishing position (int) indexed by three-letter ``driver_abbr``,
67
+ sorted by position. Drivers without a classified position (e.g. DNS
68
+ with no time) are dropped. Series name is ``finishing_position`` to
69
+ match driver_data.csv.
70
+ """
71
+ import fastf1
72
+
73
+ _enable_cache(cache_dir)
74
+
75
+ session = fastf1.get_session(year, rnd, "R")
76
+ # Results are always loaded; skip the heavy laps/telemetry/weather data.
77
+ session.load(laps=False, telemetry=False, weather=False, messages=False)
78
+
79
+ positions = (
80
+ session.results.set_index("Abbreviation")["Position"]
81
+ .dropna()
82
+ .astype(int)
83
+ .sort_values()
84
+ )
85
+ positions.index.name = "driver_abbr"
86
+ positions.name = "finishing_position"
87
+ return positions
88
+
89
+
90
+ def finishing_positions_frame(year: int, rounds, cache_dir=None) -> pd.DataFrame:
91
+ """
92
+ Long-format finishing positions for one or more rounds.
93
+
94
+ Parameters
95
+ ----------
96
+ year : int
97
+ Season, e.g. ``2026``.
98
+ rounds : int or iterable of int
99
+ Round number(s) to fetch.
100
+ cache_dir : path-like, optional
101
+ FastF1 cache location. Defaults to ``~/.cache/fastf1``.
102
+
103
+ Returns
104
+ -------
105
+ pandas.DataFrame
106
+ Columns ``round``, ``driver_abbr``, ``finishing_position`` — the shape
107
+ of the per-race data recorded in driver_data.csv, handy for filling in
108
+ new rounds.
109
+ """
110
+ if isinstance(rounds, int):
111
+ rounds = [rounds]
112
+
113
+ frames = []
114
+ for rnd in rounds:
115
+ frame = race_finishing_positions(year, rnd, cache_dir=cache_dir).reset_index()
116
+ frame.insert(0, "round", rnd)
117
+ frames.append(frame)
118
+
119
+ return pd.concat(frames, ignore_index=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gr_analytics
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: Scoring and salary calculation for GridRival fantasy F1
5
5
  Author: nce8
6
6
  License-Expression: MIT
@@ -14,6 +14,8 @@ Description-Content-Type: text/markdown
14
14
  Requires-Dist: pandas
15
15
  Requires-Dist: scipy
16
16
  Requires-Dist: numpy
17
+ Provides-Extra: fastf1
18
+ Requires-Dist: fastf1; extra == "fastf1"
17
19
 
18
20
  # gr_analytics
19
21
 
@@ -39,7 +41,8 @@ scenario = pd.read_csv("my_race.csv")
39
41
  # Score the event (defaults to the latest round in driver_data)
40
42
  result = score_event(scenario)
41
43
 
42
- # Or score a specific round
44
+ # Or score a specific race: pass the round one less than the race
45
+ # (round=1 is the post-race-1 state, so this scores race 2). See "Driver Data".
43
46
  result = score_event(scenario, round=1)
44
47
 
45
48
  # Score your specific team selection
@@ -135,19 +138,31 @@ Bundled driver data (`gr_analytics/data/driver_data.csv`) contains starting sala
135
138
  - `round=0` — pre-season (before Australia 2026)
136
139
  - `round=1` — post-Australia 2026
137
140
 
141
+ Each `round=N` row holds the state **after** race N: the post-race salaries,
142
+ the finishing position from race N, and the 8-race average including race N.
143
+
144
+ **Round semantics for `score_event`:** the `round` argument is that
145
+ post-race state, so `score_event(scenario, round=N)` scores the race that
146
+ *follows* it. Pass the round **one less** than the race you're scoring:
147
+ `round=0` scores race 1 (off the pre-season seeds), `round=1` scores race 2,
148
+ and so on. Improvement points therefore compare each finish to the 8-race
149
+ average *going into* that race (`calculate_eight_race_averages(through_round=round)`).
150
+
138
151
  ## Eight-Race Average
139
152
 
140
153
  GridRival's "8 race average" can be computed instead of entered by hand.
141
154
  GridRival seeds the season with 8 slots holding a hard-coded initial
142
155
  average (the `round=0` values in driver_data); each race replaces one
143
156
  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`.
157
+ value is the **ceiling** of the slot mean. Each driver's finishing
158
+ position per race is recorded in the `finishing_position` column of
159
+ `driver_data.csv` (the `round=N` row holds the finish from race N), so
160
+ the average is derived entirely from `driver_data`.
146
161
 
147
162
  ```python
148
163
  from gr_analytics import calculate_eight_race_averages, eight_race_average
149
164
 
150
- # All drivers, after the latest round in race_results.csv
165
+ # All drivers, after the latest round with recorded finishing positions
151
166
  calculate_eight_race_averages()
152
167
 
153
168
  # All drivers, after round 2
@@ -160,6 +175,12 @@ eight_race_average(1, [6, 16])
160
175
  This reproduces GridRival's displayed values exactly for all rounds so
161
176
  far (verified in `tests/test_eight_race_average.py`).
162
177
 
178
+ `score_event` uses this computed average for improvement points (the value
179
+ *going into* the race, i.e. `calculate_eight_race_averages(through_round=round)`)
180
+ rather than the stored `eight_race_average` column. That column is kept only as
181
+ GridRival's transcribed ground-truth/test oracle and may be blank for recent
182
+ rounds; scoring no longer depends on it.
183
+
163
184
  ## Lineup Optimisation
164
185
 
165
186
  `optimal_lineup` uses mixed-integer linear programming (via `scipy.optimize.milp`) to find the best 5-driver + 1-constructor lineup within a salary budget.
@@ -1,12 +1,12 @@
1
1
  README.md
2
2
  pyproject.toml
3
3
  gr_analytics/__init__.py
4
+ gr_analytics/get_positions.py
4
5
  gr_analytics.egg-info/PKG-INFO
5
6
  gr_analytics.egg-info/SOURCES.txt
6
7
  gr_analytics.egg-info/dependency_links.txt
7
8
  gr_analytics.egg-info/requires.txt
8
9
  gr_analytics.egg-info/top_level.txt
9
10
  gr_analytics/data/driver_data.csv
10
- gr_analytics/data/race_results.csv
11
11
  tests/test_eight_race_average.py
12
12
  tests/test_scoring.py
@@ -1,3 +1,6 @@
1
1
  pandas
2
2
  scipy
3
3
  numpy
4
+
5
+ [fastf1]
6
+ fastf1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gr_analytics"
7
- version = "0.3.2"
7
+ version = "0.4.0"
8
8
  description = "Scoring and salary calculation for GridRival fantasy F1"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -18,11 +18,16 @@ classifiers = [
18
18
  ]
19
19
  dependencies = ["pandas", "scipy", "numpy"]
20
20
 
21
+ [project.optional-dependencies]
22
+ # Only needed for gr_analytics.get_positions (fetching finishing positions).
23
+ fastf1 = ["fastf1"]
24
+
21
25
  [project.urls]
22
26
  Homepage = "https://github.com/nce8/gr_analytics"
23
27
 
24
28
  [tool.setuptools.packages.find]
25
29
  where = ["."]
30
+ include = ["gr_analytics*"]
26
31
 
27
32
  [tool.setuptools.package-data]
28
33
  gr_analytics = ["data/*.csv"]
@@ -13,7 +13,6 @@ from gr_analytics import (
13
13
  calculate_eight_race_averages,
14
14
  driver_data,
15
15
  eight_race_average,
16
- race_results,
17
16
  )
18
17
 
19
18
  _TESTS_DIR = Path(__file__).parent
@@ -69,8 +68,8 @@ def _rounds_with_recorded_averages():
69
68
  """Rounds whose driver rows have GridRival eight_race_average values.
70
69
 
71
70
  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.
71
+ left blank until the corresponding finishing positions are added), so we
72
+ only check rounds where GridRival's own values exist to compare against.
74
73
  """
75
74
  dd = driver_data()
76
75
  drivers = dd[dd["type"] == "driver"]
@@ -99,7 +98,11 @@ class TestAgreementWithGridRival:
99
98
  )
100
99
 
101
100
  def test_default_round_is_latest(self):
102
- latest = race_results()["round"].max()
101
+ drivers = driver_data()
102
+ drivers = drivers[drivers["type"] == "driver"]
103
+ latest = int(
104
+ drivers.loc[drivers["finishing_position"].notna(), "round"].max()
105
+ )
103
106
  pd.testing.assert_series_equal(
104
107
  calculate_eight_race_averages(),
105
108
  calculate_eight_race_averages(through_round=latest),
@@ -107,25 +110,34 @@ class TestAgreementWithGridRival:
107
110
 
108
111
 
109
112
  # ---------------------------------------------------------------------------
110
- # race_results.csv data integrity
113
+ # Recorded finishing-position data integrity (driver_data.csv)
111
114
  # ---------------------------------------------------------------------------
112
115
 
113
116
 
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
- )
117
+ def _finishes_by_round():
118
+ """driver_data finishing positions, one frame per round that records them."""
119
+ drivers = driver_data()
120
+ drivers = drivers[(drivers["type"] == "driver") & drivers["finishing_position"].notna()]
121
+ return {rnd: group for rnd, group in drivers.groupby("round")}
122
+
123
+
124
+ class TestFinishingPositionData:
125
+ @pytest.mark.parametrize("rnd", sorted(_finishes_by_round()))
126
+ def test_positions_complete_each_round(self, rnd):
127
+ group = _finishes_by_round()[rnd]
128
+ positions = sorted(group["finishing_position"].astype(int))
129
+ assert positions == list(range(1, len(group) + 1)), (
130
+ f"Round {rnd} finishing positions are not a complete 1..n sequence"
131
+ )
122
132
 
123
133
  def test_round_one_matches_australia_fixture(self):
124
134
  """Race 1 positions must agree with the Australia scenario fixture."""
125
135
  australia = pd.read_csv(_TESTS_DIR / "test_australia.csv").set_index(
126
136
  "driver_abbr"
127
137
  )
128
- rr = race_results()
129
- round_one = rr[rr["round"] == 1].set_index("driver_abbr")
138
+ drivers = driver_data()
139
+ round_one = drivers[
140
+ (drivers["type"] == "driver") & (drivers["round"] == 1)
141
+ ].set_index("driver_abbr")
130
142
  for abbr, expected in australia["finishing_position"].items():
131
143
  assert round_one.at[abbr, "finishing_position"] == expected
@@ -13,6 +13,8 @@ import pytest
13
13
  from gr_analytics import (
14
14
  _score_constructors,
15
15
  _score_drivers,
16
+ calculate_eight_race_averages,
17
+ driver_data,
16
18
  optimal_lineup,
17
19
  score_event,
18
20
  )
@@ -415,6 +417,59 @@ class TestAustraliaRound0:
415
417
  assert _get_row(australia_result, abbr)["salary_change"] == exp_change
416
418
 
417
419
 
420
+ # ---------------------------------------------------------------------------
421
+ # score_event derives the eight-race average from driver_data
422
+ # ---------------------------------------------------------------------------
423
+
424
+
425
+ class TestScoreEventEightRaceAverage:
426
+ """score_event computes the eight-race average from the recorded
427
+ finishing positions (calculate_eight_race_averages) rather than trusting
428
+ the hard-coded eight_race_average column. So it works for rounds whose
429
+ GridRival value was never transcribed (the column is blank), and uses the
430
+ average going *into* the race being scored (through_round == round)."""
431
+
432
+ @staticmethod
433
+ def _scenario_from_finishes(rnd):
434
+ d = driver_data()
435
+ d = d[(d["type"] == "driver") & (d["round"] == rnd)][
436
+ ["driver_abbr", "finishing_position"]
437
+ ].copy()
438
+ d["finishing_position"] = d["finishing_position"].astype(int)
439
+ return pd.DataFrame(
440
+ {
441
+ "driver_abbr": d["driver_abbr"],
442
+ "qualifying_position": d["finishing_position"],
443
+ "race_position": d["finishing_position"],
444
+ }
445
+ )
446
+
447
+ @pytest.mark.parametrize("rnd", [1, 2, 3, 4, 5, 6, 7])
448
+ def test_uses_average_through_round(self, rnd):
449
+ """The average score_event applies equals calculate_eight_race_averages
450
+ through that round — the state going into the race scored at `round`."""
451
+ result = score_event(self._scenario_from_finishes(rnd), round=rnd)
452
+ used = (
453
+ result[result["type"] == "driver"]
454
+ .set_index("driver_abbr")["eight_race_average"]
455
+ .astype(int)
456
+ .sort_index()
457
+ )
458
+ expected = (
459
+ calculate_eight_race_averages(through_round=rnd).astype(int).sort_index()
460
+ )
461
+ pd.testing.assert_series_equal(used, expected, check_names=False)
462
+
463
+ @pytest.mark.parametrize("rnd", [5, 6, 7])
464
+ def test_blank_column_rounds_score_finite(self, rnd):
465
+ """Rounds whose eight_race_average column is blank must still score —
466
+ before the average was computed, this raised on NaN improvement."""
467
+ result = score_event(self._scenario_from_finishes(rnd), round=rnd)
468
+ drivers = result[result["type"] == "driver"]
469
+ assert drivers["pts_improvement"].notna().all()
470
+ assert drivers["points_earned"].notna().all()
471
+
472
+
418
473
  # ---------------------------------------------------------------------------
419
474
  # optimal_lineup star_salary_cap tests
420
475
  # ---------------------------------------------------------------------------
@@ -1,232 +0,0 @@
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
@@ -1,89 +0,0 @@
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
File without changes