| """Synthetic data generation and CSV loading for claims and adjusters. |
| |
| Generates data/claims.csv and data/adjusters.csv. To use real data, produce |
| CSVs with the same columns (or adapt load_claims / load_adjusters). |
| |
| Time columns are stored as "HH:MM" strings for readability and parsed to |
| integer minutes since midnight on load. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import csv |
| import os |
| import random |
| from dataclasses import dataclass, field |
|
|
| import config |
|
|
|
|
| def hhmm_to_min(s: str) -> int: |
| h, m = s.split(":") |
| return int(h) * 60 + int(m) |
|
|
|
|
| def min_to_hhmm(m: int) -> str: |
| return f"{m // 60:02d}:{m % 60:02d}" |
|
|
|
|
| @dataclass |
| class Claim: |
| claim_id: str |
| lat: float |
| lon: float |
| peril: str |
| priority: int |
| window_start: int |
| window_end: int |
| service_minutes: int |
| age_days: int = 0 |
| assigned_to: str | None = None |
| |
| |
| |
| |
| extra_windows: list[tuple[int, int]] | None = None |
|
|
|
|
| @dataclass |
| class Adjuster: |
| adjuster_id: str |
| name: str |
| home_lat: float |
| home_lon: float |
| skills: list[str] = field(default_factory=list) |
| shift_start: int = 8 * 60 |
| shift_end: int = 17 * 60 |
| |
| |
| max_radius_miles: float | None = None |
|
|
|
|
| |
| |
| |
|
|
| ADJUSTER_NAMES = [ |
| "Alice Chen", "Bob Rivera", "Carol Nguyen", "David Okafor", "Erin Walsh", |
| "Frank Kim", "Grace Patel", "Hank Torres", "Ivy Johnson", "Jack Murphy", |
| ] |
|
|
| |
| WINDOW_CHOICES = [ |
| ("all-day", "08:00", "17:00", 0.4), |
| ("morning", "08:00", "12:00", 0.2), |
| ("afternoon", "12:00", "17:00", 0.2), |
| ("narrow-am", "09:00", "11:00", 0.1), |
| ("narrow-pm", "13:00", "15:00", 0.1), |
| ] |
|
|
|
|
| def _rand_point(rng: random.Random) -> tuple[float, float]: |
| lat = config.REGION_CENTER[0] + rng.uniform(-1, 1) * config.REGION_SPREAD_DEG |
| lon = config.REGION_CENTER[1] + rng.uniform(-1, 1) * config.REGION_SPREAD_DEG |
| return round(lat, 5), round(lon, 5) |
|
|
|
|
| def generate(n_claims: int = 25, n_adjusters: int = 5, seed: int = 42, |
| data_dir: str = config.DATA_DIR) -> None: |
| rng = random.Random(seed) |
| os.makedirs(data_dir, exist_ok=True) |
|
|
| |
| adjusters = [] |
| for i in range(n_adjusters): |
| lat, lon = _rand_point(rng) |
| n_skills = rng.choice([2, 2, 3]) |
| skills = rng.sample(config.PERILS, n_skills) |
| shift_start, shift_end = rng.choice([("08:00", "17:00"), |
| ("07:00", "16:00"), |
| ("08:00", "17:00")]) |
| adjusters.append({ |
| "adjuster_id": f"ADJ-{i + 1:02d}", |
| "name": ADJUSTER_NAMES[i % len(ADJUSTER_NAMES)], |
| "home_lat": lat, |
| "home_lon": lon, |
| "skills": "|".join(sorted(skills)), |
| "shift_start": shift_start, |
| "shift_end": shift_end, |
| }) |
| |
| for peril in config.PERILS: |
| holders = [a for a in adjusters if peril in a["skills"].split("|")] |
| while len(holders) < 2: |
| victim = rng.choice([a for a in adjusters if a not in holders]) |
| victim["skills"] = "|".join(sorted(victim["skills"].split("|") + [peril])) |
| holders.append(victim) |
|
|
| |
| claims = [] |
| for i in range(n_claims): |
| lat, lon = _rand_point(rng) |
| peril = rng.choice(config.PERILS) |
| r = rng.random() |
| if r < 0.12: |
| priority = config.PRIORITY_MUST_TODAY |
| elif r < 0.40: |
| priority = config.PRIORITY_HIGH |
| else: |
| priority = config.PRIORITY_NORMAL |
| _, ws, we, _ = rng.choices( |
| WINDOW_CHOICES, weights=[w for *_, w in WINDOW_CHOICES])[0] |
| |
| if priority == config.PRIORITY_MUST_TODAY: |
| service = rng.choice([120, 150, 180]) |
| else: |
| service = rng.choice([60, 75, 90, 120]) |
| claims.append({ |
| "claim_id": f"CLM-{i + 1:03d}", |
| "lat": lat, |
| "lon": lon, |
| "peril": peril, |
| "priority": priority, |
| "window_start": ws, |
| "window_end": we, |
| "service_minutes": service, |
| }) |
|
|
| with open(os.path.join(data_dir, "adjusters.csv"), "w", newline="") as f: |
| w = csv.DictWriter(f, fieldnames=list(adjusters[0].keys())) |
| w.writeheader() |
| w.writerows(adjusters) |
| with open(os.path.join(data_dir, "claims.csv"), "w", newline="") as f: |
| w = csv.DictWriter(f, fieldnames=list(claims[0].keys())) |
| w.writeheader() |
| w.writerows(claims) |
|
|
|
|
| |
| |
| |
|
|
| def claim_windows(c: Claim) -> list[tuple[int, int]]: |
| """All availability windows of a claim, primary first, sorted.""" |
| wins = [(c.window_start, c.window_end)] |
| for lo, hi in (c.extra_windows or []): |
| wins.append((int(lo), int(hi))) |
| return sorted(set(wins)) |
|
|
|
|
| def arrival_ranges(c: Claim) -> list[tuple[int, int]]: |
| """Allowed SERVICE-START ranges: inside a window, finishing by its |
| end (the same tightening every backend applies), merged/sorted.""" |
| ranges = [] |
| for lo, hi in claim_windows(c): |
| ranges.append((lo, max(lo, hi - c.service_minutes))) |
| ranges.sort() |
| merged = [list(ranges[0])] |
| for lo, hi in ranges[1:]: |
| if lo <= merged[-1][1] + 1: |
| merged[-1][1] = max(merged[-1][1], hi) |
| else: |
| merged.append([lo, hi]) |
| return [(lo, hi) for lo, hi in merged] |
|
|
|
|
| def win_str(c: Claim) -> str: |
| """Display text for all of a claim's availability windows.""" |
| return " | ".join(f"{min_to_hhmm(lo)}-{min_to_hhmm(hi)}" |
| for lo, hi in claim_windows(c)) |
|
|
|
|
| def _parse_windows(text): |
| """'08:00-10:30|16:00-17:00' -> list of (start_min, end_min).""" |
| wins = [] |
| for part in text.split("|"): |
| lo, hi = part.strip().split("-") |
| wins.append((hhmm_to_min(lo.strip()), hhmm_to_min(hi.strip()))) |
| return wins |
|
|
|
|
| def load_claims(data_dir: str = config.DATA_DIR) -> list[Claim]: |
| claims = [] |
| with open(os.path.join(data_dir, "claims.csv"), newline="") as f: |
| for row in csv.DictReader(f): |
| claims.append(Claim( |
| claim_id=row["claim_id"], |
| lat=float(row["lat"]), |
| lon=float(row["lon"]), |
| peril=row["peril"], |
| priority=int(row["priority"]), |
| window_start=hhmm_to_min(row["window_start"]), |
| window_end=hhmm_to_min(row["window_end"]), |
| service_minutes=int(row["service_minutes"]), |
| age_days=int(row.get("age_days") or 0), |
| assigned_to=(row.get("assigned_to") or "").strip() or None, |
| )) |
| wtext = (row.get("windows") or "").strip() |
| if wtext: |
| wins = _parse_windows(wtext) |
| c = claims[-1] |
| c.window_start, c.window_end = wins[0] |
| c.extra_windows = wins[1:] or None |
| return claims |
|
|
|
|
| def load_adjusters(data_dir: str = config.DATA_DIR) -> list[Adjuster]: |
| adjusters = [] |
| with open(os.path.join(data_dir, "adjusters.csv"), newline="") as f: |
| for row in csv.DictReader(f): |
| adjusters.append(Adjuster( |
| adjuster_id=row["adjuster_id"], |
| name=row["name"], |
| home_lat=float(row["home_lat"]), |
| home_lon=float(row["home_lon"]), |
| skills=row["skills"].split("|"), |
| shift_start=hhmm_to_min(row["shift_start"]), |
| shift_end=hhmm_to_min(row["shift_end"]), |
| max_radius_miles=(float(row["max_radius_miles"]) |
| if row.get("max_radius_miles") |
| else None), |
| )) |
| return adjusters |
|
|
|
|
| |
| |
| |
|
|
| def write_claims_csv(claims: list[Claim], path: str) -> str: |
| """Write claims in the exact upload schema, optional columns included.""" |
| with_assign = any(c.assigned_to for c in claims) |
| with_windows = any(c.extra_windows for c in claims) |
| with open(path, "w", newline="") as f: |
| w = csv.writer(f) |
| header = ["claim_id", "lat", "lon", "peril", "priority", |
| "window_start", "window_end", "service_minutes", |
| "age_days"] |
| if with_assign: |
| header.append("assigned_to") |
| if with_windows: |
| header.append("windows") |
| w.writerow(header) |
| for c in claims: |
| row = [c.claim_id, c.lat, c.lon, c.peril, c.priority, |
| min_to_hhmm(c.window_start), |
| min_to_hhmm(c.window_end), |
| c.service_minutes, c.age_days] |
| if with_assign: |
| row.append(c.assigned_to or "") |
| if with_windows: |
| row.append("|".join(f"{min_to_hhmm(lo)}-{min_to_hhmm(hi)}" |
| for lo, hi in claim_windows(c)) |
| if c.extra_windows else "") |
| w.writerow(row) |
| return path |
|
|
|
|
| def write_adjusters_csv(adjusters: list[Adjuster], path: str) -> str: |
| """Write adjusters in the exact upload schema.""" |
| with open(path, "w", newline="") as f: |
| w = csv.writer(f) |
| w.writerow(["adjuster_id", "name", "home_lat", "home_lon", |
| "skills", "shift_start", "shift_end", |
| "max_radius_miles"]) |
| for a in adjusters: |
| w.writerow([a.adjuster_id, a.name, a.home_lat, a.home_lon, |
| "|".join(a.skills), |
| min_to_hhmm(a.shift_start), |
| min_to_hhmm(a.shift_end), |
| "" if a.max_radius_miles is None |
| else a.max_radius_miles]) |
| return path |
|
|