chat_bot_sentinel / seed_data.py
vicfeuga's picture
First upload
4a09f2e verified
Raw
History Blame Contribute Delete
12.1 kB
"""
One-shot script that builds `demo.sqlite` with synthetic market-performance data.
What it does:
1. Loads the anonymized parameter CSVs from `parameters/` into the SQLite
parameter tables (cluster_mapping, market_summary_mapping, threshold_matrix,
country_region_mapping, company_products).
2. Generates fully fictional metrics rows for every (Region × Cluster × Market ×
Calculation_Type × Period) combination declared in the parameters.
3. Writes everything to `demo.sqlite` (replacing existing contents).
All numbers are produced with `numpy.random` using a fixed seed so the demo is
reproducible. No real-world data is used.
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
from db import ( # noqa: E402 (sys.path edit above)
create_all_tables,
insert_metrics,
replace_metrics,
replace_param_table,
)
PARAMS_DIR = HERE / "parameters"
PERIODS: list[str] = ["24Q4", "25Q1", "25Q2", "25Q3", "25Q4"]
CALCULATION_TYPES: list[str] = ["MAT_YoY", "MAT_QoQ", "QTR_YoY", "QTR_QoQ"]
# Market → TA Market (therapeutic area) mapping.
MARKET_TO_TA: dict[str, str] = {
"FSH/hMG Market": "FE",
"Growth Hormone Market": "FE",
"Hypothyroid Market": "FE",
"Hyperthyroid Market": "FE",
"MS High Efficacy Market": "N&I",
"Injectable Platform Market": "N&I",
"Anti-EGFR Market": "ONC",
"MET Inhibitor Market": "ONC",
"Beta Blocker Market": "CV",
"Antihypertensive Combo Market": "CV",
}
# Which NovaPharma product (if any) competes in each market, plus fully fictional
# competitor products. None of the brand names exist in the real world.
MARKET_PRODUCTS: dict[str, list[str]] = {
"FSH/hMG Market": ["NOVAFERT-A", "NOVAFERT-B", "OVAFOL", "FOLLINOR", "MENOSTIM"],
"Growth Hormone Market": ["NOVATROPIN", "GROWSTAT", "STATUREX", "PEDISOM", "GENOTROPE"],
"Hypothyroid Market": ["NOVATHYRO", "THYRESOL", "LEVOSTIM", "THYROCID", "LEVOFORM"],
"Hyperthyroid Market": ["NOVAZOL", "METHIMOL", "CARBITHY", "PROPYTHY", "TIAMAZO"],
"MS High Efficacy Market": ["NOVALAD", "OCRELIN", "TYSALINE", "KESINAL", "ZEPOSIO"],
"Injectable Platform Market": ["NOVASCLER", "INTERON", "BETAGEN", "AVOCOXIN", "PEGSTAT"],
"Anti-EGFR Market": ["NOVAGFR", "PANIMAX", "CETUVEXX", "EGFRULIN", "NIMOTUXX"],
"MET Inhibitor Market": ["NOVAMETI", "CAPVARIN", "METARIB", "METVARIO", "CRISETIB"],
"Beta Blocker Market": ["NOVACOR", "PROPRANOL", "METOPROL", "ATENOROL", "BISOPROL"],
"Antihypertensive Combo Market": ["NOVACOMBO", "AMLODIL", "LISINOX", "VALSARTO", "RAMIPROX"],
}
# How big each region is, roughly (multiplier on absolute value/volume).
REGION_SIZE: dict[str, float] = {
"NA": 1.0,
"EU": 0.95,
"APAC": 0.75,
"LATAM": 0.45,
"MEAR": 0.40,
"Global": 2.5,
}
RNG = np.random.default_rng(seed=2025)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _load_csv(filename: str) -> pd.DataFrame:
path = PARAMS_DIR / filename
return pd.read_csv(path, keep_default_na=False)
def _split_countries(value: str) -> list[str]:
return [c.strip() for c in str(value).split(",") if c.strip()]
def _allocate_shares(n_products: int) -> np.ndarray:
"""Random market shares summing to ~100 across competitors (TOTAL row excluded)."""
raw = RNG.dirichlet(alpha=np.ones(n_products) * 1.5) * 100.0
return np.round(raw, 2)
def _ranks_from_shares(shares: np.ndarray) -> np.ndarray:
"""Lower rank = larger share."""
order = np.argsort(-shares, kind="stable")
ranks = np.empty_like(order)
ranks[order] = np.arange(1, len(shares) + 1)
return ranks
def _round_value(x: float) -> float:
"""Round monetary value to a nice 2-decimal number."""
return float(np.round(x, 2))
def _signed_growth(low: float, high: float) -> float:
"""Sample a percentage growth in [low, high] and round to 2 dp."""
return float(np.round(RNG.uniform(low, high), 2))
# ---------------------------------------------------------------------------
# Row generators
# ---------------------------------------------------------------------------
def _make_product_rows(
*,
period: str,
region: str,
level: str,
country_field: str,
cluster_field: str,
market: str,
calc_type: str,
base_value: float,
base_volume: float,
) -> list[dict]:
products = MARKET_PRODUCTS[market]
ta_market = MARKET_TO_TA[market]
value_shares = _allocate_shares(len(products))
volume_shares = _allocate_shares(len(products))
value_ranks = _ranks_from_shares(value_shares)
volume_ranks = _ranks_from_shares(volume_shares)
rows: list[dict] = []
# Per-product rows
for i, product in enumerate(products):
v_share = float(value_shares[i])
vol_share = float(volume_shares[i])
v_rank = int(value_ranks[i])
vol_rank = int(volume_ranks[i])
v_growth = _signed_growth(-18.0, 28.0)
vol_growth = _signed_growth(-15.0, 22.0)
asp_growth = _signed_growth(-4.0, 6.0)
current_value = _round_value(base_value * v_share / 100.0)
current_volume = _round_value(base_volume * vol_share / 100.0)
rows.append({
"Period": period,
"Region": region,
"Level": level,
"Product": product,
"Country": country_field,
"Cluster": cluster_field,
"TA Market": ta_market,
"Class": market,
"Current_Value": current_value,
"Current_Value_MS": v_share,
"Value_MS_Change": _signed_growth(-3.5, 3.5),
"Current_Value_Rank": v_rank,
"Value_Rank_Change": int(RNG.integers(-2, 3)),
"Value_Growth": v_growth,
"Value_Trend_Reversal": float(RNG.choice([0.0, 1.0], p=[0.92, 0.08])),
"Current_Volume": current_volume,
"Current_Volume_MS": vol_share,
"Volume_MS_Change": _signed_growth(-3.5, 3.5),
"Current_Volume_Rank": vol_rank,
"Volume_Rank_Change": int(RNG.integers(-2, 3)),
"Volume_Growth": vol_growth,
"Volume_Trend_Reversal": float(RNG.choice([0.0, 1.0], p=[0.92, 0.08])),
"ASP_Growth": asp_growth,
"Calculation_Type": calc_type,
})
# TOTAL row (aggregate of the whole market) — values 100% of base, shares 100%
rows.append({
"Period": period,
"Region": region,
"Level": level,
"Product": "TOTAL",
"Country": country_field,
"Cluster": cluster_field,
"TA Market": ta_market,
"Class": market,
"Current_Value": _round_value(base_value),
"Current_Value_MS": 100.0,
"Value_MS_Change": 0.0,
"Current_Value_Rank": 0,
"Value_Rank_Change": 0,
"Value_Growth": _signed_growth(-8.0, 14.0),
"Value_Trend_Reversal": 0.0,
"Current_Volume": _round_value(base_volume),
"Current_Volume_MS": 100.0,
"Volume_MS_Change": 0.0,
"Current_Volume_Rank": 0,
"Volume_Rank_Change": 0,
"Volume_Growth": _signed_growth(-6.0, 12.0),
"Volume_Trend_Reversal": 0.0,
"ASP_Growth": _signed_growth(-3.0, 5.0),
"Calculation_Type": calc_type,
})
return rows
def _base_value(region: str, n_countries: int) -> float:
"""Plausible cluster-level market value in USD millions."""
size = REGION_SIZE.get(region, 0.5)
return float(np.round(50.0 * size * (0.6 + 0.4 * n_countries) * RNG.uniform(0.8, 1.4), 2))
def _base_volume(region: str, n_countries: int) -> float:
"""Plausible cluster-level volume in units (millions)."""
size = REGION_SIZE.get(region, 0.5)
return float(np.round(2.0 * size * (0.6 + 0.4 * n_countries) * RNG.uniform(0.7, 1.5), 2))
# ---------------------------------------------------------------------------
# Main seeding loop
# ---------------------------------------------------------------------------
def seed_parameters() -> None:
"""Replace SQLite parameter tables with the contents of the CSV seeds."""
print(" → seeding parameter tables …")
replace_param_table("cluster_mapping", _load_csv("cluster_mapping.csv"))
replace_param_table("market_summary_mapping", _load_csv("market_summary_mapping.csv"))
replace_param_table("threshold_matrix", _load_csv("threshold_matrix.csv"))
replace_param_table("country_region_mapping", _load_csv("country_region_mapping.csv"))
replace_param_table("company_products", _load_csv("company_products.csv"))
def seed_metrics(batch_size: int = 5000) -> int:
"""Generate synthetic metrics rows from cluster_mapping and stream into SQLite."""
cluster_df = _load_csv("cluster_mapping.csv")
buffer: list[dict] = []
total = 0
def flush() -> None:
nonlocal total
if not buffer:
return
n = insert_metrics(pd.DataFrame(buffer))
total += n
buffer.clear()
# Truncate metrics first (replace_metrics deletes then no-ops on empty input)
replace_metrics(pd.DataFrame())
for _, row in cluster_df.iterrows():
market = str(row["market"]).strip()
cluster = str(row["cluster"]).strip()
region = str(row["region"]).strip()
if market not in MARKET_PRODUCTS:
continue
countries = _split_countries(row["country"])
if not countries:
continue
country_csv = ", ".join(countries)
for calc_type in CALCULATION_TYPES:
for period in PERIODS:
base_value_cluster = _base_value(region, len(countries))
base_volume_cluster = _base_volume(region, len(countries))
# Cluster-level rows
buffer.extend(_make_product_rows(
period=period,
region=region,
level="Cluster",
country_field=country_csv,
cluster_field=cluster,
market=market,
calc_type=calc_type,
base_value=base_value_cluster,
base_volume=base_volume_cluster,
))
# Country-level rows, splitting the cluster total across countries
country_weights = RNG.dirichlet(alpha=np.ones(len(countries)) * 2.0)
for c_idx, country in enumerate(countries):
weight = float(country_weights[c_idx])
buffer.extend(_make_product_rows(
period=period,
region=region,
level="Country",
country_field=country,
cluster_field=country,
market=market,
calc_type=calc_type,
base_value=base_value_cluster * weight,
base_volume=base_volume_cluster * weight,
))
if len(buffer) >= batch_size:
flush()
flush()
return total
def main() -> None:
print("Creating SQLite schema …")
create_all_tables()
seed_parameters()
print("Generating synthetic metrics rows (this takes a few seconds) …")
total_rows = seed_metrics()
print(f"Done. Inserted {total_rows:,} rows into sentinel_metrics.")
print(f"Database: {Path('demo.sqlite').resolve()}")
if __name__ == "__main__":
main()