portal-synthetic-data
Generate reproducible synthetic financial silos (creditos, liquidez, derivados) with Polars + Faker, injected anomalies, catalog and user seeds. Use when writing ml/data/generators.py, the make data t
SKILL.md.claude/skills/portal-synthetic-data/SKILL.md
GitHubPortal Synthetic Data Skill
Generators: ! ls ml/data/generators.py 2>/dev/null || echo "not created yet"
Rules — NON-NEGOTIABLE
- SIEMPRE semilla fija (
SEED = 20260720) en Polars, Faker ynumpy.random:make datadebe producir bytes idénticos entre corridas (base de las consultas de referencia de la capa semántica). - Esquemas crípticos y heterogéneos A PROPÓSITO: cada silo nombra distinto el mismo concepto (
cli_refvsid_clientevsctpty_cd) — sin esto la capa semántica y el catálogo no tienen razón de ser. - IDs de cliente COMPARTIDOS entre silos (mismo pool) para habilitar joins por contraparte.
- ~0.1 % de anomalías inyectadas deliberadamente (fechas imposibles, montos negativos, duplicados) y DOCUMENTADAS en
data/README.mdcon conteos exactos. - Volúmenes objetivo:
creditos1–5 M filas,liquidez~1 M,derivados~500 K → Parquet endata/silos/*.parquet.data/está en.gitignore(solo el README versionado). - Honestidad de demo: los datos son sintéticos y se declaran como tales; cualquier previsión derivada se etiqueta "proyección simulada".
- Seed relacional: catálogo 200–400 entradas + ~30 notas tribales curadas; 7 usuarios (1 admin + 2 por perfil) con Argon2 — vía migraciones dbmate (skill
portal-db-migrations).
Esquemas por silo (crípticos, heterogéneos)
| Silo | Columnas (físicas) | Concepto compartido |
|---|---|---|
creditos |
cli_ref, prod_cd, sdo_cap, dias_mora, tasa_pct, f_apert, f_venc |
cliente = cli_ref |
liquidez |
fec_pos, bucket_venc, mto_disp, ratio_lcr, id_cliente |
cliente = id_cliente |
derivados |
op_id, subyacente, nocional_usd, ctpty_cd, mtm_val, f_trade |
cliente = ctpty_cd |
Generador (ml/data/generators.py)
import numpy as np
import polars as pl
from faker import Faker
SEED = 20260720
N_CLIENTS = 50_000
ANOMALY_RATE = 0.001
fake = Faker("es_MX")
Faker.seed(SEED)
rng = np.random.default_rng(SEED)
def client_pool() -> np.ndarray:
"""Shared client IDs across silos (enables counterparty joins)."""
return np.array([f"C{100000 + i}" for i in range(N_CLIENTS)])
def generate_creditos(n_rows: int = 2_000_000) -> pl.DataFrame:
clients = client_pool()
df = pl.DataFrame({
"cli_ref": rng.choice(clients, n_rows),
"prod_cd": rng.choice(["HIP", "AUT", "PYM", "TDC", "PER"], n_rows),
"sdo_cap": rng.lognormal(11.5, 1.2, n_rows).round(2),
"dias_mora": rng.choice([0, 0, 0, 0, 15, 30, 60, 90, 180], n_rows),
"tasa_pct": rng.uniform(6.0, 42.0, n_rows).round(2),
"f_apert": pl.date_range(date(2018, 1, 1), date(2026, 6, 30), eager=True)
.sample(n_rows, with_replacement=True, seed=SEED),
})
return inject_anomalies(df, silo="creditos")
Inyección de anomalías (documentada)
def inject_anomalies(df: pl.DataFrame, silo: str) -> pl.DataFrame:
"""Inject ~0.1% deliberate anomalies; counts are logged for data/README.md."""
n = int(df.height * ANOMALY_RATE)
idx = rng.choice(df.height, n, replace=False)
third = n // 3
df = df.with_row_index("_i")
df = df.with_columns( # negative amounts
pl.when(pl.col("_i").is_in(idx[:third]))
.then(-pl.col("sdo_cap")).otherwise(pl.col("sdo_cap")).alias("sdo_cap"),
).with_columns( # impossible dates
pl.when(pl.col("_i").is_in(idx[third:2 * third]))
.then(pl.date(2099, 2, 30, strict=False)).otherwise(pl.col("f_apert"))
.alias("f_apert"),
).drop("_i")
dupes = df.sample(n - 2 * third, seed=SEED) # exact duplicates
logger.info("anomalies_injected", silo=silo,
negative=third, bad_dates=third, duplicates=n - 2 * third)
return pl.concat([df, dupes])
make data
data:
poetry run python -m ml.data.generators --out data/silos/
poetry run python -m ml.data.seed_catalog # 200-400 catalog entries from schemas
@echo "Silos written to data/silos/ (fixed seed, see data/README.md)"
# ml/data/generators.py __main__: writes parquet deterministically
generate_creditos().write_parquet(out / "creditos.parquet")
generate_liquidez().write_parquet(out / "liquidez.parquet")
generate_derivados().write_parquet(out / "derivados.parquet")
Seed del catálogo desde los esquemas
# ml/data/seed_catalog.py — one catalog_field per physical column + curated tribal notes
FIELD_DEFS = {
("creditos", "sdo_cap"): ("saldo de capital", "Outstanding principal balance", "sum"),
("liquidez", "ratio_lcr"): ("ratio LCR", "Liquidity coverage ratio (LCR-like)", "mean"),
("derivados", "mtm_val"): ("mark to market", "MtM valuation of the trade", "sum"),
# ... 200-400 entries generated from silo schemas + manual curation
}
TRIBAL_NOTES = [
("liquidez", "fec_pos", "La fecha valor es T+1 respecto a la fecha de posicion",
"aplica solo a posiciones de mercado local"),
# ... ~30 curated notes with Tk-Boost applicability condition
]
data/README.md obligatorio
Debe documentar: semilla, volúmenes generados, conteo exacto de anomalías por tipo y silo, mapa de columnas crípticas → nombre de negocio, y la advertencia de datos sintéticos (honestidad de demo).