-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathupdate_average.py
More file actions
executable file
·163 lines (139 loc) · 5.43 KB
/
update_average.py
File metadata and controls
executable file
·163 lines (139 loc) · 5.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#!/usr/bin/env python3
"""
update_averages.py
Updates per-rider averages in a denormalised 'riders' table:
- average_position: mean of r1..r12 overall positions (ignoring None/0/placeholder)
- average_points: mean of r1..r12 points (ignoring None/0/placeholder)
Usage:
python update_averages.py /path/to/race.db
python update_averages.py /path/to/race.db --placeholder 999
python update_averages.py /path/to/race.db --dry-run
"""
import argparse
import sqlite3
from typing import Iterable, Tuple, Optional, Any
POSITION_COLS = [
"r1_overall_position","r2_overall_position","r3_overall_position",
"r4_overall_position","r5_overall_position","r6_overall_position",
"r7_overall_position","r8_overall_position","r9_overall_position",
"r10_overall_position","r11_overall_position","r12_overall_position",
]
POINTS_COLS = [
"r1_points","r2_points","r3_points","r4_points","r5_points","r6_points",
"r7_points","r8_points","r9_points","r10_points","r11_points","r12_points",
]
def to_float(x: Any) -> Optional[float]:
if x is None:
return None
if isinstance(x, (int, float)):
return float(x)
s = str(x).strip()
if s == "":
return None
try:
return float(s)
except ValueError:
return None
def mean_filtered(values: Iterable[Any], placeholder: Optional[float] = 999.0, round_mode: str = "nearest"):
"""
Compute mean ignoring None, 0, and placeholder.
Returns (mean_float_or_None, count_used)
"""
cleaned = []
for v in values:
f = to_float(v)
if f is None:
continue
if f == 0:
continue
if placeholder is not None and f == float(placeholder):
continue
cleaned.append(f)
if not cleaned:
return None, 0
m = sum(cleaned) / len(cleaned)
if round_mode == "nearest":
return round(m), len(cleaned) # integer style (for positions)
elif round_mode == "ceil":
import math
return math.ceil(m), len(cleaned)
elif round_mode == "floor":
import math
return math.floor(m), len(cleaned)
elif round_mode == "1dp":
return round(m, 1), len(cleaned)
else:
return m, len(cleaned)
def ensure_columns(conn: sqlite3.Connection):
"""Create average columns if they don't exist."""
cur = conn.cursor()
# Check pragma table_info to see if columns exist
cur.execute("PRAGMA table_info(riders)")
cols = {row[1] for row in cur.fetchall()}
stmts = []
if "average_position" not in cols:
stmts.append("ALTER TABLE riders ADD COLUMN average_position INTEGER")
if "average_points" not in cols:
stmts.append("ALTER TABLE riders ADD COLUMN average_points REAL")
for s in stmts:
cur.execute(s)
if stmts:
conn.commit()
def update_averages(db_path: str, placeholder: float = 999.0, dry_run: bool = False) -> Tuple[int, int]:
conn = sqlite3.connect(db_path)
try:
ensure_columns(conn)
conn.row_factory = sqlite3.Row
cur = conn.cursor()
# Pull only the columns we need
select_cols = ["id"] + POSITION_COLS + POINTS_COLS
cur.execute(f"SELECT {', '.join(select_cols)} FROM riders")
rows = cur.fetchall()
upd_pos = 0
upd_pts = 0
# Begin a transaction
cur.execute("BEGIN")
for row in rows:
rider_id = row["id"]
# Positions: integer average (nearest)
pos_vals = [row[c] for c in POSITION_COLS if c in row.keys()]
avg_pos, used_pos = mean_filtered(pos_vals, placeholder=placeholder, round_mode="nearest")
# Points: 1 decimal place
pts_vals = [row[c] for c in POINTS_COLS if c in row.keys()]
avg_pts_raw, used_pts = mean_filtered(pts_vals, placeholder=placeholder, round_mode="1dp")
# Only update if we computed something (avoid writing 0s)
if avg_pos is not None:
upd_pos += 1
if not dry_run:
cur.execute(
"UPDATE riders SET average_position = ? WHERE id = ?",
(int(avg_pos), rider_id),
)
if avg_pts_raw is not None:
upd_pts += 1
if not dry_run:
cur.execute(
"UPDATE riders SET average_points = ? WHERE id = ?",
(float(avg_pts_raw), rider_id),
)
if dry_run:
conn.rollback()
else:
conn.commit()
return upd_pos, upd_pts
finally:
conn.close()
def main():
ap = argparse.ArgumentParser(description="Update rider averages in SQLite DB.")
ap.add_argument("input_file", help="Path to the SQLite DB file")
ap.add_argument("--placeholder", type=float, default=999.0,
help="Value to treat as placeholder (ignored in averages). Default: 999")
ap.add_argument("--dry-run", action="store_true",
help="Compute but do not write changes")
args = ap.parse_args()
print(f"Updating database {args.input_file} with new average values "
f"(placeholder={args.placeholder}{', DRY-RUN' if args.dry_run else ''})")
pos_count, pts_count = update_averages(args.input_file, placeholder=args.placeholder, dry_run=args.dry_run)
print(f"Updated average_position for {pos_count} riders; average_points for {pts_count} riders.")
if __name__ == "__main__":
main()