SPB Git

spb/airiskindex Public

The most methodologically rigorous, fully transparent AI job-exposure index.

TypeScript 88% Python 6.1% SQL 2.7% CSS 1.2% JavaScript 0.9% Shell 0.8%
3.0 KB · 86 lines python
Raw Blame History
1#  File:    load.py2#  Path:    apps/etl/src/airiskindex_etl/load.py3#  Project: AI Risk Index — airiskindex.io4#  Author:  Simon-Pierre Boucher5#  Contact: contact@spboucher.ai6#  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.7#8#  Description: Load derived CSVs into Postgres (idempotent upserts).910"""Load derived O*NET + OEWS CSVs into Postgres (idempotent upserts).1112Occupations are keyed by O*NET-SOC code; wages join by SOC prefix13("15-1252.00" -> "15-1252"). Demo-seeded rows are overwritten by real data14where codes collide, which is the desired direction of truth.15"""1617from __future__ import annotations1819import csv20import os21from pathlib import Path2223import psycopg2425REPO_ROOT = Path(__file__).resolve().parents[4]26DERIVED = REPO_ROOT / "data" / "derived" / "onet"272829def read(name: str) -> list[dict[str, str]]:30    with (DERIVED / name).open(encoding="utf-8", newline="") as fh:31        return list(csv.DictReader(fh))323334def main() -> None:35    dsn = os.environ["DATABASE_URL"]36    occupations = read("occupations.csv")37    tasks = read("tasks.csv")38    wages = {row["soc"]: row for row in read("wages.csv")}3940    with psycopg.connect(dsn) as conn, conn.cursor() as cur:41        for occ in occupations:42            wage = wages.get(occ["code"].split(".")[0], {})43            median = wage.get("median_wage_cents") or None44            employment = wage.get("employment") or None45            cur.execute(46                """47                INSERT INTO "Occupation" (code, title, description, "medianWageCents",48                                          "wageCurrency", employment)49                VALUES (%s, %s, %s, %s, 'USD', %s)50                ON CONFLICT (code) DO UPDATE SET51                  title = EXCLUDED.title,52                  description = EXCLUDED.description,53                  "medianWageCents" = COALESCE(EXCLUDED."medianWageCents", "Occupation"."medianWageCents"),54                  employment = COALESCE(EXCLUDED.employment, "Occupation".employment)55                """,56                (occ["code"], occ["title"], occ["description"], median, employment),57            )5859        for task in tasks:60            cur.execute(61                """62                INSERT INTO "Task" (id, "occupationCode", statement, importance)63                VALUES (%s, %s, %s, %s)64                ON CONFLICT (id) DO UPDATE SET65                  statement = EXCLUDED.statement,66                  importance = EXCLUDED.importance67                """,68                (69                    task["task_id"],70                    task["code"],71                    task["statement"],72                    float(task["importance"]) if task["importance"] else None,73                ),74            )7576        cur.execute('SELECT count(*) FROM "Occupation"')77        occ_count = cur.fetchone()[0]78        cur.execute('SELECT count(*) FROM "Task"')79        task_count = cur.fetchone()[0]8081    print(f"loaded: {occ_count} occupations, {task_count} tasks (incl. pre-existing)")828384if __name__ == "__main__":85    main()86