Blameless post mortem: Ajustes de función en `bootstrapping_tools`

30 Jun 2023 - Mario y Memo

Resumen del incidente

¿Qué nos condujo al evento?

Falla

random_resample_data_by_blocks

La función random_resample_data_by_blocks no seleccionaba los últimos bloques. Los ajustes no tomaban en cuenta los últimos años.

Por ejemplo, para una serie completa de 10 años y bloque de tamaño l = 3 tiene 10 posibles bloques. Los bloques son: 123, 234, 345, 456, 567, 678, 789, 890, 901 y 012. Seleccionábamos ceil(10/l) = 4 bloques al azar de los primeros cuatro bloques: 123, 234, 345 y 456. No podíamos seleccionar los últimos bloques: 567, 678, 789, 890, 901 y 012.

lambdas_calculator

La función lambdas_calculator estaba asumiendo que el primer elemento es el primer año (eje x ordenado). lambdas_calculator no sabe que los datos están remuestreados en la implementación. Por ejemplo, consideremos que nuestros bloques remuestreados son: 234, 456, 345 y 234. Los índices de los años en la serie de tiempo eran 234456345234. Los índices no les correspondían al tiempo que debían. El ajuste toma como año inicial el primer elemento del primer bloque de la serie remuestreada. En este ejemplo el dos se vuelve el año inicial, es decir 0.

Aquí tendremos la serie de tiempo con los años correspondientes al 23456. Así que tendremos años con más peso.

Population_Trend_Model

En Population_Trend_Model desvinculamos las lambdas remuestreadas de su par $N_0$, la población inicial.

Plotter_Population_Trend_Model

En Plotter_Population_Trend_Model el tiempo empieza en 1. Es decir, graficábamos $N_1$ en $t_0$, $N_2$ en $t_1$ y así sucesivamente. Este no fue un error crítico pero consumió tiempo para corregir y analizar si era la causa raíz del problema.

Impacto

Detección

Nos dimos cuenta cuando reutilizamos el código de las clases Population_Trend_Model y Plotter_Population_Trend_Model con los datos de GUMU.

El pipeline no nos tronó, así que la detección fue visual. Aquí enlistamos algunas pistas que tuvimos:

¿Hubiéramos podido hacer una prueba automática o lo debe hacer un humano (checklist)? ¿Cuándo decidimos hacer una inspección?

Respuesta

Dependencia circular Inversión de dependencia Dependencia no circular

Recuperación

Hicimos pruebas manuales. Quitamos la semilla y notamos que no seleccionábamos los últimos bloques. Agregamos una prueba que tronara debido al bug. Corregimos el bug.

Reducción del tiempo de recuperación

Hacer el ajuste a los datos crudos nos hubiera ayudado a descartar el método de graficado. Los datos sintéticos nos hubiera ayudado a tomar de decisión de que los resultados estaban mal.

Línea de tiempo

2020-01-21 Usamos calcula_lambda sin remuestrear.

2021-06-06 Nos trajimos lambda_calculator de otro repositorio. bootstrap_from_time_series le pasa los datos remuestreados ordenados a lambda_calculator pero el remuestreo podría haber quitado el primer año.

2022-06-10. Aquí agregamos la funcionalidad de _get_labels.

2022-06-10. Extrajimos la función _get_labels con pruebas tronando.

2022-06-23. Usamos _get_labels en el remuestreo.

2022-07-20 Implementamos random_resample_by_blocks. Aquí le pasamos los datos remuestreados y desordenados a lambda_calculator.

2023-06-28 Regresamos implementación de lambda_calculator para obtener la población inicial. Nos damos cuenta que el error está en niveles más abajo.

Cinco por qué

  1. Los datos que ajustábamos no contenían todos los datos originales, faltaban los últimos datos. El ajuste de la gráfica no se ve bien porque la N_0 y la lambda están desvinculadas. Además el modelo no empieza en t=0 y el ajuste está desplazado.

  2. Faltaban los últimos datos, porque solo seleccionábamos los primeros ceil(n/l) bloques. La N_0 y la lambda están desvinculadas porque solo le pasamos las lambdas remuestreadas sin sus correspondientes N_0. El modelo no empieza en t=0 porque no obtenemos el dominio de los datos originales. El ajuste está desplazado porque metemos los datos remuestreados de manera incorrecta.

  3. Solo los primero bloques porque nos confundimos la serie debía formarse por 4 bloques de los 10 posibles. Solo le pasamos las lambdas remuestreadas sin sus correspondientes N_0 porque en bootstrapping_from_time_series solo guardamos las lambdas. No obtenemos el dominio de los datos originales porque reciclamos la variable en el graficado. Metemos los datos remuestreados de manera incorrecta porque no aseguramos que están ordenados y con el primer año de los datos originales.

  4. Nos confundimos porque tuvimos malas prácticas: no probamos los límites, nombres mejorables y cambios muy grandes (extrajimos una función y cambiamos las pruebas). En bootstrapping_from_time_series solo guardamos las lambdas porque es el resultado que reportamos. Reciclamos la variable en el graficado porque las clases comparten variables. No aseguramos que los datos remuestreados estén ordenados y con el primer año de los datos originales porque no sabíamos que lambda_calculator los necesitaba así.

  5. Tuvimos malas prácticas por inexperiencia en el TDD. Las clases comparten variables porque rompemos con la encapsulación. No sabíamos y tampoco las pruebas que lambda_calculator necesitaba los datos ordenados y/o con el primer año porque las pruebas y la documentación no eran suficientes.

Causa raíz

Lecciones aprendidas y cosas ganadas

Acciones correctivas