| """Seed traffic with explicit income drift. |
| |
| Variant of ``scripts/seed_traffic.py`` that multiplies ``AMT_INCOME_TOTAL`` |
| by a configurable factor (default 1.3 = +30%) before each POST. Simulates |
| a population-level shift where the average demanding client suddenly has |
| significantly higher income — the kind of drift a real production |
| monitor must catch and alert on. |
| |
| The API re-derives ratios (INCOME_CREDIT_PERC, ANNUITY_INCOME_PERC, |
| INCOME_PER_PERSON) server-side, so the shifted income propagates to |
| those derived features automatically. |
| |
| Usage: |
| uv run python scripts/seed_traffic_drifted.py # 1.3x income |
| uv run python scripts/seed_traffic_drifted.py --income-factor 2.0 |
| uv run python scripts/seed_traffic_drifted.py --known 100 --unknown 0 |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import logging |
| import random |
| import sys |
| from pathlib import Path |
| from typing import Any |
|
|
| import pandas as pd |
|
|
| from seed_traffic import ( |
| APP_TRAIN_PATH, |
| DEFAULT_BASE_URL, |
| DEFAULT_DELAY_S, |
| DEFAULT_KNOWN, |
| DEFAULT_SEED, |
| DEFAULT_UNKNOWN, |
| UNKNOWN_ID_START, |
| _row_to_payload, |
| run, |
| ) |
|
|
| logger = logging.getLogger("scripts.seed_traffic_drifted") |
| logging.basicConfig(level=logging.INFO, format="%(message)s") |
|
|
| DEFAULT_INCOME_FACTOR = 1.3 |
|
|
|
|
| def build_payloads( |
| app_train_path: Path, |
| n_known: int, |
| n_unknown: int, |
| income_factor: float, |
| rng: random.Random, |
| ) -> list[dict[str, Any]]: |
| if not app_train_path.exists(): |
| raise SystemExit( |
| f"{app_train_path} not found. Place the Kaggle application_train.csv there." |
| ) |
| logger.info("Loading %s ...", app_train_path) |
| df = pd.read_csv(app_train_path) |
| df = df[df["CODE_GENDER"] != "XNA"] |
| df = df[df["DAYS_BIRTH"].notna()] |
| logger.info("application_train clean rows: %d", len(df)) |
|
|
| seed_state = rng.randint(0, 2**31 - 1) |
| sample = df.sample(n=n_known + n_unknown, random_state=seed_state) |
|
|
| payloads: list[dict[str, Any]] = [] |
| for i, (_, row) in enumerate(sample.iterrows()): |
| payload = _row_to_payload(row) |
| if payload.get("AMT_INCOME_TOTAL") is not None: |
| payload["AMT_INCOME_TOTAL"] = float(payload["AMT_INCOME_TOTAL"]) * income_factor |
| if i >= n_known: |
| payload["SK_ID_CURR"] = UNKNOWN_ID_START + (i - n_known) |
| payloads.append(payload) |
|
|
| rng.shuffle(payloads) |
| logger.info( |
| "Built %d payloads with income x%.2f (%d known + %d unknown)", |
| len(payloads), income_factor, n_known, n_unknown, |
| ) |
| return payloads |
|
|
|
|
| def main() -> int: |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument("--base-url", default=DEFAULT_BASE_URL) |
| parser.add_argument("--delay", type=float, default=DEFAULT_DELAY_S) |
| parser.add_argument("--seed", type=int, default=DEFAULT_SEED) |
| parser.add_argument("--app-train-path", type=Path, default=APP_TRAIN_PATH) |
| parser.add_argument("--known", type=int, default=DEFAULT_KNOWN) |
| parser.add_argument("--unknown", type=int, default=DEFAULT_UNKNOWN) |
| parser.add_argument( |
| "--income-factor", |
| type=float, |
| default=DEFAULT_INCOME_FACTOR, |
| help="Multiplier applied to AMT_INCOME_TOTAL (default %(default)s).", |
| ) |
| args = parser.parse_args() |
|
|
| rng = random.Random(args.seed) |
| payloads = build_payloads( |
| args.app_train_path, args.known, args.unknown, args.income_factor, rng |
| ) |
| return run(payloads, args.base_url, args.delay) |
|
|
|
|
| if __name__ == "__main__": |
| sys.exit(main()) |
|
|