Skip to content

English · Español

Lab 02 — Escribe validate_corpus.py y split_corpus.py

Objetivo: valida la salida del generador contra las 12 reglas de la teoría 02. Luego divide en train/val/test por (verbo, tiempo). Emite el manifest final.

Tiempo estimado: 3–4 horas.

Prerrequisito: lab 01 commiteado; data/raw/all_rows.jsonl existe.


Lo que produces

  • scripts/validate_corpus.py — ejecuta cada verificación de la teoría 02. Emite data/raw/validation_report.json.
  • scripts/split_corpus.py — produce data/processed/{train,val,test}.jsonl.
  • scripts/build_manifest.py (o el mismo script validate extendido) — emite data/MANIFEST.json según la teoría 03.
  • tests/test_validation.py — tests unitarios sobre una pequeña fixture.
  • tests/test_split.py — tests unitarios sobre una pequeña fixture.

TODOs

Bloque A — scripts/validate_corpus.py

  • CLI: argparse con --input (default data/raw/all_rows.jsonl), --spec (default data/corpus_spec.md), --report (default data/raw/validation_report.json).
  • Carga todas las filas.
  • Ejecuta las 12 verificaciones de la teoría 02 (numeradas abajo). Cada verificación es su propia función; agrega resultados.

Las 12 verificaciones:

  1. Validez del esquema. Cada fila parsea contra el JSONSchema en data/corpus_spec.md. Usa jsonschema (ya fijado según pyproject.toml).
  2. Sin duplicados exactos. Cada fingerprint es único a través de todas las filas.
  3. Cobertura de celda. Las 360 celdas (verbo, tiempo, persona) tienen ≥ 1 fila correct. (Enumera el producto cruzado; verifica pertenencia.)
  4. Los 20 verbos presentes. set(row.verb_lemma for row in rows) == VERB_TABLE_SET (los 20 lemas).
  5. Las 6 superficies de tiempo presentes por verbo. Para cada verbo, la unión de valores tense a través de sus filas es las 6 completas.
  6. Las 3 personas presentes por (verbo, tiempo). Nota: para el tiempo infinitive, esta restricción se relaja — to work es el mismo para las 3 personas. Decisión: emite una fila por (verbo, infinitive, persona) de todas formas (según spec), o una fila por (verbo, infinitive) compartida entre personas. v1: una fila por (verbo, infinitive, 1sg) solamente, sin variación por persona. Documenta en spec; el validador permite la relajación para infinitive.
  7. Tipos de mis-conjugación de la taxonomía canónica. Cada mis_conjugation_type no-null ∈ la taxonomía de 6 tipos.
  8. Las filas de mis-conjugación tienen correct_form poblado. label == "mis_conjugated"correct_form no-null y no vacío.
  9. Las filas correctas tienen mis_conjugation_type = null. label == "correct"mis_conjugation_type is None and correct_form is None.
  10. Cada fila tiene un campo spanish. No vacío.
  11. Normalización NFC. Para cada fila, unicodedata.is_normalized('NFC', text) y unicodedata.is_normalized('NFC', spanish).
  12. Longitud de texto en rango. 2 <= len(text.encode('utf-8')) <= 30 para inglés; 2 <= len(spanish.encode('utf-8')) <= 40 para español.

  13. Cada verificación puebla un dict de resultado con passed: bool, failures: list[row_id], message: str.

  14. Emite validation_report.json resumiendo las 12.
  15. Código de salida 0 si todas pasan, 1 en caso contrario.

Bloque B — scripts/split_corpus.py

  • CLI: argparse con --input (default data/raw/all_rows.jsonl), --output-dir (default data/processed/), --seed (default 42), --ratios (default 0.8,0.1,0.1).
  • Llama seed_everything(args.seed).
  • Bucketiza filas por par (verb_lemma, tense).
  • Ordena las claves del bucket deterministamente (alfabético) antes de barajar.
  • Baraja las claves del bucket (con semilla).
  • Slice: 80% → train_keys, siguiente 10% → val_keys, último 10% → test_keys.
  • Asigna cada fila a su split basándose en su clave.
  • Escribe data/processed/{train,val,test}.jsonl (un objeto JSON por línea).
  • Verifica la invariante post-split: ningún fingerprint aparece en dos splits (guardia de cordura).
  • Verifica la invariante de (verbo, tiempo): cada (verbo, tiempo) aparece en exactamente un split.
  • Emite data/processed/split_log.json con conteo de filas por split, asignación por (verbo, tiempo) y la semilla.

Bloque C — scripts/build_manifest.py

  • Computa SHA256 de cada data/processed/*.jsonl.
  • Cuenta filas por celda (verbo × tiempo × persona × etiqueta).
  • Cuenta mis-conjugaciones por tipo.
  • Lee versiones (Python, NumPy, versión de corpus_spec desde el preámbulo de data/corpus_spec.md).
  • Emite data/MANIFEST.json según el esquema en la teoría 03.

Bloque D — tests/test_validation.py

  • Fixture: una pequeña lista en memoria de ~10 filas incluyendo 2 correctas + 1 mis-conjugada para (work, present_simple), similar para (go, past_simple).
  • Testea cada una de las 12 verificaciones: construye la fixture para pasar, luego introduce una violación deliberada por verificación y asegura que esa verificación falla.
  • Testea que una fixture limpia produce las 12 con passed=True.
  • Testea que una fixture sucia (p. ej., español codificado en NFD) es detectada por la verificación 11.

Bloque E — tests/test_split.py

  • Fixture: filas con 4 pares (verbo, tiempo).
  • Testea que el splitting cumpla la invariante de (verbo, tiempo).
  • Testea reproducibilidad: misma semilla → mismo split (verifica IDs de filas).
  • Testea aproximación de ratios: con 100 pares (verbo, tiempo) y 80/10/10, los splits obtienen 80 / 10 / 10 claves.
  • Testea que las mis-conjugaciones siguen a su (verbo, tiempo) al mismo split.

Bloque F — cordura end-to-end

  • Ejecuta el pipeline: python scripts/gen_corpus.py --seed 42 && python scripts/validate_corpus.py && python scripts/split_corpus.py --seed 42 && python scripts/build_manifest.py.
  • Inspecciona data/MANIFEST.json. Verifica los conteos por celda, los totales, los splits.
  • Ejecuta dos veces; asegura que los SHA256s coincidan. De lo contrario el pipeline no es determinista.

Restricciones

  • Python puro. Biblioteca estándar + jsonschema.
  • mypy --strict limpio.
  • ruff limpio.
  • bandit limpio — sin shells de subprocess, sin eval.
  • Determinismo. Tanto gen_corpus.py como split_corpus.py son independientemente seedables y reproducibles.

Condiciones de parada

Hecho cuando:

  1. Los tres scripts + tests commiteados.
  2. pytest -q tests/test_validation.py tests/test_split.py verde.
  3. python scripts/validate_corpus.py sale con código 0 sobre la salida del lab 01.
  4. data/processed/{train,val,test}.jsonl existen; los conteos de filas por split suman el conteo de filas de entrada.
  5. data/MANIFEST.json existe con las 12 verificaciones registradas como passed.
  6. Re-ejecutar el pipeline produce SHA256s idénticos.

Escollos

  • Verificación NFC en el campo incorrecto. Recuerda verificar tanto text como spanish (y correct_form si no-null).
  • Clave de orden para shuffle. random.shuffle(list(some_set)) de Python es no-determinista entre ejecuciones porque el orden de iteración de set es no-determinista. Siempre sorted(set(...)) primero, luego baraja.
  • Ratios de split que no redondean a enteros. Con 120 pares (verbo, tiempo) y 80/10/10, obtienes 96/12/12. Con 99 pares, obtienes 79/9/11 — el off-by-one importa. Usa int(n * ratio) consistentemente y documenta el redondeo.
  • Off-by-one en verificación de longitud. ¿Debería len('I work'.encode('utf-8')) = 6 pasar? Sí (≥ 2). ¿Debería len('to work'.encode('utf-8')) = 7 pasar? Sí. Spanish vacío: falla. Testea casos límite.
  • Paths del manifest. Usa pathlib.Path y escribe paths relativos a la raíz del repo, no absolutos. De lo contrario el manifest no es portable.

Pista de último recurso

Si llevas 3 horas y los hashes del manifest no se reproducen: la causa más probable es el orden de iteración de dict de Python. JSON-serializa con sort_keys=True en todas partes. Verifica que json.dumps(d, sort_keys=True) da bytes idénticos entre ejecuciones.

Cuándo consultar solutions/

Tras que todos los tests pasen y las ejecuciones end-to-end se reproduzcan. Solución: solutions/02-validate-and-split-ref.md (apertura de fase).


Siguiente lab: lab/03-version-with-dvc.md.