Changelog¶
Formato basado en Keep a Changelog;
el proyecto sigue SemVer: desde 1.0, el pipeline de scorecard (F1)
es API estable; las superficies que aún crecen (modelado ML, provisiones, forward-looking,
contratos transversales) quedan marcadas como experimentales, fuera de la garantía SemVer 2.x.
Hasta la 1.20.0 la librería se publicó como nikodym; desde la 2.0.0 se llama bayesrisk, y las
entradas anteriores conservan el nombre con el que se publicaron.
[2.0.0] — 2026-09-27¶
nikodym ahora se llama bayesrisk. Esta versión es equivalente funcional a nikodym 1.20.0:
con la misma configuración da los mismos resultados, bit a bit, y el mismo config_hash. Medido
sobre la proyección canónica completa de dos corridas —el preset de scorecard y una cartera real de
49.999 préstamos—: cero diferencias de cálculo; sólo cambian los nombres. Es una versión mayor
porque cambia el nombre de la distribución; ninguna API cambia de comportamiento.
Cómo migrar¶
Una línea:
e instalar con los mismos extras: pip install "bayesrisk[scoring,report,ui]". La guía completa
está en Migrar desde nikodym.
Cambiado¶
- Paquete, distribución e imports:
bayesrisk. La interfaz local se abre conbayesrisk-ui(opython -m bayesrisk.ui). - Clases con la marca:
BayesRiskConfig,BayesRiskBaseConfig,BayesRiskError,BayesRiskClassifier,BayesRiskTransformeryBaseBayesRiskEstimator. Los nombres anteriores (NikodymConfig,NikodymError…) siguen funcionando en toda la serie 2.x: son el mismo objeto. - Nombres que cambian en lo que la librería escribe: la clave
bayesriskdelibrary_versionsen el linaje y enenvironment.json(antesnikodym; al cargar un estudio viejo se compara con la versión de bayesrisk);bayesrisk_versionen el linaje deapplyy en los estimadores guardados consave; el tema del informebayesrisk(un config contheme: nikodymsigue siendo válido y se lee igual); la clavebayesrisk:del encabezado del informe editable.qmd; la clase CSSbayesrisk-summaryde los resúmenes en un notebook. - Carpetas por defecto:
bayesrisk-runsenbayesrisk.Scorecardy.bayesrisk_uien la interfaz. Para seguir con las anteriores:run_dir="nikodym-runs"ybayesrisk-ui --workdir .nikodym_ui. - Marca: bayesrisk, de Bayes Advisory. Documentación en https://docs.bayesadvisory.cl y demo en https://demo.bayesadvisory.cl.
- Garantía de estabilidad: pasa a la serie 2.x; el pipeline de scorecard no rompe hasta un 3.0.
Los tres alias deprecados que avisaban que se retirarían «en 2.0» —
model.engine="glm_binomial",report.formatscon"html"yselection.priority_ordercon"gini"— se siguen aceptando igual y ahora avisan que se retiran en 3.0: esta versión no retira nada.
Sin cambios, a propósito¶
- Ningún número, ningún
config_hash, ningúndata_hash. Las identidades internas que llevan la palabra «nikodym» —la personalización del hash que reparte las filas entre muestras, el prefijo dedata_hash, los formatos y hashes del bundle del scorecard y del lote, la versión del prompt de la narración— se conservan tal cual: cambiarlas habría movido resultados o hashes publicados. - Tu registro de MLflow: los tags
nikodym.*y los nombres por defectonikodym-studyynikodym-modelse mantienen, para que repetir una corrida no registre un modelo duplicado. - Lo que ya guardaste sigue cargando: los estudios y estimadores guardados con
joblibcargan con bayesrisk y nikodym 1.21 instalados juntos, y los bundles del scorecard de nikodym 1.19 y 1.20 se aplican con bayesrisk 2.0 sin reentrenar (misma fuente de dependencias).
nikodym 1.21.0, el último release con ese nombre¶
Capa de compatibilidad sin código propio: depende de bayesrisk>=2.0,<3, reexporta todo, avisa una
sola vez con un DeprecationWarning y mantiene funcionando import nikodym, sus submódulos,
nikodym-ui, python -m nikodym.ui y los pickles de nikodym ≤ 1.20. Trae los mismos extras que
bayesrisk, así que pip install "nikodym[scoring]" sigue instalando lo mismo. No recibirá mejoras.
Sabido¶
- La poda de opciones que se aprobó para «un 2.0» no entra en esta versión, que es equivalente funcional por diseño: queda para la próxima versión mayor.
[1.20.0] — 2026-09-27¶
Añadido¶
- La corrida puntúa a la población through-the-door que no entra al ajuste (D-TTD-1…5). Las
operaciones fuera del ajuste —indeterminadas, excluidas o con desenlace que la partición dejó
fuera— reciben puntaje y PD con la tarjeta y la calibración ya ajustadas, sin reajustar nada,
siempre que la población total (TTD) las incluya. Viajan en cuatro claves nuevas
(
out_of_model_*enbinning,model,scorecardycalibration), que el informe entrega como adjuntos cuando tienen filas. El resumen de la tarjeta dice cuántas son y su puntaje medio; el de calibración, su PD media y la de toda la población que pidió crédito; el de estabilidad, un PSI de representatividad frente a Desarrollo, con los cortes de la corrida y fuera del veredicto. Las categorías que no existían en Desarrollo se cuentan por muestra y «Tramos y WoE» lo avisa. Puntuar esas filas nunca detiene la corrida. Ningún número existente cambia ni se agrega una opción. Con la muestra SBA: 6.225 préstamos sin desenlace puntuados, que el modelo ve con menor riesgo que Desarrollo (PSI 0,162). - El notebook mínimo, como cuaderno de verdad:
docs_site/notebooks/primer-scorecard.ipynb, enlazado desde «Instalación y primeros pasos» y descargable desde docs.nikodym.cl. Es el mismo flujo de las guías —datos, corrida completa, una etapa por dentro, una decisión humana conexcludeyresume, el resumen final y los once libros de Excel— con sus salidas reales, en 22 líneas de usuario. Se ejecuta entero en la integración continua —en el job con todos los extras, porque termina exportando a Excel—, sin añadir jupyter a las dependencias: un gate corre sus celdas en orden como lo haría un kernel y compara cada salida guardada con la de la ejecución, así que una cifra vieja publicada pone el CI en rojo; comprueba además que su flujo es el del notebook mínimo publicado, que no trae celdas en error y que no filtra rutas de la máquina en la que se generó (las salidas muestran rutas relativas a la carpeta del cuaderno).
Cambiado¶
- El informe escribe sus cifras en español (es-CL), y un redondeo ya no se hace pasar por un
corte. Las tablas, los anexos, los gráficos y la página ejecutiva del HTML, el PDF, el Word y
la fuente
.qmdusaban punto decimal y seis decimales fijos (0.042677),true/falsey conteos sin agrupar. Ahora usan coma decimal y cuatro decimales (0,0427), miles con punto en los conteos (30.316),sí/no, dos cifras significativas bajo 0,001 (nunca0,0000para algo que no es cero) y p-valores< 0,001. Una cifra redondeada que termina en cero y no es exacta lleva los decimales que hagan falta: un PSI de 0,24996 ya no se escribe0,2500junto a su corte de 0,25. Los cortes del config se escriben exactos (0,125, no0,12). Las columnas numéricas van alineadas a la derecha. Sólo presentación:results, los exports CSV/XLSX, los libros de Excel y elconfig_hashno cambian. Revierte la excepción de 1.4.0 que dejaba las tablas en punto decimal: las cifras crudas están enresults.json, enexport_excel()y en las tablas de la puerta guiada. - El informe usa el ancho de una pantalla grande y se lee en un teléfono. La caja crece a 1.920 px (antes 1.360), el índice de la derecha aparece desde 1.600 px, una tabla toma el ancho de su contenido y un identificador se corta en sus guiones bajos. En el informe de una corrida real, las tablas que se desplazan dentro de su caja pasan de 10 a 3 a 1.440 px y de 10 a 1 a 2.560 px; en un teléfono de 375 px la página ya no se desplaza hacia el lado (medía 825 px). El PDF conserva su maquetación.
Corregido¶
- En el informe HTML, una cifra ya no se parte carácter por carácter. Las celdas podían
partirse en cualquier punto, y el navegador angostaba una columna hasta el ancho de una letra:
bajo un encabezado corto (
iv,js),0.094802salía en seis renglones. En el informe de una corrida real eran 874 celdas en 23 tablas, y 765 aun en una pantalla de 2.560 px. Ahora, en pantalla, una celda se parte sólo entre palabras y una tabla que no cabe se desplaza dentro de su caja. En el PDF no cambia nada: ahí, sin desplazamiento posible, la celda sigue partiéndose para no invadir la vecina. - Las tablas de
sc.results[...]ysc.bins(...)se ven como las lee una persona. En el notebook y en la consola salían crudas —0.547746,None,NaN, punto decimal— al lado de un resumen que decía0,548. Ahora son unDataFramecon los números intactos para calcular que se muestra con la misma regla del resumen de su etapa y de la pantalla: coma decimal, miles, porcentajes y «—» en las ausencias, sin el índice. - El resumen de datos dice qué pasa con lo que queda fuera del ajuste. Nombra sus tres grupos y, desde D-TTD, que la tarjeta los puntúa aparte; con la TTD sin excluidos, que no se puntúan.
- Lo que la prueba real con el SBA mostró mal escrito (D-CPY-1…6). La partición
fuera_de_modelose lee «Fuera del ajuste» —también en la tabla de particiones del informe— y sus malos son «—» cuando no hay desenlace conocido, no cero. La línea de validación nombra la variable de cada CSI y el eje temporal («PSI temporal por período»), y cada Hosmer-Lemeshow que falla suma la brecha media agregada (sin cambiar el veredicto). Los tramos categóricos se leen «2001, 2000», no['2001' '2000'], y los rangos con comparadores en es-CL desde los bordes efectivos del motor (≥ 50.450 y < 102.230,5), exactos: la etiqueta de OptBinning los redondeaba a dos decimales. Un ajuste manual de puntos casa con la etiqueta del motor o con ese rótulo; uno que no casa ya no se ignora en silencio: lo avisan el trail y el resumen. Los p-valores de las tablas dicen «< 0,001» y las guías del sitio escriben los decimales con coma. El cuaderno publicado se regeneró; las cifras del modelo no cambian. - Un bin de faltantes sin incumplimientos ya no mata la corrida (D-FAL-1/2). Con una muestra
pública de préstamos 7(a) de la SBA partida por fecha, ocho préstamos no declaraban la
antigüedad de la empresa y en desarrollo su bin de faltantes quedaba con 4 operaciones y
ningún malo: el WoE no existía y la corrida moría en «Tramos y WoE». Ahora el motor le asigna el
WoE del tramo de la misma variable con la mayor tasa de malos observada —el conservador: un
grupo sin evidencia no se lleva el mejor puntaje—, con IV 0 en su fila, y lo mismo para un bin
de valores especiales sin una clase. Tabla, transformación, puntos y bundle leen el mismo
número; el bin asignado comparte los puntos de su tramo de referencia, hereda su ajuste manual
y rechaza uno propio. La asignación queda en el trail (
bin_sin_clase_asignado), en la card debinning(assigned_bins, aditivo) y en una línea del resumen. Con un valor declarado enmetric_missing/metric_specialnada cambia; ninguna corrida que hoy termina cambia un número. exclude()excluye de verdad (D-EXC-1). Escribía sólo la lista forzada de la selección, así que una variable que tumbaba el binning seguía tumbándolo aunque se excluyera. Ahora escribebinning.exclude_columnsy retira la variable de las listas forzadas de selección y modelo; sus tramos fijados conset_bins/merge_binsquedan en suspenso ykeep()los reactiva. Una variable excluida ya no aparece en «Tramos y WoE» ni en la tabla de selección; la decisión, con su motivo, sigue en el trail. Los mensajes de error que ofrecen excluir dicen cómo. El cuadernoprimer-scorecard.ipynbse regeneró: la variable que excluye ya no figura en esas dos etapas y las cifras del modelo no cambian.- Una cartera sin columna de fecha ya no mata la corrida en el análisis exploratorio
(D-SC-17/18). Con
partition="random"—o con cualquier config que agrupe la tasa por período sin indicar la columna— sobre un archivo que no trae ninguna columna de fecha, la corrida moría eneda, la segunda etapa del pipeline, y se llevaba por delante las nueve siguientes: sin scorecard, sin PD, sin informe y sin ficha. Era justamente el caso que el contrato declaraba soportado. Ahora el motor degrada: la tasa de incumplimiento en el tiempo se publica «No evaluable» con su causa —el mismo mecanismo que la estabilidad ya tenía—, la tabla por período sale vacía, la tasa global de la población se calcula igual y el resto del análisis exploratorio —el perfil por tramo de cada variable y la calidad de datos por columna— se hace completo. La decisión queda en el registro de auditoría; el resumen de la etapa, el panel de Resultados y los capítulos «Contexto» y «Resultados» del informe lo dicen en palabras, sin publicar ni una tabla vacía ni las frases que dependían del eje. En la pantalla, la opción «Por la fecha de observación» deja de estar marcada como que exige otro campo —ya no detiene nada— y su aviso pasa a la ayuda de la opción. Nada cambia para una corrida que hoy termina: con el mismo config los resultados son bit a bit los de antes y ningúnconfig_hashse mueve. - El análisis exploratorio ya no detiene la corrida por ningún error (D-SC-19/20). La
corrección anterior dejaba como errores fatales las contradicciones con lo declarado, y el
archivo de banco más común —fecha de originación y fecha de corte— moría igual en
edaa los 3,5 s con «más de una columna datetime». El análisis exploratorio es descriptivo y ninguna etapa del modelo depende de él, así que ahora cada una de sus partes falla por separado: la tasa en el tiempo, la señal de deterioro, la descripción de las columnas y la revisión de calidad. La que no se pudo calcular sale «no se pudo calcular» con la causa del motor, el resto se hace igual, la tasa global se conserva siempre que haya población y la corrida sigue hasta el informe. Nada de lo que falló se publica como resultado negativo: ni «ninguna marca de calidad», ni «0 columnas descritas», ni «estable» en el canal de métricas. Cada falla va al registro de auditoría, al resumen de la etapa como aviso, a «Qué revisar» del resumen final, al panel de Resultados y al informe; la corrida se da por «completada — con el análisis exploratorio parcial» y la página ejecutiva del informe deja de decir «sin fallos». Un error que no sea del análisis —un defecto del motor— también degrada, pero se publica con su tipo. La verificación previa deja de predecir un corte que ya no ocurre: una columna deedaque falta ya no hace «incompatible» el archivo, y la opción «Por cohorte o añada» deja de estar marcada como que exige otro campo. Las piezas usadas por código —DefaultRateAnalyzer,UnivariateProfiler,DataQualityProfiler— siguen levantando su error de siempre. Nada cambia para una corrida que hoy termina: con el mismo config los resultados son bit a bit los de antes y ningúnconfig_hashse mueve. - Una categoría rara que se queda sola y sin incumplimientos ya no mata la corrida en «Tramos y
WoE» (D-RAR-1/2). El umbral de categorías raras (0,01 de fábrica) podía dejar un solo nivel
por debajo —el grupo de «raras» que debía protegerlo quedaba igual de solo— y, si ese nivel no
tenía un solo incumplimiento en desarrollo, su WoE no existía y la corrida moría. Le pasaba a UCI
German Credit (
proposito, nivelA48: 5 operaciones, ninguna incumplida). Ahora el motor lo reagrupa una vez con el menor umbral que deja dos niveles debajo, sólo en esa columna, lo registra en el trail como dos decisiones —el intento antes de reajustar y la reagrupación sólo si resuelve— y publica el umbral efectivo junto al declarado en la card debinning; el resumen de la etapa lo dice en una línea. El config guardado y suconfig_hashconservan lo declarado: la corrida se reproduce porque la regla es determinista. Si ni así aparece un incumplimiento, la corrida se detiene con un mensaje que dice qué se intentó y ofrece las dos salidas que funcionan para una categórica: excluir la variable o subir su umbral envariable_overrides. No se inventa ningún WoE, no se descarta ninguna variable ni fila, y ninguna corrida que hoy termina cambia un número.DefaultRateResulty la sección EDA de la ficha ganan cada uno un campo aditivo con la causa (not_evaluable_reasonydefault_rate_not_evaluable_reason), nulos en toda corrida anterior.
[1.19.0] — 2026-09-22¶
Añadido¶
- El informe abre con la página ejecutiva «Resumen de la corrida» (capa C de
FLUJO-GUIADO-SCORECARD, C1; D-FLU-4). Tras la portada y antes del resumen ejecutivo, en HTML,
PDF, Word y fuente editable, un capítulo sin número reproduce el resumen final de la corrida
—qué corrió sin fallos hasta el informe, el estado técnico de la validación formal en palabras,
las cinco cifras clave, qué revisar, las decisiones humanas con su motivo y dónde queda cada
archivo— desde los mismos constructores que
Scorecard.summary()y que la pestaña Resultados (nikodym.guided.summaries): ningún renderer calcula ni formatea nada. Sin decisiones humanas el capítulo dice lo mismo que la pantalla («Ninguna decisión humana registrada; lo que se decidió vive en el config de la corrida»); sólo aparece en corridas con etapas del scorecard (una corrida IFRS 9 sin scorecard no recibe su molde); un informe armado a mano, sin corrida, no lo trae y su HTML es byte a byte el de siempre; y un resumen que no se pueda armar no tumba el informe: el capítulo dice por qué no hay resumen. El informe se escribe con la corrida en curso: la página dice qué corrió sin fallos antes de él y, sirun.stepspuso pasos después del informe (la validación formal es un insumo opcional y puede ir detrás), cuáles quedan por correr y que este documento no los refleja, en vez de afirmar que cierra la corrida o que la validación «no está en el config». Los archivos se nombran sólo con lo que el config manda al renderizar —el informe se escribe antes de que la corrida consolide su evidencia, así que el registro de auditoría y la ficha se citan por su nombre en esa carpeta, no por una ruta inventada—. El eventodecision_del_usuariodel registro de auditoría gana la clave aditivavariables(las variables que la decisión nombró;valorsigue siendo la hoja escrita), yStudyexponepreamble, lo que la corrida declaró antes del primer paso, persistido enrun_metadata.json(clave aditivapreamble, vacía en los archivos anteriores; se rellena evento a evento después de que cada uno llegó al trail, así que ante un sink que falla a medias trail y preámbulo traen el mismo prefijo) para que un informe regenerado desde unStudyrecargado diga las mismas decisiones que el trail y la ficha. En la fuente editable todo valor dinámico de la página —estados, cifras, alertas, decisiones, archivos y el motivo de un resumen que no se armó— va como texto literal de pandoc. El gate de códigos internos cubre la página; el golden del informe del step se re-ancló midiendo que el HTML cambia sólo por el capítulo nuevo y sus entradas de índice. -
La ficha del modelo muestra quién tomó cada decisión humana y por qué (capa C3; D-GOB-17, aprobada por Cami el 2026-09-21).
DecisionRecordganaautorymotivo, aditivos yNoneen las reglas del motor: son las claves con que la puerta guiada firma en el registro de auditoría cadaexclude/keep/merge_bins/set_bins(y sus propias inferencias, comopuerta_guiada). Los muestranmodel_card.jsonymodel_card.md(la línea de la decisión suma «— “motivo” (autor)», como texto literal en una línea: un motivo con saltos, encabezados o enlaces no fabrica estructura en la ficha), la tabla de decisiones de «Ficha del modelo» en Resultados (columnas «Autor» y «Motivo», vacías en las del motor y en las fichas escritas antes, que no traen las claves) y el capítulo «Ficha del modelo» del informe, que pasa a listar las decisiones humanas de la corrida con su motivo desde la misma fuente que la página ejecutiva —en la fuente editable, como texto literal—; las decisiones del motor, las métricas y las fechas siguen en la ficha que el motor emite. -
El informe usa la tipografía y la paleta del sitio (capa C2; D-FLU-11 fila C). El tema
nikodymdel HTML —y por tanto el PDF, que se dibuja del mismo HTML— incrusta Roboto 400 y 700 (la fuente que sirve docs.nikodym.cl) como subconjunto latino en base64 dentro de su CSS, con la pila del sistema detrás para los glifos que el subconjunto no trae: el informe sigue siendo un solo archivo, sin red ni fuentes del sistema, y se ve igual en cualquier máquina; el Word declara Roboto en el cuerpo y en los títulos, con el navy y el azul de la marca en vez del azul de fábrica de Word (lo monoespaciado sigue en Consolas). La paleta ya era la del sitio. Los archivos viajan en el paquete bajoreport/templates/fonts/con su licencia (Apache-2.0) y su origen; el temaplainno cambia. La allowlist de contenido del wheel y del sdist admite esas dos rutas, y un gate nuevo censa el paquete para que ningún archivo de datos vuelva a quedarse fuera de ella sin que se note antes del CI. Los goldens del HTML se re-anclaron midiendo que, fuera del bloque de estilos, el documento es idéntico; el PDF con la fuente incrustada lo verifica el jobtest-pdfde CI.
Sabido¶
- La página ejecutiva del informe se escribe durante la corrida, como todo el informe
(
reportes un paso del pipeline): dice qué corrió sin fallos antes de él y qué queda por correr, pero no puede reflejar cómo terminó la corrida ni un paso posterior. El estado terminal —«completada» o «fallida en …»— lo dicenScorecard.summary()y la pestaña Resultados al terminar, y una corrida que falla después del informe deja ese informe dentro de su carpeta.run.failed.*, no como entregable. Regenerar el informe al terminar la corrida sería un segundo render del mismo documento y queda como decisión de producto, no como cambio de esta capa (pasada 2 de Codex sobre C1). - El registro durable de las decisiones es el audit-trail; el preámbulo que
run_metadata.jsonpersiste es un espejo para que un informe regenerado no quede mudo. Cada declaración entra en el espejo sólo después de que su emisión terminó sin error, así que trae un prefijo de lo emitido, nunca más; con un sink compuesto (trail + MLflow) que falle en uno de sus subordinados, el trail puede conservar una declaración que el espejo no tiene, y el informe la omite en vez de inventarla. Cerrar esa ventana exige que el núcleo sepa cuál sink es el durable, y hoy recibe el sink ya compuesto (CT-4) (pasada 6 de Codex sobre la capa C).
[1.18.0] — 2026-09-21¶
Añadido¶
- La pantalla pinta abiertos los campos esenciales de cada sección y pliega el resto en un
bloque «Avanzado» cerrado (capa B de FLUJO-GUIADO-SCORECARD, D-FLU-8; SDD-31 D-SIM-4). En las
doce secciones del scorecard —las que ya declaran sus esenciales en el schema, con la marca de
sección
ui_essentials_declaredque emitedeclara_esenciales— el formulario muestra sólo los campos conui_essential(35 a la vez, ninguna sección sobre 6) y un único bloque «Avanzado» que dice cuántos de sus campos difieren del valor de fábrica («3 campos cambiados», «sin cambios») y, si los hay, cuántos errores tiene dentro. Un sub-modelo con esenciales dentro (data.load.source,report.document.author) se divide campo a campo; la estrategia de partición, como toda unión discriminada, va entera a esenciales. Los grupos, la ayuda, la validación en vivo y las decisiones institucionales no cambian, y las dos vistas editan el mismo config por los mismospath. El bloque se abre solo cuando el motor reporta un error en un campo plegado (y no se puede cerrar mientras dure) y cuando un aviso pide el foco de un campo plegado («Ir al campo»).edadeclara cero esenciales y lo dice en pantalla; una sección de un módulo que todavía no pasó por su enmienda de simplicidad (IFRS 9, supervivencia, provisiones) se pinta entera, como siempre. El golden del front (ESSENTIALS_BY_SECTION) es el espejo del de Python, atado en los dos sentidos. Ningúnconfig_hashse mueve: la marca es metadato. - Excel opcional por etapa y paquete de la corrida en la puerta guiada (capa B, D-FLU-5;
SDD-31 D-SIM-7; hallazgo #8 de integración externa).
sc.export_excel()escribe en<run_dir>/<name>/excel/un libro por etapa que corrió, numerado en el orden del pipeline y rotulado como el resumen —01 Datos y muestras.xlsx…10 Validación formal.xlsx— más11 Decisiones.xlsxcon las decisiones del registro de auditoría en tres hojas (humanas, de la puerta guiada y del motor). Cada libro trae el resumen de la etapa, su tabla de decisión (la desc.results[<etapa>], con rótulos en español), las tablas completas que el informe publica para ese dominio —las del anexo y las que entrega por observación— y un índice; se escriben por la misma vía que los exports del informe, con la misma protección de celdas de planilla, y una tabla escrita por las dos vías es la misma celda a celda (gate). Exige el extraexcel; sin él se detiene con el comando de instalación.sc.export("corrida.zip")empaqueta la carpeta del proyecto —config vigente, snapshot de datos, evidencia, informe y Excel— sin el candado ni los respaldos de corridas anteriores. Ninguno corre solo. - La pestaña Resultados muestra el resumen de la corrida (D-FLU-8, D-SC-12): el resumen final
con sus dos estados —Ejecución y Validación técnica—, las cifras clave, qué revisar, las
decisiones humanas y dónde quedó cada archivo, y plegado por etapa lo que cuenta cada una con su
tabla de decisión. Es la misma fuente que
Scorecard.summary():results.jsonganasummaries, serializado por los mismos constructores con las celdas ya escritas como las lee una persona, y la pantalla no formatea ni calcula nada. Una corrida fallida conserva los resúmenes de lo que corrió; si un resumen no se puede armar, la corrida se persiste igual y el panel dice por qué no hay resumen. Las corridas guardadas antes de esta versión no lo traen y el panel no lo fabrica.partition_label(cómo se separa la muestra, en palabras) pasa a ser una sola función para la puerta guiada y la pantalla. - Cortes por variable:
binning.variable_overrides[].user_splitsyuser_splits_fixed, ymerge_bins/set_binsen la puerta guiada (§8-9 (a) de FLUJO-GUIADO-SCORECARD, decidido por Cami el 2026-09-20: la única excepción al presupuesto cero de perillas de esa enmienda, porque «junta estos dos tramos» y «fija estos cortes» son decisiones humanas del flujo del banco que no existían en ninguna puerta). La hoja lleva los cortes entre tramos de una variable numérica (crecientes, finitos, al menos uno) y, opcionalmente, cuáles no puede juntar el motor; se cablea auser_splits/user_splits_fixedde OptBinning por variable y una categórica la rechaza con su motivo. En la puerta guiada,sc.bins(col)numera los tramos de la última corrida con su rango, filas, malos, tasa y WoE;sc.merge_bins(col, [i, j], reason=)junta dos tramos adyacentes —dos no adyacentes, un tramo que no existe o juntar los dos únicos tramos se rechazan con el mensaje y la lista de tramos— ysc.set_bins(col, cuts, reason=)fija los cortes; las dos escriben la hoja con todos los cortes fijados (y suben el tope de tramos de la variable si hiciera falta), llegan al registro de auditoría como un eventodecisioncon autorusuarioy motivo, y en la corrida siguiente el motor tramifica exactamente así. Sabido: los cortes son para variables numéricas; agrupar niveles de una categórica sigue siendo trabajo previo a la carga. Sin la hoja nada cambia: ningún resultado niconfig_hashde preset se mueve (HOJAS_DEL_FORMULARIO572 → 576 y perillas del scorecard 409 → 413: dos campos y sus dos filas de lista).
Cambiado¶
- La puerta guiada
nikodym.Scorecarddeja de ser experimental y entra a la garantía SemVer 1.x: salió en la 1.17.0 como adelanto declarado hasta que cerrara la capa B, y con esta versión cierran sus tres puertas (código, config completo y pantalla). Su firma, sus resúmenes y sus decisiones sólo crecen de forma aditiva. - La puerta guiada copia también un archivo por ruta al proyecto (
<run_dir>/<name>/input/, con la huella del contenido en el nombre) y el config referencia esa copia, como ya hacía con unDataFrame: la inferencia y cada corrida leen exactamente los mismos bytes —un archivo que otro proceso reemplaza entre la lectura y la corrida ya no puede entrenar otro modelo con inferencias viejas— yconfig.yaml+input/reproducen la corrida por sí solos. Si la copia se edita en disco,run()se detiene y pide reconstruir el Scorecard;nametiene que ser un nombre de carpeta simple (sin separadores ni «..»);export()exige una corrida propia (verifica elrun_idde la evidencia y toma el candado mientras empaqueta); el Excel opcional se construye aparte y sustituye entero al anterior (una corrida parcial no conserva libros de una completa previa, y un fallo a mitad deja el anterior intacto). Además, el paquete lleva sólo lo que es de la corrida (config.yaml,input/,run/,reports/,excel/; sin enlaces simbólicos, sin un archivo ajeno que el usuario deje en la carpeta y sin el propio ZIP si el destino está dentro del proyecto);export_excel()exige también la evidencia propia bajo el candado (otro Scorecard con el mismorun_dir/nameno puede escribir su Excel junto a decisiones ajenas); y si al publicar el Excel falla el segundo movimiento, la exportación anterior vuelve a su ruta. Hallazgos de las tres pasadas de Codex sobre la capa B. - El resumen final ya no afirma «la corrida usa los valores de fábrica» cuando no hay decisiones humanas registradas: dice «Ninguna decisión humana registrada; lo que se decidió vive en el config de la corrida», que es cierto también con argumentos distintos de los de fábrica y con un config editado en el formulario.
[1.17.0] — 2026-09-20¶
Añadido¶
- Puerta guiada del scorecard:
nikodym.Scorecard(experimental). Un scorecard de comportamiento de punta a punta con la entrada mínima —los datos, qué es «malo», el identificador, el eje temporal (date=ocohort=) con su muestra fuera de tiempo, opartition="random"sin eje— y valores de fábrica que funcionan. Lo que se puede inferir se infiere y se declara en el registro de auditoría (el esquema, las categóricas, las predictoras sin las columnas que definen el incumplimiento, las muestras); lo institucional no se siembra: sin la frontera fuera de tiempo la puerta se detiene antes de correr y dice el rango del archivo y el valor que usaría.run()corre el pipeline F1 completo connikodym.runy cuenta cada etapa en español —de tres a ocho líneas, sus alertas y una tabla de decisión con rótulos en español, disponibles ensc.summary(<etapa>)ysc.results[<etapa>], con la misma fuente para consola y notebook—;run(until=<etapa>)corre el prefijo del pipeline como una corrida parcial con su propioconfig_hash;resume()es una corrida nueva y completa sobre el config vigente, con la anterior apartada a un respaldo lateral que conserva su informe. El resumen final (sc.summary()) separa Ejecución («completada» o «fallida en …») de Validación técnica (la palabra del motor y qué prueba la decidió), publica cinco cifras (AUC, Gini y KS fuera de tiempo, la caída del AUC y el peor PSI con su banda), qué revisar, las decisiones humanas registradas y dónde quedó cada archivo;run(raise_on_error=True)levanta en vez de devolver el estado.purpose=enciende la gobernanza y con ella la ficha del modelo;track=enciende el registro en MLflow.sc.configes elNikodymConfigcompleto,sc.to_yaml()lo exporta y la puerta completa produce con él los mismos resultados: la procedencia (inferencias y decisiones con autor y motivo) es lo único que la corrida guiada añade al trail. Ninguna hoja nueva de config, ningúnconfig_hashde preset se mueve y con el mismo config los resultados son bit a bit los de antes. La guía «Empezar» y el tutorial abren con «Tu primer scorecard en 15 líneas», ejecutado en CI. - Decisiones humanas con motivo y comparación de corridas (puerta guiada).
sc.exclude(cols, reason=)escribeselection.force_exclude(y retira la variable de las listas de inclusión forzada de selección y modelo);sc.keep(cols, reason=)escribeselection.force_includeymodel.force_include(sólo con la primera, el modelo no vería una variable que la selección descartó); la última decisión sobre una variable gana. En la corrida siguiente (resume()) cada decisión llega al registro de auditoría como un eventodecisioncon los seis campos de siempre másautor="usuario"ymotivo(la ficha muestra la decisión; el motivo llega a ella con la capa C), y el resumen final las lista con su motivo.sc.compare(otra)pone dos corridas lado a lado: ejecución, validación técnica, cifras clave, variables finales y decisiones.merge_bins/set_binsno entran todavía: el motor no tiene dónde fijar cortes por variable (binning.variable_overridesno lleva cortes) y añadirlos es una hoja nueva de config que la enmienda no presupuestó; está elevado a Cami en su §8-9. - Dos diagnósticos que un validador pregunta primero, como artefactos aparte.
("selection", "iv_by_partition"): el IV de cada variable tramificada en Desarrollo, Holdout y Fuera de tiempo, con los tramos fijados en Desarrollo y las distribuciones de cada muestra (el de Desarrollo coincide con el del binning); una muestra con una sola clase publicaivnulo con la causa.("binning", "event_rate_by_partition"): filas, malos y tasa de malos por variable, tramo y muestra, coninvertscuando la tasa contradice la tendencia resuelta en Desarrollo (sólo tendencias ascendente o descendente;Special/Missingfuera de la comparación; un tramo con menos de 30 filas en la muestra no se evalúa, constante del diagnóstico).selection_tabley las tablas de binning no cambian; el resumen de selección muestra el IV por muestra y el de binning avisa «invierte la tendencia en». Sólo alertan. - Los campos esenciales de las doce secciones del scorecard llevan la marca
ui_essentialen el schema (35 visibles a la vez, ninguna sección sobre 6; tabla §3.8 de la enmienda), con golden bidireccional. Es un metadato: no es una hoja de config y elconfig_hashno lo mira. La pantalla los pinta abiertos y pliega el resto en «Avanzado» en la capa B. nikodym.runyStudy.run: el preámbulo de procedencia va dentro del manejo de fallos (un sink que no pueda escribirlo deja la corrida fallida con diagnóstico, no «running»), y el ganchoon_stepde la puerta guiada convierte un resumen que no se pudo armar en un fallo de la corrida con la etapa y el motivo, con lo calculado conservado en la evidencia.-
nikodym.runyStudy.runganan dos ganchos aditivos, opcionales y sin efecto sobre el cálculo:preamble=—pares(paso, payload)que se emiten al registro de auditoría como decisiones justo después derun_start, la vía con que una puerta de entrada declara su procedencia— yon_step=—un callback(nombre_del_paso, study)tras cada paso—. -
Un resultado vacío del target es desconocido, no «bueno». La puerta guiada arma las tres reglas del target —«malo», «bueno» e «indeterminado»—: las filas con el resultado vacío (o con la columna de la regla vacía) quedan indeterminadas, se puntúan y no entran al ajuste, y la puerta lo declara en el registro de auditoría y en el resumen de datos con la cifra. Con sólo la regla de «malo», el motor las habría tomado por buenas sin avisar.
Sabido¶
- La carpeta de un proyecto guiado (
<run_dir>/<name>/) admite una corrida a la vez: un candado.lockrechaza la segunda con un error legible antes de mover nada, y el sistema operativo lo suelta si el proceso muere. Para correr en paralelo, otroname=orun_dir=. - La puerta guiada es sólo por código: la pantalla no muestra todavía los campos esenciales
plegando el resto en «Avanzado» (capa B de la enmienda).
merge_bins/set_binsesperan la decisión de Cami sobre la hoja de cortes por variable (enmienda §8-9). - Un snapshot de
DataFramese nombra por su contenido y nunca se pisa. Cada informe queda con la evidencia de su propia corrida, por identidad de intento: el de la corrida anterior viaja al respaldo lateral que crea la consolidación (run/reportsdentro de.run.old.*); el de un intento que falló después de escribirlo y antes de consolidar va con su evidencia fallida (.run.failed.*/reports) y el anterior vuelve areports/; sin evidencia a la que asociarlo, se conserva en.reports.old.*. Ningún reintento sobrescribe un informe ni lo asocia al trail de otra corrida. Dos corridas con el mismonamese comparan con las columnas rotuladas «(esta)» y «(otra)»; un tramo de desarrollo sin filas en una muestra se publica conn=0y corta la cadena de comparación de la monotonía.
[1.16.0] — 2026-09-15¶
Cambiado¶
- Los tres avisos de brecha del motor de la validación formal se retiran, porque el cotejo los cerró. La validación declaraba desde su diseño que la forma del contraste de medias del backtesting, la orientación del p-valor de Jeffreys y los cortes del semáforo por grado no estaban verificados contra el documento oficial. Se cotejaron por doble vía —texto extraído y página renderizada del PDF del BCE de febrero de 2019, más las fórmulas de Excel de sus plantillas oficiales de reporte; Basilea 1996 y BCBS WP14 para el semáforo y el binomial—: el t-test y el Jeffreys del motor coinciden con el BCE en forma, ponderación, orientación y distribución, y los cortes del semáforo no tienen anclaje regulatorio que verificar. La card de validación ya no lleva esos tres códigos, el informe ya no dice que los cortes «siguen la convención de Basilea (1996)» —no la seguían: Basilea 1996 define zonas sobre el conteo de excepciones de un VaR, otra herramienta—, y el catálogo de avisos declarados los retira (sus números no se reutilizan). El registro del cotejo, con fuente, URL, sha256, página y método, vive en el SDD de la validación.
- Cada fila del contraste por grado publica los dos cortes con que se decidió su color. La
tabla
calibrationganagreen_alphayred_alphaal final (nulas en Hosmer-Lemeshow y Brier), la card publicatraffic_light_cutscuando corrió el contraste ynullsin él, el trail registra los dos cortes como umbral del semáforo —elumbraldel eventocalibration_semaforopasa de un número (el nivel de significancia, que no es un corte) a un objeto congreen_alphayred_alpha; la validación formal es experimental y el sobre del trail no cambia— y además una decisión única con los cortes y el recuento de colores cada vez que corre el contraste, para que una corrida toda en verde también los deje en el trail; el informe y Resultados nombran los cortes de la corrida con todos sus dígitos junto a la cobertura por grado, diciendo cuáles trae el motor por defecto (0,05 y 0,01) y sin atribuir la elección a nadie: son un parámetro de la política de validación de cada institución, no un umbral fijado por norma. En el documento las dos columnas nuevas no se pintan —el hecho va en prosa—; viajan en el JSON, en el CSV y en la card, y una variable del usuario que se llame igual en otra tabla se sigue pintando. El copy de «Contrastar la PD por grado de rating» y de «Ejecutar backtesting IFRS 9» deja de hablar de una brecha del motor. - El mínimo de operaciones para evaluar protege también cada grupo de PD de Hosmer-Lemeshow, y
una prueba que no se pudo correr lo dice. Hasta ahora el mínimo (30 de fábrica) protegía la
muestra entera y cada grado de rating, pero no los grupos de la prueba de Hosmer-Lemeshow: una
muestra de 100 operaciones recibía veredicto con grupos de 10. Ahora, si el grupo más chico queda
bajo el mínimo, esa muestra queda sin veredicto —con diez grupos y el mínimo de fábrica hacen
falta al menos 300 operaciones—; la demo pública (4.019 / 973 / 1.008 operaciones) no cambia. Un
Hosmer-Lemeshow sin veredicto ya no publica un estadístico
0.0—que es el valor de un ajuste perfecto— sino ninguno, y dice por qué con una de cuatro causas cerradas: la muestra bajo el mínimo, un grupo de PD bajo el mínimo, un grupo sin variabilidad o un estadístico que desbordó con PD extremas. La causa viaja en la tablacalibration(columnanot_evaluable_reason, al final; en el documento no se pinta y la prosa la cuenta con sus números), en la card (metric_sections.validation.not_evaluable_partitions, siempre presente, junto al umbral efectivomin_rows_per_groupcon que se decidieron esas ausencias) y en el trail (regla nuevacalibration_hl_not_evaluable), y Resultados la muestra junto al «Sin veredicto» de la fila. Las pruebas que no se pudieron correr no cuentan en «pruebas fallidas»:n_testsyn_failedcuentan sólo las decisiones con veredicto en las cuatro familias (antes contaban también los Hosmer-Lemeshow y los backtests sin veredicto). Y una validación sin ninguna prueba evaluable y sin decisión de estabilidad ya no dice «Pasa»: su estado técnico es «No evaluable», la misma palabra que las bandas del PSI, en Resultados, en el informe y en la card (overall_statusgana el valornot_evaluable). Por la misma razón, una fila del contraste de Jeffreys por grado —cuyozasintótico no está definido— publica el estadístico nulo en vez de un0.0que se leería como «observado igual a esperado». El copy de «Mínimo de operaciones para evaluar» dice lo que el motor hace. - La validación formal puede recalcular el PSI en vez de reusarlo, y el resultado dice de dónde
salió cada fila. La casilla «Reusar el PSI que ya calculó la etapa de estabilidad» sale del
formulario (estaba oculta porque apagarla abortaba la corrida: el paso nunca armaba el frame
que el recálculo exigía). Apagada, la validación recalcula con el mismo ensamblador y el mismo
evaluador que la etapa de estabilidad, por la misma llamada —con la misma configuración las
filas son idénticas, fila a fila—; con la sección de estabilidad declarada usa su eje temporal y
su fuente de CSI, y sin ella recalcula entre particiones sin eje temporal (receta mínima). La
comprobación previa declara exactamente lo que el recálculo va a leer —el score, la PD
calibrada y, según la sección de estabilidad, el dataset y los bins congelados—, así que un
artefacto ausente o una sección de estabilidad inválida se acusan antes de ejecutar ningún
paso, no a mitad de corrida. La tabla
stabilitylleva la procedencia ensource(stability_artifactorecomputed), la card la repite enmetric_sections.validation.stability_sourcey dice la receta enstability_recompute, el trail registra una decisiónstability_sourcecon la receta y añadesourceal valor de cadastability_psi, Resultados muestra la procedencia como nota de la sección y el informe la dice en el capítulo. Un YAML conconsume_stability: falseque hasta hoy abortaba ahora corre y lo declara. La guarda de la dirección del score del paso de estabilidad lee la ficha del scorecard también cuando llega inyectada por la puerta pública (antes una ficha inyectada con la dirección contraria pasaba en silencio). Los identificadores y las palabras están en la referencia de la API. La receta de recálculo con kwargs sueltos (stability_recomputed) se retira: no hay dos formas de recalcular. - Las tablas del capítulo «Validación formal» del informe dicen palabras, no identificadores.
Las cuatro tablas de la validación —discriminación, calibración, estabilidad y backtesting— del
HTML, del PDF, del Word y del Markdown imprimían los identificadores del motor en sus celdas
(
hosmer_lemeshow,pass,not_evaluable,performance_artifact…) mientras Resultados y la guía ya los traducían. Ahora pintan las mismas palabras que la pantalla, con los mismos mapas de fuente única («Hosmer-Lemeshow», «Puntaje de Brier», «Pasa», «Sin veredicto», «Verde», «Reusada de la etapa de desempeño», «Desarrollo vs. Holdout»…). Sólo en esas cuatro tablas: una variable del usuario que se llametestodecisionen otra tabla se sigue pintando tal cual. Los encabezados no cambian y el JSON, el CSV y la card siguen llevando el identificador.
[1.15.1] — 2026-09-13¶
Corregido¶
- Una cartera con muchos puntajes empatados ya no tumba la evaluación del desempeño. Cuando
un decil traía decenas de operaciones con exactamente la misma PD calibrada —una cartera pequeña
apilada, un scorecard con pocos valores distintos de puntaje—, la corrida moría en
performancecon «min_pd <= mean_pd <= max_pd debe cumplirse»: la media de N valores idénticos, calculada en coma flotante, quedaba una o dos unidades de redondeo por encima del máximo (medido: 99 PD iguales, 1,1·10⁻¹⁶ de diferencia). La media de cada decil se proyecta ahora al rango de sus propios valores, en la PD y en el puntaje; una media que ya estaba dentro del rango —toda corrida que antes terminaba— viaja intacta, byte a byte.
[1.15.0] — 2026-09-13¶
Cambiado¶
- La tasa de incumplimiento por período o cohorte viaja a la interfaz hasta un tope. Agrupar
la tasa por una cohorte casi única —una columna de identificador— produce una fila por
operación, y la respuesta de la interfaz la publicaba entera: un millón de cohortes eran 73 s,
135 MB de respuesta y 634 MB de memoria al serializar. Ahora la respuesta publica como máximo
las primeras 1.000 filas, en el orden del motor, y declara cuántas calculó el motor y si
recortó (
eda.default_rate_window); con el recorte, la tabla completa queda como archivo de la corrida (eda_default_rate.csv, junto aresults.json), que Resultados ofrece como descarga y la interfaz sirve enGET /api/results/{run_id}/eda-default-rate. Resultados dice «se muestran las primeras 1.000 de N»; el informe ya recortaba la tabla a su máximo de filas y lo decía al pie —ahora nunca por encima de 1.000, aunque ese máximo se configure más alto—, y formatea sólo las filas que muestra —con un millón de filas, 5,9 s → 0,0 s—. El motor no rechaza el eje yDefaultRateResult.by_periodsigue trayendo todas las filas: cierra la decisión pendiente que la 1.13.0 dejó declarada como sabida. Medido: un millón de cohortes pasan de 71 s, 138 MB y 634 MB de pico a 0,07 s, 0,14 MB y 0,6 MB.
Corregido¶
-
Los archivos que se abren en una planilla protegen las celdas que Excel leería como fórmula. El identificador de la operación, los niveles de una categórica o la etiqueta de una cohorte vienen del archivo del usuario, y un texto que empieza por
=,+,-,@, tabulador o retorno de carro se convierte en una fórmula viva —un enlace, una llamada externa— al abrir el CSV o el libro en Excel, LibreOffice o Google Sheets. Los exports de datos del informe (.csvy.xlsx) y la tabla completa de la tasa por período o cohorte anteponen una comilla simple a esas celdas de texto —valores, nombres de columna y nombre del índice, también en columnas categóricas, y también con salto de línea o con las variantes de ancho completo de esos cuatro caracteres—, y sólo a ellas: los números, las fechas y el resto del texto viajan intactos, y un archivo sin celdas de ese tipo es, byte a byte, el de siempre. Un texto que ya empezaba por comilla recibe otra, para que dos valores distintos nunca salgan iguales; lo que un texto lleve dentro —un;, un tabulador, un salto de línea— no se toca. En los CSV todo el texto viaja entre comillas y los números sin ellas. Límite declarado: el CSV se emite con coma; abierto con otro separador (un doble clic en un Excel cuyo separador de lista es;) la tabla sale rota y la protección no aplica a esa vista rota: ábrelo con su separador, o usa el libro.xlsx. -
Una corrida de la interfaz se guarda entera o no se guarda. Sus archivos se construyen en un temporal y se publican de una vez; si el disco se llena a mitad de camino —la tabla completa de la tasa puede ser grande—, no queda una corrida a medias que la interfaz sirva sin su tabla, ni un archivo grande huérfano que se acumule con cada reintento: sólo se conserva el audit-trail, y una corrida previa con el mismo identificador vuelve a su sitio si la publicación nueva falla —también si el proceso se corta a mitad del reemplazo: el arranque siguiente de la interfaz la recupera—.
[1.14.0] — 2026-09-12¶
Añadido¶
- El informe trae la ficha del modelo. Con la gobernanza declarada, el informe HTML, PDF, Word y Quarto gana el capítulo «Ficha del modelo», entre la introducción y el contexto: el propósito, los supuestos y las limitaciones tal cual los escribió la institución, la identidad de inventario (nombre, cartera, motor, fase, estado de la revisión independiente, responsable) con sus rótulos, y la periodicidad de revisión; el capítulo de limitaciones remite a él en vez de repetirlo. Las métricas, las decisiones y las fechas siguen en la ficha que el motor emite al cierre de la corrida —una sola ficha por corrida—, y el capítulo lo dice sin nombrar archivos. Sin gobernanza el informe es, byte a byte, el de siempre. En la fuente Quarto las declaraciones van como texto literal, línea a línea: lo que la institución escriba en el propósito —una celda de código, un enlace, HTML— se lee, no se ejecuta ni se interpreta al compilar.
Cambiado¶
-
Los presets F1 «Estándar consumo» y F5 «Provisión interna genérica» traen el análisis exploratorio encendido. Agrupan la tasa por la misma cohorte con la que particionan y describen las variables que entran al binning, y el informe lo exige como sección obligatoria: quien corra
standard_preset()oprovision_interna_preset()ve la población antes de modelar, en Resultados y en el informe. Es cálculo, así que la identidad de los dos presets cambia: F1ec10eb43…→1063d6cf…, F5b36318b5…→a7476bf2…; F3 y F4 no cambian. Los fixtures de la demo se recapturan con esta versión. -
La tabla de calidad de datos describe las columnas del archivo, no las que produce el motor. Diagnosticaba también el target derivado, el estado de la etiqueta, la partición y el rol TTD, y sobre la muestra de desarrollo la partición y el TTD salían «casi constante» por construcción: dos marcas que parecían un problema del archivo y eran del propio motor. Las columnas del usuario —incluidas la fecha, la cohorte y la que define el incumplimiento— se siguen diagnosticando.
[1.13.0] — 2026-09-12¶
Añadido¶
-
El análisis exploratorio entra al formulario, a los dos trabajos del scorecard y a la pestaña Resultados. La sección existía en el motor desde el principio y no tenía una sola pantalla: no se podía encender desde la interfaz ni, hasta ahora, correr con sus valores de fábrica sobre una cartera sin columna de fecha. Ahora «Análisis exploratorio» está en el formulario —entre «Esquema y target» y «Optimal Binning», que es su lugar en el pipeline— y viene encendida en «Scorecard de comportamiento (PD)» y «PD + LGD en una corrida»; su resultado se pinta primero entre los paneles de Resultados: la tasa de incumplimiento global y en el tiempo —una línea si se agrupó por fecha, barras si por cohorte, y ninguna figura cuando hay un solo período—, la señal de estabilidad temporal o la causa por la que no se evaluó, la calidad de datos por columna con sus marcas en palabras y un desplegable por variable descrita. Sin
edaen la respuesta, el bloque no se renderiza. La respuesta de la interfaz publica la card y tres tablas agregadas (tasa por período o cohorte, calidad por columna y perfiles por tramo), nunca el frame. Y el preflight avisa antes de correr si se agrupa por cohorte sin decir qué columna la trae. -
La casilla «Todas las columnas». Es el primer control del formulario donde «en blanco» y «lista vacía» significan cosas distintas: el motor describe todas las columnas sólo con la lista sin definir, y respeta una lista vacía como «ninguna». Un multiselect corriente escribía la lista vacía al desmarcar todo, así que quien lo hiciera perdía los perfiles creyendo que se describían todas. La casilla escribe «sin lista»; desmarcada, se eligen una a una.
-
Guía nueva: «Análisis exploratorio». Sobre qué población se describe, los dos ejes de la tasa y qué hace el motor cuando el archivo no trae fecha, las tres causas por las que la señal temporal no se evalúa, los perfiles por variable y las marcas de calidad, con los números de una corrida real. La referencia de la API publica la correspondencia entre cada identificador y su palabra, y el ejemplo por código se ejecuta en un test.
-
El análisis exploratorio publica sus métricas al canal del model card: la tasa de incumplimiento observada, cuántos períodos o cohortes la componen y si la señal temporal quedó marcada. Hasta ahora era el único dominio del scorecard que declaraba no publicar ninguna.
-
La validación formal entra al formulario y a la pestaña Resultados. Era la única sección del pipeline del scorecard que existía en el motor, corría en los ejemplos y salía en el informe, pero que quien entraba por la interfaz no podía ni ver ni tocar. Ahora «Validación formal» está en el formulario —después de «Estabilidad» y antes de «Survival»—, en los dos trabajos del scorecard, y su resultado se pinta en Resultados: el estado técnico con el conteo de pruebas fallidas, las familias que corrieron y una tabla por familia con lo que publicó el motor, sin recalcular ni reinterpretar nada. Sin
validationen la respuesta, la sección no se renderiza. -
La cobertura por grado se publica junto a la tabla, no en su lugar. Un grado de rating con menos operaciones que el mínimo técnico no recibe semáforo —sin esa población, cualquier veredicto sería ruido— y por eso no entra en la tabla ni cuenta en las pruebas fallidas. El panel dice siempre cuántos grados se evaluaron de cuántos y enumera aparte los que quedaron fuera, con sus conteos y el mínimo que los excluyó. Sin esa línea, un «Pasa · 0 de 1 pruebas fallidas» podía convivir con media cartera sin evaluar.
-
Una familia de pruebas que se pidió y no publicó nada se dice en pantalla. El motor registra en su resultado las familias que el config declaró, no las que produjeron algo: con el backtesting encendido pero sin el cálculo IFRS 9, o con las tres pruebas de calibración apagadas, la corrida termina en «Pasa» con la familia listada y cero filas. El panel la nombra y explica las dos razones posibles, y el contador de pruebas fallidas dice «Sin pruebas de pasa o falla» en vez de «0 de 0», que es cierto y engañoso a la vez.
-
Guía nueva: «Validación formal». Qué prueba cada familia, cómo se lee el estado técnico, por qué el puntaje de Brier no tiene veredicto de pasa o falla y qué hace un validador con el resultado, sobre una corrida real que falla en la muestra fuera de tiempo. La referencia de la API publica además la correspondencia entre cada identificador y su palabra.
-
La pestaña Resultados pinta la selección de variables. Entre «Análisis por variable (WoE)» y «Escala y calibración» aparece «Selección de variables»: cuántas candidatas había, cuántas quedaron y cuántas se descartaron; los umbrales de esta corrida con el mismo nombre que tienen en el formulario; y una fila por variable con la decisión, el motivo en español, su IV con la banda diagnóstica, AUC, KS, la peor correlación y con quién, VIF y CSI. El detalle que dejó escrito el motor —
iv=0.0029 < min_iv=0.02— se muestra tal cual: es lo que ata la fila al audit-trail y reescribirlo la desconectaría. Las variables que el motor marcó por IV alto o por inestabilidad se avisan aparte, porque marcadas no es lo mismo que excluidas. Esta información viajaba entera en la respuesta de la interfaz y no se veía en ninguna pantalla; sinselection, la sección no se renderiza. -
El resumen del peor PSI se ve en pantalla, no sólo en el informe. «Estabilidad del score» gana, arriba de las series, una línea por comparación: el peor PSI entre score y PD calibrada, cuál de las dos magnitudes es la peor, y la banda de esa misma magnitud. Los tres datos viajaban juntos desde que se corrigió el resumen del informe, y la pantalla mostraba sólo las series: había que leer dos gráficos para saber qué decía el semáforo.
Una corrida guardada antes de esa corrección no recibe semáforo agregado: aquellas versiones publicaban el peor valor entre score y PD junto a la banda del score, así que atribuirlo sería inventar. La pantalla lo dice en una línea y conserva las series completas, que sí son correctas.
Y en la selección, un criterio que la corrida no ejecutó no se presenta como suyo: la acción ante inestabilidad y el clustering por correlación sólo aparecen cuando su filtro estuvo encendido, y la acción ante IV alto sólo cuando había un IV sospechoso que alcanzar.
-
La pestaña Resultados pinta la ficha del modelo. Cuando la corrida lleva gobernanza, justo después de los artefactos de la corrida aparece «Ficha del modelo»: el propósito, los supuestos y las limitaciones que declaró la institución, la fecha de emisión y la de próxima revisión, las métricas por dominio con su evidencia estructurada, y el conteo de decisiones registradas con su detalle. Hasta ahora la ficha llegaba completa a la API de la interfaz y no se veía en ninguna pantalla. Sin gobernanza no cambia nada: no hay bloque vacío ni ficha fabricada. Si una decisión o la evidencia de un dominio lleva un aviso declarado —algo que le corresponde a la institución, o una brecha declarada del motor—, la ficha marca la fila, dice que es una salvedad y remite el significado exacto de cada código a la referencia de avisos declarados, sin recortar el código que la respalda.
-
El model card ya sale con las métricas del modelo. Hasta ahora una corrida completa terminaba con el resumen de métricas vacío: el model card se generaba sin AUC, sin KS, sin PSI y sin decisiones, que es justo el bloque que exige la guía de gobierno de modelos. Había dos consumidores esperando ese resumen —el model card y el registro en MLflow— y ningún productor que lo llenara. Ahora cada dominio publica una lista corta y declarada de métricas y el núcleo las reúne bajo
<dominio>.<métrica>.
La lista es de dominio y no un volcado automático, y la diferencia importa: AUC, Gini, KS y PSI no son campos sueltos de ninguna ficha —viven por partición y por comparación—, así que copiar «todo lo numérico» habría publicado el paso de puntaje y el número de deciles como «las métricas del modelo», y habría dejado fuera el AUC.
Una métrica que no se puede evaluar no aparece. Una cartera demasiado corta para estimar un AUC no publica un AUC de cero: publica nada, y la ausencia queda registrada en el audit-trail. Un cero se lee como una medición pésima; la ausencia se lee como lo que es.
nikodym.run(config, run_dir=...)deja la evidencia de la corrida en disco. Conrun_dirse escriben ahí el audit-trail, el snapshot del entorno, elmodel_card.jsony su versión en Markdown, más la corrida serializada. Cada archivo aparece sólo si su sección está activa: con auditoría pero sin gobernanza hay trail y entorno, y no hay model card.
Sin run_dir no se escribe nada, exactamente como antes. Una librería no debe empezar a
dejar archivos en el directorio de trabajo de quien la importa.
Si el directorio ya tiene una corrida, la nueva no se mezcla con ella: se aparta a un respaldo al lado y sólo cuando la corrida nueva está completa ocupa su lugar. Si algo falla antes —al preparar la corrida, durante ella o al escribir la evidencia—, la corrida previa queda exactamente donde estaba, y lo que la corrida fallida alcanzó a dejar —su audit-trail con el diagnóstico del fallo, en particular— se conserva en un directorio hermano en vez de borrarse; la excepción dice dónde quedó. Un audit-trail con ruta absoluta dentro del directorio de la corrida se escribe en la corrida nueva, nunca en la que ya estaba.
-
Los cuatro ejemplos de fábrica traen la auditoría encendida. Es lo que hace que el model card llegue con sus decisiones registradas en vez de una lista vacía. La gobernanza sigue apagada de fábrica a propósito: exige declarar el propósito del modelo, y ese dato sólo lo puede fijar la institución. El motor no lo inventa.
-
El schema que sirve la interfaz describe la sección de gobernanza campo a campo. Hasta ahora
GET /api/schemala entregaba como un bloque opaco —sin sus campos ni sus rangos— aunque el motor la validaba completa. Ahora viaja expandida, como el informe, sin convertirse en un paso del pipeline: la gobernanza describe la corrida, no la calcula, y la identidad de la corrida (config_hash) no se mueve. -
La gobernanza se ve en la interfaz: «Gobernanza» es una sección del formulario en los diez trabajos. Hasta ahora sólo se podía encender importando un YAML que la trajera. Llega apagada —el motor sigue sin inventar un propósito— y encenderla con su interruptor activa la pregunta «¿Para qué se va a usar este modelo y sobre qué cartera decide?» en el bloque «Esto lo decides tú», junto a las decisiones sobre la cartera. Mientras la sección esté apagada esa pregunta no cuenta como pendiente; con la sección encendida y el propósito en blanco, la corrida no arranca y la tarjeta dice qué falta. Las descripciones de sus campos están escritas para quien mira la pantalla, no para quien lee el código. El nombre del diario de escenarios no se ofrece: ese archivo hoy no se escribe, y ofrecer su nombre sería un control sin efecto.
Cambiado¶
-
Agrupar la tasa de incumplimiento por cohorte ya no aborta la corrida del análisis exploratorio. Con el eje de cohorte el motor levantaba un error —«solo aplica al eje temporal»— y con él se perdían la tasa por cohorte, los perfiles y la calidad de datos que sí estaban calculados. Ahora la señal de estabilidad temporal se declara no evaluable, con su causa, y todo lo demás se publica: las cohortes no tienen un orden cronológico que el motor pueda inferir, así que sobre ellas no hay deterioro en el tiempo que medir. Es aditivo: ninguna corrida que hoy termina cambia de resultado. La causa viaja en el resultado (
not_evaluable_reason: eje de cohorte, menos de dos períodos con observaciones suficientes, o tasa media cero con un indicador relativo) y la regla es una sola —hay causa si y sólo si el indicador configurado no es finito—; la tercera causa antes salía en silencio. -
Sin columna de fecha, el eje de la tasa se toma de la partición por cohorte. Con el eje temporal de fábrica y sin fecha declarada, el motor usaba la única columna de fecha del archivo o fallaba. Si no hay ninguna y los datos se particionan por cohorte, ahora agrupa por esa misma cohorte y lo registra como decisión auditable (
eje_eda_inferido): no se inventa un eje, se usa el que ya se declaró para particionar. Es lo que permite que la sección corra con sus valores de fábrica sobre las carteras de ejemplo, que no traen fecha. Sin fecha y sin cohorte el error es el de siempre, ahora anclado al campo de la fecha. El resumen del análisis publica el eje efectivo y si lo eligió el motor, y la pantalla y el informe lo dicen. -
Las columnas que definen el incumplimiento salen del perfil por variable de fábrica. Describir «frente al incumplimiento» la columna con la que se construyó el target daba una tasa de 0 % o 100 % por tramo que no dice nada. Con «Todas las columnas», el perfil excluye ahora las columnas que leen las reglas de «malo» y «bueno» —el mismo criterio con que el binning descarta candidatas por fuga— además de las estructurales; quien las pida por su nombre las obtiene igual. Cambia el resultado de una configuración de fábrica sobre un dominio estable, y por eso se declara aquí.
-
Las palabras del análisis exploratorio tienen una sola fuente. El eje («por fecha de observación», «por cohorte»), los tres indicadores («variación relativa», «peor desvío», «tendencia»), las tres causas y las tres marcas de calidad («casi constante», «casi única», «alta cardinalidad») viven en
nikodym.eda, el informe las consume y la pantalla las replica con un gate en los dos sentidos. De paso, la prosa del informe decía «cardinalidad excesiva» donde la pantalla dice «alta cardinalidad», y nombraba el indicador con su identificador (cv). -
La subsección «Población y calidad de datos» del informe trae sus tablas y sus figuras en el cuerpo. Las claves con que el documento pedía la tasa por período y la calidad por columna no casaban con las que el motor publica, así que la subsección salía sin tablas y éstas iban al anexo con su clave interna por título; y las figuras que la prosa anunciaba —«N figuras»— no las dibujaba nadie, porque el informe nunca tuvo un constructor de gráficos para esta sección. Ahora las dos tablas van en el cuerpo con título propio, los perfiles por variable al anexo, uno por columna descrita, también con título, y se dibujan dos figuras: la tasa de incumplimiento en el tiempo —barras por cohorte, línea por fecha, y ninguna con un solo período— y la tasa por tramo de cada variable descrita, hasta doce paneles y el título dice cuántas quedaron fuera. En HTML/QMD salen como SVG y en Word como PNG, como el resto de los gráficos, y la prosa dice lo que el documento reproduce. El anexo de tablas deja de traer el recuadro vacío que rotulaba esas recetas con su clave interna. Una cohorte, un período o un tramo sin casos elegibles no tiene tasa, y ni la figura ni el panel lo dibujan como un 0 %: la figura pone una cruz en la base y la leyenda lo dice, la línea se corta en ese período en vez de puentearlo, y el panel deja el hueco y lo dice. Y como el eje de cohorte acepta cualquier columna, la figura y el panel grafican como máximo 60 cohortes —las primeras en el orden del motor, y el título lo dice— y la tabla trae todas; con muchos períodos, el eje rotula sólo algunas marcas para que se lean.
-
Dos cohortes que se escriben igual no se funden en la pantalla. El motor agrupa las cohortes con su tipo —la numérica
2024, la decimal2024.0y la textual"2024"son tres— y JSON pierde parte de esa identidad. La respuesta de la interfaz publica ahora, al lado de cada período o cohorte, el tipo con que el motor lo distinguió, y el panel lo usa para rotular las que coinciden («2024 (entero)», «2024 (texto)») y para no repetir una clave. Y un entero mayor que el que un navegador puede leer sin pérdida (2⁵³ − 1) viaja como texto, porque dos cohortes vecinas llegarían fundidas en el mismo número; su tipo sigue diciendo que es entero. -
El estado técnico de la validación se llama igual en la pantalla y en el informe: Pasa, Revisar y Falla. Sustituyen a «Pass técnico / Requiere revisión / Falla técnica», que era el vocabulario que la prosa del informe usaba en solitario porque no había panel con el que contrastarlo. Mismo criterio que las bandas de estabilidad: una sola fuente (
nikodym.validation.results), el informe la consume, la pantalla la replica y un gate compara los dos lados. El identificador (pass,warn,fail) no cambia. De paso, el informe dejó de publicar el identificador crudo en una frase que decía «el estado técnico … es "pass"». -
Ocho campos de la validación dejan de ofrecerse en el formulario, y cada uno con su razón medida. El criterio de agrupación de Hosmer-Lemeshow (su segundo valor lo rechaza el propio motor), las tres columnas del artefacto interno de PD calibrada (las escribe el motor con nombre fijo), el reúso del PSI (apagarlo aborta la corrida: esa rama no está cableada) y la columna de segmento del backtesting (sale del detalle de IFRS 9, no del archivo). Ninguno desaparece del config: por YAML o por Python siguen alcanzables. Y las cuatro columnas que sí aporta el usuario —el grado de rating y las tres realizadas— pasan a comprobarse contra el archivo antes de correr, pero sólo cuando su rama corre: con el contraste por grado apagado, el formulario ya no reclama una columna que el motor no va a abrir.
-
El copy de los 32 campos de la validación está escrito para quien mira la pantalla. Salen los literales del motor —«chi2», «G-2 gl», los nombres de las instituciones que publicaron cada convención, los identificadores en inglés— y entran frases que dicen qué hace cada control y qué exige. Un gate nuevo impide que una marca interna de aviso declarado vuelva a colarse en un tooltip del formulario.
-
Las bandas de estabilidad se llaman igual en todas partes: Estable, Revisar, Redesarrollar y No evaluable. Hasta ahora el mismo dato salía con dos vocabularios —la interfaz decía «Revisar» y el informe «Requiere revisión»— y las guías publicaban el identificador en inglés. Ahora hay una sola fuente (
nikodym.stability.results.BAND_LABELS), el informe la consume, la pantalla la replica con un gate que compara ambos lados y las guías publican las palabras. El identificador (stable,review,redevelop,not_evaluable) no cambia: sigue siendo lo que viaja en el JSON, enpsi_table, enstability_metricsy en la model card, y ahora está documentado junto a su palabra en la referencia de la API. Mismo criterio para los motivos de selección y las bandas de IV, que ya se leían en el informe y ahora también en pantalla. -
El caso de referencia de norma local ya no se ofrece en el catálogo por defecto de la interfaz. La primera pantalla describe lo que el motor hace para cualquiera, así que los dos trabajos atados a una jurisdicción —«Provisiones CMF» y «Comparar provisiones (CMF vs. interna)»— y su ejemplo de fábrica dejan de aparecer en la landing y en el selector. No se retiró nada del paquete: el motor, sus pruebas, sus matrices, su evidencia y la página «Aterrizar una norma local» siguen enteros, y hay tres caminos para llegar.
-
nikodym-ui --casos-de-referencialos vuelve a ofrecer: la landing recupera el bloque «Normativa local · casos de referencia» y el selector lista su ejemplo. - Un config propio con un bloque
provisioning_cmf:sigue funcionando sin la opción. Al cargarlo, la interfaz selecciona el trabajo que le corresponde, muestra sus secciones, pide en Datos la PD calibrada que el método interno necesita y la corrida arranca. El catálogo no te lo ofrece; no te lo esconde. - Por código,
get_preset("f3-provisiones-consumo")yGET /api/config/preset/{id}siguen resolviendo: pedir el ejemplo por su nombre es pedirlo explícitamente.
GET /api/jobs es aditivo: sigue publicando los diez trabajos y añade por trabajo un campo
offered que dice si esta sesión los ofrece. Un cliente que no lo mire se comporta como antes.
La demo pública pasa a mostrar los ejemplos de scorecard e IFRS 9.
-
Un propósito en blanco ya no construye la gobernanza.
governance.purposese guarda sin espacios alrededor y exige al menos un carácter: un texto vacío, de solo espacios o de solo saltos de línea se rechaza señalando el campo. Es la validación que hace verdadera la promesa de que la ficha del modelo no se emite sin propósito. La superficie es experimental —fuera de la garantía SemVer 1.x— y un config que traía el propósito vacío tiene que declararlo. -
⚠️ Correr un ejemplo de fábrica por código ahora pide decir dónde va la evidencia. Como los cuatro ejemplos traen la auditoría encendida, y el audit-trail ya no puede caer en el directorio de trabajo, un script que hacía
ahora falla con un error que nombra el arreglo. La corrección es añadir el destino:
Un config propio con la auditoría apagada —o con una ruta absoluta para el trail— no cambia. Desde la interfaz tampoco hay nada que hacer: ella misma archiva el trail junto a su corrida.
Corregido¶
-
El quickstart publicado corría con una llamada que ya no arranca. README, la portada de la documentación, «Empezar», el tutorial y tres guías ejecutaban el ejemplo de fábrica con
nikodym.run(config); desde que los ejemplos traen la auditoría encendida, esa llamada se detiene pidiendorun_dir. Los ejemplos pasan ahorarun_dir, un test los ejecuta tal como se publican, y README, portada y «Empezar» llevan el mismo bloque, byte a byte. Comorun_dirtodavía no está en la versión publicada, cada uno de esos ejemplos avisa cómo correrlo ahí, y la portada dice desde qué código se construye la documentación. -
La documentación prometía una gobernanza «automática». README, portada y dos guías decían que la model card y el audit-trail eran automáticos, cuando la ficha del modelo sólo se emite al declarar un propósito y la sección llega apagada. El copy dice ahora qué viene solo (el lineage), qué traen encendido los ejemplos de fábrica (la auditoría) y qué se enciende (la ficha), y una guía nueva —«Gobernanza y ficha del modelo»— explica cómo encenderla desde la interfaz y qué muestra Resultados. De paso: la portada afirmaba que el hash del
uv.lockviajaba vacío en el lineage, y el motor lo firma; «Empezar» fijaba a mano una serie de versión de dos releases atrás; y el catálogo de trabajos de la interfaz se publica completo, con sus rótulos reales. -
El audit-trail ya no se escribe en el directorio desde el que se lanza la corrida. Se escribía ahí pese a que su documentación decía «dentro del directorio del run», así que dos corridas lanzadas desde el mismo sitio concatenaban sus eventos en el mismo archivo, que es justo lo que el contrato de auditoría prohíbe: un trail por corrida. Ahora se resuelve contra el directorio de la corrida. Una ruta absoluta se sigue respetando; una ruta relativa sin directorio de corrida pasa a ser un error explícito en vez de ensuciar el directorio de trabajo en silencio.
-
El análisis de supervivencia era inalcanzable con la auditoría encendida. El estimador viaja dentro de su resultado y arrastraba el destino del audit-trail —un archivo abierto— al copiarse, de modo que la corrida moría con un error de tipo antes de publicar nada, y sin dejar un estudio inspeccionable. No se notaba porque ningún ejemplo de fábrica traía la auditoría encendida.
Sabido¶
-
Agrupar la tasa de incumplimiento por una cohorte casi única —una columna de identificador, por ejemplo— produce una fila por operación: el motor la calcula tal como se pidió, y la respuesta de la interfaz la publica entera, porque la tabla de la tasa viaja completa por contrato. Medido: un millón de cohortes son 135 MB de respuesta y 634 MB de memoria al serializar. Las figuras y el panel ya acotan lo que dibujan; acotar la respuesta, o rechazar ese eje antes de correr, es una decisión pendiente.
-
La demo publicada corre sin una gobernanza declarada, así que su ficha del modelo sigue vacía y su informe no la trae; el propósito de la demo es un dato de la institución y no se inventa. Sus fixtures se recapturaron con esta versión desde corridas reales (F1 y F4): la identidad de las corridas no cambia, y el informe de la demo dice ya «Falla» donde antes decía «Falla técnica».
[1.12.0] — 2026-08-27¶
Cambiado¶
- La garantía SemVer ahora significa algo, y cubre la regresión logística. Desde 1.0 la promesa
es que el pipeline de scorecard (F1) no rompe hasta un 2.0, pero la etiqueta la escribía a mano
cada paquete, no la comprobaba ningún test y la referencia de la API publicaba una tercera lista.
Se contradecía en las dos direcciones a la vez:
nikodym.model—la regresión logística PD, que es el corazón de F1— se declaraba experimental, mientras quenikodym.audit, que no forma parte de F1, se declaraba estable. Para quien instalaba conpip, la etiqueta no distinguía nada.
Ahora la lista vive en un solo sitio (nikodym.testing.stability) y los gates la atan al
docstring de cada paquete y a la página de referencia. model queda dentro de la garantía,
que es lo que la promesa ya decía; y audit queda dentro por decisión explícita: el trail
JSONL, el hashing y el replay ya son superficie de integración, y romperlos en un minor costaría
más que sostenerlos.
Añadido¶
-
La interfaz que se distribuye se ejecuta ahora en un navegador real antes de publicarse. El árbol estático que viaja dentro del wheel se verificaba por SHA-256 contra su procedencia y no lo cargaba nunca un navegador: los tests del frontend corren sin DOM y el smoke de instalación llama a la aplicación en proceso. Un bundle íntegro byte a byte pero roto al ejecutarse —un import muerto, un asset renombrado— pasaba todos los gates y llegaba a PyPI. El nuevo trabajo de integración instala el paquete candidato fuera del repositorio, levanta
nikodym-uidesde ahí y recorre con un navegador el camino completo: elegir un ejemplo, ejecutar, ver los resultados con su procedencia y abrir el informe. -
El smoke de instalación recorre los cuatro ejemplos de fábrica, no sólo el de scorecard. La documentación promete que el extra
[ui]los corre todos hasta el informe con una sola instalación, y hasta ahora esa promesa no la ejercía ningún gate sobre el paquete distribuido. -
La deriva entre
uv.locky el manifiesto de build es ahora un gate. La comprobación existía pero no la invocaba nadie, así que un desajuste se descubría en la corrida de quien usa la librería, no en la nuestra.
Corregido¶
-
El informe ya no imprime códigos internos en su prosa. Tres párrafos —el de validación, el del orquestador de provisiones y el de IFRS 9— volcaban identificadores como
FALTA-DATO-VAL-2en medio del texto que lee una persona. La regla publicada es que esos códigos aparecen sólo en el volcado de auditoría del anexo, donde el código es el dato, y que la prosa explica la limitación en palabras. Ahora cada aviso se redacta, y uno que el motor no sepa redactar se declara igualmente, sin nombrarse: callar una limitación en un informe regulatorio sería el error contrario, y peor. -
El
READMEsubdeclaraba la reproducibilidad. Decía que el hash deluv.lockestaba pendiente y que el campo «viaja vacío». Es falso desde hace varias versiones: el hash se calcula y se escribe en el lineage de cada corrida. -
La referencia de la API decía publicar las firmas de
1.4.0en un paquete que iba por1.11.0. -
El sitio afirmaba que survival tiene capítulo propio en el informe. No lo tiene, y el código lo dice por escrito: su curva alimenta la prosa del capítulo de IFRS 9. También contaba tres ejemplos de fábrica cuando hay cuatro.
[1.11.0] — 2026-08-05¶
Añadido¶
- El panel de resultados dice de dónde salió lo que muestra. Quien corre por la interfaz ve el
panel antes que el informe, y hasta ahora esa pantalla no publicaba ninguna procedencia: ni el
hash de los datos, ni la versión con la que se corrió, ni si el árbol de código tenía cambios sin
confirmar, ni las advertencias de determinismo, ni qué artefactos entraron desde fuera. El informe
sí lo publica desde siempre, así que la deuda era la asimetría entre dos superficies de la misma
corrida. Ahora
results.jsontrae esa procedencia entera —la misma que el anexo del informe— y el panel la enseña. Es aditivo: un cliente que no conozca la clave la ignora, y los resultados guardados antes de este cambio no la traen y se leen igual.
De paso, el panel deja de mostrar el hash de configuración del formulario y muestra el de la corrida: bastaba teclear en cualquier campo después de ejecutar para que la pantalla afirmara un hash que ninguna corrida había producido.
-
Las columnas que identifican una fila se eligen del propio archivo, con casillas, en vez de escribirse como una lista en JSON a mano. Afectaba sólo a ese campo, y la causa era que al desempaquetar un campo opcional se perdía la marca que dice «esto nombra columnas de tus datos».
-
Los conjuntos de datos de ejemplo también traen su perfil de columnas, así que el aviso de «esta columna parece un identificador» —que hasta ahora sólo alcanzaba a los archivos que subías— funciona igual con ellos, incluso si ya los habías usado antes de esta versión.
-
La interfaz ya muestra el valor que el motor usará en un campo que no has llenado.
GET /api/schemapublica un catálogo nuevo,effective_defaults, con el valor predeterminado real de cada campo del config: el mismo que ejecutan las clases del motor, no una copia escrita a mano. El formulario lo usa sólo para pintar, marcando esos valores como «Predeterminado; se usará mientras no elijas otro». Es aditivo: los tres campos anteriores del payload (json_schema,defaults,section_order) conservan su significado exacto y un cliente que no lo conozca lo ignora. Un dominio cuyo extra no esté instalado no se expande: su sección viaja sin valores debajo, igual que ya no aparecía expandida en el esquema, así que el formulario no ofrece ni un valor para ella. -
Un ejemplo completo de provisiones sin ninguna normativa local. Entran un conjunto de datos (
provision_interna_generica), un preset listo para correr (f5-provision-interna-generica) y una guía, Provisiones sin normativa local. La cartera se llama como la nombra la institución —nomina,microempresa,consumo_senior—, no hay ninguna categoría de supervisor, y la corrida produce su informe con el capítulo de provisiones sin nombrar ningún país. Existía el motor y no existía forma de enseñarlo. -
El capítulo «Provisiones regulatorias» se emite cuando se calcularon provisiones, y no sólo cuando se compararon dos métodos. Una corrida con un único motor —el interno, o sólo el estándar— llegaba a su informe con la provisión escondida en el anexo de configuración, porque el capítulo dependía del comparador, que por definición exige dos fuentes distintas. Es aditivo: ningún informe pierde nada y los que corren un solo motor ganan su capítulo.
-
La interfaz se organiza por TRABAJOS, y la sesión ya no arranca sembrada con un ejemplo. Al entrar eliges a qué viniste —«Scorecard de comportamiento (PD)», «Validar un modelo existente», «PD + LGD en una corrida»…— y ese trabajo decide qué secciones existen: la pantalla deja de ofrecer las catorce a todo el mundo.
GET /api/jobspublica el catálogo, y con él el abanico metodológico: 69 puntos de elección con sus 172 opciones, cada una con qué hace, qué exige y por qué está disponible o bloqueada, en idioma de negocio. Los ejemplos precargados siguen ahí, ahora como lo que son: «ver un ejemplo con datos de muestra». -
nikodym.run()ynikodym.check_pipeline()aceptan artefactos externos. Un parámetroartifacts=permite inyectar tablas que la corrida no calcula —una PD ya calibrada, el puntaje de un modelo existente— para arrancar el pipeline por la mitad. Es lo que hace ejecutables los trabajos «Validar un modelo existente» y «Provisión interna / LGD». Aditivo y keyword-only: la firma anterior sigue valiendo. Por HTTP la puerta es estrictamente menos poderosa que por código: sólo entran tablas, nunca objetos serializados, y sólo las de los trabajos disponibles. -
La división de la muestra se puede LEER del archivo en vez de derivarla.
partition.strategygana una cuarta forma,columna: si su panel ya trae la marca de desarrollo/validación/OOT, se declara qué columna es y qué valor corresponde a cada partición. Nada se adivina —ni por parecido de nombre, ni por orden, ni por frecuencia—: un valor declarado que no aparezca en los datos es un error que nombra los que sí aparecen. -
kaplan_meierdeja de exigir el extralifelines. Ese método no lo usaba; ahora un config que lo elija corre con la instalación base, donde antes fallaba pidiendo una dependencia que no iba a utilizar.
Cambiado¶
- 🔴 Un
target_pdescrito junto al ancla por defecto ya no se descarta en silencio: detiene la corrida.anchor_source='development_observed'—el valor de fábrica— calcula la tasa central como el promedio observado en Desarrollo, así que eltarget_pdque el usuario escribía no se usaba y la corrida terminabadonesin decirlo. Medido sobre el preset F3, la diferencia era de 569 millones en la provisión, con los dos campos contiguos en la pantalla. Ahora esa combinación se rechaza al validar, nombrando la salida.
⚠️ Es un cambio de comportamiento que puede romper un pipeline que hoy corre. Si su config
tiene ese par, hasta 1.10.0 se ejecutaba (con una cifra que no era la que usted pidió) y desde
1.11.0 no arranca. La salida es una línea: elija anchor_source='business_input' si quiere que
su target_pd gobierne, o quite el target_pd si quiere el ancla observada.
-
Survival deja de ajustar en silencio sobre toda la población. Cuando existe una columna de partición y no hay filas de desarrollo, el motor se detiene en vez de ajustar sobre todo: medido, un coeficiente pasaba de
+1,92a−0,02sin que nada lo señalara. -
Tres opciones que el config aceptaba y morían a mitad de corrida ahora se rechazan al validar:
binning.solver='cp', el modo de proyecciónperiod_matricesde markov, yperformance.partitionscon una sola partición. Ninguna funcionaba en1.10.0; lo que cambia es cuándo se entera usted — antes de cargar el archivo, no en el paso 8 de 10. -
GET /api/datasetssepara el índice de las columnas. La columna identificador (loan_iden los conjuntos del catálogo) sale decolumnsy aparece en la clave nuevaindex_columns. Un cliente que la leyera dentro decolumnsdeja de encontrarla ahí. -
UiConfig.upload_max_mbgobierna de verdad. Era un campo muerto: declaraba 200 MB y el tope real eran 100 MiB fijos. Ahora el valor declarado manda y se comprueba antes de traer el cuerpo a memoria, también en los cinco POST de JSON, que no tenían ninguna cota. Un despliegue que lo hubiera fijado en 10 corría de hecho con 100 MiB y ahora corre con 10. -
🔴 El informe ya no afirma que los montos van en pesos chilenos. Hasta ahora los rotulaba «pesos chilenos (CLP)» en tres capítulos —incluido el de IFRS 9, que es un marco contable internacional— sin que nadie lo hubiera declarado en ninguna parte. Nace
report.currency: si declaras una moneda, el informe la publica; si la dejas en blanco, no afirma ninguna y los montos siguen legibles con el símbolo genérico$. El motor no inventa un dato que sólo la institución conoce, y afirmar la moneda equivocada en un documento auditable es peor que no afirmar ninguna.
⚠️ No mueve el config_hash: report es una sección de presentación y está excluida de la
identidad de la corrida, igual que la portada del entregable. Un informe existente que quiera
seguir diciendo «CLP» sólo tiene que declararlo.
⚠️ La convención numérica del informe (coma decimal, punto de miles) no cambia: es la del idioma en que está escrito —hoy sólo español—, no la de una moneda.
- 🔴 El método interno de provisiones ya no pide de fábrica una columna con nombre chileno. El
valor por defecto de
provisioning_internal.portfolio_colpasa de"cmf_portfolio"a"portfolio". Ese motor es jurisdiccionalmente neutro —no conoce ninguna tabla de supervisor, y su cálculo esPE = PI · PDI · Exposiciónsobre los grupos que tú formas—, pero su estado de fábrica exigía la taxonomía de un supervisor concreto: un banco de cualquier otro país tenía que renombrar su columna para correr un cálculo que no interpreta ninguna norma.
⚠️ Nota de contrato SemVer. Este cambio recalcula el config_hash de un config que
omite esa clave y se apoya en el default, y con él su clave de idempotencia en el inventario
de MLflow. Por eso sale como minor y no como patch — mismo criterio que 1.4.0 y 1.8.0.
Medido, el alcance es más estrecho de lo que sugiere: los presets de fábrica no se mueven
—f1, f3 y f4 conservan byte a byte el config_hash que tenían en 1.10.0— y un config que
declara portfolio_col tampoco, sea cual sea su valor. Sólo cambia quien lo omitía.
⚠️ El método estándar de la CMF conserva "cmf_portfolio", que ahí sí nombra su contenido: la
cartera regulatoria chilena. La consecuencia es que los dos defaults dejan de estar alineados,
así que una institución que compare estándar contra interno con los defaults de fábrica debe
declarar portfolio_col en una de las dos secciones si tiene una sola columna de cartera. La
comprobación previa del dataset lo señala antes de ejecutar nada cuando la columna nueva no
existe en su archivo.
🔴 El caso que hay que mirar es el otro, y hasta esta versión no avisaba nadie. Si el archivo
trae las dos columnas —lo que ocurre por construcción en quien corre IFRS 9 y provisión
interna sobre un mismo panel, porque provisioning_ifrs9.portfolio_col también vale
"portfolio"—, la comprobación previa daba verde: la columna que el config nombra existe de
verdad. La corrida terminaba bien y la agrupación cambiaba en silencio. Ahora esa ambigüedad
se avisa en la comprobación previa y queda registrada en el resultado de la corrida, que es lo
que la lleva al informe para quien usa la librería por código. El aviso no detiene nada: si
"portfolio" es la columna correcta, no hay que hacer nada.
- 🔴 La provisión que se compara de fábrica es ahora la que exige la norma chilena. El valor por
defecto de la segunda fuente de
provisioningpasa de la pérdida esperada bajo NIIF 9 al método interno del banco. La regla del Capítulo B-1 de la CMF (Circular N° 2.346, hoja 10-11) es el mayor valor entre el método estándar y el método interno, por institución; el Capítulo A-2 num. 5 excluye el deterioro de NIIF 9 sobre las colocaciones y los créditos contingentes. El default anterior existía por retrocompatibilidad y publicaba un comparativo entre marcos contables que ninguna norma local pide — útil para una filial que reporta a su matriz extranjera, pero que había que saber que estaba mal para corregirlo.
A quién afecta: a quien active la sección provisioning sin declarar la segunda fuente.
Su corrida seguirá corriendo, pero comparará contra otra cosa y su config_hash cambiará —y con
él la clave de idempotencia de su inventario en MLflow—. Por eso va en minor y no en patch,
igual que en 1.4.0. Para conservar el comportamiento anterior basta declararlo:
Ningún preset ni ejemplo del proyecto se mueve, y conviene decir la razón exacta: sólo f3
activa la sección provisioning, y escribe sus dos fuentes explícitamente; los demás la dejan
apagada, así que no tienen dónde heredar el default.
- El capítulo del informe deja de rotular «Chile» sobre una comparación que la norma no pide. Su título decía «la regla del máximo (Chile)» siempre, aunque se estuviera comparando contra NIIF 9 o por cartera en vez de por institución; el matiz estaba en el cuerpo y la etiqueta honesta, enterrada en el anexo. Ahora el título se deriva de la comparación configurada, con el mismo criterio que el motor ya usaba para elegir su referencia normativa.
Corregido¶
-
🔴 El capítulo de provisiones ya no sale mudo cuando corren los dos motores sin comparador. Al emitirse el capítulo por «se calcularon provisiones» y no por «se compararon dos métodos», la combinación de método estándar y método interno con el comparador apagado quedaba con el título puesto y el titular vacío: el lector saltaba directo a la primera subsección y el total nunca aparecía. Ahora publica las dos cifras y dice explícitamente que no se aplicó ninguna regla de selección entre ellas, porque no se comparó nada.
-
🔴 El informe ya no invoca la regla del máximo del Capítulo B-1 sobre una comparación que no se hizo. Cuando el comparador queda configurado pero sólo una de las dos fuentes llega a calcularse, el capítulo abría citando la Circular N° 2.346 —«el mayor valor entre el método estándar y el método interno»— y no publicaba ni una cifra, porque los montos exigen las dos. Era una afirmación normativa sin respaldo en su propia corrida, dentro de un documento auditable. Ahora dice que la comparación no se realizó y reporta la única fuente que sí corrió.
-
La guía de provisiones sin normativa local publicaba un fragmento de código que no se podía ejecutar: el ejemplo para declarar la moneda usaba una forma que no corresponde al objeto que la propia guía deja en pantalla. El fragmento quedó corregido y ahora lo ejecuta un test junto con el resto del ejemplo, que es lo que impedía verlo.
-
🔴 La comprobación previa dejó de mentir sobre lo que un paso necesita. Con
ml.feature_source='selection_woe',nikodym.check_pipelinerechazaba pipelines que corren —exigía el WoE debinning, que en ese modo el paso no abre nunca— y aceptaba pipelines que mueren —callaba los dos artefactos deselectionque sí lee—. La causa:tuningyexplaindeclaraban sus requisitos con el valor de fábrica deml, copiado a mano en dos constantes, en vez de con el que la corrida iba a usar. De paso, quien traía esos artefactos pornikodym.run(..., artifacts=…)leía que no los usaba nadie, sobre las dos claves que el paso iba a consumir. Los resultados nunca estuvieron en riesgo —el paso revalida antes de calcular—; lo que fallaba era lo que se prometía antes de correr. Un gate nuevo recorre ahora todos los pasos del motor y exige que quien componga sus requisitos con decisiones de otra sección las reciba de verdad, para que el próximo caso no nazca en silencio. -
🔴 Pedir la planilla sin tener su librería ya no deja sin informe.
xlsxera el único formato que no degradaba: siopenpyxlno estaba instalado, la corrida moría al escribir el adjunto y se llevaba por delante el informe entero, incluido el HTML, que no depende de ningún extra —y hasta el.csvque ya se había escrito en disco quedaba fuera del resultado—. Ahora hace lo mismo que el PDF y el Word: avisa, entrega todo lo demás, y el documento no nombra un adjunto que no existe. Quien prefiera lo contrario tiene su interruptor,report.xlsx.fail_if_unavailable, con el mismo default apagado que sus dos hermanos. -
🔴 Un puntaje ya no puede medirse al revés de como se construyó. La dirección del puntaje —«un puntaje más alto, ¿es mejor o peor cliente?»— se pregunta en tres sitios, y hasta ahora cada uno se leía por su cuenta: con la tarjeta construida en un sentido y el desempeño midiendo en el otro, la corrida terminaba bien y el informe publicaba un modelo con la discriminación invertida —Gini negativo— sin un solo aviso, con todas las comprobaciones previas en verde. Ahora la dirección viaja con el puntaje: si la respuesta de una sección contradice la escala con que se construyó la tarjeta, se avisa antes de correr y la corrida se detiene diciendo cuál cambiar. Quien trae un puntaje ya construido desde fuera —el caso de «validar un modelo existente»— sigue declarando la suya, porque ahí sólo él la sabe.
-
El formulario dejó de mostrar un config distinto del que la corrida iba a ejecutar. Un campo que el archivo no traía se pintaba vacío, apagado o en cero aunque el motor fuera a usar otro valor: la interfaz leía el
defaultdel JSON Schema, que no existe para los bloques que el motor construye solo (report.sections,report.html,model.stepwise,selection.correlationy otros ~80 campos al activar una sección). Dos casos vivían en la configuración estándar F1: «Renderizar gráficos» se veía desactivado corriendo activado, y «Bloques por completar» se veía en blanco corriendo conshow.
Ahora esos valores se ven, marcados como predeterminados, sin escribirse en tu config:
montar la aplicación, cambiar de sección, abrir un YAML o descargarlo sin editar no añade ni una
clave y no mueve el config_hash. El primer gesto sobre un control materializa sólo ese
campo; activar una sección o cambiar de variante escribe su bloque completo, que es lo que ese
gesto significa. Un valor que escribiste tú se respeta literalmente aunque sea null, false,
0, "" o una lista vacía: ya no se confunde «no lo decidí» con «lo dejé vacío a propósito».
-
Un YAML parcial vuelve al formulario tal como lo escribiste.
POST /api/config/from-yamldevuelve la proyección de lo que el archivo traía y ya no su expansión completa, así que un config de veinte líneas deja de convertirse en uno de trescientas. Elconfig_hashsigue calculándose sobre el config completo y no cambia. -
Inyectar una ficha que el informe sí lee ya no se anuncia como «no la usa nadie». Al pasar una card por
nikodym.run(..., artifacts=...)—o cualquiera de las que el informe adopta si existen—, el registro de la corrida la declaraba inerte aunque el documento la fuera a leer. Las tablas y figuras que el informe también adopta siguen declarándose inertes: el contrato cubre las fichas, y ampliarlo cambiaría ese veredicto para otros casos. -
Un informe ya no exige la ficha de una sección que nadie va a correr. Si
reportdeclara una sección obligatoria cuyo dominio está apagado en esa corrida, la comprobación previa (nikodym.check_pipeline) la daba por inejecutable con un error del grafo de pasos, en vez de dejar que decidierareport.sections.missing_policy. Ahora el config es ejecutable y la política hace su trabajo:errordetiene la corrida en el pasoreportdiciendo qué falta,warntermina publicando la sección ausente yskipla omite dejando la limitación declarada en el informe. Es un cambio observable: un config que antes se rechazaba ahora corre —o falla más tarde y con mejor diagnóstico—. Un paso que sí corre y no publica la ficha que prometió sigue deteniendo la corrida antes del informe: la política no oculta un productor roto. -
El comodín de binning ya no usa como predictor una columna que define el target. Con
feature_columns="*", las columnas nombradas pordata.target.bad_ruleygood_rulequedan fuera de las candidatas;indeterminate_ruleyexclusion_rulesconservan sus columnas porque seleccionan la muestra, no la etiqueta. Una lista explícita sigue respetándose y la decisión queda en el audit-trail y en el model card de gobernanza; la card de binning publica las columnas que el comodín excluyó automáticamente. El cambio funciona igual con la seccióndatatipada u opaca. Si el AUC baja al actualizar, ésa es la corrección de una fuga previa, no una regresión. No cambianconfig_hashnidata_hash; sí pueden cambiar variables finales, coeficientes, métricas e informe. -
Una llave de unicidad simple ya no entra al binning por el comodín. Si
data.schema.unique_keysdeclara una sola columna,feature_columns="*"la excluye como ruido de identificador. Si declara una combinación de dos o más columnas, ninguna se elimina por separado: cada una puede conservar señal predictiva legítima. Las listas explícitas siguen respetándose. Esta corrección es distinta de la fuga del target y queda separada para que un cambio de AUC tenga una causa auditable.
[1.10.0] — 2026-07-29¶
El formulario de la interfaz deja de ser una vitrina: ya se puede llevar un dataset propio del
archivo al informe sin escribir una línea de YAML. Hasta la 1.9.0 ese camino estaba cortado en
tres sitios distintos, y el corte no se veía hasta que uno lo intentaba de verdad.
Añadido¶
-
Elegir las variables del binning ya es posible. Los tres multiselect de
binning—feature_columns,exclude_columns,categorical_columns— pintaban «Sin opciones.» incluso con las variables ya dentro del config: no es que no se pudieran editar, es que no se podía ni ver qué variables entraban al modelo. Una lista de nombres de columna no puede traer sus opciones en el schema —dependen del archivo que cargues—, y era lo único que el formulario miraba. Ahora las opciones salen del dataset activo, y un valor que el archivo no trae se conserva marcado en vez de borrarse en silencio. -
Las once listas de objetos del config se editan fila a fila.
data.schema.columnsera un<textarea>de cinco líneas con 1.552 caracteres de JSON crudo, etiquetado «Editor JSON (tipo no mapeado)». Ahora cada fila tiene sus campos, con botones para añadir, eliminar y reordenar. Efecto colateral medido: los avisos del preflight que enfocan el campo exacto al hacer click pasan de 0 a 18 de 18 — el salto ya probaba del id más específico al más general, sólo le faltaba que el campo existiera. -
La interfaz ofrece la sección «Informe», con la portada del entregable (modelo, entidad, cartera, responsable del desarrollo, versión), el idioma, los formatos de salida y qué capítulos exige el documento. Antes esos cinco campos sólo se escribían por YAML o por código, así que el informe que salía del camino por interfaz llegaba con la primera página en blanco. El catálogo de secciones editables pasa de 13 a 14.
-
El config se comprueba contra sus propias invariantes, no sólo contra los nombres de columna.
nikodym.check_datasetavisa ahora de cinco requisitos que un config puede incumplir aunque todas sus columnas existan; el caso que lo motivó: con partición aleatoria ystability.temporal_axisen su default"period", la corrida moría en el paso 8 de 10 con las dos comprobaciones previas en verde. Entran ademásdata.partition.strategy.oot_fromen blanco o no parseable como fecha,validation.familiesvacío, ystability.comparisons/performance.partitionscon duplicados. Cada aviso nombra su campo y avisa sin bloquear: la corrida sigue siendo la autoridad sobre sí misma.
Cambiado¶
-
El error de validación de esquema se lee como una frase, no como un volcado de
pandera. Decía «validación lazy=True … check:column_in_dataframe; valor ofensor: …; índice:<sin valor>» y lo lee un usuario, no un desarrollador de la librería. Ahora explica en español qué columna falta, qué tipo se esperaba o qué regla se incumplió. Es copy público: el código interno no viaja al lector. -
report.sections.required_sectionsdeclara que sus valores no son columnas. Nombra secciones del informe, y sin esa declaración el formulario lo trataba como una lista de columnas y marcaba sus ocho valores de fábrica como ausentes del dataset. No amplía el alcance del preflight.
Corregido¶
-
ReportResult.html_pathapuntaba a un archivo que no existe cuandoreport.output_dires relativo —el caso por defecto de quien usa la librería por código, porque es lo que trae el preset F1—: devolvíareports/reports/scorecard_report.htmlen vez dereports/scorecard_report.html. Los otros tres formatos nunca tuvieron el defecto. Quien lea el HTML por esa ruta pasa de unFileNotFoundErrora abrir el informe. -
El aviso del preflight ya no llama «columnas» a lo que no lo es, y el mensaje del eje temporal nombra la opción con el literal que el selector muestra de verdad.
Aditivo: no cambia el config_hash de ningún config existente, ni el veredicto de /api/run,
ni ninguna firma del pipeline F1. Sale como MINOR porque añade capacidades de interfaz y de
comprobación, no porque mueva identidad.
[1.9.0] — 2026-07-28¶
Añadido¶
- El config y tu dataset se comparan ANTES de correr.
nikodym.check_dataset(config, columnas)y su espejo RESTPOST /api/preflightresponden qué columnas declara el config que tu archivo no tiene, todas de una vez y sin ejecutar nada. Hasta ahora eso se descubría de a una: cada corrida fallida destapaba el siguiente desajuste. Medido sobre un CSV con nombres de columna propios y el preset F1, eran seis corridas para llegar a la primera ejecución. - Cada mensaje trae la ruta del campo en el config (
data.partition.strategy.cohort_col), para que la interfaz pueda llevarte directo al campo que hay que corregir. - Caso especial de
index_col: un archivo CSV no puede transportar un índice, así que cuando esa columna existe pero como columna corriente, el aviso lo dice y nombra las dos salidas.
Aditivo: no cambia comportamiento existente, ni el config_hash, ni el veredicto de /api/run.
La comprobación informa, no bloquea — la corrida sigue siendo la autoridad sobre sí misma.
Alcance: el pipeline F1. provisioning*, survival, markov, forward y stress quedan fuera
por ahora, y el gate de cobertura lo declara en vez de callarlo.
- La interfaz lo usa: el aviso aparece mientras trabajas. En «Cargar datos», junto a las columnas de tu archivo, y en cada sección de «Configuración» con los desajustes que le tocan. Un click en un aviso te lleva al campo que hay que corregir. El botón Ejecutar cambia de aspecto y avisa, pero no se bloquea nunca: puedes correr igual.
Salvedad conocida: las listas de variables de binning (
feature_columns,exclude_columns,categorical_columns) todavía no son editables desde el formulario —su control se dibuja a partir de las variables WoE, que no existen hasta que la corrida las produce—, así que un aviso sobre ellas te lleva a su sección pero no enfoca un campo. Para ajustarlas, usa «Descargar YAML» / «Cargar YAML» en la misma página. Es anterior a esta versión; el preflight sólo la hace visible. - El formulario alcanza la secciónstability(PSI/CSI, umbrales, comparaciones y eje temporal): era parte del camino F1 y sólo se podía editar por YAML o por código.
pip install nikodym[ui]ya instala una interfaz que corre. El extra traía el servidor y la interfaz pero no el motor que ésta dispara: medido en venv limpio, los tres presets fallaban —F1 y F3 en el binning, F4 en survival—. Ahora[ui]compone tambiénscoringysurvival, así que los tres corren hasta el informe con esa sola instalación. Pesa ~700 MB en disco, y es deliberado: un extra llamadouique no puede ejecutar nada promete algo que no cumple. El PDF sigue aparte (nikodym[pdf]), por licencia, y también el backend de lecturapolars(nikodym[polars]), que sólo acelera la carga sin cambiar el resultado.
Nota de contrato — lee esto si ya usabas
nikodym[ui]. El extra cambia de composición, no sólo de tamaño: de 310 a 703 MB y de 0 a 3 presets ejecutables. Dos consecuencias prácticas: (a) la instalación tarda más y ocupa el triple, así que revisa tus imágenes de contenedor y cachés de CI; (b)[ui]hereda ahora el techoscikit-learn<1.8quescoringya imponía. Si teníasnikodym[ui]conviviendo conscikit-learn1.8 o 1.9 —que pip te dejaba instalar, porque el extra no lo acotaba—, al actualizar pip degradaráscikit-learno fallará la resolución. Es la contrapartida de que la interfaz traiga un motor que de verdad corre. No cambia API pública niconfig_hash. - La interfaz gráfica ya está documentada (B2.5). El README ydocs_siteexplican cómo instalarla y levantarla en dos comandos —pip install 'nikodym[ui]'ynikodym-ui—, sus opciones (--port,--workdir,--no-open), que escucha sólo en127.0.0.1sin forma de cambiarlo, y que edita el mismoNikodymConfigque usarías por código. Hasta ahora el comando no se mencionaba en ninguna parte: sólo se llegaba a él leyendo elpyproject.toml.
Corregido¶
-
Un invariante de config roto devolvía HTTP 500 en
/api/validate, cuyo contrato es responder siempre 200, y la interfaz lo mostraba como «Backend no disponible» — una afirmación falsa sobre un backend sano. Ocurría con algo tan simple como activar un campo opcional sin escribirle valor. La causa:ConfigErrorno hereda deValueError, así que Pydantic no lo envuelve y la excepción escapaba entera. Ahora esvalid=falsecon su mensaje, y 422 en/api/preflight,/api/runy/api/config/to-yaml— nunca un 500. Alcanza a las seis secciones de config que validan invariantes propios, no sólo a la que lo destapó. -
El preflight declaraba compatible un
data.schema.index_colque el dataset no tiene. Era el tercer estado de ese campo —ni índice, ni columna corriente— y no tenía diagnóstico: el aviso respondía «todo bien», con la lista de desajustes y la de secciones no inspeccionadas vacías, y la corrida moría en su primer paso. En el caso que motivó la función —un CSV con nombres de columna propios contra el preset F1— los desajustes reportados pasan de 15 a 16: faltaba justo el identificador de la observación.nikodym.check_datasetacepta ahoraindex_columns=para distinguirlo; omitirlo conserva el comportamiento anterior, porque sin ese dato un índice correcto es indistinguible de uno inexistente. -
POST /api/preflightno exigía el token de sesión. Respondía 200 —y materializaba el dataset en el directorio de trabajo— a cualquier proceso local, mientras/api/rundevolvía 403 en las mismas condiciones: el endpoint nació fuera de la lista de guardas. Ahora pide token y same-origin como los demás. Sigue disponible conallow_live_execution=false: comprobar no es correr, y es el modo donde un aviso de config↔dataset más se agradece.
[1.8.0] — 2026-07-27¶
La identidad criptográfica del config dejó de depender de qué módulos hubiera importado el proceso.
Nota de contrato (SemVer): sale como minor, no como patch, porque recalcula el
config_hash de los configs con secciones opacas y campos omitidos — y con él la clave de
idempotencia del inventario MLflow. Un patch se lo llevaría quien tenga un pin ~=1.7.0 sin haberlo
decidido. Mismo criterio que 1.4.0, que también recalculó identidad al excluir data.load.source.
El algoritmo de canonicalización no cambia y sigue estable dentro de 1.x.
Corregido¶
- ⚠️ El
config_hashdependía de qué módulos hubiera importado el proceso. El mismo config producía dos identidades distintas según si la capa de ese dominio ya estaba importada: sin ella, la sección viaja como un blob opaco y se canonicaliza sin normalizar, así que los defaults que el YAML no traía no se materializaban. Con la capa importada, sí. Es la identidad criptográfica de la que cuelgan el lineage, el hash del model card, el del informe y el ancla de idempotencia de MLflow.
Se ve con dos líneas, en dos procesos distintos:
# proceso A (sin importar nikodym.binning) → 8fb0c28b…
# proceso B (con import nikodym.binning) → e8afdfca…
from nikodym.core.config import NikodymConfig, config_hash
config_hash(NikodymConfig.model_validate({"binning": {"max_n_prebins": 15}}))
Ahora config_hash coacciona las secciones que lleguen opacas antes de canonicalizar: la
identidad es la del config que se ejecutaría, que es la misma semántica que el lineage ya
adoptó en 1.7.0. El blob opaco del núcleo liviano no cambia — import nikodym.core.config
sigue sin arrastrar dominios.
Cambio de comportamiento. Un config con secciones opacas y campos omitidos —típicamente un
config.yaml escrito a mano y cargado sin importar sus capas— cambia de config_hash, y con
él su clave de idempotencia en el inventario MLflow. Un Study guardado con save() no se ve
afectado: escribe el config ya coaccionado y completo, así que su digest no se mueve (verificado
con un round-trip save→load entre procesos). Precedente del mismo tipo: la exclusión de
data.load.source en 1.4.0. El algoritmo de canonicalización no cambia y sigue estable bajo
SemVer 1.x.
POST /api/validatepodía aprobar un config inválido si era el primer request del proceso. Por la misma causa: un rango violado dentro de una sección de dominio —binning.min_bin_size: -1— devolvíavalid: truecon cero errores, y publicaba unconfig_hashpara ese config. Por la interfaz no se alcanzaba (el formulario no valida hasta recibir el schema, yGET /api/schemaimporta los dominios), pero sí un cliente que llame a la API directamente. El significado devalidno cambia: ahora significa lo mismo siempre.
[1.7.0] — 2026-07-27¶
Corregido¶
- ⚠️ Un config inejecutable no dejaba ningún rastro, y en la interfaz daba un HTTP 500. Cuando el
pipeline no se puede resolver —por ejemplo, activar
provisioning_ifrs9sinsurvival, que le provee la term-structure— el motor produce un diagnóstico exacto:
El paso 'provisioning_ifrs9' necesita 'term_structure', que produce 'survival',
y ningún paso anterior lo genera: active 'survival' antes de 'provisioning_ifrs9'
o quite este paso.
Ese mensaje se perdía entero. La resolución del pipeline ocurría antes de que la corrida tuviera
run_id y fuera del bloque que registra el fallo, así que nikodym.run() devolvía un Study con
status="created" —ni "done" ni "failed"—, run_id=None y error=None: nada que
inspeccionar. La interfaz, incapaz de persistir una corrida sin run_id, respondía un 500 opaco.
Ahora una corrida inejecutable queda status="failed" con su run_context.error, su run_id y
su lineage, igual que una que falla dentro de un paso, y la interfaz muestra el mensaje del motor.
Cambio de comportamiento para quien inspeccione el Study de un config inejecutable: donde
antes veía "created" ahora ve "failed". Es el valor que la documentación siempre dijo que
vería. Study.run() sigue re-levantando: el primitivo fail-loud no cambia.
- La curva de ECL del panel publicaba un plazo que no descuenta. El bloque
provisioning_ifrs9.ecl_term_structurede la respuesta de resultados traíatime_valuecrudo —en la unidad del productor de la term-structure— junto a undiscount_factor_meancalculado sobre el plazo ya convertido a años. Con una curva mensual, la tabla mostraba un plazo y un factor que no se corresponden:DF ** (-1/time_value) - 1daba 1,5 % donde la EIR era 20 %, y el lector no podía verificarDF = (1 + EIR)^(-τ)con los números que tenía delante.
Ahora la curva publica las dos columnas, time_value y time_value_years, como ya hacía el
artefacto del motor desde 1.6.0. Es un campo nuevo, aditivo: nada de lo que había cambia de
significado ni de valor.
- Una sección de configuración no se podía apagar desde el formulario. El schema compuesto que
sirve
GET /api/schemaperdía la nulabilidad de cada sección de dominio: las secciones son camposAnycondefault=None, y al empotrar el sub-schema se sustituía el nodo entero, llevándose el"default": nullque era su único portador. El compuesto acababa declarandotype: "object"+required, o sea lo contrario de la verdad — el mismo payload trae todas las secciones ennullen susdefaults.
Ahora cada sección expandida viaja como anyOf: [<objeto>, {"type": "null"}] con
default: null, la misma forma que Pydantic emite para un X | None. Afectaba a todas las
secciones, no sólo a las de provisiones.
- Lanzar la interfaz dentro de un clon del repositorio dejaba datos listos para commitear.
python -m nikodym.uicrea su directorio de trabajo en el directorio actual, así que arrancarla desde la raíz de un checkout dejaba sin vetar el parquet del dataset materializado y elresults.jsonde cada corrida. Sólo afecta a quien trabaja sobre el repositorio —no al paquete instalado—, pero en un repositorio público es una fuga a ungit addde distancia.
Añadido¶
- El formulario de la interfaz pasa de 7 secciones a 12: entran survival y las cuatro de
provisiones. Hasta ahora, quien instalaba la interfaz sólo podía configurar por formulario el
pipeline de scorecard; para calcular provisiones CMF o IFRS 9 había que escribir el config en
Python, aunque el motor ya las soportara. Ahora se editan desde la interfaz
survival,provisioning_cmf,provisioning_internal,provisioning_ifrs9yprovisioning, con sus 178 campos, cada uno con el mismo tipo, rango y ayuda que declara el config.
El backend ya las enviaba completas: era el front el que las descartaba. En el camino, el
formulario deja de pintar la fontanería del config —schema_version, los discriminadores type
y otros 38 campos que el usuario no debe tocar— y aprende los 20 tipos de control que el motor
declara, de los que antes reconocía cuatro.
- La interfaz avisa que un config no se puede ejecutar mientras se edita, no al ejecutar.
Activar
provisioning_ifrs9sinsurvivalproduce un config que reconstruye perfectamente y que el motor no puede correr. Antes eso se descubría apretando Ejecutar; ahoraPOST /api/validateresponde además un bloquepipelineconexecutable, losstepsque correrían y, si no es ejecutable, el diagnóstico del motor, que el formulario muestra en un aviso.
El aviso no bloquea la corrida: el motor sigue siendo la autoridad y registra el intento
fallido con su diagnóstico, su run_id y su lineage.
-
nikodym.check_pipeline(config): responde si un config es ejecutable sin ejecutarlo. La misma respuesta que obtiene la interfaz, disponible por código —devuelveexecutable, los pasos en el orden en que correrían y el diagnóstico si no es ejecutable—, para que trabajar por código y por interfaz no den información distinta. No lee el dataset, no monta sumideros de auditoría ni inventario, y no deja rastro de corrida: comprobar no es correr. El primitivo del núcleo equivalente esStudy.check_pipeline(), que re-levanta en vez de capturar. -
Gate de staleness del fixture del schema del front.
web/src/fixtures/schema.jsonlo generascripts/gen_schema_fixture.py, pero nada comprobaba que se hubiera corrido —y ya se había desincronizado en silencio una vez, publicando en la demo un encuadre normativo que el código había corregido—.tests/unit/test_ui_schema_fixture.pylo compara ahora con el payload vivo en cada corrida de la suite, tolerando los dominios cuyo extra no esté instalado.
[1.6.0] — 2026-07-26¶
Corregido¶
- ⚠️ La ECL de IFRS 9 se calculaba mal cuando la curva de PD no venía en años. El descuento
DF(t) = (1 + EIR)^(-τ)usabatime_valuecomo exponente asumiendo años, sin verificarlo. La misma cartera, en los mismos instantes, declarada en meses en vez de años perdía del orden de un 40-50 % de provisión, en silencio: nada en el motor lo impedía ni lo declaraba.
Desde esta versión la term-structure transporta su unidad temporal en una columna time_unit
—la declaran survival (time_grid.time_unit) y markov (dynamics.time_unit), y forward la
propaga— e IFRS 9 convierte a años antes de descontar. La evidencia publica las dos columnas,
time_value cruda y time_value_years convertida, para que la conversión sea un paso aritmético
comprobable y siga reconciliando fila a fila con la curva de origen.
Si su curva ya estaba en años, sus cifras no cambian.
Cambiado¶
- ⚠️ Una curva que no declara su unidad temporal ahora detiene la corrida de IFRS 9. Cuando la
term-structure no trae
time_unit, o trae un literal no convertible, el motor presume años y emiteDATO-INSTITUCIONAL-IFRS-7. Es un aviso gobernable, yfail_on_falta_datoviene enTrue, así que la corrida se detiene en vez de entregar una cifra que podría estar mal por un factor de 12 o de 365.
Esto afecta a quien nunca tocó el campo, porque el default de fábrica de survival y
markov es "period", que nombra un índice y no una duración. Las dos salidas, ambas explícitas:
# (a) declarar la unidad — recomendado, y es de una línea
cfg.survival.time_grid.time_unit = "month" # o "year", "quarter", "day", …
# (b) aceptar la presunción de años, dejando el aviso en el resultado
cfg.provisioning_ifrs9.fail_on_falta_dato = False
La tabla de unidades reconocidas vive en nikodym.core.time_units y acepta español e inglés,
singular y plural, con o sin tildes.
- ⚠️ IFRS 9 verifica que el horizonte de 12 meses dure de verdad un año, y detiene si no
(
FALTA-DATO-IFRS-8).horizon_12m_periodsdeclara cuántos períodos de su curva cubren doce meses, y hasta ahora nadie lo contrastaba con la curva recibida. Con la unidad ya declarada, el motor mira el período del horizonte y comprueba que caiga cerca de un año.
Esto afecta al default de fábrica. horizon_12m_periods viene en 12, que es correcto para
una curva mensual y está mal para una anual o trimestral: sobre una curva anual, el «ECL a 12
meses» de Stage 1 cubría doce años y quedaba unas 7,5 veces sobreestimado, en silencio. La
salida es declarar el horizonte que corresponde a su periodicidad —12 mensual, 4 trimestral,
1 anual— o apagar fail_on_falta_dato para que quede sólo anotado.
El aviso cubre también el extremo opuesto: un horizonte por debajo del primer período de la curva, donde Stage 1 provisionaba cero sin error.
- ⚠️
survivalcumplefail_on_falta_dato, que hasta ahora era un campo sin efecto. El propio config lo admitía por escrito («campo reservado: hoy no altera la corrida»). Desde esta versión gobierna sus tres avisos declarados —DATO-INSTITUCIONAL-SUR-1/2/3— con la misma semántica que las otras seis capas: con el flag activo, un aviso detiene la corrida; desactivado, queda registrado en la card y la corrida sigue.
Es un cambio de comportamiento observable y el default es True. Si su config no declara
grilla temporal (time_grid.horizon_periods ni time_grid.evaluation_times), survival venía
cayendo a los tiempos observados y emitiendo DATO-INSTITUCIONAL-SUR-1 como aviso; ahora esa
misma corrida se detiene. Las dos salidas, ambas explícitas:
# (a) declarar la grilla — recomendado: es la definición que el aviso venía pidiendo
cfg.survival.time_grid.horizon_periods = 5
# (b) conservar el comportamiento anterior, dejando el aviso en el resultado
cfg.survival.fail_on_falta_dato = False
Con esto, fail_on_falta_dato significa una sola cosa en las siete capas del paquete (CRP-6
del contrato de resolución de parámetros, cerrado). Los avisos que un motor emite en toda
corrida por una capacidad diferida propia siguen sin detener nunca: se registran igual, pero
abortar por ellos dejaría el motor inservible con su propio valor por defecto.
- El preset
f4-ifrs9-retaildeclara los intervalos de confianza de Kaplan-Meier (confidence_level=0.95,confidence_transform="loglog"). El preset corrediscrete_hazard, así que sus cifras no cambian; lo que cambia es que ahora sigue corriendo si usted cambia el método akaplan_meierdesde el formulario. Suconfig_hashcambió, y los fixtures de la demo pública se recapturaron contra él.
Añadido¶
- Una corrida que falla ahora dice por qué, por el camino que la documentación recomienda.
run_contextgana el campoerror(RunError:type,message,step,is_domain_error,ts). Hasta ahora el diagnóstico del motor —que es bueno: nombra la columna, el parámetro o el paso concreto— se emitía sólo al audit-trail, y como el preset F1 traeaudit: null, quien seguía el getting-started al pie de la letra se quedaba constatus == "failed",resultsvacío y 413 bytes de hashes en el lineage. El campo se puebla sin configurar nada:
study = nikodym.run(config)
if study.run_context.status == "failed":
print(study.run_context.error.step) # "data"
print(study.run_context.error.message) # el mensaje del motor, íntegro
Extensión aditiva: el campo lleva default None, así que un run_metadata.json guardado
antes sigue recargando, y el evento run_end conserva su clave error y sólo suma error_type
y step. La documentación decía que el fallo vivía «en el audit-trail y en el lineage» en cinco
superficies (README, docs_site/index, tutorial, getting-started y el docstring de run);
de los dos lugares, el lineage no lo guardaba nunca y el audit-trail sólo con sink configurado.
Corregidas las cinco.
Corregido¶
- IFRS 9 dejó de calcular mal en silencio en dos puntos, y de validar tarde en un tercero (primer paso del contrato de resolución de parámetros, CRP-5). Los tres se midieron corriendo el motor, no leyéndolo:
- La LGD del enfoque
workoutse subestimaba cuando faltabarecovery_cost. El motor asumía coste cero: con EAD 100 y recuperación 50 devolvía0.50en vez de0.70, 20 puntos porcentuales menos, sin emitir ningún aviso. Era además asimétrico con su insumo hermanorecovery_time_years, que siempre levantó. Ahora la columna se exige; si la institución no incurre en costos de recuperación, declara ceros explícitos. - Una operación en incumplimiento genuino salía Stage 1. Si la columna
is_default—declarada por defecto— no venía en el frame, el gatillo de Stage 3 devolvía «no» para toda la cartera sin decir nada. El motor trataba igual una elección del usuario y una carencia del dato. Ahora la ausencia levanta, y para no evaluar ese gatillo se declarastaging.is_default_col=None. - Los pesos de escenario inválidos ya habían ponderado la PD antes de ser rechazados. La validación vivía sólo en el cálculo de la ECL, es decir después de calcular con el número malo. El veredicto era correcto y el momento no: ahora se validan al resolverlos, antes de ponderar.
Cambio de comportamiento: una corrida que hoy pasa puede empezar a fallar si el frame no trae
recovery_cost (sólo con lgd.method="workout") o is_default. En ambos casos el fallo sustituye
a un resultado que era incorrecto o incompleto. provisioning/ifrs9 es experimental y queda fuera
de la garantía SemVer 1.x.
-
El panel de resultados de la UI muestra el fallo real, no un texto genérico. El backend no tenía otro que dar —el mensaje moría en el sink— y el front ya sabía mostrarlo. Ahora publica el mensaje del motor con el paso que falló («El paso 'scorecard' falló: …»), sin el código de aviso declarado: once
raisedel motor lo traen dentro del texto y el panel es copy público, así que se recorta constrip_declared_codes(). El mensaje íntegro, con el código, sigue disponible por código enrun_context.error.message. Un fallo que no es error de dominio conserva el mensaje genérico más el tipo de la excepción: su texto es detalle interno y puede traer rutas del servidor. -
Una corrida fallida sellaba
finished_atenNone. Terminaba, y el contexto no decía cuándo.
Cambiado¶
- La marca
FALTA-DATOse separa en dos, porque cubría dos cosas opuestas. Un aviso declarado puede tener dos causas distintas: que a Nikodym le falte algo, o que le falte a la institución un parámetro que sólo ella puede fijar. Hasta 1.5.0 ambas compartían la marcaFALTA-DATO, de modo que el argumento de venta del producto —el motor se niega a inventar un supuesto que no le corresponde— aparecía rotulado como defecto propio 34 veces. Ahora: FALTA-DATOqueda sólo para las brechas del motor:IFRS-4(EAD constante, sin panel longitudinal),IFRS-6/FWD-6(LGD forward descartada),FWD-8(el motor no selecciona el rango de cointegración del VECM),STR-5(motor ECL no conectado a stress),ML-1(data_rawdiferido),VAL-1/VAL-2/VAL-3(convención del t-test ECB, cortes del semáforo y p-valor de Jeffreys, pendientes de verificar contra el documento oficial) y los dospending_itemsdel manifiesto CMF. Se le suma el aviso devalidationcuando eldetailde IFRS 9 llega sin las columnas estimadas que ese mismo motor produce.DATO-INSTITUCIONALes la marca nueva de los 34 códigos que declaran un input de la institución: shocks macro, taxonomía de estados, definición operacional de default, umbrales de gobierno,rho, EIR, cobertura del comparativo de provisiones.
Familia y número se conservan (FALTA-DATO-FWD-1 → DATO-INSTITUCIONAL-FWD-1), así que la
trazabilidad contra los SDD y este changelog se mantiene. Tres normalizaciones de códigos que
habían nacido sin número: STR-LGD → STR-8, el PROV sin número → PROV-2 y el FWD sin
número → FWD-8. Todos los códigos que cambian pertenecen a capas
experimentales; el pipeline F1 estable no emite ninguno. Los campos falta_dato y
fail_on_falta_dato del config no cambian: siguen nombrando el conjunto de avisos declarados,
para no romper el config de quien instaló 1.5.0.
- Los códigos que no significaban nada para un usuario salen del contrato. Siete eran TODO de
ingeniería —pins de
fastapi/uvicorn, librería de charts del front, presupuesto de CI del tuning, evaluador de importancia de Optuna, determinismo cross-versión de los backends y deshap— y viven como issues del repo. Uno de ellos,UI-3, estaba marcado ✅ RESUELTO y se seguía contando como brecha abierta. Otros cuatro (SUR-6,MKV-3,MKV-4,MKV-6) sólo esperaban que otro SDD fijara algo que ya está implementado y se cierran con su evidencia.
Eliminado¶
- Extra
sweep(hydra-core+omegaconf), que nunca tuvo consumidor. Se declaraba como «barridos CLI» y entraba en el meta-extraall, de modo que todopip install "nikodym[all]"bajabahydra-core,omegaconfyantlr4-python3-runtimesin que una sola línea del paquete las importara: no existesrc/nikodym/sweep/ni un consumidor de Hydra/OmegaConf ensrc/. En un paquete que se audita por licencias y por contenido de distribución, un extra sin código que lo use es superficie muerta que hay que auditar igual. Se retira depyproject.toml, delall, del mapaEXTRA_TO_DISTRIBUTIONSy de la tabla de extras de la documentación; el cierre runtime deallbaja de 155 a 152 distribuciones.pip install "nikodym[sweep]"deja de existir; nada más cambia, porque no había funcionalidad detrás. Si algún día se implementan barridos por CLI (diseño en SDD-05 §5.6, que sigue vigente como diseño), el extra vuelve en el mismo cambio que su consumidor, no antes.
[1.5.0] — 2026-07-22¶
Bloque B1 del plan operativo, completo: higiene de lo que el informe y el editor de configuración le muestran a quien instala la librería. Ninguna cifra de negocio cambia; lo que cambia es que el artefacto deje de mostrar precisión que el dato no tiene, jerga del repositorio y afirmaciones que el motor no respalda.
Añadido¶
- Rótulo de las dos cifras de ECL del anexo IFRS 9.
total_ecl_reportedyecl_by_scenariodifieren ~2× por construcción y salían pegadas sin explicación, lo que se lee como descuadre contable. Se añade la clave hermanaecl_by_scenario_basis—extensión aditiva, la clave original no se toca— que declara las dos razones de la brecha: la cifra por escenario no aplicascenario_weightsy cubre el horizonte completo de la curva, mientras la reportada sí pondera y trunca Stage 1 a 12 meses. No es, ni era, un error de cálculo.
Cambiado¶
total_expected_loss_rate(método interno) pasa de texto a número. Se publicaba comostr(Decimal)—única forma de cruzar el gate JSON demetric_sections— y el anexo mostraba sus 51 dígitos:"0.038203224448922777376082337534204431258983735881240". Ahora es unfloat, que cruza el mismo gate, llega como número a quien lo consuma y el informe formatea a0,0382. ⚠️ Cambio de tipo en una superficie experimental (provisiones, fuera de la garantía SemVer 1.x): un consumidor que tratara ese campo como cadena debe leerlo como número. Las cifras contables (total_exposure,total_internal_provision) siguen exactas enDecimal.- Los diagnósticos de selección y de modelo se publican con 6 cifras significativas, no con 12.
El informe mostraba
iv=0.650693017601y, en el caso del VIF, la mantisa completa delfloat; ahoraiv=0.650693. Alcanza a los camposdetailde IV, VIF, correlación, contribución de IV y p-valores Wald/LR. Se usa.6gy no.6fa propósito: una cifra diminuta conserva su magnitud (1.23457e-06) en vez de aplanarse a0.000000. Los mensajes de excepción conservan las 12 cifras, que ahí sí sirven para depurar. - El editor de configuración deja de hablarle al usuario en jerga del repositorio. El texto de
ayuda de cada campo venía citando documentos internos de diseño que quien instala la librería no
tiene: el índice de secciones decía cosas como
Sección de binning supervisado WoE/IV (capa binning, SDD-06)y 22 campos mostraban literalmente== @register('standard', domain='...'). Se reescribieron 185 textos —description,titley encabezados de grupo— en castellano funcional, más los descriptores de preset y dataset que la demo publica. Se conservan, porque no son jerga: las referencias normativas (Circular N° 2.346,Cap. B-1,NIIF 9 B5.5.37), la terminología de riesgo (WoE, IV, PD, LGD, ECL, Stage 1/2/3, PSI, SICR) y los códigosFALTA-DATO-*que el motor imprime en el informe y el usuario necesita rastrear.
Corregido¶
- Cinco textos del config afirmaban cosas que el motor no hace. Salieron de verificar cada enunciado contra el código, y varias venían de antes de esta limpieza:
- Los dos gatillos SICR de Stage 2 se describían con un mínimo (
>=2x,>=3x) que el validador no impone: ambos campos songt=1.0, así que el motor acepta1,05. Ahora el texto dice que 2,0 y 3,0 son el valor de referencia y que moverlos cambia la población en Stage 2. - El indicador de incumplimiento declarado (
is_default_col, CMF) decía mover solo el factor de conversión B-3 de contingentes; también clasifica al deudor de consumo en incumplimiento (PI 100 %) con mora menor a 90 días, lo que cambia la provisión de sus operaciones directas. - La orquestación de provisiones decía aplicar "la regla del máximo" cuando la regla es
configurable (
maxouse_internal). - El umbral de nulos por columna prometía que la selección de variables eliminaría esas columnas: ninguna etapa las elimina; solo se registran como decisión en el audit-trail.
- Las métricas del challenger decían tomarse de la etapa de desempeño "sin recalcularlas"; el paso instancia su propio evaluador y las recalcula con el mismo motor.
- Tres campos que no hacen nada dejan de anunciarse como operativos.
repro.strict_determinism,tuning.validation.fit_partitionysurvival.fail_on_falta_datono los lee ningún componente del motor, yreport.pdf.enabledsolo aplica al uso directo del renderizador —en una corrida el PDF se activa incluyendo'pdf'enformats—. Los cuatro textos ahora lo declaran. Cablearlos o retirarlos es trabajo aparte.
[1.4.1] — 2026-07-21¶
Corrección de tres defectos que la verificación adversarial previa a la reunión con Interbank encontró en lo que la demo y el informe muestran, más la documentación que un comprador institucional busca primero y no encontraba.
Añadido¶
SECURITY.md: cómo reportar una vulnerabilidad, qué esperar y en qué plazos, versiones con soporte, y el alcance de lo que consideramos un problema de seguridad. Incluye la nota de datos: el paquete no lleva telemetría y el pipeline de cálculo no abre conexiones por sí solo; las dos salidas posibles (narración por IA y registro MLflow) son opcionales y las controla quien lo usa.SUPPORT.md: qué canal atiende qué, y —explícito— lo que el proyecto no promete: no hay SLA en el canal abierto, no somos el validador de tu institución, las matrices normativas exigen validación humana, y qué superficie está bajo garantía SemVer 1.x frente a la experimental.- README: sección «Tus datos no salen de tu infraestructura». El argumento que más pesa para una institución financiera —que esto es una librería que corre en su red, sin telemetría, sin llamadas de red en el cálculo y sin depender de un servicio nuestro para funcionar— no estaba escrito en ninguna parte.
Cambiado¶
- Clasificador de madurez en PyPI:
3 - Alpha→4 - Beta.pypi.organunciaba «Alpha» mientras el README declara el pipeline de scorecard como API estable bajo SemVer 1.x desde 1.0: la primera señal de madurez que recibía un evaluador contradecía al propio proyecto. No se sube a5 - Production/Stableporque las provisiones siguen marcadas experimentales por madurez.
Corregido¶
- Preset F1: la PD ancla se estima de los datos en vez de fijarse a mano. El preset
f1-estandar-consumocalibraba conanchor_source="business_input"ytarget_pd=0.20sobre una cartera cuya tasa observada en Desarrollo es 23,3 %. La calibración desplazaba el intercepto hacia un nivel que los datos no respaldan, y Hosmer-Lemeshow rechazaba en las tres particiones (desarrollo χ²=37,2 p=0,00001 · holdout χ²=20,0 p=0,010 · OOT χ²=32,2 p=0,00008): el informe insignia del pipeline estable abría su resumen ejecutivo con «3 de 3 tests fallidos · Falla técnica». Conanchor_source="development_observed"ytarget_pdnulo —la misma configuración que el preset F3 ya usaba, y el default del motor— el sesgo de nivel desaparece (desarrollo χ²=6,0 p=0,65 · holdout χ²=10,1 p=0,26) y sólo queda el rechazo de OOT (χ²=19,5 p=0,013), que es deriva temporal de la cartera y no un defecto de configuración. Cambia elconfig_hashdel preset F1 y las PD calibradas; no cambia la discriminación (AUC/Gini/KS son invariantes al nivel de calibración) ni el config efectivo del F3, que ya aplicaba el override. - Informe: las cifras de plata dejan de mostrarse con seis decimales. Las celdas de exposición,
provisión y pérdida esperada volcaban
697376973.922913—doce dígitos de precisión falsa— justo debajo de la prosa que muestra el mismo monto como $697.376.974; eran 63 celdas en el informe de provisiones. Los floats de magnitud ≥ 1.000 se emiten ahora con dos decimales. El corte es por magnitud y no por nombre de columna, de modo que alcanza a cualquier monto futuro; los indicadores de riesgo (AUC, KS, PSI, IV, PD, LGD, tasas) viven muy por debajo y conservan su precisión. Sólo presentación: no cambia valores nidata_hash, y se mantiene el punto decimal sin agrupar miles, porque estas tablas son volcado técnico pensado para copiarse a una herramienta de análisis. - Demo: el editor de configuración ya no afirma tener un backend en vivo. La demo pública marca la fuente del schema como «backend» para que el editor no se vea degradado, y el aviso decía «Schema en vivo desde el backend (/api/schema)» en una página estática donde no hay ninguna llamada de red. Ahora declara que el schema fue capturado de una corrida real y que la demo no ejecuta cálculo en el navegador.
- Demo: separador de miles consistente. Los tooltips del histograma de score y del gráfico de
lift formateaban las frecuencias en
en-US(n = 3,961) mientras el catálogo de datasets y la pestaña de datos usanes-CL(3.961 filas), de modo que la misma pantalla mostraba las dos convenciones.
[1.4.0] — 2026-07-20¶
Informe con formato editorial, capítulo de validación formal y contexto poblacional; cierre de seis brechas del contrato forward→IFRS 9; y pulido del informe y la demo previo a la reunión Interbank.
Añadido¶
- Informe: capítulo condicional «Validación formal». Cuando la corrida publica el
ValidationResultatómico (validation.result), el informe emite un capítulo nuevo —tras «Resultados», antes de provisiones— con una subsección por familia declarada enfamilies_run(discriminación, calibración, estabilidad, backtesting). Las tablas se copian del DTO:reportno recalcula métricas ni decisiones. El resumen ejecutivo suma la métrica «Estado técnico de validación formal», y el veredicto sigue siendo un bloque humano POR COMPLETAR: el estado del motor no lo sustituye. Si la corrida trae además una card sueltavalidation.cardque no coincide conresult.card, el builder falla en vez de mezclar lecturas de momentos distintos. La prosa de alcance deja de prometer futuro: sin validación, el informe dice que esta corrida no ejecutó la capa formal. El contrato es aditivo (validationno entra enReportStep.requiresni enrequired_sections): ninguna cadena existente se rompe. - Informe: subsección «Población, particiones y exclusiones» en el capítulo de Contexto. Si el
dominio
datapublicó su card, el informe proyecta tres tablas —estados, particiones (tamaño y tasa de incumplimiento) y exclusiones por motivo— copiadas literalmente delDataCardSection: no infiere conteos ni recalcula estadísticas. Es opcional: su ausencia no es error. - Anexo C: cada dominio configurado publica su
effective_config. Antes sólo lo anexabansurvivalyprovisioning_ifrs9. Ahora lo hace todo dominio presente en el config de la corrida —data, el pipeline scorecard,survival,markov,forward,provisioning_ifrs9,validationy las tres secciones de provisiones F3 (provisioning_cmf,provisioning_internaly el orquestadorprovisioningcon su regla del máximo)—, incluso si no emitió card por ser config puro. No se agrega un dump top-level deNikodymConfig, yeffective_configqueda excluido del payload que se envía a la narrativa IA opcional.
Corregido¶
- Identidad del config (
config_hash): la ruta del dataset ya no altera la identidad. El campodata.load.source(ubicación del archivo en disco) entraba alconfig_hash, de modo que el mismo dataset en otra ruta —o el preset consource: nullfrente a la corrida con la ruta real— producía un hash distinto, desalineando elconfig_hashque muestra la app y el que aparece en el informe. Eldata_hashya captura el contenido del dataset; la ruta es incidental. Ahoradata.load.sourcese excluye delconfig_hash(además de las secciones de infraestructura). Nota de contrato (SemVer): esto recalcula elconfig_hashde todo config que fijaba una ruta de dataset; el hash del config por defecto (sindata) no cambia. Es una corrección de defecto, no una nueva convención. POST /api/config/to-yamlera no-determinista frente al estado de imports. La secciónreport(report: Anyen el schema) se coacciona aReportConfigsólo sinikodym.reportya fue importado, y esa coacción materializareport.document(default_factory) que el config del cliente no traía. El YAML salía con o sin ese bloque según qué se hubiera importado antes (así se colabareport.documental capturar los fixtures de la demo tras generar un informe). Ahora el endpoint vuelca conexclude_unset=True: idéntico byte a byte en ambos casos, sin tocar elconfig_hashni el lineage de corrida destudy.- Informe: coma decimal (es-CL) en la prosa y en las cifras destacadas. Los porcentajes y números
(
_pct/_num) emitían punto decimal (2.99 %,IV 0.03) mientras las cifras en pesos ya usaban el punto como separador de miles. Ahora el decimal es coma (2,99 %), coherente con la convención chilena. Sólo presentación: no cambia valores nidata_hash. Las tablas de detalle y los ejes de los gráficos conservan el punto a propósito: son volcado técnico deresults, pensado para copiarse a una herramienta de análisis. - Informe: marcador único «—» para celdas sin valor en las tablas de detalle.
NaNse volcaba comonany el sentinel de dominio"none"(de los enumsiv_band/expected_sign/action= «sin banda/ signo/acción») comonone; ambos parecían celdas rotas. Se unifican a un em-dash (convención de estados financieros para nil/ninguno/no-aplica), sin tocar los enums de la API de results ni eldata_hash. Losinf/-infse conservan crudos a propósito (anomalía real que debe verse). - Informe: las celdas de tabla ya no vuelcan
Decimalcrudo ni colecciones vacías. Los motores de provisiones publican sus cifras enDecimalpara no perder exactitud contable, pero el renderer no tenía rama para ese tipo y caía enstr(value): la tabla «Provisión interna por grupo homogéneo» mostrabapd_group = 0.0052290061597687685198855454245661540747073248842022(52 dígitos), y lo mismolgd_groupyexpected_loss_rate. Ahora se formatean con la misma regla que un float (el anexo JSON ya lo hacía así). En la misma línea, una colección vacía (warning_codes: []) muestra el em-dash de «ninguno» en vez de[]/{}crudos. Sólo presentación: las cifras no cambian. - PDF: las tablas de 10+ columnas eran ilegibles y ahora van en hoja apaisada. En A4 vertical
(«Desempeño por decil de score»: 20 columnas sobre 170 mm útiles) cada columna recibía ~8 mm: los
encabezados se solapaban entre sí y cada cifra se partía en tres líneas (
505./4441/62), de modo que la evidencia de monotonía y lift del scorecard llegaba al comité como un amasijo. Esas tablas se marcantable-block--widey en@media printvan a@page wide-table(A4 apaisada) con ancho automático, tipografía menor y celdas sin partir. No se descarta ninguna columna —perder una sería perder trazabilidad— y en pantalla no cambia nada. - Informe IFRS 9: la tabla insignia se titulaba con la clave interna. La única tabla del cuerpo
—la que sostiene ECL, EAD y cobertura— salía como
Tabla «provisioning_ifrs9.summary»(en mayúsculas por CSS) porque faltaba del catálogo de títulos, donde su hermana CMF sí estaba. Pasa a «Pérdida crediticia esperada (ECL) por etapa». - IFRS 9 (experimental): seis brechas del contrato forward→IFRS 9, cerradas con guards fail-fast.
Cuatro configuraciones que el motor aceptaba y luego degradaba en silencio ahora fallan con un
mensaje que dice qué usar en su lugar: (1)
pd.rho_colse rechaza al construirIfrsPdConfig—el motor v1 sólo consumepd.rhoescalar, y honrar la columna con el escalar sería una etiqueta falsa—; (2)pit_mode='apply_vasicek'exige siempresystemic_factor_col: se elimina la exención descenarios.source='forward', que suponía un factor sistémico Z que forward no publica (sus curvas ya son PIT ⇒pit_mode='consume_pit'); (3) aplicar Vasicek sobre una term-structure ya etiquetadapd_basis='pit'queda bloqueado, evitando el doble ajuste macro (espejo del guard queconsume_pitya tenía); (4)forbid_mean_scenario=Truepasa de auditado a bloqueante, en el config y en el motor, sobre las tres fuentes de escenarios y sin distinguir mayúsculas (mean/average/weighted_mean_input) —se ponderan outputs por escenario, nunca inputs macro promediados—; el escape hatchflag=Falsesigue disponible y queda auditado. Queda además caracterizada con tests y golden (sin tocar el motor) la frontera de pesos cero:forwardadmite peso 0 y IFRS 9 exige peso > 0; su resolución de fondo es una decisión de política pendiente. Nota de contrato: ninguna ruta válida cambia de resultado numérico (config_hashy demo F4 invariantes), pero un config que antes corría degradado ahora aborta. La capa IFRS 9 es experimental, fuera de la garantía SemVer 1.x. - IFRS 9: la LGD de la capa forward ya no se descarta en silencio. Si la term-structure trae una
columna
lgdcon algún valor no nulo, el motor —que en v1 estima la LGD desde elframesegúnIfrsLgdConfig— declara el descarte con el códigoFALTA-DATO-IFRS-6: aparece en loswarning_codesde cada fila, encard.falta_dato, en la traza de auditoríaifrs9_lgdy como frase explícita en el informe. Sólo declaración: las cifras no cambian. - IFRS 9: descriptions honestas de
rho_colyfail_on_falta_dato. Ambas prometían conducta que el motor no implementa:rho_coldecía «sobrescribe rho por fila» pero el motor la rechaza fail-fast (guard introducido en este mismo release, ver arriba; correlación heterogénea diferida en v1);fail_on_falta_datosugería un modo «marcar FALTA-DATO y continuar» ante Vasicek sin rho/Z que no existe (el motor falla en cálculo siempre). Se reescriben las descriptions (y títulos) para reflejar la conducta real. - Captura y config de la UI: la ruta del dataset deja de ser específica del host. Con
workdirrelativo (el default), la ruta querun_pipelinecablea adata.load.sourcese conserva relativa y en separadores POSIX, de modo que el mismo config sale idéntico en distintos checkouts y en Windows/macOS/Linux; una ruta con ancla (raíz, unidad o UNC) conserva su semántica nativa porque es una elección explícita del usuario. Además, la validación de contención del directorio de datasets ahora resuelve enlaces simbólicos también en el directorio, no sólo en el archivo: unworkdir/datasetssymlinkeado fuera del workdir pasaba el control anterior.
Cambiado¶
- Informe HTML con formato editorial (tema
nikodym, el de fábrica). El HTML pasa a un layout de tres columnas en pantalla —sidebar de secciones con la marca Nikodym, contenido e índice lateral «En esta página» con los entregables— sobre las clases ya existentes (portada, lineage, firmas SR 11-7, veredicto/callouts, chips de estado, tablas). Los rieles son de pantalla: en@media printel documento colapsa a una columna A4, así que el PDF (WeasyPrint) queda intacto. El temaplainno cambia. El markup del documento —data-section-id, orden canónico de secciones,id/thead/tbodyde las tablas y los literalesconfig_hash=/data_hash=/git_sha=/root_seed=— se conserva: quien parsea el HTML no se ve afectado. - Preset F1 de la UI: la corrida estándar ejecuta validación formal.
standard_preset()deja de traervalidation: nully activa discriminación, calibración (Hosmer-Lemeshow + Brier sobre la PD calibrada) y estabilidad, reusando lo que ya calculanperformance/stability. El contraste por grado y el backtesting quedan apagados (el dataset no traegradeni realizados). Los presets F3 (CMF) y F4 (IFRS 9) declaranvalidation: nullexplícitamente: conservan su alcance previo. - Demo: badge «Experimental» en la card de provisiones CMF (F3), igual que la de IFRS 9 (F4), pues ambos motores de provisiones son experimentales por madurez.
- Bump de versión 1.3.0 → 1.4.0.
[1.3.0] — 2026-07-17¶
Añadido¶
- Extra opcional
markov(pip install nikodym[markov], que proveescipy) para el módulo de cadenas de Markov (nikodym.markov). Los mensajes de dependencia ausente enmarkov/*.pyya apuntaban anikodym[markov]; ahora ese extra existe de verdad. - Smoke clean-room del wheel para el preset
f4-ifrs9-retail.scripts/smoke_instalacion_pip.pyqueda parametrizado por preset (argumento CLI o variable de entorno), conservando el modo scorecard F1 como comportamiento por defecto (el que usa el CI). Sobre una instalación real por pip, verifica que la corrida IFRS 9 termina endoney produceprovisioning_ifrs9con staging (Stage 1/2/3) y ECL/cobertura.
Cambiado¶
- Bump de versión 1.2.0 → 1.3.0.
- Documentación (
docs_site/index.md,docs_site/api.md) y estado del repo (AGENTS.md,CLAUDE.md) reflejan 1.3.0. - Recaptura de los informes demo (F1 · F3 · F4): el lineage
library_versions.nikodymde los fixtures reporta 1.3.0. Sin cambio de cifras insignia ni delconfig_hashde los presets.
[1.2.0] — 2026-07-15¶
🔴 Corregido — una regla normativa que afirmábamos y que NO EXISTE¶
Nikodym declaraba, en el código y en toda su documentación, que la provisión reportada es el máximo entre el ECL de IFRS 9 y un "piso prudencial CMF", y lo presentaba como norma citada. La fuente era un documento interno de este proyecto que no citaba ninguna circular.
Verificado contra el texto oficial del Compendio de Normas Contables para Bancos (CMF):
- Cap. A-2, num. 5 — "Lo establecido en el Capítulo 5.5 (deterioro de valor) de la NIIF9 (…) no será aplicado respecto de las colocaciones (…) ni sobre los "Créditos contingentes", ya que los criterios para estos temas se definen en los Capítulos B-1 a B-3 de este Compendio." → En Chile, un banco no calcula ECL de NIIF 9 sobre su cartera de colocaciones: el B-1 lo sustituye. No hay nada contra lo que comparar.
- Cap. B-1, hoja 10-11 (Circular N° 2.346 / 06.03.2024) — "La constitución de provisiones se efectuará considerando el mayor valor obtenido entre el respectivo método estándar y el método interno. (…) Esta regla se deberá aplicar para cada institución en Chile que consolida con el banco." → La regla del máximo es estándar vs. interno, a nivel de entidad.
max(CMF, IFRS 9) no es "el piso prudencial de la CMF" y deja de presentarse como tal en todas
las superficies. El comparativo entre ambos marcos se mantiene —es útil, por ejemplo, para una
filial que reporta ECL a su matriz extranjera— pero declarado como lo que es: un comparativo entre
marcos contables, sin norma chilena que lo exija.
🔴 Corregido — el motor subprovisionaba al deudor refinanciado¶
El incumplimiento del Cap. B-1 numeral 3.2 tiene tres causales: mora ≥ 90 días, un crédito otorgado para dejar vigente una operación con más de 60 días de atraso, y reestructuración forzosa o condonación parcial. El motor de consumo derivaba solo la primera, de la mora.
Consecuencia: un deudor reestructurado o refinanciado al día recibía la PI de su tramo de mora
(6,6 %) en vez del 100 % que la norma exige. Sub-provisión de 15×, y en la dirección que
un regulador no perdona. La columna is_default existía en la config y el motor ya la leía para
los contingentes B-3, pero en consumo nunca la consultaba.
Ahora el motor la lee también en consumo: la columna es opcional (sin ella, el comportamiento no
cambia) y sus nulos se leen como "no marcado", de modo que el flag solo puede sumar
incumplimiento, nunca quitar el que impone la mora. El incumplimiento se consolida a nivel
deudor —la norma arrastra todos los créditos del deudor— y la traza de auditoría reporta la
categoría incumplimiento, no el tramo de mora, para que el PI de 100 % sea visible.
Verificado — la matriz de consumo, contra el compendio oficial¶
Las 23 celdas de consumer_standard_v2025 (16 de PI, 6 de PDI y el PI = 100 % de
incumplimiento) se cotejaron una a una contra el texto del Compendio de Normas Contables,
Cap. B-1 numeral 3.1.3, hojas 16-18 (Circular N° 2.346 / 06.03.2024). Coinciden exactamente.
Sigue sin ser una validación de la CMF —la Comisión no certifica implementaciones de
terceros—, pero deja de ser una transcripción sin contrastar.
También queda anclado el benchmark del dataset provisiones_consumo: el índice de riesgo de la
cartera de consumo del sistema bancario es 8,30 % (CMF, Informe del Desempeño del Sistema
Bancario y Cooperativas, noviembre 2025, sección 2.2). La cartera sintética produce 8,63 % —
33 pb sobre el sistema. Antes se comparaba contra el 2,59 % del sistema completo, que es el
agregado de todas las carteras y no es comparable con una cartera de consumo (consumo va 3,2×
sobre ese agregado).
Añadido¶
nikodym.provisioning.internal— el motor del método interno del Cap. B-1, que faltaba:provisión(g) = Exposición(g) · PD(g) · LGD(g)por grupo homogéneo, tal como la norma lo describe textualmente. La PD sale del scorecard calibrado, de modo que el modelo del banco entra por fin en la provisión reportada. Métodospd_lgdydirect_loss_rate(los dos que el B-1 admite), agrupación por banda de score / segmento / provista, aritmética enDecimal, y golden verificado a mano al centavo.provisioning.source_a/source_b/rule— el orquestador compara fuentes configurables en vez de estar cableado a CMF↔IFRS 9.rule="use_internal"implementa la otra mitad de la norma: con método interno evaluado y no objetado por la Comisión, la provisión se constituye según el interno aunque el estándar sea mayor.- Dataset sintético
provisiones_consumo— cartera de consumo con las columnas económico-regulatorias que exigen los motores (exposición, mora, deudor con varias operaciones, producto, flags de sistema, LGD), coherente por construcción y con tasa de default de un dígito. - Capítulo de provisiones en el informe — un capítulo condicional (
ChapterSpec.requires_domain) que solo aparece cuando la corrida calculó provisiones. Su titular es el número que vende: la provisión a constituir y el sobrecosto del método estándar en CLP (para la cartera de la demo, $388.732.916 por encima de lo que el método interno pediría). Trae las tres tablas agregadas (comparación estándar-vs-interno, provisión estándar por categoría, provisión interna por grupo), declara la asimetría de consolidación que la norma impone (estándar por deudor, interno por banda de score) e imprime los warnings del orquestador. Con el capítulo ya presente, el informe deja de decir que las provisiones "corresponden a fases posteriores". Como consecuencia,reportahora corre al final del pipeline (antes su builder nunca veía las cards de provisiones). - Las cards de provisiones en
results.json— el serializer de la UI expone las tres cards (estándar CMF, método interno, orquestador con la regla del máximo) y sus frames agregados graficables: el desglose del estándar por categoría, el del interno por grupo homogéneo y la comparación estándar-vs-interno. Los framesdetailpor operación (6.000 filas) jamás entran al payload — reventarían/api/results. LosDecimalcontables salen como número, no string. - Preset
f3-provisiones-consumo+ rutas REST — un config listo para correr sin tocar nada que, encima del scorecard F1, calcula el método estándar de la CMF, el método interno y la regla del máximo estándar-vs-interno a nivel de entidad. El selector del front lo descubre porGET /api/config/presets; el detalle se pide porGET /api/config/preset/{id}. La calibración usadevelopment_observed, NO se hereda del F1: con eltarget_pd=0.20del F1 la PD se inflaría 3x, el método interno superaría al estándar y la regla del máximo no mordería — un test end-to-end corre la cadena entera y falla si el estándar deja de ser el que manda. El delta de provisiones se deriva —y se verifica corriendo— conscripts/derive_provisiones_preset.py. - Capítulos condicionales del informe (
ChapterSpec.requires_domain): un informe de scorecard ya no puede traer un capítulo de provisiones vacío, ni uno con provisiones declarar que no las cubre. scripts/gen_schema_fixture.py— regenera el fixture del schema de la demo, que hasta ahora se actualizaba a mano y se desincronizaba en silencio.
Corregido¶
- Los
Decimalde provisiones tumbaban/api/resultsentero. El serializador de la UI no conocíaDecimal(los motores de provisiones trabajan enDecimalporque es una cifra contable) y su guard de serialización es global: no fallaba la sección de provisiones, fallaba todo el payload. Igual en el informe, que imprimía{"unsupported_type": "Decimal"}donde va la cifra. - Coherencia del material público (auditoría adversarial pre-1.2.0). El informe insignia
renderizaba el markdown
**mayor valor**como asteriscos crudos (la prosa va autoescapada), y su frase de alcance seguía declarando "solo validación de scorecard" pese a incluir el capítulo de provisiones — ahora la salvedad reconoce el capítulo regulatorio (experimental, fuera de SemVer 1.x). En la landing, el dominio de provisiones se rotulaba "CMF + IFRS 9" con superficie UI cuando la pantalla compara CMF vs. método interno (IFRS 9 corre en el motor, no en la UI). Y el pie de la demo declara ahora que la corrida real usa un dataset sintético de ejemplo, no la cartera de un banco.
Nota para quien audite¶
Los parámetros normativos de las matrices CMF siguen siendo una transcripción del compendio asistida por IA, con verificación visual: no son parámetros oficiales de la CMF ni están validados por ella, y requieren validación humana contra la norma vigente antes de cualquier uso productivo.
[1.1.3] — 2026-07-13¶
Release de documentación y metadata. Sin cambios de código: el motor es idéntico al 1.1.2.
Añadido¶
- Sección "Quién lo construye" en el README y en la home de la documentación: el motor lo construye Nexo Labs, y hasta ahora ninguna superficie decía cómo llegar a quien lo mantiene.
[project.urls]:Demo(demo.nikodym.cl) y el enlace a la consultora en la barra lateral de PyPI.
Corregido¶
Documentationapuntaba al propio README (#readme), no a docs.nikodym.cl. Lo mismo en la sección "Documentación" del README, que enlazaba a sí misma.- El enlace a la licencia era relativo, y los enlaces relativos se rompen en la página de PyPI
(el README es la
long_description).
[1.1.2] — 2026-07-13¶
Corregido¶
pip install nikodymno corría: la primera corrida de todo usuario nuevo moría. Las dependencias publicadas declaraban rangos abiertos (pandas>=2.0,scikit-learn>=1.6), y la resolución libre de pip —la que hace cualquiera que instale desde PyPI— traía hoyscikit-learn1.9, que eliminó elforce_all_finitequeoptbinninginvoca (el binning muere con unTypeError), ypandas3.0, que rompe la serialización de resultados con un 500. El motor no arrancaba en 1.0.0, 1.1.0 ni 1.1.1.
El CI no podía verlo: corre con uv sync --locked, que fija pandas 2.3.3 y scikit-learn
1.7.2. El techo scikit-learn<1.8 incluso ya existía, pero en [tool.uv]
constraint-dependencies —donde protege al desarrollador y al CI, y no viaja en el wheel—. Ahora
los techos (pandas<3, scikit-learn<1.8) están donde el usuario los recibe.
Se añade el gate que faltaba: el CI instala el wheel con pip resolviendo libre y corre el
preset estándar de punta a punta (scripts/smoke_instalacion_pip.py). El smoke anterior solo
hacía import nikodym, y importar siempre funcionaba.
[1.1.1] — 2026-07-13¶
Corregido¶
- La base editable se descargaba con todas las imágenes rotas.
GET /api/report/{run_id}/mdentregaba un ZIP con el.qmdy ninguna figura: el documento citaba sus cinco SVG por ruta relativa (scorecard_report_figuras/…) y ninguna viajaba en el paquete, así quequarto renderno compilaba y el informe se abría sin gráficos. La causa está en la costura entre dos funciones que por separado eran correctas:runs.savenormaliza el documento areport.qmdpero copia la carpeta de figuras con elbasenameque el propio.qmdreferencia, mientras que el empaquetador derivaba el nombre de esa carpeta del stem del archivo persistido (report_figuras), que nunca existe. Ahora el ZIP empaqueta las carpetas*_figurasque realmente están en la corrida. El test del endpoint fabricaba a mano un estado quesavenunca produce, y por eso pasaba en verde: ahora monta el estado real y exige la invariante que importa —toda figura citada por el documento viaja en el paquete, con la misma ruta relativa—. - La UI ofrecía marcar
jsonen los formatos del informe, y marcarlo garantizaba un error. ElLiteraldeBasicReportFormatseguía declarandojsonpese a que ningún motor lo genera, así queGET /api/schemalo publicaba en el enum, el multiselect pintaba su checkbox y quien lo marcaba se llevaba unValidationErrorque no tenía forma de prever desde la interfaz. El formato sale del enum: lo que la UI ofrece es ahora exactamente lo que la corrida puede cumplir. El validador que ya rechazaba los formatos sin motor se conserva como red de seguridad para quien amplíe el enum sin cablear la generación. Por coherencia,jsontambién sale deReportOutputFormat: un formato que no se puede pedir tampoco se puede producir.
[1.1.0] — 2026-07-13¶
Añadido¶
- Metodología y Resultados llevan prosa generada y determinista, redactada con los parámetros efectivos de la corrida (método de binning y sus umbrales, criterios de selección, estimación, escalado, calibración). Sin red y sin IA: un informe regulatorio no puede variar entre dos corridas del mismo modelo.
- Base editable descargable: el informe se exporta como
.qmd(Quarto/Markdown, con front-matter y el lineage, para editarlo y compilarlo) y como.docx(Word, con estilos de encabezado reales, tablas nativas y figuras embebidas). Introducción, Contexto y Conclusiones vienen como placeholders con guía de qué escribir, ocultables conreport.document.placeholders="hide". - Los formatos
csvyxlsxahora existen de verdad: exportan las tablas por observación (puntaje, PD, datasets WoE) completas, y se publican enReportResult.data_exports. - Extra nuevo
nikodym[docx](python-docx, MIT) para el export Word. Entra en el meta-extraally enui, así que quien instalanikodym[all]onikodym[ui]ya lo tiene. El extrapdfsigue aparte a propósito: WeasyPrint arrastra Pyphen (tri-licencia con GPL) y el gate de licencias del CI lo mantiene fuera del cierre redistribuible.
Cambiado¶
- El reporte pasa de ser un volcado a ser un documento. Antes emitía una sección por paso del
pipeline, cada una con su
Payloady tablas tituladas con el nombre de la variable interna. Ahora es un informe de validación: portada con campos de proyecto, resumen ejecutivo (veredicto y métricas clave), índice, Introducción, Contexto, Metodología, Resultados, Conclusiones, Limitaciones y anexos técnicos. Todo el detalle de antes se conserva: baja a los anexos. Quien parsee el HTML o lossection.iddelReportManifestverá otra estructura (el esquema deReportManifestno cambió: los campos nuevos deReportSectionson aditivos y traen default). report.formatsya no acepta en silencio lo que no implementa: pedir un formato sin ruta real falla con un error explícito en vez de validar y no producir nada. Cambio incompatible acotado: un config conjsonenreport.formats—que en 1.0.0 validaba pero no producía archivo alguno— ahora falla al cargar.- Las tablas por observación salen del cuerpo del documento (iban truncadas a 200 filas, sin servir ni como dato ni como informe) y pasan a los exports de datos. El informe de referencia de la demo baja de 58 a 39 páginas.
- El preset estándar pide los cuatro entregables (HTML, PDF,
.qmd,.docx). Antes pedía solo HTML y, como la interfaz no expone dónde cambiarlo, las descargas de PDF y base editable respondían 404 siempre.reportes infraestructura, así que elconfig_hashdel preset no cambia. - Las funciones de
nikodym.report.chartsaceptanfmt="svg" | "png"y su retorno pasa destrastr | bytes. El default no cambia (svg), así que el comportamiento en runtime es el mismo.
Corregido¶
ranking_preservedreportaba rankings rotos que no lo estaban. Comparaba los rangos con igualdad exacta, así que una calibración monótona (intercept_offset) que colapsara dos PD separadas por ~1e-17 al mismofloat64se reportaba como ranking degradado, sin que ningún par se hubiera invertido. Ahora distingue la inversión de orden y el colapso de deudores que el modelo sí distinguía (ambos,False) del empate por precisión de coma flotante (True). El colapso se sigue contando enties_created.- Sin las librerías nativas de WeasyPrint, la corrida entera moría.
pdf.fail_if_unavailable=Falsepromete degradar y entregar el HTML igual, pero solo cubría "WeasyPrint no instalado": las nativas ausentes (Pango/HarfBuzz/libffi) escapaban comoOSErrorcrudo. Es el caso normal depip install nikodym[pdf]en macOS o Windows sin Pango.
[1.0.0] — 2026-07-12¶
Primer release estable. Congela la superficie pública del pipeline de validación de scorecard
(F1) bajo garantía SemVer 1.x (no rompe hasta un 2.0): nikodym.run, el config raíz
(run → Study → NikodymConfig) y los dominios data, eda, binning, selection,
scorecard, calibration, performance (AUC/KS/Gini), stability (PSI/CSI) y el reporte HTML.
Estable (SemVer 1.x)¶
- Pipeline scorecard F1 de punta a punta y su config declarativo; audit-trail y reproducibilidad
(
config_hash).
Sigue experimental (fuera de la garantía SemVer 1.x)¶
- Modelado ML/tuning/explicabilidad, forward-looking, Markov, survival y stress testing.
- Provisiones CMF e IFRS 9/ECL (motores implementados y deterministas, pero su superficie regulatoria aún crece y no está battle-tested en producción).
- Validación avanzada (backtesting/discriminación), gobernanza/tracking, formatos de reporte PDF/DOCX y narrativa por IA, y los contratos transversales de resultados/métricas/orquestación.
Changed¶
- Marcadores de estabilidad por módulo:
Experimental (SemVer 0.x)→Estable (SemVer 1.x)en el core F1, y →Experimental (fuera de la garantía SemVer 1.x)en la superficie que crece.
[0.9.0] — 2026-07-10¶
Primer release público en PyPI. Motor V1 completo (F0–F7) y verde en CI; API pública versionada como 0.x honesto (puede cambiar hasta la 1.0).
Incluye¶
- Núcleo reproducible (F0): config declarativo Pydantic v2,
Study/lineage, audit-trail, artifacts namespaced, gobernanza SR 11-7. - Scorecard (F1): binning/WoE monotónico (optbinning), selección, regresión logística, scorecard escalado, calibración, desempeño (AUC/KS/Gini) y estabilidad (PSI/CSI).
- Backends ML (F2): XGBoost, LightGBM, CatBoost, tuning (Optuna) y explicabilidad (SHAP) como extras selectivos.
- Provisiones: motores CMF (Chile) e IFRS 9/ECL separados (provisión = máximo).
- Forward-looking y stress testing.
- UI (F7): flujo Scorecard F1 (Datos · Ejecutar · Resultados · Reporte) — React + backend FastAPI, con modo claro/oscuro y reporte HTML del modelo.
- Empaquetado: publicación en PyPI vía Trusted Publishing (OIDC, sin tokens).
Detalle de la Fundación (F0)¶
- Esqueleto del paquete:
pyproject.toml(uv + hatchling, layoutsrc/, 7 deps base, extras de usuario y grupos de desarrollo PEP 735),LICENSEApache-2.0,README,CHANGELOG. nikodym.core.exceptions: jerarquía de excepciones con raízNikodymError(código regulatorio, cobertura objetivo 100 %).nikodym.core.seeding:SeedManager— derivación determinista por nombre víaSeedSequence(entropy=[root_seed, hashlib])(código regulatorio, cobertura objetivo 100 %).nikodym.core.config: configuración declarativa (Pydantic v2).NikodymConfigfrozen construible sin argumentos, seccionesReproConfig/RunConfig;config_hash(SHA-256 del JSON canónico que excluyeINFRA_SECTIONS, estable e idéntico entre procesos);load_config/dump_config(round-trip YAML consafe_load); version-gatemigrate+ decorador@migration(registro vacío en 1.0.0, cadena lineal validada en import-time). Experimental (SemVer 0.x).nikodym.utils.optional:require_extra/has_extra/EXTRA_TO_DISTRIBUTIONS(import perezoso de extras con mensaje accionable).- Paths regulatorios declarados (
nikodym.provisioning.cmf,nikodym.provisioning.ifrs9) para el gate de cobertura regulatoria; su implementación llega en F3/F4. nikodym.core(resto de la Fundación, 9 módulos): primerStudyend-to-end con lineage reproducible.audit(AuditEvent/AuditKind/AuditSink,NullAuditSink/InMemoryAuditSink/FanOutSink);results(Protocols económicosProvisionResultLike/ECLResultLikeconterm_structure(), CT-2);base(BaseNikodymEstimatorraíz propia + 6 familias, semánticaget_params/set_params/from_configestilo scikit-learn sin heredarlo);mixins(AuditableMixin,SerializationMixincon puertatrust);registry/artifacts(registro y almacén namespaced(domain, key));steps(Step/StepAdapter,requires/provides, CT-1);lineage(LineageBundle/RunContext);study(Study: orquestador motor v1 con validación de prerequisitos CT-1, persistencia en directorio atómico, recarga con verificación deconfig_hashy reproducibilidad). Experimental (SemVer 0.x): orquestación y Protocols de resultados crecerán.nikodym.data(B2a — capadata, configuración + endurecimiento, SDD-02 §5): sub-config declarativoDataConfig(nikodym/data/config.py): árbol Pydantic completo (Loading/Schema/Target/ Missing/Partition), mini-DSL declarativoPredicate/Rule(allowlist cerrada de operadores, sineval), unión discriminada anidada de la estrategia de partición (temporal/random/cohort) por factory local,model_validatorde fracciones (suman 1) y de regla no vacía, aliasschemaconpopulate_by_name. EndurecidoNikodymConfig.datadeAnyaDataConfig | None(tipado estricto para mypy; coerción en runtime vía hook_DATA_CONFIG_CLSquenikodym.datapuebla al importarse — el núcleo sigue liviano, no importadata). Goldenconfig_hashpor defecto invariante. Deps base activadas:pandera>=0.24(usoimport pandera.pandas) ypyarrow>=14. Experimental (SemVer 0.x).