gr-analytics 0.1.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.
- gr_analytics-0.1.0/PKG-INFO +156 -0
- gr_analytics-0.1.0/README.md +139 -0
- gr_analytics-0.1.0/gr_analytics/__init__.py +628 -0
- gr_analytics-0.1.0/gr_analytics/data/driver_data.csv +67 -0
- gr_analytics-0.1.0/gr_analytics.egg-info/PKG-INFO +156 -0
- gr_analytics-0.1.0/gr_analytics.egg-info/SOURCES.txt +10 -0
- gr_analytics-0.1.0/gr_analytics.egg-info/dependency_links.txt +1 -0
- gr_analytics-0.1.0/gr_analytics.egg-info/requires.txt +3 -0
- gr_analytics-0.1.0/gr_analytics.egg-info/top_level.txt +3 -0
- gr_analytics-0.1.0/pyproject.toml +30 -0
- gr_analytics-0.1.0/setup.cfg +4 -0
- gr_analytics-0.1.0/tests/test_scoring.py +438 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gr_analytics
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Scoring and salary calculation for GridRival fantasy F1
|
|
5
|
+
Author: nce8
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nce8/gr_analytics
|
|
8
|
+
Keywords: fantasy,f1,formula1,gridrival,motorsport
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Games/Entertainment
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: pandas
|
|
15
|
+
Requires-Dist: scipy
|
|
16
|
+
Requires-Dist: numpy
|
|
17
|
+
|
|
18
|
+
# gr_analytics
|
|
19
|
+
|
|
20
|
+
Python package for scoring and salary calculation in [GridRival](https://www.gridrival.com/) fantasy F1.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install -e .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import pandas as pd
|
|
32
|
+
from gr_analytics import score_event, score_my_team, optimal_lineup
|
|
33
|
+
|
|
34
|
+
# Load your race scenario
|
|
35
|
+
scenario = pd.read_csv("my_race.csv")
|
|
36
|
+
|
|
37
|
+
# Score the event (defaults to the latest round in driver_data)
|
|
38
|
+
result = score_event(scenario)
|
|
39
|
+
|
|
40
|
+
# Or score a specific round
|
|
41
|
+
result = score_event(scenario, round=1)
|
|
42
|
+
|
|
43
|
+
# Score your specific team selection
|
|
44
|
+
points, salary_change = score_my_team(
|
|
45
|
+
scenario,
|
|
46
|
+
drivers=["RUS", "ANT", "LEC", "BEA", "LIN"],
|
|
47
|
+
team="MER",
|
|
48
|
+
star_driver="BEA",
|
|
49
|
+
round=1,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Find the optimal lineup (maximise points, £100M budget)
|
|
53
|
+
lineup = optimal_lineup(result)
|
|
54
|
+
|
|
55
|
+
# With locked-in drivers (already under contract, cost nothing)
|
|
56
|
+
# and a budget for the remaining open spots
|
|
57
|
+
lineup = optimal_lineup(
|
|
58
|
+
result,
|
|
59
|
+
locked_in=["HAM", "LEC"], # driver_abbr or team code
|
|
60
|
+
optimize_for="points", # or "salary_change"
|
|
61
|
+
budget=60.0, # £M available for non-locked picks
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Scenario Format
|
|
66
|
+
|
|
67
|
+
The scenario DataFrame must have one row per driver with these columns:
|
|
68
|
+
|
|
69
|
+
| Column | Type | Description |
|
|
70
|
+
|--------|------|-------------|
|
|
71
|
+
| `driver_abbr` | str | Driver abbreviation (e.g. `"RUS"`, `"VER"`) |
|
|
72
|
+
| `qualifying_position` | int | Official qualifying position (1–22) |
|
|
73
|
+
| `completed_qualifying` | int | `1` if driver completed qualifying, `0` if DNQ |
|
|
74
|
+
| `finishing_position` | int | Race finishing position (1–22) |
|
|
75
|
+
| `completed_pct` | float or `"DNS"` | Fraction of race completed (0.0–1.0), or `"DNS"` if driver did not start |
|
|
76
|
+
|
|
77
|
+
**DNQ drivers** get 0 qualifying points but their `qualifying_position` is still used to calculate overtake points.
|
|
78
|
+
|
|
79
|
+
**DNS drivers** (`completed_pct="DNS"`) get 0 for all race-related points (race, overtake, improvement, completion, teammate).
|
|
80
|
+
|
|
81
|
+
**DNF drivers** (e.g. `completed_pct=0.3`) receive partial completion bonus points and their finishing position is used normally.
|
|
82
|
+
|
|
83
|
+
Qualifying and race positions must each form a complete sequence `1..n` with no duplicates or gaps.
|
|
84
|
+
|
|
85
|
+
## Output
|
|
86
|
+
|
|
87
|
+
`score_event` returns a DataFrame with all drivers and constructors, with scoring columns appended:
|
|
88
|
+
|
|
89
|
+
**Drivers:**
|
|
90
|
+
- `pts_qualifying`, `pts_race`, `pts_overtake`, `pts_improvement`, `pts_completion`, `pts_teammate`
|
|
91
|
+
- `points_earned` — total fantasy points
|
|
92
|
+
- `salary_after_event`, `salary_change`
|
|
93
|
+
|
|
94
|
+
**Constructors:**
|
|
95
|
+
- `pts_qualifying`, `pts_race` (sum across both drivers, using constructor-specific tables)
|
|
96
|
+
- `points_earned`, `salary_after_event`, `salary_change`
|
|
97
|
+
|
|
98
|
+
## Scoring Rules
|
|
99
|
+
|
|
100
|
+
All scoring follows GridRival's rules for Grand Prix events (no sprint races).
|
|
101
|
+
|
|
102
|
+
### Drivers
|
|
103
|
+
| Bonus | Rule |
|
|
104
|
+
|-------|------|
|
|
105
|
+
| Qualifying | P1=50, P2=48, … P22=8 (step −2) |
|
|
106
|
+
| Race finish | P1=100, P2=97, … P22=37 (step −3) |
|
|
107
|
+
| Overtake | (qualifying pos − finishing pos) × 3, min 0 |
|
|
108
|
+
| Improvement | Points for finishing ahead of 8-race average (2 pos=2 pts, 3=4, 4=6, 5=9, 6=12, 7=16, 8=20, 9=25, 10+=30) |
|
|
109
|
+
| Completion | 3 pts each at 25%, 50%, 75%, 90% of race distance (max 12) |
|
|
110
|
+
| Teammate | Points for beating teammate by margin: ≥1 pos=2 pts, ≥4=5, ≥8=8, ≥13=12 |
|
|
111
|
+
|
|
112
|
+
### Constructors
|
|
113
|
+
Constructor qualifying and race points use separate tables (P1=30/60, step −1/−2 per driver) summed across both drivers. No overtake, improvement, completion, or teammate bonuses.
|
|
114
|
+
|
|
115
|
+
### Salary Adjustment
|
|
116
|
+
After each race, salaries adjust based on the gap between a driver's actual starting salary and the default salary for their points-ranking position:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
adjustment = truncate(variation / 4, to nearest £100K)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Capped at ±£2M for drivers, ±£3M for constructors.
|
|
123
|
+
|
|
124
|
+
## Driver Data
|
|
125
|
+
|
|
126
|
+
Bundled driver data (`gr_analytics/data/driver_data.csv`) contains starting salaries and 8-race averages by round:
|
|
127
|
+
|
|
128
|
+
- `round=0` — pre-season (before Australia 2026)
|
|
129
|
+
- `round=1` — post-Australia 2026
|
|
130
|
+
|
|
131
|
+
## Lineup Optimisation
|
|
132
|
+
|
|
133
|
+
`optimal_lineup` uses mixed-integer linear programming (via `scipy.optimize.milp`) to find the best 5-driver + 1-constructor lineup within a salary budget.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
lineup = optimal_lineup(
|
|
137
|
+
scored, # DataFrame from score_event()
|
|
138
|
+
locked_in=None, # list of driver_abbr / team codes already on your team
|
|
139
|
+
optimize_for="points", # "points" or "salary_change"
|
|
140
|
+
budget=100.0, # £M for non-locked picks
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
- **`locked_in`** picks are included free (already under contract) and don't count against the budget.
|
|
145
|
+
- **`optimize_for="points"`** selects the optimal star driver (who earns double points) across all candidates.
|
|
146
|
+
- **`optimize_for="salary_change"`** maximises total salary gain for the next race's team valuation.
|
|
147
|
+
|
|
148
|
+
The returned DataFrame has a `star` column (`1` = starred driver, points mode only).
|
|
149
|
+
|
|
150
|
+
## Running Tests
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
python -m pytest tests/test_scoring.py -v
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Tests include hand-calculated unit tests for all scoring components and a full integration test against real GridRival results from Australia 2026 (22 drivers + 3 constructors).
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# gr_analytics
|
|
2
|
+
|
|
3
|
+
Python package for scoring and salary calculation in [GridRival](https://www.gridrival.com/) fantasy F1.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -e .
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import pandas as pd
|
|
15
|
+
from gr_analytics import score_event, score_my_team, optimal_lineup
|
|
16
|
+
|
|
17
|
+
# Load your race scenario
|
|
18
|
+
scenario = pd.read_csv("my_race.csv")
|
|
19
|
+
|
|
20
|
+
# Score the event (defaults to the latest round in driver_data)
|
|
21
|
+
result = score_event(scenario)
|
|
22
|
+
|
|
23
|
+
# Or score a specific round
|
|
24
|
+
result = score_event(scenario, round=1)
|
|
25
|
+
|
|
26
|
+
# Score your specific team selection
|
|
27
|
+
points, salary_change = score_my_team(
|
|
28
|
+
scenario,
|
|
29
|
+
drivers=["RUS", "ANT", "LEC", "BEA", "LIN"],
|
|
30
|
+
team="MER",
|
|
31
|
+
star_driver="BEA",
|
|
32
|
+
round=1,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Find the optimal lineup (maximise points, £100M budget)
|
|
36
|
+
lineup = optimal_lineup(result)
|
|
37
|
+
|
|
38
|
+
# With locked-in drivers (already under contract, cost nothing)
|
|
39
|
+
# and a budget for the remaining open spots
|
|
40
|
+
lineup = optimal_lineup(
|
|
41
|
+
result,
|
|
42
|
+
locked_in=["HAM", "LEC"], # driver_abbr or team code
|
|
43
|
+
optimize_for="points", # or "salary_change"
|
|
44
|
+
budget=60.0, # £M available for non-locked picks
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Scenario Format
|
|
49
|
+
|
|
50
|
+
The scenario DataFrame must have one row per driver with these columns:
|
|
51
|
+
|
|
52
|
+
| Column | Type | Description |
|
|
53
|
+
|--------|------|-------------|
|
|
54
|
+
| `driver_abbr` | str | Driver abbreviation (e.g. `"RUS"`, `"VER"`) |
|
|
55
|
+
| `qualifying_position` | int | Official qualifying position (1–22) |
|
|
56
|
+
| `completed_qualifying` | int | `1` if driver completed qualifying, `0` if DNQ |
|
|
57
|
+
| `finishing_position` | int | Race finishing position (1–22) |
|
|
58
|
+
| `completed_pct` | float or `"DNS"` | Fraction of race completed (0.0–1.0), or `"DNS"` if driver did not start |
|
|
59
|
+
|
|
60
|
+
**DNQ drivers** get 0 qualifying points but their `qualifying_position` is still used to calculate overtake points.
|
|
61
|
+
|
|
62
|
+
**DNS drivers** (`completed_pct="DNS"`) get 0 for all race-related points (race, overtake, improvement, completion, teammate).
|
|
63
|
+
|
|
64
|
+
**DNF drivers** (e.g. `completed_pct=0.3`) receive partial completion bonus points and their finishing position is used normally.
|
|
65
|
+
|
|
66
|
+
Qualifying and race positions must each form a complete sequence `1..n` with no duplicates or gaps.
|
|
67
|
+
|
|
68
|
+
## Output
|
|
69
|
+
|
|
70
|
+
`score_event` returns a DataFrame with all drivers and constructors, with scoring columns appended:
|
|
71
|
+
|
|
72
|
+
**Drivers:**
|
|
73
|
+
- `pts_qualifying`, `pts_race`, `pts_overtake`, `pts_improvement`, `pts_completion`, `pts_teammate`
|
|
74
|
+
- `points_earned` — total fantasy points
|
|
75
|
+
- `salary_after_event`, `salary_change`
|
|
76
|
+
|
|
77
|
+
**Constructors:**
|
|
78
|
+
- `pts_qualifying`, `pts_race` (sum across both drivers, using constructor-specific tables)
|
|
79
|
+
- `points_earned`, `salary_after_event`, `salary_change`
|
|
80
|
+
|
|
81
|
+
## Scoring Rules
|
|
82
|
+
|
|
83
|
+
All scoring follows GridRival's rules for Grand Prix events (no sprint races).
|
|
84
|
+
|
|
85
|
+
### Drivers
|
|
86
|
+
| Bonus | Rule |
|
|
87
|
+
|-------|------|
|
|
88
|
+
| Qualifying | P1=50, P2=48, … P22=8 (step −2) |
|
|
89
|
+
| Race finish | P1=100, P2=97, … P22=37 (step −3) |
|
|
90
|
+
| Overtake | (qualifying pos − finishing pos) × 3, min 0 |
|
|
91
|
+
| Improvement | Points for finishing ahead of 8-race average (2 pos=2 pts, 3=4, 4=6, 5=9, 6=12, 7=16, 8=20, 9=25, 10+=30) |
|
|
92
|
+
| Completion | 3 pts each at 25%, 50%, 75%, 90% of race distance (max 12) |
|
|
93
|
+
| Teammate | Points for beating teammate by margin: ≥1 pos=2 pts, ≥4=5, ≥8=8, ≥13=12 |
|
|
94
|
+
|
|
95
|
+
### Constructors
|
|
96
|
+
Constructor qualifying and race points use separate tables (P1=30/60, step −1/−2 per driver) summed across both drivers. No overtake, improvement, completion, or teammate bonuses.
|
|
97
|
+
|
|
98
|
+
### Salary Adjustment
|
|
99
|
+
After each race, salaries adjust based on the gap between a driver's actual starting salary and the default salary for their points-ranking position:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
adjustment = truncate(variation / 4, to nearest £100K)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Capped at ±£2M for drivers, ±£3M for constructors.
|
|
106
|
+
|
|
107
|
+
## Driver Data
|
|
108
|
+
|
|
109
|
+
Bundled driver data (`gr_analytics/data/driver_data.csv`) contains starting salaries and 8-race averages by round:
|
|
110
|
+
|
|
111
|
+
- `round=0` — pre-season (before Australia 2026)
|
|
112
|
+
- `round=1` — post-Australia 2026
|
|
113
|
+
|
|
114
|
+
## Lineup Optimisation
|
|
115
|
+
|
|
116
|
+
`optimal_lineup` uses mixed-integer linear programming (via `scipy.optimize.milp`) to find the best 5-driver + 1-constructor lineup within a salary budget.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
lineup = optimal_lineup(
|
|
120
|
+
scored, # DataFrame from score_event()
|
|
121
|
+
locked_in=None, # list of driver_abbr / team codes already on your team
|
|
122
|
+
optimize_for="points", # "points" or "salary_change"
|
|
123
|
+
budget=100.0, # £M for non-locked picks
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- **`locked_in`** picks are included free (already under contract) and don't count against the budget.
|
|
128
|
+
- **`optimize_for="points"`** selects the optimal star driver (who earns double points) across all candidates.
|
|
129
|
+
- **`optimize_for="salary_change"`** maximises total salary gain for the next race's team valuation.
|
|
130
|
+
|
|
131
|
+
The returned DataFrame has a `star` column (`1` = starred driver, points mode only).
|
|
132
|
+
|
|
133
|
+
## Running Tests
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
python -m pytest tests/test_scoring.py -v
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Tests include hand-calculated unit tests for all scoring components and a full integration test against real GridRival results from Australia 2026 (22 drivers + 3 constructors).
|