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.
@@ -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).