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.jsonlexiste.
Lo que produces¶
scripts/validate_corpus.py— ejecuta cada verificación de la teoría 02. Emitedata/raw/validation_report.json.scripts/split_corpus.py— producedata/processed/{train,val,test}.jsonl.scripts/build_manifest.py(o el mismo scriptvalidateextendido) — emitedata/MANIFEST.jsonsegú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:
argparsecon--input(defaultdata/raw/all_rows.jsonl),--spec(defaultdata/corpus_spec.md),--report(defaultdata/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:
- Validez del esquema. Cada fila parsea contra el JSONSchema en
data/corpus_spec.md. Usajsonschema(ya fijado segúnpyproject.toml). - Sin duplicados exactos. Cada
fingerprintes único a través de todas las filas. - Cobertura de celda. Las 360 celdas (verbo, tiempo, persona) tienen ≥ 1 fila
correct. (Enumera el producto cruzado; verifica pertenencia.) - Los 20 verbos presentes.
set(row.verb_lemma for row in rows) == VERB_TABLE_SET(los 20 lemas). - Las 6 superficies de tiempo presentes por verbo. Para cada verbo, la unión de valores
tensea través de sus filas es las 6 completas. - Las 3 personas presentes por (verbo, tiempo). Nota: para el tiempo
infinitive, esta restricción se relaja —to workes 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 parainfinitive. - Tipos de mis-conjugación de la taxonomía canónica. Cada
mis_conjugation_typeno-null ∈ la taxonomía de 6 tipos. - Las filas de mis-conjugación tienen
correct_formpoblado.label == "mis_conjugated"⇒correct_formno-null y no vacío. - Las filas correctas tienen
mis_conjugation_type = null.label == "correct"⇒mis_conjugation_type is None and correct_form is None. - Cada fila tiene un campo
spanish. No vacío. - Normalización NFC. Para cada fila,
unicodedata.is_normalized('NFC', text)yunicodedata.is_normalized('NFC', spanish). -
Longitud de texto en rango.
2 <= len(text.encode('utf-8')) <= 30para inglés;2 <= len(spanish.encode('utf-8')) <= 40para español. -
Cada verificación puebla un dict de resultado con
passed: bool,failures: list[row_id],message: str. - Emite
validation_report.jsonresumiendo las 12. - Código de salida
0si todas pasan,1en caso contrario.
Bloque B — scripts/split_corpus.py¶
- CLI:
argparsecon--input(defaultdata/raw/all_rows.jsonl),--output-dir(defaultdata/processed/),--seed(default 42),--ratios(default0.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.jsoncon 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.jsonsegú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 --strictlimpio.rufflimpio.banditlimpio — sin shells desubprocess, sineval.- Determinismo. Tanto
gen_corpus.pycomosplit_corpus.pyson independientemente seedables y reproducibles.
Condiciones de parada¶
Hecho cuando:
- Los tres scripts + tests commiteados.
pytest -q tests/test_validation.py tests/test_split.pyverde.python scripts/validate_corpus.pysale con código 0 sobre la salida del lab 01.data/processed/{train,val,test}.jsonlexisten; los conteos de filas por split suman el conteo de filas de entrada.data/MANIFEST.jsonexiste con las 12 verificaciones registradas como passed.- Re-ejecutar el pipeline produce SHA256s idénticos.
Escollos¶
- Verificación NFC en el campo incorrecto. Recuerda verificar tanto
textcomospanish(ycorrect_formsi 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. Siempresorted(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')) = 6pasar? Sí (≥ 2). ¿Deberíalen('to work'.encode('utf-8')) = 7pasar? Sí. Spanish vacío: falla. Testea casos límite. - Paths del manifest. Usa
pathlib.Pathy 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.