<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Yeison Liscano</title><description>Articles by Yeison Liscano on MCP, AI agents, security, Python, algorithms and economics.</description><link>https://yeison-liscano.github.io/</link><item><title>Pruebas de hipótesis paso a paso: una diferencia no basta</title><link>https://yeison-liscano.github.io/portfolio/blog/pruebas-de-hipotesis/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/pruebas-de-hipotesis/</guid><description>Cómo planificar y ejecutar una prueba de hipótesis con dos proporciones: hipótesis, supuestos, valor p, potencia e intervalos para evaluar una mejora sin confundir significancia con utilidad.</description><pubDate>Wed, 04 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Palabras clave: pruebas de hipótesis, hipótesis nula, valor p, significancia
estadística, potencia, intervalo de confianza.&lt;/p&gt;
&lt;p&gt;En una prueba de cupones con 900 clientes por grupo, el abandono fue del 28 %
sin cupón y del 22 % con cupón. Desplegar la campaña cuesta dinero: ¿qué
evidencia ofrece esa diferencia para hacerlo? Recorreremos una prueba de
hipótesis desde su planificación hasta el informe final. Veremos cómo formular
la pregunta, elegir el contraste, calcular el valor p y evaluar la importancia
práctica del efecto.&lt;/p&gt;
&lt;p&gt;Retomamos la tienda de &lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/tipos-de-analisis-de-datos/&quot;&gt;los cuatro tipos de análisis de datos&lt;/a&gt;.
Los recuentos son un &lt;strong&gt;ejemplo didáctico&lt;/strong&gt;, no datos de una campaña real. Aquí
suponemos asignación aleatoria, seguimiento completo y una observación
independiente por cliente. El código calcula los resultados de esos recuentos;
no demuestra que tales condiciones se cumplan en una tienda real.&lt;/p&gt;
&lt;h2&gt;La pregunta debe convertirse en una afirmación medible&lt;/h2&gt;
&lt;p&gt;«El cupón funciona» deja demasiadas cosas abiertas. Nuestra pregunta será:
&lt;strong&gt;¿cambia un cupón del 15 % la proporción de clientes que no compra durante los
90 días siguientes?&lt;/strong&gt; La población objetivo son los clientes elegibles para
esa campaña. El resultado de cada cliente es binario: abandonó o no abandonó.&lt;/p&gt;
&lt;p&gt;El parámetro que queremos estimar es la diferencia de proporciones:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Δ = proporción de abandono sin cupón − proporción con cupón
H₀: Δ = 0       (no hay diferencia)
H₁: Δ ≠ 0       (hay diferencia en alguna dirección)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La hipótesis nula, H₀, proporciona el escenario de referencia.
La alternativa, H₁, expresa qué diferencias queremos detectar. Con nuestra
convención, un Δ positivo favorece al cupón.&lt;/p&gt;
&lt;p&gt;Elegimos una prueba &lt;strong&gt;bilateral&lt;/strong&gt; porque nos interesa detectar tanto una
reducción como un aumento del abandono. Una alternativa unilateral, Δ &amp;gt; 0,
respondería otra pregunta. Debe justificarse antes de observar los resultados;
no se escoge después para obtener un valor p menor.&lt;/p&gt;
&lt;p&gt;La falsabilidad se explica en &lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/metodo-cientifico-en-software-y-seguridad/&quot;&gt;el método científico aplicado al software y la seguridad&lt;/a&gt;.
Aquí añadimos una dificultad: las muestras fluctúan. Una diferencia observada
no refuta por sí sola una hipótesis estadística.&lt;/p&gt;
&lt;h2&gt;El diseño y la muestra se deciden antes del resultado&lt;/h2&gt;
&lt;p&gt;Antes de enviar el primer cupón, dejamos escrito el plan:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unidad y asignación:&lt;/strong&gt; cada cliente pertenece a un solo grupo, elegido al
azar. Diez compras del mismo cliente no son diez clientes independientes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Métrica principal:&lt;/strong&gt; abandono a 90 días, con la misma definición en ambos
grupos. Fijamos cómo tratar datos faltantes y exclusiones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contraste:&lt;/strong&gt; bilateral de dos proporciones independientes, con nivel de
significancia α = 0,05.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Muestra y parada:&lt;/strong&gt; 900 clientes por grupo en este ejemplo, todos con su
ventana de seguimiento completa. El número es ilustrativo; no resulta aquí
de un cálculo de potencia.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Relevancia práctica:&lt;/strong&gt; qué reducción justificaría el costo y qué otros
resultados, como margen o quejas, debemos vigilar.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;El nivel de significancia, α, controla un error del procedimiento.
Con α = 0,05, una prueba válida tiene una tasa de falsos rechazos de hasta el
5 % cuando H₀ es cierta. &lt;strong&gt;No significa que nuestro resultado tenga un 5 % de
probabilidad de ser un error.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Hay dos errores posibles:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decisión&lt;/th&gt;
&lt;th&gt;H₀ es cierta&lt;/th&gt;
&lt;th&gt;Existe una diferencia&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rechazar H₀&lt;/td&gt;
&lt;td&gt;Error de tipo I: falso positivo&lt;/td&gt;
&lt;td&gt;Detectamos una diferencia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No rechazar H₀&lt;/td&gt;
&lt;td&gt;Decisión correcta: no rechazar H₀&lt;/td&gt;
&lt;td&gt;Error de tipo II: falso negativo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;La potencia es la probabilidad de detectar un efecto especificado.
«Potencia del 80 %» está incompleto si no indicamos para qué diferencia.
Planificar la muestra requiere una tasa de abandono esperada, un efecto que
queremos detectar, α y la potencia deseada. Bajar α reduce falsos positivos,
pero, manteniendo muestra y efecto, también reduce la potencia.&lt;/p&gt;
&lt;p&gt;El manual del NIST (National Institute of Standards and Technology (Instituto Nacional de Estándares y Tecnología de Estados Unidos))
explica estas relaciones en su &lt;a href=&quot;https://www.itl.nist.gov/div898/handbook/eda/section3/eda35.htm&quot;&gt;introducción a los contrastes&lt;/a&gt;.
Ningún cálculo de muestra corrige una asignación sesgada o clientes contados
varias veces como si fueran independientes.&lt;/p&gt;
&lt;h2&gt;La prueba depende de los datos y de cómo se obtuvieron&lt;/h2&gt;
&lt;p&gt;La elección empieza por el diseño y la variable que medimos. Esta tabla
orienta; no sustituye comprobar los supuestos:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situación&lt;/th&gt;
&lt;th&gt;Prueba posible&lt;/th&gt;
&lt;th&gt;Condición que debemos revisar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dos proporciones independientes&lt;/td&gt;
&lt;td&gt;z de dos proporciones&lt;/td&gt;
&lt;td&gt;Suficientes eventos y no eventos para la aproximación normal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tabla 2 × 2 con recuentos pequeños&lt;/td&gt;
&lt;td&gt;Exacta de Fisher&lt;/td&gt;
&lt;td&gt;Diseño compatible con su modelo y condicionamiento a los márgenes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medias de dos grupos independientes&lt;/td&gt;
&lt;td&gt;t de Welch&lt;/td&gt;
&lt;td&gt;Independencia; con muestras pequeñas importa la distribución&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mediciones numéricas emparejadas&lt;/td&gt;
&lt;td&gt;t de las diferencias&lt;/td&gt;
&lt;td&gt;Parejas independientes; con pocas parejas importa la normalidad de las diferencias&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;La documentación oficial describe la &lt;a href=&quot;https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.fisher_exact.html&quot;&gt;prueba de Fisher&lt;/a&gt;
y la &lt;a href=&quot;https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.ttest_ind.html&quot;&gt;prueba de Welch&lt;/a&gt;.
El NIST explica por qué las &lt;a href=&quot;https://www.itl.nist.gov/div898/handbook/prc/section3/prc311.htm&quot;&gt;observaciones emparejadas se analizan como diferencias&lt;/a&gt;.
Welch permite varianzas distintas, pero no elimina la dependencia ni resuelve
automáticamente una asimetría extrema.&lt;/p&gt;
&lt;p&gt;Para los cupones elegimos z: hay dos grupos independientes y suficientes
abandonos y permanencias en ambos. Suponemos también que el cupón de un cliente
no cambia el resultado de otro. La asignación aleatoria ayuda a interpretar el
efecto causal en el experimento; generalizarlo a otros clientes exige justificar
que el contexto es comparable.&lt;/p&gt;
&lt;h2&gt;El valor p mide la incompatibilidad con la hipótesis nula&lt;/h2&gt;
&lt;p&gt;Al cerrar el seguimiento obtenemos estos recuentos:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grupo&lt;/th&gt;
&lt;th&gt;Abandonaron&lt;/th&gt;
&lt;th&gt;Permanecieron&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;th&gt;Abandono&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sin cupón&lt;/td&gt;
&lt;td&gt;252&lt;/td&gt;
&lt;td&gt;648&lt;/td&gt;
&lt;td&gt;900&lt;/td&gt;
&lt;td&gt;28 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Con cupón&lt;/td&gt;
&lt;td&gt;198&lt;/td&gt;
&lt;td&gt;702&lt;/td&gt;
&lt;td&gt;900&lt;/td&gt;
&lt;td&gt;22 %&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;La diferencia observada es &lt;strong&gt;6 puntos porcentuales&lt;/strong&gt;. Bajo H₀ suponemos una
misma proporción de abandono en ambos grupos. La estimamos agrupando sus
recuentos: 450 abandonos entre 1800 clientes, es decir, 0,25.&lt;/p&gt;
&lt;p&gt;El estadístico z divide la diferencia por
su error estándar bajo H₀:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p_agrupada = (252 + 198) / (900 + 900) = 0,25
error_H₀ = raíz[p_agrupada × (1 − p_agrupada) × (1/900 + 1/900)]
z = (0,28 − 0,22) / error_H₀ ≈ 2,939
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La diferencia queda a unas 2,94 unidades de error estándar del cero. Usamos
la &lt;a href=&quot;https://www.itl.nist.gov/div898/software/dataplot/refman2/auxillar/diffprop.htm&quot;&gt;aproximación normal descrita por NIST&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;El &lt;strong&gt;valor p bilateral&lt;/strong&gt; es la probabilidad, bajo H₀ y los demás supuestos,
de obtener un estadístico al menos tan extremo como el observado, en cualquiera
de las dos direcciones. Para este contraste sumamos las dos colas más allá de
−2,939 y +2,939. El resultado es aproximadamente 0,00329.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: Bajo H₀, las dos colas más allá de ±2,939 suman p ≈ 0,00329. El área se refiere al comportamiento del estadístico bajo ese modelo, no a la probabilidad de que H₀ sea cierta.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/pruebas-de-hipotesis/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Este código usa solo la biblioteca estándar de Python. &lt;code&gt;erfc&lt;/code&gt; permite calcular
las colas normales sin instalar paquetes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from math import erfc, sqrt

n_control, n_cupon = 900, 900
x_control, x_cupon = 252, 198
p_control = x_control / n_control
p_cupon = x_cupon / n_cupon
delta = p_control - p_cupon

agrupada = (x_control + x_cupon) / (n_control + n_cupon)
error_nulo = sqrt(
    agrupada * (1 - agrupada) * (1 / n_control + 1 / n_cupon)
)
z = delta / error_nulo
p_bilateral = erfc(abs(z) / sqrt(2))

# Intervalo de Wald aproximado: error estimado sin agrupar.
error_estimacion = sqrt(
    p_control * (1 - p_control) / n_control
    + p_cupon * (1 - p_cupon) / n_cupon
)
# Cuantil 0,975 de la normal estándar para un IC bilateral del 95 %.
margen = 1.959963984540054 * error_estimacion
inferior, superior = delta - margen, delta + margen

print(f&quot;Diferencia: {100 * delta:.2f} puntos porcentuales&quot;)
print(f&quot;z: {z:.6f}&quot;)
print(f&quot;p bilateral: {p_bilateral:.8f}&quot;)
print(f&quot;IC 95% aproximado: [{100 * inferior:.3f}, {100 * superior:.3f}] pp&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La ejecución produce:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Diferencia: 6.00 puntos porcentuales
z: 2.939388
p bilateral: 0.00328861
IC 95% aproximado: [2.009, 9.991] pp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Como p &amp;lt; 0,05, rechazamos H₀ con el criterio fijado. La diferencia positiva
aporta evidencia de menor abandono con cupón bajo los supuestos del análisis.
Eso todavía no decide si conviene desplegarlo.&lt;/p&gt;
&lt;h2&gt;La significancia debe acompañarse del efecto y su incertidumbre&lt;/h2&gt;
&lt;p&gt;El valor p no es la probabilidad de que H₀ sea cierta. Tampoco mide el tamaño
del efecto ni su importancia económica. Son distinciones centrales de la
declaración de la ASA (American Statistical Association (Asociación Estadística Estadounidense))
sobre &lt;a href=&quot;https://www.amstat.org/asa/files/pdfs/p-valuestatement.pdf&quot;&gt;significancia estadística y valores p&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;El efecto estimado es una reducción de 6 puntos porcentuales.
Su IC (Intervalo de confianza) del 95 % va de 2,01 a 9,99 puntos.
En muestras repetidas, el procedimiento produce intervalos que cubren el
parámetro en aproximadamente el 95 % de los casos, bajo sus supuestos. El
intervalo observado no asigna una probabilidad del 95 % a un parámetro fijo.&lt;/p&gt;
&lt;p&gt;Usamos un intervalo de &lt;strong&gt;Wald aproximado&lt;/strong&gt;, con el error sin agrupar. La
documentación de &lt;a href=&quot;https://www.statsmodels.org/stable/generated/statsmodels.stats.proportion.confint_proportions_2indep.html&quot;&gt;statsmodels para dos proporciones&lt;/a&gt;
incluye este método y otras alternativas. No debe aplicarse automáticamente con
pocos eventos o proporciones cercanas a cero o uno. Además, este intervalo y
el contraste agrupado no son inversiones exactas del mismo procedimiento.&lt;/p&gt;
&lt;p&gt;Ahora supongamos que el negocio hubiera fijado antes un requisito: reducir el
abandono &lt;strong&gt;más de 5 puntos&lt;/strong&gt; para justificar el costo. El intervalo incluye
valores por debajo de ese umbral. Encontramos evidencia frente a cero, pero
no hemos establecido que el efecto supere 5 puntos.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: El intervalo aproximado excluye cero, pero contiene el umbral práctico ilustrativo de 5 puntos. Una diferencia estadísticamente significativa puede dejar sin resolver la decisión económica.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/pruebas-de-hipotesis/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Para evaluar directamente ese requisito habría que planificar un contraste
frente al umbral, con su potencia correspondiente, y considerar costos y
márgenes. El umbral no debe aparecer solo porque el resultado nos gustó.&lt;/p&gt;
&lt;p&gt;El caso contrario también importa: &lt;strong&gt;no rechazar H₀ no demuestra igualdad&lt;/strong&gt;.
Una muestra puede ser demasiado pequeña para detectar una diferencia relevante.
Si queremos demostrar que dos opciones son suficientemente parecidas,
necesitamos una prueba de equivalencia con márgenes definidos previamente.&lt;/p&gt;
&lt;h2&gt;Buscar significancia hasta encontrarla altera la prueba&lt;/h2&gt;
&lt;p&gt;En el diseño de muestra fija, mirar cada día y detenerse cuando p baja de 0,05
modifica la tasa de falsos positivos. También la modifica probar muchas
métricas o segmentos y publicar solo el resultado favorable.&lt;/p&gt;
&lt;p&gt;Un &lt;a href=&quot;https://www.bitss.org/wp-content/uploads/2014/06/false-positive-psychology2.pdf&quot;&gt;estudio de Simmons, Nelson y Simonsohn&lt;/a&gt;
documenta estos riesgos. La defensa práctica es fijar métrica, exclusiones,
análisis y parada antes de observar los resultados. Si hay varias hipótesis
confirmatorias, hay que planificar cómo controlar los errores de ese conjunto.&lt;/p&gt;
&lt;p&gt;Se puede detener un experimento antes mediante un diseño secuencial adecuado.
Lo que cambia es el procedimiento y sus reglas, no solo el momento de mirar.
Podemos explorar segmentos después, pero debemos presentar sus hallazgos como
exploratorios y buscar confirmación con datos nuevos.&lt;/p&gt;
&lt;h2&gt;La conclusión debe responder la pregunta y declarar sus límites&lt;/h2&gt;
&lt;p&gt;El informe del ejemplo puede quedar así:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;En este caso didáctico, el abandono a 90 días fue del 28 % sin cupón y del
22 % con cupón, con 900 clientes por grupo. La reducción estimada fue de
6 puntos porcentuales, con IC de Wald aproximado del 95 % de 2,01 a 9,99.
El contraste bilateral de dos proporciones dio p = 0,00329. Con α = 0,05,
rechazamos la igualdad bajo los supuestos declarados. El resultado aporta
evidencia de una reducción, pero no establece que supere 5 puntos ni que
la campaña sea rentable.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Antes de la próxima prueba, escribe una frase con &lt;strong&gt;qué vas a medir, qué efecto
importa y cuándo terminarás de observar&lt;/strong&gt;. Esa frase hace que el cálculo
responda una pregunta definida, en lugar de dejar que el resultado elija la
pregunta.&lt;/p&gt;
&lt;h2&gt;Referencias&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;El &lt;a href=&quot;https://www.itl.nist.gov/div898/handbook/eda/section3/eda35.htm&quot;&gt;NIST/SEMATECH e-Handbook: Quantitative Techniques&lt;/a&gt;
explica los fundamentos del contraste.&lt;/li&gt;
&lt;li&gt;La documentación de &lt;a href=&quot;https://www.itl.nist.gov/div898/software/dataplot/refman2/auxillar/diffprop.htm&quot;&gt;NIST Dataplot: Difference of Proportion Hypothesis Test&lt;/a&gt;
presenta el estadístico del ejemplo.&lt;/li&gt;
&lt;li&gt;La documentación de &lt;a href=&quot;https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.fisher_exact.html&quot;&gt;SciPy: fisher_exact&lt;/a&gt;
describe una alternativa exacta para tablas 2 × 2.&lt;/li&gt;
&lt;li&gt;La documentación de &lt;a href=&quot;https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.ttest_ind.html&quot;&gt;SciPy: ttest_ind&lt;/a&gt;
explica la comparación de medias independientes.&lt;/li&gt;
&lt;li&gt;El manual de &lt;a href=&quot;https://www.itl.nist.gov/div898/handbook/prc/section3/prc311.htm&quot;&gt;NIST: Analysis of paired observations&lt;/a&gt;
desarrolla el caso emparejado.&lt;/li&gt;
&lt;li&gt;La &lt;a href=&quot;https://www.amstat.org/asa/files/pdfs/p-valuestatement.pdf&quot;&gt;declaración de la ASA sobre valores p&lt;/a&gt;
(7 de marzo de 2016) fundamenta sus límites de interpretación.&lt;/li&gt;
&lt;li&gt;La documentación de &lt;a href=&quot;https://www.statsmodels.org/stable/generated/statsmodels.stats.proportion.confint_proportions_2indep.html&quot;&gt;statsmodels: confint_proportions_2indep&lt;/a&gt;
detalla los métodos de intervalos disponibles.&lt;/li&gt;
&lt;li&gt;El artículo de &lt;a href=&quot;https://www.bitss.org/wp-content/uploads/2014/06/false-positive-psychology2.pdf&quot;&gt;Simmons, Nelson y Simonsohn, False-Positive Psychology&lt;/a&gt;
(&lt;em&gt;Psychological Science&lt;/em&gt;, 2011) examina el efecto de las decisiones de análisis
no declaradas.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>statistics</category><category>data-science</category></item><item><title>5 Pourquoi : le cas d&apos;école du Jefferson Memorial n&apos;a pas la fin qu&apos;on raconte, et comment chercher plus d&apos;une cause</title><link>https://yeison-liscano.github.io/portfolio/blog/5-pourquoi/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/5-pourquoi/</guid><description>Le cas d&apos;école des 5 Pourquoi, les moucherons du Jefferson Memorial, confronté aux archives ; deux postmortems réels, GitLab et AWS ; l&apos;arbre des causes de l&apos;INRS ; des règles pour savoir quand s&apos;arrêter, et un script qui vérifie une analyse.</description><pubDate>Tue, 03 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Mots-clés : 5 Pourquoi, analyse des causes racines, arbre des causes, INRS,
postmortem, erreur humaine, Jefferson Memorial, Toyota&lt;/p&gt;
&lt;p&gt;Dans les formations aux 5 Pourquoi, un exemple revient sans cesse. Les pierres
du Jefferson Memorial s&apos;abîment. Pourquoi ? On les lave trop souvent. Pourquoi ?
À cause des fientes d&apos;oiseaux, attirés par des araignées, elles-mêmes attirées
par des moucherons, qui viennent pour les projecteurs. On allume donc les
lumières une heure plus tard, et le problème disparaît. Les archives racontent
autre chose. L&apos;essai a surtout eu lieu au Lincoln Memorial, l&apos;eau du nettoyage
menaçait davantage la pierre, et l&apos;éclairage retardé a été abandonné après les
plaintes des touristes.&lt;/p&gt;
&lt;p&gt;Une seule chaîne de pourquoi avait caché plusieurs causes et une contrainte.
L&apos;enjeu dépasse les monuments : dans un grand centre hospitalier, 302 analyses
de causes ont surtout produit des actions faibles, et plusieurs types
d&apos;événements se sont répétés. Cet article reprend le cas, puis le compare à deux
postmortems réels, celui de GitLab et celui d&apos;AWS, et à l&apos;arbre des causes de
l&apos;INRS. Vous repartirez avec des règles pour savoir quand s&apos;arrêter, et un
script qui signale les pourquoi qui s&apos;arrêtent sur une personne ou sur un
« manque ».&lt;/p&gt;
&lt;h2&gt;Le cas d&apos;école tient en une chaîne, une action et un chiffre&lt;/h2&gt;
&lt;p&gt;En septembre 2018, un équipementier automobile, Nexteer, publie un support de
formation aux 5 Pourquoi pour ses fournisseurs. En annexe, une diapositive
intitulée « A good example » raconte l&apos;histoire du &lt;a href=&quot;https://www.nexteer.com/wp-content/uploads/5-Why-Training-Nexteer-Supplier-Version-17-Sept-18.pdf&quot;&gt;Jefferson Memorial&lt;/a&gt;.
Le National Park Service remarque que le monument se dégrade plus vite que les
autres. Les gardes mènent un 5 Pourquoi et obtiennent cette chaîne :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Le mémorial se dégrade plus vite, parce qu&apos;il est lavé plus souvent.&lt;/li&gt;
&lt;li&gt;Il est lavé plus souvent, parce qu&apos;il reçoit plus de fientes.&lt;/li&gt;
&lt;li&gt;Il reçoit plus de fientes, parce que plus d&apos;oiseaux viennent.&lt;/li&gt;
&lt;li&gt;Plus d&apos;oiseaux viennent, parce que plus d&apos;araignées vivent autour.&lt;/li&gt;
&lt;li&gt;Plus d&apos;araignées vivent autour, parce que plus d&apos;insectes volent au
crépuscule.&lt;/li&gt;
&lt;li&gt;Plus d&apos;insectes volent au crépuscule, parce que l&apos;éclairage les attire.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;La diapositive se demande ensuite s&apos;il faut aller plus loin. Pourquoi la lumière
attire-t-elle les insectes ? La réponse ne serait pas « within their control »,
donc on s&apos;arrête. Elle envisage aussi de s&apos;arrêter plus tôt : enduire la pierre
d&apos;un produit hydrofuge pour supporter les lavages, ou traiter les araignées aux
pesticides. C&apos;est possible, juge-t-elle, mais d&apos;autres causes restent à
considérer. Les gardes retiennent l&apos;éclairage. Action : allumer une heure plus
tard. Résultat annoncé : « Bird dropping problem reduced by 90%! »&lt;/p&gt;
&lt;p&gt;L&apos;histoire plaît parce que chaque réponse appelle la suivante, que l&apos;action est
gratuite et que le résultat est un chiffre. Elle compte d&apos;ailleurs six
pourquoi, pas cinq, ce qui n&apos;a pas d&apos;importance : le &lt;a href=&quot;https://www.lean.org/lexicon-terms/5-whys/&quot;&gt;lexique du Lean Enterprise Institute&lt;/a&gt;,
qui cite Taiichi Ohno, précise que « the specific number five is not the
point ». L&apos;exemple de la machine d&apos;Ohno est raconté dans l&apos;article sur
le &lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/5w2h-qqoqccp/&quot;&gt;5W2H&lt;/a&gt;. Mais trois questions restent ouvertes. Qui a mesuré ces 90 % ?
Qu&apos;a-t-on essayé d&apos;autre ? Et pourquoi une seule cause à chaque maillon ?&lt;/p&gt;
&lt;h2&gt;Les archives montrent d&apos;autres causes, un autre mémorial, un essai abandonné&lt;/h2&gt;
&lt;p&gt;En avril 1990, une dépêche AP (Associated Press) reprise par le &lt;a href=&quot;https://www.deseret.com/1990/4/17/18856959/toxic-effects-of-nature-are-aging-lincoln-and-jefferson/&quot;&gt;Deseret News&lt;/a&gt;
annonce une étude à 2 millions de dollars, commandée par le National Park
Service à des consultants privés. Elle porte sur le vieillissement des deux
mémoriaux, Lincoln et Jefferson, et recense plusieurs menaces. Le moucheron
n&apos;est que l&apos;une d&apos;elles.&lt;/p&gt;
&lt;p&gt;En mai 1992, Carla Helfferich raconte la suite dans une &lt;a href=&quot;https://www.gi.alaska.edu/alaska-science-forum/unfortunately-ecological-abe&quot;&gt;chronique scientifique&lt;/a&gt;
de l&apos;université d&apos;Alaska à Fairbanks. Le monument est cette fois le Lincoln
Memorial. Les projecteurs attirent des nuées de moucherons au crépuscule. Les
araignées s&apos;en nourrissent, et les oiseaux mangent les araignées. Les équipes de
nettoyage doivent laver fientes, toiles et moucherons écrasés, et l&apos;eau du
lavage fait entrer dans le marbre les polluants de l&apos;air. Le Park Service laisse
alors les projecteurs éteints une heure de plus. Le nombre de moucherons morts
sur la statue baisse de 90 %.&lt;/p&gt;
&lt;p&gt;Joel Gross a reconstitué toute l&apos;affaire en 2014, dans une &lt;a href=&quot;https://thekaizone.com/2014/08/5-whys-folklore-the-truth-behind-a-monumental-mystery/&quot;&gt;enquête&lt;/a&gt;
qu&apos;Alan Card cite dans sa critique des 5 Pourquoi. Selon Gross, le rapport des
consultants désigne une menace principale : le grand volume d&apos;eau utilisé au
nettoyage. L&apos;eau pénètre dans les inclusions du marbre et du calcaire, et fait
progresser les fissures.&lt;/p&gt;
&lt;p&gt;L&apos;essai d&apos;éclairage dure six semaines, les lundis, mercredis et vendredis, avec
85 % de moucherons en moins. En janvier 1992, le Park Service étudie encore un
changement permanent. En septembre 1995, l&apos;idée est abandonnée, à cause du
« nombre écrasant de plaintes de touristes venus prendre des photos ».&lt;/p&gt;
&lt;p&gt;Le même article de 1995 décrit les autres mesures prises en cinq ans : des fils,
des pics, des filets et du plastique transparent, contre les oiseaux, les
araignées et les moucherons. Selon Gross, elles ont peu réduit le nettoyage.&lt;/p&gt;
&lt;p&gt;Les sources ne s&apos;accordent ni sur l&apos;année de l&apos;essai ni sur le chiffre : 90 %
selon la chronique de 1992, 85 % selon Gross. Elles s&apos;accordent sur l&apos;essentiel,
résumé dans ce tableau :&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Point&lt;/th&gt;
&lt;th&gt;Version racontée&lt;/th&gt;
&lt;th&gt;Archives&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lieu&lt;/td&gt;
&lt;td&gt;Jefferson Memorial&lt;/td&gt;
&lt;td&gt;Surtout le Lincoln Memorial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Menace&lt;/td&gt;
&lt;td&gt;Lavages fréquents&lt;/td&gt;
&lt;td&gt;L&apos;eau du lavage qui pénètre la pierre fissurée&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ce qui est mesuré&lt;/td&gt;
&lt;td&gt;Fientes, −90 %&lt;/td&gt;
&lt;td&gt;Moucherons morts, −85 % ou −90 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Statut&lt;/td&gt;
&lt;td&gt;Solution&lt;/td&gt;
&lt;td&gt;Essai de six semaines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Suite&lt;/td&gt;
&lt;td&gt;Problème réglé&lt;/td&gt;
&lt;td&gt;Abandonné en 1995, après les plaintes des touristes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Personne n&apos;a mesuré une baisse de 90 % des fientes. Et l&apos;action retenue n&apos;a
jamais été appliquée durablement.&lt;/p&gt;
&lt;h2&gt;Une chaîne de pourquoi choisit une branche et cache les autres&lt;/h2&gt;
&lt;p&gt;La chaîne racontée n&apos;est pas fausse : chacun de ses maillons est plausible, et
l&apos;essai a bien réduit les moucherons. Son défaut est ce qu&apos;elle laisse de côté.
À chaque pourquoi, elle ne retient qu&apos;une réponse. Or la pierre ne s&apos;abîme pas
seulement parce qu&apos;on la lave : il faut aussi que l&apos;eau entre dans le marbre, et
qu&apos;elle y apporte des polluants. La figure compare les deux lectures, étape par
étape.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: La chaîne racontée suit la colonne principale de l&apos;arbre. Les archives ajoutent deux conjonctions, une contrainte qui bloque l&apos;action choisie, et un autre levier essayé en parallèle.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/5-pourquoi/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Ce que la chaîne ne voit pas a deux origines. D&apos;abord, elle cherche une cause
et s&apos;arrête dès qu&apos;elle en tient une. La fiche de l&apos;&lt;a href=&quot;https://www.ihi.org/library/tools/5-whys-finding-root-cause&quot;&gt;IHI&lt;/a&gt;
sur les 5 Pourquoi rappelle pourtant qu&apos;un problème peut avoir plusieurs causes
racines. Ensuite, elle se construit en salle, par déduction. Teruyuki Minoura,
ancien dirigeant de Toyota, cité par &lt;a href=&quot;https://taproot.com/teruyuki-minoura-toyota-exec-talks-about-problems-with-5-whys/&quot;&gt;TapRooT&lt;/a&gt;
d&apos;après une page de Toyota aujourd&apos;hui retirée, le dit sans détour. Ohno
demandait de poser « pourquoi ? » cinq fois. Mais seule l&apos;observation sur le
terrain permet de répondre, et les équipes « reel off five causes as quick as a
flash by deduction ». Les consultants des mémoriaux, eux, avaient examiné la
pierre. C&apos;est ainsi qu&apos;ils ont trouvé l&apos;eau.&lt;/p&gt;
&lt;p&gt;Même l&apos;exemple d&apos;Ohno n&apos;échappe pas à la critique. Mark Graban montre en 2026,
sur le &lt;a href=&quot;https://www.leanblog.org/2026/04/ohno-5-whys-actually-seven/&quot;&gt;Lean Blog&lt;/a&gt;,
que la chaîne de la machine compte en réalité sept pourquoi, et qu&apos;elle s&apos;arrête
trop tôt : pourquoi n&apos;y avait-il pas de filtre ?&lt;/p&gt;
&lt;h2&gt;GitLab et AWS ont corrigé les conditions, pas la personne&lt;/h2&gt;
&lt;p&gt;Le 31 janvier 2017, un ingénieur de GitLab supprime le répertoire de données de
la base principale, au lieu de celui de la base secondaire. GitLab.com reste
indisponible environ 18 heures. Le &lt;a href=&quot;https://about.gitlab.com/blog/postmortem-of-database-outage-of-january-31/&quot;&gt;postmortem&lt;/a&gt;,
publié le 10 février, contient une section intitulée « The 5 Whys ». Elle
traite deux problèmes, et donc deux chaînes.&lt;/p&gt;
&lt;p&gt;La première explique la panne en cinq pourquoi. Le répertoire a été supprimé par
accident. Il fallait le vider parce que la réplication s&apos;était arrêtée et que
reconstruire la base secondaire exigeait un répertoire vide, une étape ni
automatisée ni bien documentée. La réplication s&apos;était arrêtée à cause d&apos;un pic
de charge. Le pic venait de deux choses à la fois : une hausse du spam, et un
traitement qui supprimait un employé de GitLab et ses données. L&apos;employé avait
été signalé pour abus par un troll, dans un système où il était trop facile
d&apos;ignorer les détails d&apos;un signalement.&lt;/p&gt;
&lt;p&gt;La seconde chaîne explique pourquoi la restauration a pris plus de 18 heures, en
huit pourquoi. Les sauvegardes &lt;code&gt;pg_dump&lt;/code&gt; échouaient sans bruit : elles
utilisaient PostgreSQL 9.2 contre une base en 9.6. Les courriels d&apos;échec étaient
rejetés faute de signature DMARC (Domain-based Message Authentication, Reporting and Conformance). Le dernier pourquoi aboutit à une
cause organisationnelle : la procédure de sauvegarde n&apos;avait pas de responsable,
donc personne ne la testait.&lt;/p&gt;
&lt;p&gt;GitLab ne s&apos;arrête pas à l&apos;erreur de l&apos;ingénieur, qui n&apos;est que le premier
pourquoi. Et ses actions visent chacune une cause de la chaîne :&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pourquoi du postmortem&lt;/th&gt;
&lt;th&gt;Action de GitLab&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Répertoire du primaire supprimé par accident&lt;/td&gt;
&lt;td&gt;Invite de commande plus distincte d&apos;un hôte à l&apos;autre&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconstruction du secondaire ni automatisée ni documentée&lt;/td&gt;
&lt;td&gt;Documentation et runbooks de réplication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sauvegardes &lt;code&gt;pg_dump&lt;/code&gt; en échec silencieux&lt;/td&gt;
&lt;td&gt;Surveillance des sauvegardes avec Prometheus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Procédure de sauvegarde jamais testée, sans responsable&lt;/td&gt;
&lt;td&gt;Tests automatiques de restauration, un responsable désigné&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Un mois plus tard, le 28 février 2017, le service S3 d&apos;AWS tombe dans la région
us-east-1. Le &lt;a href=&quot;https://aws.amazon.com/message/41926/&quot;&gt;résumé d&apos;AWS&lt;/a&gt;
décrit le déclencheur sans nommer de coupable : « an authorized S3 team member
using an established playbook executed a command ». Il ajoute : « one of the
inputs to the command was entered incorrectly ». AWS n&apos;utilise pas les 5
Pourquoi. Mais
ses correctifs portent sur les conditions, pas sur la personne : l&apos;outil retire
désormais la capacité plus lentement, et refuse de descendre sous le minimum
requis par chaque sous-système. AWS ajoute : « This will prevent an incorrect
input from triggering a similar event in the future », et vérifie ses autres
outils. Une chaîne qui s&apos;arrêterait à « une entrée mal saisie » n&apos;aurait produit
aucune de ces actions.&lt;/p&gt;
&lt;h2&gt;S&apos;arrêter sur une personne ou sur un manque ferme l&apos;analyse&lt;/h2&gt;
&lt;p&gt;Deux règles reviennent dans les méthodes sérieuses. La première interdit de
s&apos;arrêter sur une personne. Le centre de sécurité des patients des anciens
combattants américains (VA NCPS (Veterans Affairs National Center for Patient Safety)) a formulé cinq règles de causalité. Un &lt;a href=&quot;https://nj.gov/health/healthcarequality/documents/ps/rca_training.pdf&quot;&gt;support de formation du New Jersey&lt;/a&gt;
les reprend, dont celle-ci : toute erreur humaine doit avoir une cause
antérieure.
Le &lt;a href=&quot;https://navigator.betsylehmancenterma.gov/pages/the-5-rules-of-causation&quot;&gt;Betsy Lehman Center&lt;/a&gt;
précise que ces règles viennent d&apos;un rapport de David Marx pour la FAA (Federal Aviation Administration), en 1999.
GitLab respecte cette règle ; une chaîne arrêtée à la commande mal saisie d&apos;AWS
la violerait.&lt;/p&gt;
&lt;p&gt;La seconde interdit les causes formulées comme un manque. La règle 2 du VA
exclut les descriptions négatives des énoncés de cause. L&apos;INRS dit la même chose
en français. Dans son guide &lt;a href=&quot;https://www.inrs.fr/dam/inrs/CataloguePapier/ED/TI-ED-6163.pdf&quot;&gt;ED 6163&lt;/a&gt;,
un fait n&apos;est « ni une interprétation, ni une opinion, ni un jugement de valeur,
ni un « fait négatif » (absence de…, manque de…) ». Un fait négatif décrit ce
qu&apos;il aurait fallu faire : « Il s&apos;agit donc de bien différencier la cause du
remède. » Le guide donne des exemples. « Il ne faisait pas attention » devient
« Il parlait avec un collègue ». « Il roulait trop vite » devient « Il roulait à
30 km/h ».&lt;/p&gt;
&lt;p&gt;Le postmortem de GitLab contient justement un fait négatif : une étape « ni
automatisée ni documentée ». La formule désigne déjà le remède, automatiser et
documenter, au lieu du fait : un ingénieur reconstruisait la base secondaire à
la main. Le remède est peut-être le bon, mais la formulation empêche d&apos;en
envisager un autre.&lt;/p&gt;
&lt;p&gt;Derrière ces deux règles, il y a une idée plus large. Richard Cook l&apos;écrit en
titre de la septième thèse de &lt;a href=&quot;https://how.complexsystems.fail/&quot;&gt;How Complex Systems Fail&lt;/a&gt;
(1998) : « Post-accident attribution to a &apos;root cause&apos; is fundamentally wrong. »
Une panne demande plusieurs défaillances à la fois. Sidney Dekker, cité par John
Allspaw, le résume ainsi : « Cause is something we construct, not find. »&lt;/p&gt;
&lt;h2&gt;L&apos;arbre des causes remplace « pourquoi ? » par « qu&apos;a-t-il fallu ? »&lt;/h2&gt;
&lt;p&gt;En France, l&apos;INRS (Institut national de recherche et de sécurité) promeut depuis les années soixante-dix une méthode d&apos;analyse des
accidents du travail qui ne pose jamais la question « pourquoi ? ». Son guide
part d&apos;un constat : « Les accidents du travail ne résultent jamais d&apos;une cause
unique : ils sont la conséquence d&apos;une combinaison de facteurs. » L&apos;analyse
recherche « une compréhension objective du processus de l&apos;accident et non la
recherche de responsabilités ».&lt;/p&gt;
&lt;p&gt;L&apos;arbre se construit à partir du dommage, avec deux questions pour chaque fait :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;« Qu&apos;a-t-il fallu pour que le fait (Y) apparaisse ? »&lt;/li&gt;
&lt;li&gt;« (X) a-t-il été nécessaire à lui seul pour que (Y) apparaisse ? Ou, en
d&apos;autres termes, a-t-il fallu autre chose ? »&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La seconde question est celle qui manque à la chaîne du mémorial. Elle
transforme chaque maillon en nœud, relié à ses causes par un enchaînement, une conjonction
ou une disjonction. Le guide en tire une conséquence pratique : « il suffit de
supprimer un seul fait pour que l&apos;accident ne se reproduise pas ». Un arbre
offre donc plusieurs leviers, et « aucune proposition ne doit être rejetée a
priori ». Au mémorial, l&apos;arbre aurait mis côte à côte l&apos;éclairage, le lavage
et les oiseaux, avant que les touristes ne tranchent.&lt;/p&gt;
&lt;p&gt;John Allspaw arrive au même endroit par l&apos;informatique. Dans &lt;a href=&quot;https://www.kitchensoap.com/2014/11/14/the-infinite-hows-or-the-dangers-of-the-five-whys/&quot;&gt;The Infinite Hows&lt;/a&gt;
(2014), il observe que « pourquoi ? » mène trop facilement à « qui ? ». Il
propose de demander « comment ? », parce que cette question décrit « (at least
some) of the conditions that allowed an event to take place ». « Qu&apos;a-t-il
fallu ? » et « comment ? » cherchent des conditions, pas un coupable ni un
motif.&lt;/p&gt;
&lt;h2&gt;Un script repère les pourquoi qui s&apos;arrêtent trop tôt&lt;/h2&gt;
&lt;p&gt;Ces règles sont assez mécaniques pour qu&apos;un script en vérifie une partie. Le
programme ci-dessous représente une analyse comme un arbre : chaque fait pointe
vers ses causes. Il signale quatre choses :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;les maillons qui n&apos;ont qu&apos;une seule cause, où il faut demander « a-t-il fallu
autre chose ? » ;&lt;/li&gt;
&lt;li&gt;les faits négatifs (« pas », « ni », « aucun », « absence », « manque »…) ;&lt;/li&gt;
&lt;li&gt;les jugements (« trop », « mal », « mauvais », « insuffisant ») ;&lt;/li&gt;
&lt;li&gt;les erreurs humaines sans cause antérieure, selon la règle 3 du VA.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Il l&apos;applique à quatre analyses : la chaîne racontée du mémorial, une chaîne
naïve sur la panne de S3, construite pour l&apos;exemple, la première chaîne de
GitLab, et l&apos;arbre documenté du mémorial. Le bouton Run l&apos;exécute dans votre
navigateur.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import re

NEGATIF = re.compile(r&quot;\b(pas|ni|aucun|aucune|absence|manque|sans|jamais)\b&quot;)
HUMAIN = re.compile(r&quot;par accident|erreur humaine|inattention|négligence|oubli&quot;)
JUGEMENT = re.compile(r&quot;\b(trop|mal|mauvais|insuffisant)\b&quot;)


def verifier(nom, arbre):
    &quot;&quot;&quot;Vérifie une analyse : chaque clé est un fait, sa valeur ses causes.&quot;&quot;&quot;
    faits = set(arbre) | {c for causes in arbre.values() for c in causes}
    maillons = [fait for fait in faits if arbre.get(fait)]
    uniques = [fait for fait in maillons if len(arbre[fait]) == 1]
    print(f&quot;{nom} : {len(faits)} faits&quot;)
    print(f&quot;  ! une seule cause : {len(uniques)} maillons sur {len(maillons)}&quot;)
    for fait in sorted(faits):
        if NEGATIF.search(fait):
            print(f&quot;  ! fait négatif : « {fait} »&quot;)
        if JUGEMENT.search(fait):
            print(f&quot;  ! jugement : « {fait} »&quot;)
        if HUMAIN.search(fait) and not arbre.get(fait):
            print(f&quot;  ! s&apos;arrête sur une erreur humaine : « {fait} »&quot;)


def chaine(*faits):
    &quot;&quot;&quot;Transforme une chaîne de pourquoi en arbre à une seule branche.&quot;&quot;&quot;
    return {effet: [cause] for effet, cause in zip(faits, faits[1:])}


verifier(&quot;Mémorial, version racontée&quot;, chaine(
    &quot;Le mémorial se dégrade plus vite&quot;,
    &quot;Il est lavé plus souvent&quot;,
    &quot;Il reçoit plus de fientes&quot;,
    &quot;Plus d&apos;oiseaux viennent&quot;,
    &quot;Plus d&apos;araignées vivent autour&quot;,
    &quot;Plus d&apos;insectes volent au crépuscule&quot;,
    &quot;L&apos;éclairage attire les insectes&quot;,
))

verifier(&quot;S3, chaîne naïve&quot;, chaine(
    &quot;S3 indisponible dans us-east-1&quot;,
    &quot;Plus de serveurs retirés que prévu&quot;,
    &quot;Une entrée mal saisie, par erreur humaine&quot;,
))

verifier(&quot;GitLab, problème 1&quot;, {
    &quot;GitLab.com indisponible environ 18 heures&quot;: [
        &quot;Répertoire du primaire supprimé par accident&quot;],
    &quot;Répertoire du primaire supprimé par accident&quot;: [
        &quot;Secondaire à reconstruire, étape ni automatisée ni documentée&quot;],
    &quot;Secondaire à reconstruire, étape ni automatisée ni documentée&quot;: [
        &quot;Réplication arrêtée par un pic de charge&quot;],
    &quot;Réplication arrêtée par un pic de charge&quot;: [
        &quot;Hausse du spam&quot;, &quot;Suppression en cours d&apos;un employé&quot;],
    &quot;Suppression en cours d&apos;un employé&quot;: [
        &quot;Signalement d&apos;un troll, détails trop faciles à ignorer&quot;],
})

verifier(&quot;Mémorial, arbre documenté&quot;, {
    &quot;Pierre qui s&apos;érode&quot;: [&quot;Eau infiltrée dans le marbre&quot;, &quot;Polluants de l&apos;air&quot;],
    &quot;Eau infiltrée dans le marbre&quot;: [&quot;Lavages à grande eau&quot;, &quot;Fissures du marbre&quot;],
    &quot;Lavages à grande eau&quot;: [&quot;Fientes et insectes à nettoyer&quot;],
    &quot;Fientes et insectes à nettoyer&quot;: [&quot;Oiseaux et araignées&quot;],
    &quot;Oiseaux et araignées&quot;: [&quot;Moucherons au crépuscule&quot;],
    &quot;Moucherons au crépuscule&quot;: [&quot;Projecteurs allumés au crépuscule&quot;],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Voici sa sortie, avec Python 3.14 :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Mémorial, version racontée : 7 faits
  ! une seule cause : 6 maillons sur 6
S3, chaîne naïve : 3 faits
  ! une seule cause : 2 maillons sur 2
  ! jugement : « Une entrée mal saisie, par erreur humaine »
  ! s&apos;arrête sur une erreur humaine : « Une entrée mal saisie, par erreur humaine »
GitLab, problème 1 : 7 faits
  ! une seule cause : 4 maillons sur 5
  ! fait négatif : « Secondaire à reconstruire, étape ni automatisée ni documentée »
  ! jugement : « Signalement d&apos;un troll, détails trop faciles à ignorer »
Mémorial, arbre documenté : 9 faits
  ! une seule cause : 4 maillons sur 6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La chaîne racontée du mémorial passe toutes les règles de forme. Elle n&apos;a ni
manque, ni jugement, ni coupable. Son seul défaut est structurel : six maillons
sur six n&apos;ont qu&apos;une cause. C&apos;est pourquoi elle semble si propre. La chaîne
naïve de S3 cumule les défauts : elle juge (« mal saisie ») et s&apos;arrête sur
l&apos;erreur humaine. GitLab, lui, continue après l&apos;accident, et l&apos;un de ses
maillons a deux causes. Il garde un fait négatif et un jugement, que la
reformulation de l&apos;INRS corrigerait.&lt;/p&gt;
&lt;p&gt;Le script vérifie la forme, pas la vérité. Une analyse peut passer toutes les
règles et rester fausse. Ses expressions régulières se trompent aussi dans les
deux sens. Elles signalent « le pas de vis est usé », où « pas » est un nom, et
laissent passer « défaut de formation », un manque formulé sans négation. Le
script dit où regarder, pas quoi conclure.&lt;/p&gt;
&lt;h2&gt;Une analyse se juge à la force de ses actions, pas au nombre de pourquoi&lt;/h2&gt;
&lt;p&gt;Une analyse de causes sert à choisir des actions. C&apos;est donc sur ces actions
qu&apos;il faut la juger. En 2017, Kellogg et ses coauteurs ont examiné 302 analyses
de causes, menées en huit ans dans un grand centre hospitalier universitaire.
Leur étude est parue dans &lt;a href=&quot;https://pubmed.ncbi.nlm.nih.gov/27940638/&quot;&gt;BMJ Quality &amp;amp; Safety&lt;/a&gt;.
Sur 731 solutions proposées, les plus fréquentes étaient la formation (20 %),
un changement de processus (19,6 %) et le rappel d&apos;une règle (15,2 %). Ce sont
des actions faibles, et plusieurs types d&apos;événements se sont répétés malgré des
analyses successives. La hiérarchie des actions de RCA² (Root Cause Analysis and Action (analyse des causes racines et actions)), présentée dans
l&apos;article sur le &lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/5w2h-qqoqccp/#la-grille-ne-priorise-pas-et-rien-ne-prouve-son-effet&quot;&gt;5W2H&lt;/a&gt;, classe ces actions parmi les plus faibles.&lt;/p&gt;
&lt;p&gt;GitLab et AWS ont choisi des actions plus fortes : un outil qui refuse les
retraits dangereux, des tests automatiques de restauration, un responsable
nommé. Au mémorial, l&apos;éclairage retardé était une action forte, puisqu&apos;elle
supprimait une condition. Elle a échoué sur une contrainte que l&apos;analyse n&apos;avait
pas cherchée. Les fils et les filets, qui laissaient la condition en place, ont
peu changé le nettoyage.&lt;/p&gt;
&lt;p&gt;De ces cas et de ces sources, on peut tirer des règles d&apos;arrêt :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Le nombre ne compte pas.&lt;/strong&gt; On s&apos;arrête quand on atteint un fait sur lequel
agir, que ce soit au troisième pourquoi ou au huitième.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Écrire des faits.&lt;/strong&gt; Pas de manque, pas de jugement : « il parlait avec un
collègue », pas « il ne faisait pas attention ».&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ne jamais s&apos;arrêter sur une personne.&lt;/strong&gt; Toute erreur humaine a une cause
antérieure, comme la commande de GitLab ou l&apos;outil d&apos;AWS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Demander à chaque maillon : a-t-il fallu autre chose ?&lt;/strong&gt; C&apos;est ce qui
transforme la chaîne en arbre.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vérifier chaque réponse sur place.&lt;/strong&gt; Les consultants des mémoriaux ont
trouvé l&apos;eau en examinant la pierre, pas en déduisant.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chercher les contraintes avant de choisir.&lt;/strong&gt; Une action qui supprime une
cause peut se heurter à un autre objectif, comme les photos des touristes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Au mémorial, il manquait une question, pas un pourquoi de plus&lt;/h2&gt;
&lt;p&gt;Le cas d&apos;école n&apos;a pas la fin qu&apos;on raconte. Les moucherons ont bien diminué,
pendant un essai de quelques semaines mené surtout au Lincoln Memorial, puis la
solution a été abandonnée. La pierre, elle, souffrait surtout de l&apos;eau. Chaque
pourquoi de la chaîne était juste. Ce qui manquait, c&apos;est la question suivante,
« a-t-il fallu autre chose ? », qui aurait montré l&apos;eau, les fissures, les
polluants et les touristes avant de choisir l&apos;éclairage.&lt;/p&gt;
&lt;p&gt;À retenir, et à appliquer au prochain postmortem : à chaque pourquoi, demandez
aussi « a-t-il fallu autre chose ? », et n&apos;arrêtez jamais l&apos;analyse sur une
personne ou sur un manque. Chaque cause retenue devient ensuite une ligne d&apos;un
plan d&apos;action &lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/5w2h-qqoqccp/&quot;&gt;5W2H&lt;/a&gt;, qui la cite dans sa colonne « pourquoi ».&lt;/p&gt;
&lt;h2&gt;Références&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Le cas du mémorial :&lt;/strong&gt; Nexteer, &lt;a href=&quot;https://www.nexteer.com/wp-content/uploads/5-Why-Training-Nexteer-Supplier-Version-17-Sept-18.pdf&quot;&gt;Supplier 3L5Y Training&lt;/a&gt;
(2018) ; Associated Press, &lt;a href=&quot;https://www.deseret.com/1990/4/17/18856959/toxic-effects-of-nature-are-aging-lincoln-and-jefferson/&quot;&gt;« Toxic effects of nature are aging Lincoln and Jefferson memorials »&lt;/a&gt;,
&lt;em&gt;Deseret News&lt;/em&gt;, 1990 ; C. Helfferich, &lt;a href=&quot;https://www.gi.alaska.edu/alaska-science-forum/unfortunately-ecological-abe&quot;&gt;« Unfortunately ecological Abe »&lt;/a&gt;,
Alaska Science Forum, 1992 ; J. Gross, &lt;a href=&quot;https://thekaizone.com/2014/08/5-whys-folklore-the-truth-behind-a-monumental-mystery/&quot;&gt;« 5 Whys Folklore: The Truth Behind a Monumental Mystery »&lt;/a&gt;,
The KaiZone, 2014 (source secondaire, qui cite le &lt;em&gt;New York Times&lt;/em&gt; de janvier
1992 et le &lt;em&gt;Chicago Tribune&lt;/em&gt; de septembre 1995).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;La méthode et ses critiques :&lt;/strong&gt; Lean Enterprise Institute, &lt;a href=&quot;https://www.lean.org/lexicon-terms/5-whys/&quot;&gt;5 Whys&lt;/a&gt;,
d&apos;après T. Ohno, &lt;em&gt;Toyota Production System&lt;/em&gt;, Productivity Press, 1988, p. 17 ;
IHI, &lt;a href=&quot;https://www.ihi.org/library/tools/5-whys-finding-root-cause&quot;&gt;5 Whys: Finding the Root Cause&lt;/a&gt; ;
M. Paradies, &lt;a href=&quot;https://taproot.com/teruyuki-minoura-toyota-exec-talks-about-problems-with-5-whys/&quot;&gt;« Teruyuki Minoura (Toyota exec) talks about problems with 5 Whys »&lt;/a&gt;,
TapRooT, 2007 (source secondaire) ; M. Graban, &lt;a href=&quot;https://www.leanblog.org/2026/04/ohno-5-whys-actually-seven/&quot;&gt;« Ohno&apos;s 5 Whys Example Is Actually Seven Whys »&lt;/a&gt;,
Lean Blog, 2026 ; A. J. Card, &lt;a href=&quot;https://doi.org/10.1136/bmjqs-2016-005849&quot;&gt;« The problem with &apos;5 whys&apos; »&lt;/a&gt;,
&lt;em&gt;BMJ Quality &amp;amp; Safety&lt;/em&gt;, 2017.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Postmortems :&lt;/strong&gt; GitLab, &lt;a href=&quot;https://about.gitlab.com/blog/postmortem-of-database-outage-of-january-31/&quot;&gt;« Postmortem of database outage of January 31 »&lt;/a&gt;,
2017 ; Amazon Web Services, &lt;a href=&quot;https://aws.amazon.com/message/41926/&quot;&gt;« Summary of the Amazon S3 Service Disruption in the Northern Virginia (US-EAST-1) Region »&lt;/a&gt;, 2017.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Causalité et erreur humaine :&lt;/strong&gt; New Jersey Department of Health, &lt;a href=&quot;https://nj.gov/health/healthcarequality/documents/ps/rca_training.pdf&quot;&gt;RCA training&lt;/a&gt;
(2008), qui reprend les règles du VA NCPS ; Betsy Lehman Center, &lt;a href=&quot;https://navigator.betsylehmancenterma.gov/pages/the-5-rules-of-causation&quot;&gt;« The 5 rules of causation »&lt;/a&gt; ;
R. I. Cook, &lt;a href=&quot;https://how.complexsystems.fail/&quot;&gt;How Complex Systems Fail&lt;/a&gt;
(1998) ; J. Allspaw, &lt;a href=&quot;https://www.kitchensoap.com/2014/11/14/the-infinite-hows-or-the-dangers-of-the-five-whys/&quot;&gt;« The Infinite Hows (or, the Dangers Of The Five Whys) »&lt;/a&gt;,
2014, qui cite S. Dekker.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Arbre des causes :&lt;/strong&gt; INRS, &lt;a href=&quot;https://www.inrs.fr/dam/inrs/CataloguePapier/ED/TI-ED-6163.pdf&quot;&gt;La méthode de l&apos;arbre des causes. L&apos;analyse de l&apos;accident du travail&lt;/a&gt;,
ED 6163, 3e édition, 2025.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Force des actions :&lt;/strong&gt; K. M. Kellogg et al., &lt;a href=&quot;https://pubmed.ncbi.nlm.nih.gov/27940638/&quot;&gt;« Our current approach to root cause analysis: is it contributing to our failure to improve patient safety? »&lt;/a&gt;,
&lt;em&gt;BMJ Quality &amp;amp; Safety&lt;/em&gt;, 2017.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>tools</category></item><item><title>PDCA et IA : vérifier les gains avant de généraliser</title><link>https://yeison-liscano.github.io/portfolio/blog/pdca-ia/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/pdca-ia/</guid><description>Appliquer le PDCA à un projet d’intelligence artificielle : diagnostic, hypothèse, pilote, indicateurs et décision. Un cas de support montre pourquoi un gain de temps peut cacher une dégradation du service.</description><pubDate>Tue, 03 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Mots-clés : PDCA, intelligence artificielle, amélioration continue, diagnostic,
mesure de référence, pilote, qualité de service&lt;/p&gt;
&lt;p&gt;Un assistant d’IA (Intelligence artificielle) peut accélérer la rédaction des réponses tout en
augmentant les erreurs ou le temps de relecture. Pour une équipe de support,
le résultat se mesure sur l’ensemble du traitement d’une demande. À partir
d’un pilote fictif, nous suivrons le cycle PDCA (Plan, Do, Check, Act : planifier, faire, vérifier, agir) pour établir une référence,
tester une hypothèse, répartir les responsabilités et décider si les résultats
justifient un déploiement plus large.&lt;/p&gt;
&lt;h2&gt;Le PDCA transforme une amélioration supposée en hypothèse à tester&lt;/h2&gt;
&lt;p&gt;Notre équipe veut réduire le temps consacré aux demandes de support. Elle
envisage un assistant qui prépare des réponses à partir de sa documentation.
Le projet commence par une question : cette assistance permettra-t-elle de
traiter les demandes plus vite, tout en maintenant la qualité ?&lt;/p&gt;
&lt;p&gt;Le PDCA donne une structure à cette investigation. La fiche de l’ASQ (American Society for Quality, association professionnelle de la qualité) décrit
quatre étapes : planifier un changement, le tester à petite échelle, analyser
les résultats, puis agir selon ce qu’on a appris. Si l’essai réussit, on peut
étendre le changement et préparer de nouvelles améliorations. Sinon, on révise
le plan. Voir la &lt;a href=&quot;https://asq.org/quality-resources/pdca-cycle&quot;&gt;définition du PDCA&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Le cycle aide à organiser l’apprentissage. Il ne fournit ni les indicateurs
adaptés à chaque projet ni une preuve automatique d’efficacité. L’ASQ précise
d’ailleurs qu’il faut choisir séparément la manière d’analyser les données.&lt;/p&gt;
&lt;p&gt;Une nuance historique compte : le &lt;a href=&quot;https://deming.org/explore/pdsa/&quot;&gt;Deming Institute&lt;/a&gt;
rappelle que Deming privilégiait le PDSA (Plan, Do, Study, Act : planifier, faire, étudier, agir).
Le mot « étudier » insiste sur la comparaison entre prédiction et observation,
puis sur la révision de l’explication. Cette exigence est utile pour notre
pilote : nous devons écrire ce qui nous ferait changer d’avis.&lt;/p&gt;
&lt;h2&gt;Planifier commence par un diagnostic et une mesure de référence&lt;/h2&gt;
&lt;p&gt;Avant de choisir un modèle, l’équipe observe le parcours d’une demande.
Où le temps passe-t-il ? Dans la recherche d’information, la rédaction,
l’attente d’un autre service ou la correction d’une première réponse ?
Une réponse rédigée plus vite ne raccourcira pas nécessairement une attente
de validation.&lt;/p&gt;
&lt;p&gt;Le billet sur les &lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/5-pourquoi/&quot;&gt;5 Pourquoi&lt;/a&gt; explique comment explorer plusieurs causes et
vérifier les faits. Ici, nous retenons une hypothèse de travail : la recherche
dans la documentation et la rédaction occupent une part du temps sur laquelle
l’équipe peut agir.&lt;/p&gt;
&lt;p&gt;La mesure de référence, ou baseline, décrit ce fonctionnement avant
le changement. Il faut conserver les définitions, les dates et les catégories
de demandes. Dans notre exemple, le périmètre couvre uniquement les demandes
simples sur l’utilisation du produit. Les litiges et les opérations sensibles
restent hors du pilote.&lt;/p&gt;
&lt;p&gt;L’équipe formule alors son hypothèse : &lt;strong&gt;l’assistance réduira d’au moins 20 %
le temps humain moyen par demande, sans augmenter les réouvertures à sept
jours.&lt;/strong&gt; Elle fixe aussi un plafond de coût par demande. Ces seuils sont des
choix illustratifs du cas, à adapter au service réel.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Indicateur&lt;/th&gt;
&lt;th&gt;Définition conservée pendant l’essai&lt;/th&gt;
&lt;th&gt;Référence fictive&lt;/th&gt;
&lt;th&gt;Cible du pilote&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Temps humain&lt;/td&gt;
&lt;td&gt;Temps cumulé de recherche, rédaction, relecture et reprises, par demande&lt;/td&gt;
&lt;td&gt;12 min&lt;/td&gt;
&lt;td&gt;Au plus 9,6 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Réouvertures&lt;/td&gt;
&lt;td&gt;Part des demandes closes puis rouvertes dans les sept jours&lt;/td&gt;
&lt;td&gt;4 %&lt;/td&gt;
&lt;td&gt;Au plus 4 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coût complet&lt;/td&gt;
&lt;td&gt;Travail humain et coûts alloués de l’outil et de son exploitation, par demande&lt;/td&gt;
&lt;td&gt;2,40 €&lt;/td&gt;
&lt;td&gt;Au plus 2,40 €&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;La mesure du temps inclut les reprises pendant la fenêtre de suivi. Il faut
donc attendre cette fenêtre pour finaliser le bilan. Le taux de réouverture
reste imparfait : un client peut abandonner malgré une mauvaise réponse.
Une revue d’un échantillon de conversations et les retours des agents
complètent ce signal.&lt;/p&gt;
&lt;p&gt;Une fois l’hypothèse écrite, la grille QQOQCCP (Qui, quoi, où, quand, comment, combien, pourquoi) aide à transformer le pilote en
actions exécutables : qui prépare les données, où l’essai se déroule, quand
il se termine et avec quelles ressources. Le billet sur le &lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/5w2h-qqoqccp/&quot;&gt;QQOQCCP&lt;/a&gt;
en détaille les deux usages.&lt;/p&gt;
&lt;h2&gt;Faire exige de repenser le processus et les responsabilités&lt;/h2&gt;
&lt;p&gt;Dans l’ancien fonctionnement, l’agent cherche une page, rédige une réponse et
l’envoie. Avec l’assistant, le parcours proposé devient : sélectionner les
informations autorisées, produire un brouillon avec ses sources, vérifier ces
sources, corriger la réponse, puis l’envoyer. Une demande hors périmètre
rejoint le traitement habituel.&lt;/p&gt;
&lt;p&gt;C’est le sens opérationnel que nous donnons ici à l’approche « AI First » :
repenser le travail à partir des capacités et des limites de l’IA. Cette
approche est un choix de conception, pas une exigence du PDCA. Le diagnostic
peut aussi conduire à améliorer la documentation ou la recherche sans IA.&lt;/p&gt;
&lt;p&gt;L’équipe commence sur un petit périmètre, après des essais hors production.
Elle forme les agents à vérifier les réponses et leur laisse la possibilité
de refuser une suggestion. Elle définit les responsabilités :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Le responsable du support possède les critères de qualité et décide de
l’extension du pilote.&lt;/li&gt;
&lt;li&gt;L’équipe technique gère l’intégration, les versions et le retour au
fonctionnement précédent.&lt;/li&gt;
&lt;li&gt;Les agents vérifient les réponses avant envoi et signalent les erreurs.&lt;/li&gt;
&lt;li&gt;Une personne désignée coordonne l’analyse des résultats et le suivi des
incidents.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Le cadre de gestion des risques de l’IA du NIST (National Institute of Standards and Technology, institut américain de normalisation et de technologie) recommande de documenter
les rôles et les interactions entre personnes et systèmes. Il invite aussi à
évaluer si une solution d’IA est appropriée. Nous nous appuyons sur son &lt;a href=&quot;https://airc.nist.gov/airmf-resources/airmf/5-sec-core/&quot;&gt;AI Risk Management Framework 1.0&lt;/a&gt;,
publié en 2023 et en cours de révision au moment de la rédaction.
Ses quatre fonctions ne sont pas des étapes ordonnées du PDCA.&lt;/p&gt;
&lt;p&gt;Notre pilote conserve les versions du modèle, des consignes et de la
documentation. L’équipe enregistre les corrections avec les précautions
adaptées aux données des clients. Elle prévoit aussi comment désactiver
l’assistance sans interrompre le support. Ce sont nos choix pour rendre
l’essai interprétable et réversible.&lt;/p&gt;
&lt;p&gt;Le schéma relie chaque phase à une trace concrète. La décision de la dernière
case dépend de l’ensemble des critères fixés dans la première.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: Dans ce cas simulé, le temps baisse de 12 à 9 minutes, mais les réouvertures passent de 4 à 7 % : l’équipe corrige le pilote avant de l’étendre.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/pdca-ia/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;Vérifier mesure le service rendu et les effets sur chaque groupe&lt;/h2&gt;
&lt;p&gt;À la fin de l’essai, l’équipe veut savoir si le changement tient sa promesse.
Le nombre de brouillons acceptés renseigne sur l’utilisation de l’outil.
Il ne suffit pas à mesurer la résolution des demandes.&lt;/p&gt;
&lt;p&gt;Voici un bilan entièrement simulé. Chaque colonne représente 1 000 demandes
du périmètre retenu, suivies pendant sept jours après leur clôture. Ces données
sont construites pour illustrer une décision ; elles ne proviennent pas
d’une entreprise.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Indicateur&lt;/th&gt;
&lt;th&gt;Référence simulée&lt;/th&gt;
&lt;th&gt;Pilote simulé&lt;/th&gt;
&lt;th&gt;Lecture du résultat&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Temps humain moyen&lt;/td&gt;
&lt;td&gt;12 min&lt;/td&gt;
&lt;td&gt;9 min&lt;/td&gt;
&lt;td&gt;Baisse de 25 %, cible atteinte&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Réouvertures à sept jours&lt;/td&gt;
&lt;td&gt;40 / 1 000, soit 4 %&lt;/td&gt;
&lt;td&gt;70 / 1 000, soit 7 %&lt;/td&gt;
&lt;td&gt;Hausse de 3 points, critère manqué&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coût complet par demande&lt;/td&gt;
&lt;td&gt;2,40 €&lt;/td&gt;
&lt;td&gt;2,10 €&lt;/td&gt;
&lt;td&gt;Sous le plafond fixé&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Le calcul suivant vérifie les deux écarts qui commandent la décision.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;reference_minutes, pilot_minutes = 12, 9
reference_reopened, pilot_reopened = 40, 70
requests_per_group = 1_000

reduction = (reference_minutes - pilot_minutes) / reference_minutes
delta = (pilot_reopened - reference_reopened) / requests_per_group

print(f&quot;Réduction du temps : {reduction:.0%}&quot;)
print(f&quot;Écart de réouverture : {delta * 100:+.1f} points&quot;)
print(&quot;Critères descriptifs temps et qualité :&quot;,
      &quot;atteints&quot; if reduction &amp;gt;= 0.20 and delta &amp;lt;= 0 else &quot;non atteints&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sortie obtenue en exécutant ce calcul sur les données simulées :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Réduction du temps : 25%
Écart de réouverture : +3.0 points
Critères descriptifs temps et qualité : non atteints
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Le gain de temps ne suffit pas à autoriser l’extension.&lt;/strong&gt; Les critères ont
été fixés ensemble : atteindre la cible de temps ne compense pas une hausse
des réouvertures. Le calcul décrit
les écarts ; il n’établit pas leur cause et ne quantifie pas leur incertitude.
La composition des demandes ou l’expérience des agents peuvent avoir changé.&lt;/p&gt;
&lt;p&gt;Quand c’est possible, une répartition aléatoire entre deux fonctionnements
comparables aide à estimer l’effet du changement. Sinon, il faut documenter
les autres explications et limiter la portée des conclusions. Le guide du
Trésor britannique sur la &lt;a href=&quot;https://www.gov.uk/government/publications/the-magenta-book/quality-in-policy-impact-evaluation-qpie-html&quot;&gt;qualité de l’évaluation d’impact&lt;/a&gt;
développe cette distinction entre suivi et attribution.
Un petit échantillon sans dégradation visible ne prouve pas non plus que la
qualité est maintenue. Le protocole doit prévoir une durée et un volume
adaptés à la précision recherchée.&lt;/p&gt;
&lt;p&gt;Les résultats doivent aussi être examinés par catégorie de demande et par
expérience des agents. L’étude &lt;a href=&quot;https://academic.oup.com/qje/article/140/2/889/7990658&quot;&gt;Generative AI at Work&lt;/a&gt;,
publiée en 2025, analyse le déploiement progressif d’une assistance auprès de
5 172 agents. Elle estime une hausse moyenne de 15 % des demandes résolues
par heure. Les moins expérimentés améliorent vitesse et qualité. Les plus
expérimentés gagnent peu en vitesse et connaissent une légère baisse de qualité.
Ce résultat concerne un contexte précis. Il ne fixe aucune cible universelle
et n’évalue pas l’efficacité du PDCA.&lt;/p&gt;
&lt;p&gt;Enfin, la fréquence de suivi dépend de l’indicateur. Une exposition de données
demande une réaction rapide. Une réouverture à sept jours exige du recul.
Le NIST prévoit des revues et un suivi après déploiement avec des fréquences
définies. Pour notre cas, nous proposons des alertes sur les incidents, une
revue hebdomadaire des conversations et un bilan à l’échéance prévue.&lt;/p&gt;
&lt;h2&gt;Agir conduit à généraliser, corriger ou arrêter&lt;/h2&gt;
&lt;p&gt;Le bilan simulé ne justifie pas une généralisation. L’équipe conserve le
périmètre limité et examine les demandes rouvertes. Les brouillons
répondent-ils à une partie seulement du problème ? Les sources sont-elles
périmées ? La clôture intervient-elle trop tôt ? Ce sont des hypothèses à
vérifier sur les conversations, pas des causes déjà établies.&lt;/p&gt;
&lt;p&gt;Si les vérifications révèlent des sources périmées, le cycle suivant peut
porter sur leur mise à jour et leur sélection. Si elles révèlent des réponses
incomplètes, il peut tester un contrôle avant clôture. Changer de modèle
n’est qu’une intervention possible parmi d’autres.&lt;/p&gt;
&lt;p&gt;Trois décisions restent ouvertes à chaque bilan :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Généraliser progressivement&lt;/strong&gt; si les résultats et leur solidité le
justifient, avec les contrôles et les moyens nécessaires.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Corriger puis retester&lt;/strong&gt; si une nouvelle hypothèse mérite un essai limité.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Arrêter l’assistance&lt;/strong&gt; si les risques, les coûts ou les résultats ne
justifient pas sa poursuite.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Agir signifie aussi mettre à jour les procédures, former l’équipe et nommer
la personne qui vérifiera les effets de la décision. Les heures libérées
restent une capacité disponible tant que leur usage n’est pas établi.
Le billet sur le &lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/roi-logiciel/&quot;&gt;retour sur investissement logiciel&lt;/a&gt; explique comment éviter
de les compter comme des économies déjà réalisées.&lt;/p&gt;
&lt;p&gt;Après extension, le suivi continue. Un nouveau modèle, une documentation
modifiée ou une nouvelle population de clients peuvent changer les résultats.
Le NIST prévoit notamment le suivi, les retours des utilisateurs et les
mécanismes de retrait dans la gestion après déploiement. Pour notre équipe,
chaque changement important déclenche une nouvelle évaluation avant extension.
Relancer le PDCA peut donc conduire à réviser le processus, les critères ou
la solution technique.&lt;/p&gt;
&lt;h2&gt;Un premier cycle commence par une fiche de décision&lt;/h2&gt;
&lt;p&gt;L’équipe du cas fictif a sa réponse : le temps baisse, mais le critère de
qualité est manqué. Elle doit comprendre les réouvertures et tester une
correction avant d’étendre l’assistance. Voici la fiche qu’elle prépare
avant ce nouvel essai.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;À écrire avant l’essai&lt;/th&gt;
&lt;th&gt;Question à trancher&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Problème et périmètre&lt;/td&gt;
&lt;td&gt;Pour quelles demandes, quelles personnes et quelle difficulté ?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hypothèse et référence&lt;/td&gt;
&lt;td&gt;Quel changement devrait agir sur quel indicateur, comparé à quoi ?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Critères de décision&lt;/td&gt;
&lt;td&gt;Quel gain attendu, quelles limites de qualité et de coût ?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protocole et échéance&lt;/td&gt;
&lt;td&gt;Quelle comparaison, quel volume, quelle fenêtre de suivi et quelle date de bilan ?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Responsabilités&lt;/td&gt;
&lt;td&gt;Qui met en œuvre, contrôle, analyse et décide ?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retour au fonctionnement précédent&lt;/td&gt;
&lt;td&gt;Comment suspendre l’assistance et continuer le service ?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Décision et apprentissage&lt;/td&gt;
&lt;td&gt;Que généraliser, corriger ou arrêter, et que vérifier ensuite ?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Avant votre prochain pilote d’IA, écrivez le résultat qui vous ferait
renoncer à le généraliser.&lt;/strong&gt; Cette phrase rend la phase « Vérifier » utile
avant même que la phase « Faire » commence.&lt;/p&gt;
&lt;h2&gt;Références&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;American Society for Quality, sans date. Voir &lt;a href=&quot;https://asq.org/quality-resources/pdca-cycle&quot;&gt;What Is the Plan-Do-Check-Act Cycle?&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The W. Edwards Deming Institute, sans date. Voir &lt;a href=&quot;https://deming.org/explore/pdsa/&quot;&gt;PDSA Cycle&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;NIST, 2023, version 1.0 ; révision en cours lors de la consultation. Voir &lt;a href=&quot;https://airc.nist.gov/airmf-resources/airmf/5-sec-core/&quot;&gt;AI RMF Core&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;HM Treasury et Evaluation Task Force, mise à jour du 15 mai 2026. Voir &lt;a href=&quot;https://www.gov.uk/government/publications/the-magenta-book/quality-in-policy-impact-evaluation-qpie-html&quot;&gt;Quality in Policy Impact Evaluation&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Erik Brynjolfsson, Danielle Li et Lindsey Raymond, 2025. Voir &lt;a href=&quot;https://academic.oup.com/qje/article/140/2/889/7990658&quot;&gt;Generative AI at Work&lt;/a&gt;,
&lt;em&gt;The Quarterly Journal of Economics&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sources consultées le 3 novembre 2026.&lt;/p&gt;
</content:encoded><category>tools</category><category>ai</category></item><item><title>5W2H (QQOQCCP) : sept questions qui rendent un plan d&apos;action exécutable, et celle qui a trois sens</title><link>https://yeison-liscano.github.io/portfolio/blog/5w2h-qqoqccp/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/5w2h-qqoqccp/</guid><description>Le 5W2H, ou QQOQCCP, oblige chaque action d&apos;un plan à dire quoi, pourquoi, où, quand, qui, comment et combien. L&apos;origine des questions, les deux usages de la grille, les trois sens du pourquoi, un script qui vérifie un plan, et ce que la grille ne fait pas.</description><pubDate>Mon, 02 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Mots-clés : 5W2H, QQOQCCP, plan d&apos;action, PDCA, 5 Pourquoi, diagramme
d&apos;Ishikawa, gestion de la qualité, postmortem&lt;/p&gt;
&lt;p&gt;Décembre 2025, fin de la revue stratégique annuelle d&apos;une équipe d&apos;ingénierie.
En octobre, un certificat expiré a fait refuser environ 1 800 paiements.
L&apos;objectif est donc clair : « aucune panne due à un certificat expiré en
2026 ». Le plan d&apos;action tient en une ligne : « Améliorer la surveillance des
certificats, équipe plateforme, T1 ». Trois mois plus tard, la ligne n&apos;a pas
bougé. Personne ne savait qui devait commencer, par où, ni avec quel budget. Le
5W2H, ou QQOQCCP en français, est l&apos;une des grilles les plus répandues contre ce
problème. Chaque action doit répondre à sept questions : quoi, pourquoi, où,
quand, qui, comment et combien. Cet article montre leurs deux usages, la
question qui a trois sens, et ce que la grille ne fait pas. Vous repartirez avec
un modèle de ligne et un petit script qui signale les cases vides ou floues d&apos;un
plan.&lt;/p&gt;
&lt;h2&gt;Une action sans responsable ni date n&apos;est qu&apos;un souhait&lt;/h2&gt;
&lt;p&gt;Le scénario de cet article est fictif, mais chacun de ses éléments est courant.
Le 14 octobre 2025 à 02:10, le certificat TLS (Transport Layer Security) d&apos;une passerelle interne,
&lt;code&gt;pay-gw&lt;/code&gt;, expire. Cette passerelle relie l&apos;API (Application Programming Interface (interface de programmation)) publique au service de
paiement. Aucune alerte ne se déclenche. Pendant deux heures, environ 1 800
paiements sont refusés, jusqu&apos;à ce que l&apos;astreinte trouve la cause et remplace
le certificat à 04:12. Le postmortem constate que personne ne savait que ce
certificat existait. En décembre, la revue stratégique en fait un objectif
pour 2026.&lt;/p&gt;
&lt;p&gt;La ligne du plan d&apos;action nomme une intention, une équipe et un trimestre.
Chacun de ces trois éléments laisse une question ouverte :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;« Améliorer » ne dit pas à quel moment le travail est terminé.&lt;/li&gt;
&lt;li&gt;« Équipe plateforme » désigne six personnes. Aucune n&apos;est chargée de
commencer.&lt;/li&gt;
&lt;li&gt;« T1 » fixe une période, pas une échéance. Le retard n&apos;apparaît que le 31
mars, quand il est trop tard pour réagir.&lt;/li&gt;
&lt;li&gt;Rien ne dit pourquoi cette action, où, comment, ni avec quel budget.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Le livre &lt;a href=&quot;https://sre.google/sre-book/postmortem-culture/&quot;&gt;Site Reliability Engineering&lt;/a&gt;
de Google définit le postmortem comme le compte rendu écrit d&apos;un incident, qui
se termine par les actions de suivi destinées à éviter qu&apos;il se reproduise. Ce
sont ces actions qui se perdent quand elles sont écrites comme la ligne de
décembre. Qu&apos;est-ce qu&apos;une ligne doit contenir pour qu&apos;on puisse l&apos;exécuter et
la relancer ? Le 5W2H répond par sept questions. Pourquoi ces sept-là ? La
réponse commence bien avant la gestion de la qualité.&lt;/p&gt;
&lt;h2&gt;Les questions ont 2 300 ans ; le « combien » n&apos;a pas d&apos;origine connue&lt;/h2&gt;
&lt;p&gt;Au IVe siècle avant notre ère, Aristote se demande dans l&apos;&lt;a href=&quot;https://classics.mit.edu/Aristotle/nicomachaen.3.iii.html&quot;&gt;Éthique à Nicomaque&lt;/a&gt;
(livre III, chapitre 1) ce qu&apos;une personne peut ignorer de son propre acte. Il
en dresse la liste : qui agit, ce qu&apos;elle fait, sur quoi ou sur qui, avec quel
instrument, en vue de quoi et comment. Vers l&apos;an 95, Quintilien classe dans l&apos;&lt;a href=&quot;https://penelope.uchicago.edu/Thayer/E/Roman/Texts/Quintilian/Institutio_Oratoria/5B*.html&quot;&gt;Institution oratoire&lt;/a&gt;
(V, 10) les arguments « tirés des circonstances » : pourquoi, où, quand, comment
et par quels moyens un acte a été accompli.&lt;/p&gt;
&lt;p&gt;Au XIIIe siècle, Thomas d&apos;Aquin cite dans la &lt;a href=&quot;https://www.newadvent.org/summa/2007.htm&quot;&gt;Somme théologique&lt;/a&gt;
(I-II, q. 7, a. 3) un vers latin qui réunit sept circonstances : &lt;em&gt;Quis, quid,
ubi, quibus auxiliis, cur, quomodo, quando&lt;/em&gt;, soit qui, quoi, où, par quels
moyens, pourquoi, comment et quand. Il l&apos;attribue à Cicéron, tandis que
Michael Sloan, dans &lt;a href=&quot;https://doi.org/10.1086/656196&quot;&gt;Classical Philology&lt;/a&gt;
en 2010, fait remonter la liste à Aristote. Personne ne peut en désigner un
inventeur unique.&lt;/p&gt;
&lt;p&gt;En avril 1900, Rudyard Kipling publie « The Elephant&apos;s Child » dans le &lt;em&gt;Ladies&apos;
Home Journal&lt;/em&gt;. Le conte entre en 1902 dans les &lt;a href=&quot;https://www.gutenberg.org/files/2781/2781-h/2781-h.htm&quot;&gt;Just So Stories&lt;/a&gt;,
suivi d&apos;un poème qui nomme six de nos sept questions :&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I Keep six honest serving-men:&lt;br /&gt;
(They taught me all I knew)&lt;br /&gt;
Their names are What and Where and When&lt;br /&gt;
And How and Why and Who.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;En traduction libre : « J&apos;ai six honnêtes serviteurs (ils m&apos;ont appris tout ce
que je savais) ; ils s&apos;appellent Quoi, Où, Quand, Comment, Pourquoi et Qui. »
La seconde strophe parle d&apos;une « petite personne » qui en garde dix millions :
« un million de Comment, deux millions de Où, et sept millions de Pourquoi ».
Déjà chez Kipling, le pourquoi est la question qui prend le plus de place.&lt;/p&gt;
&lt;p&gt;En 1913, le manuel &lt;a href=&quot;https://gutenberg.org/files/65884/65884-h/65884-h.htm&quot;&gt;Newspaper Writing and Editing&lt;/a&gt;
de Willard Bleyer demande que le premier paragraphe d&apos;un article de presse
réponde aux questions &lt;em&gt;What? Who? When? Where? Why? How?&lt;/em&gt; Ce sont les « 5W1H »
du journalisme. Six questions suffisent pour raconter un fait.&lt;/p&gt;
&lt;p&gt;Pour planifier, il en manque une. La fiche de l&apos;AHRQ (Agency for Healthcare Research and Quality (agence fédérale américaine pour la recherche et la qualité en santé)) sur le &lt;a href=&quot;https://digital.ahrq.gov/health-it-tools-and-resources/evaluation-resources/workflow-assessment-health-it-toolkit/all-workflow-tools/5w2h&quot;&gt;5W2H&lt;/a&gt;
ajoute &lt;em&gt;How much&lt;/em&gt;, combien, et cite comme source &lt;em&gt;The Quality Toolbox&lt;/em&gt; de Nancy
Tague (ASQ (American Society for Quality) Quality Press, 2005). Le second H est la question des
ressources : temps, argent, personnes. Un journaliste peut s&apos;en passer. Un plan
d&apos;action, non : c&apos;est cette question qui permet de comparer deux actions. C&apos;est
aussi la case qui manquait à la ligne de décembre : personne ne savait avec quel
budget.&lt;/p&gt;
&lt;p&gt;Quand, et par qui, ce « combien » a-t-il été ajouté ? Je n&apos;ai trouvé aucune
source fiable pour le dire. On lit souvent que la grille viendrait de Toyota ou
de la qualité totale japonaise, mais ces attributions ne renvoient à aucun texte
primaire.&lt;/p&gt;
&lt;p&gt;En français, la grille s&apos;appelle QQOQCCP : qui, quoi, où, quand, comment,
combien, pourquoi. La &lt;a href=&quot;https://www.techniques-ingenieur.fr/base-documentaire/genie-industriel-th6/methodes-de-resolution-de-problemes-42592210/la-methode-qqoqccp-un-outil-d-analyse-fic0447/&quot;&gt;fiche de Techniques de l&apos;Ingénieur&lt;/a&gt;
la présente comme une série de questions standard pour clarifier une situation
mal connue. Le second C est parfois omis, ce qui donne QQOQCP. L&apos;ordre des
lettres ne compte pas : la fiche de l&apos;AHRQ précise que l&apos;ordre des questions n&apos;a
pas d&apos;importance.&lt;/p&gt;
&lt;h2&gt;Le même tableau sert à décrire un problème ou à planifier une action&lt;/h2&gt;
&lt;p&gt;La fiche de l&apos;AHRQ énumère plusieurs usages : planifier un projet, améliorer un
processus, définir un problème soupçonné ou mal cerné. Dans tous les cas, on
répond à chaque question, et on prévoit comment répondre à celles qui n&apos;ont pas
encore de réponse. Deux usages dominent, et ils ne regardent pas dans la même
direction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Décrire un problème&lt;/strong&gt; consiste à poser les questions sur un fait passé. C&apos;est
l&apos;étape D2 de la méthode 8D. Christoph Roser, professeur et auteur sur le lean,
précise dans son &lt;a href=&quot;https://www.allaboutlean.com/8d-d2-and-d3/&quot;&gt;guide du 8D&lt;/a&gt;
le sens de trois questions. Le pourquoi y désigne l&apos;impact, et pas encore la
cause racine. Le comment décrit l&apos;écart observé par rapport à l&apos;état normal. Le
combien quantifie le problème : taux de défauts, réclamations, coût des rebuts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Planifier une action&lt;/strong&gt; consiste à poser les questions sur un fait futur, une
ligne par action. Au début de la pandémie de COVID-19, une équipe infirmière
d&apos;un hôpital universitaire de Rio de Janeiro a d&apos;abord classé les problèmes
avec un diagramme d&apos;Ishikawa. Elle a ensuite construit son plan avec le
5W2H : sept actions, arrêtées en six réunions du 16 mars au 31 mai 2020. Le
cas est décrit par &lt;a href=&quot;https://pmc.ncbi.nlm.nih.gov/articles/PMC9897221/&quot;&gt;Braga et al. (2023)&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;La figure applique les deux usages au scénario. À gauche, la grille décrit la
panne du 14 octobre. À droite, elle planifie l&apos;action A2, l&apos;alerte avant
expiration.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: Les mêmes sept questions décrivent la panne et planifient l&apos;action A2. Seul le pourquoi change de nature : l&apos;impact à gauche, la justification à droite.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/5w2h-qqoqccp/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Les six autres lignes changent d&apos;objet, pas de nature. Le quand reste une date :
passée à gauche, échéance à droite. Le combien reste une quantité : des
paiements refusés à gauche, des jours-personne à droite. Le pourquoi, lui, ne
répond plus à la même question. Mélanger les deux colonnes produit des plans
qui décrivent au lieu d&apos;engager. On y lit par exemple « Pourquoi : 2 h sans
paiement » en face d&apos;une action, sans savoir quelle cause elle traite.&lt;/p&gt;
&lt;h2&gt;Le « pourquoi » a trois sens selon le moment où on le pose&lt;/h2&gt;
&lt;p&gt;Quand on décrit, le pourquoi demande en quoi la situation est un problème : la
réponse est l&apos;impact. Quand on planifie, il demande pourquoi cette action plutôt
qu&apos;une autre : la réponse est une justification. Entre les deux, une troisième
méthode pose la même question plusieurs fois pour trouver la cause : les 5
Pourquoi.&lt;/p&gt;
&lt;p&gt;Le Lean Enterprise Institute rappelle l&apos;exemple classique de Taiichi Ohno, l&apos;un
des architectes du système de production de Toyota (&lt;a href=&quot;https://www.lean.org/the-lean-post/articles/five-whys/&quot;&gt;Ballé, 2012&lt;/a&gt;).
Une machine s&apos;arrête. Pourquoi ? Une surcharge a fait sauter le fusible.
Pourquoi la surcharge ? Le palier était mal lubrifié. Pourquoi ? La pompe de
lubrification pompait mal. Pourquoi ? Son axe était usé. Pourquoi ? Il n&apos;y avait
pas de filtre, et des copeaux de métal sont entrés. Remplacer le fusible
traiterait le symptôme. Poser un filtre traite la cause.&lt;/p&gt;
&lt;p&gt;Appliquée à la panne du 14 octobre, la chaîne donne ceci :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Pourquoi les paiements ont-ils échoué ? Le certificat de &lt;code&gt;pay-gw&lt;/code&gt; avait
expiré.&lt;/li&gt;
&lt;li&gt;Pourquoi a-t-il expiré ? Personne ne l&apos;a renouvelé.&lt;/li&gt;
&lt;li&gt;Pourquoi ? Il ne figurait dans aucun inventaire, et son renouvellement était
manuel.&lt;/li&gt;
&lt;li&gt;Pourquoi la panne a-t-elle duré deux heures ? Rien ne surveillait la date
d&apos;expiration, et l&apos;astreinte a dû chercher la cause.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;La chaîne aboutit à trois causes : un certificat hors inventaire, un
renouvellement manuel et une expiration que rien ne surveillait. Voici la
réponse à la question ouverte au début : dans un plan d&apos;action, le pourquoi de
chaque ligne doit citer la cause qu&apos;elle traite. « 2 h sans paiement » justifie
l&apos;urgence de l&apos;objectif, pas le choix d&apos;une action. Un pourquoi qui ne cite
aucune cause signale une action qui traite un symptôme, ou rien du tout.&lt;/p&gt;
&lt;p&gt;Les 5 Pourquoi ont leurs propres limites. Michael Ballé note que beaucoup de
séances restent superficielles ou tournent en rond, et que leur valeur dépend de
l&apos;expertise de ceux qui les mènent. Alan Card, dans &lt;a href=&quot;https://doi.org/10.1136/bmjqs-2016-005849&quot;&gt;BMJ Quality &amp;amp; Safety&lt;/a&gt;
en 2017, avance que la méthode doit « probablement » sa place dans l&apos;analyse des
causes à son pedigree, à sa simplicité et à sa valeur pédagogique.&lt;/p&gt;
&lt;h2&gt;Dans le PDCA, le 5W2H vient après l&apos;analyse des causes, pas avant&lt;/h2&gt;
&lt;p&gt;Les trois sens du pourquoi correspondent à trois moments d&apos;un même cycle.
Le PDCA (Plan, Do, Check, Act (planifier, faire, vérifier, agir)) enchaîne quatre phases : planifier un changement, le mettre en
œuvre, évaluer les résultats, puis standardiser ou recommencer. Le &lt;a href=&quot;https://www.lean.org/lexicon-terms/pdca/&quot;&gt;Lean Enterprise Institute&lt;/a&gt;
attribue le premier cycle d&apos;amélioration à Walter Shewhart, et son introduction
au Japon, dans les années 1950, à W. Edwards Deming. La phase Plan contient à
elle seule les trois usages du pourquoi.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: Dans la phase Plan, on décrit, puis on explique, puis on planifie. Le 5W2H arrive en troisième, quand les causes sont connues.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/5w2h-qqoqccp/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;L&apos;hôpital de Rio suit cet ordre : l&apos;Ishikawa d&apos;abord, le 5W2H ensuite. Dans une
étude de cas industrielle, &lt;a href=&quot;https://www.revistas.usp.br/rai/article/view/79051&quot;&gt;Mariani (2005)&lt;/a&gt;
utilise aussi le 5W2H à l&apos;intérieur d&apos;un PDCA, avec le diagramme de cause à
effet. Planifier avant d&apos;expliquer revient à choisir des actions avant de
connaître les causes. La ligne de décembre est née ainsi, d&apos;un objectif et non
d&apos;une analyse.&lt;/p&gt;
&lt;p&gt;Il manque pourtant à la grille une case que la norme exige. La clause 6.2.2 de
l&apos;&lt;a href=&quot;https://www.iso.org/standard/62085.html&quot;&gt;ISO 9001:2015&lt;/a&gt;
demande, pour chaque objectif qualité, de déterminer ce qui sera fait, les
ressources nécessaires, le responsable, l&apos;échéance et la manière d&apos;évaluer les
résultats. Les quatre premiers points correspondent au quoi, au combien, au qui
et au quand. Le cinquième n&apos;a pas d&apos;équivalent dans le 5W2H. Pour A2, ce
critère pourrait être : chaque certificat de l&apos;inventaire A1 a une alerte, et un
certificat de test qui expire dans 29 jours déclenche l&apos;alerte en préproduction.&lt;/p&gt;
&lt;p&gt;Le tableau d&apos;actions de l&apos;&lt;a href=&quot;https://sre.google/sre-book/example-postmortem/&quot;&gt;exemple de postmortem&lt;/a&gt;
du livre SRE (Site Reliability Engineering) a, lui, quatre colonnes : &lt;em&gt;Action Item&lt;/em&gt;, &lt;em&gt;Type&lt;/em&gt;, &lt;em&gt;Owner&lt;/em&gt; et &lt;em&gt;Bug&lt;/em&gt;.
Il couvre le quoi, le qui et le suivi par un ticket. Le type, « atténuer » ou
« prévenir », est un pourquoi très court. Le quand et le combien manquent. Si
votre modèle de postmortem reprend ce tableau, ajoutez deux colonnes :
l&apos;échéance et l&apos;effort estimé.&lt;/p&gt;
&lt;h2&gt;Réécrire le plan ligne par ligne fait apparaître ce qui manque&lt;/h2&gt;
&lt;p&gt;Le 16 mars 2026, l&apos;équipe réécrit la ligne de décembre. Les trois causes
deviennent trois actions, et chaque action répond aux sept questions :&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;A1&lt;/th&gt;
&lt;th&gt;A2&lt;/th&gt;
&lt;th&gt;A3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;quoi&lt;/td&gt;
&lt;td&gt;Inventorier les certificats TLS de production&lt;/td&gt;
&lt;td&gt;Alerter l&apos;astreinte 30 jours avant chaque expiration&lt;/td&gt;
&lt;td&gt;Automatiser le renouvellement des certificats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pourquoi&lt;/td&gt;
&lt;td&gt;Le certificat expiré n&apos;était dans aucun inventaire&lt;/td&gt;
&lt;td&gt;L&apos;expiration n&apos;a déclenché aucune alerte&lt;/td&gt;
&lt;td&gt;Le renouvellement était manuel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;où&lt;/td&gt;
&lt;td&gt;Répartiteurs de charge et passerelles internes&lt;/td&gt;
&lt;td&gt;Supervision Prometheus existante&lt;/td&gt;
&lt;td&gt;Cluster Kubernetes de production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;quand&lt;/td&gt;
&lt;td&gt;3 avril 2026&lt;/td&gt;
&lt;td&gt;24 avril 2026&lt;/td&gt;
&lt;td&gt;30 juin 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qui&lt;/td&gt;
&lt;td&gt;Camille&lt;/td&gt;
&lt;td&gt;Sacha&lt;/td&gt;
&lt;td&gt;Dominique&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;comment&lt;/td&gt;
&lt;td&gt;Script sur l&apos;API du cloud, export CSV versionné&lt;/td&gt;
&lt;td&gt;Sonde TLS du blackbox exporter, règle d&apos;alerte&lt;/td&gt;
&lt;td&gt;cert-manager avec ACME, d&apos;abord en préproduction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;combien&lt;/td&gt;
&lt;td&gt;3 jours-personne&lt;/td&gt;
&lt;td&gt;2 jours-personne&lt;/td&gt;
&lt;td&gt;à définir&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A2 repose sur la métrique &lt;code&gt;probe_ssl_earliest_cert_expiry&lt;/code&gt; du &lt;a href=&quot;https://github.com/prometheus/blackbox_exporter&quot;&gt;blackbox exporter&lt;/a&gt;,
qui donne la date d&apos;expiration du premier certificat de la chaîne à expirer. A3
repose sur &lt;a href=&quot;https://cert-manager.io/docs/&quot;&gt;cert-manager&lt;/a&gt;,
qui renouvelle les certificats dans le cluster Kubernetes avec le protocole ACME (Automatic Certificate Management Environment),
décrit dans la &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc8555.html&quot;&gt;RFC 8555&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Une relecture peut vérifier ce tableau. Un script aussi, au moins pour la forme.
Le programme ci-dessous applique cinq règles à chaque ligne :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;aucune des sept cases n&apos;est vide ;&lt;/li&gt;
&lt;li&gt;le quoi ne commence pas par un verbe flou, comme « améliorer » ou
« renforcer » ;&lt;/li&gt;
&lt;li&gt;le qui nomme une personne, pas une équipe ni une liste ;&lt;/li&gt;
&lt;li&gt;le quand est une date, postérieure au jour de la réécriture ;&lt;/li&gt;
&lt;li&gt;le combien contient au moins un chiffre.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Le bouton Run l&apos;exécute dans votre navigateur. Vous pouvez modifier le plan et
le relancer.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import re
from datetime import date

# Jour de la réécriture : chaque échéance doit tomber après.
AUJOURD_HUI = date(2026, 3, 16)

QUESTIONS = (&quot;quoi&quot;, &quot;pourquoi&quot;, &quot;où&quot;, &quot;quand&quot;, &quot;qui&quot;, &quot;comment&quot;, &quot;combien&quot;)
VERBES_FLOUS = (&quot;améliorer&quot;, &quot;renforcer&quot;, &quot;optimiser&quot;, &quot;suivre&quot;, &quot;garantir&quot;)
COLLECTIFS = (&quot;équipe&quot;, &quot;tous&quot;, &quot;direction&quot;, &quot; et &quot;, &quot;,&quot;, &quot;/&quot;)


def verifier(action):
    &quot;&quot;&quot;Renvoie la liste des problèmes d&apos;une ligne du plan.&quot;&quot;&quot;
    problemes = []
    for question in QUESTIONS:
        if not action.get(question, &quot;&quot;).strip():
            problemes.append(f&quot;{question} : vide&quot;)

    quoi = action.get(&quot;quoi&quot;, &quot;&quot;).lower()
    if quoi.startswith(VERBES_FLOUS):
        problemes.append(f&quot;quoi : verbe flou « {quoi.split()[0]} »&quot;)

    qui = action.get(&quot;qui&quot;, &quot;&quot;)
    if qui and any(mot in f&quot; {qui.lower()} &quot; for mot in COLLECTIFS):
        problemes.append(f&quot;qui : « {qui} » n&apos;est pas une personne&quot;)

    quand = action.get(&quot;quand&quot;, &quot;&quot;)
    if quand:
        try:
            if date.fromisoformat(quand) &amp;lt;= AUJOURD_HUI:
                problemes.append(f&quot;quand : {quand} est déjà passé&quot;)
        except ValueError:
            problemes.append(f&quot;quand : « {quand} » n&apos;est pas une date&quot;)

    combien = action.get(&quot;combien&quot;, &quot;&quot;)
    if combien and not re.search(r&quot;\d&quot;, combien):
        problemes.append(f&quot;combien : « {combien} » sans chiffre&quot;)
    return problemes


PLAN = {
    &quot;A0&quot;: {
        &quot;quoi&quot;: &quot;Améliorer la surveillance des certificats&quot;,
        &quot;qui&quot;: &quot;équipe plateforme&quot;,
        &quot;quand&quot;: &quot;T1&quot;,
    },
    &quot;A1&quot;: {
        &quot;quoi&quot;: &quot;Inventorier les certificats TLS de production&quot;,
        &quot;pourquoi&quot;: &quot;Le certificat expiré n&apos;était dans aucun inventaire&quot;,
        &quot;où&quot;: &quot;Répartiteurs de charge et passerelles internes&quot;,
        &quot;quand&quot;: &quot;2026-04-03&quot;,
        &quot;qui&quot;: &quot;Camille&quot;,
        &quot;comment&quot;: &quot;Script sur l&apos;API du cloud, export CSV versionné&quot;,
        &quot;combien&quot;: &quot;3 jours-personne&quot;,
    },
    &quot;A2&quot;: {
        &quot;quoi&quot;: &quot;Alerter l&apos;astreinte 30 jours avant chaque expiration&quot;,
        &quot;pourquoi&quot;: &quot;L&apos;expiration n&apos;a déclenché aucune alerte&quot;,
        &quot;où&quot;: &quot;Supervision Prometheus existante&quot;,
        &quot;quand&quot;: &quot;2026-04-24&quot;,
        &quot;qui&quot;: &quot;Sacha&quot;,
        &quot;comment&quot;: &quot;Sonde TLS du blackbox exporter et règle d&apos;alerte&quot;,
        &quot;combien&quot;: &quot;2 jours-personne&quot;,
    },
    &quot;A3&quot;: {
        &quot;quoi&quot;: &quot;Automatiser le renouvellement des certificats&quot;,
        &quot;pourquoi&quot;: &quot;Le renouvellement était manuel&quot;,
        &quot;où&quot;: &quot;Cluster Kubernetes de production&quot;,
        &quot;quand&quot;: &quot;2026-06-30&quot;,
        &quot;qui&quot;: &quot;Dominique&quot;,
        &quot;comment&quot;: &quot;cert-manager avec ACME, d&apos;abord en préproduction&quot;,
        &quot;combien&quot;: &quot;à définir&quot;,
    },
}

for nom, action in PLAN.items():
    problemes = verifier(action)
    print(f&quot;{nom} : {len(problemes)} problème(s)&quot;)
    for probleme in problemes:
        print(f&quot;  - {probleme}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Voici sa sortie, avec Python 3.14 :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A0 : 7 problème(s)
  - pourquoi : vide
  - où : vide
  - comment : vide
  - combien : vide
  - quoi : verbe flou « améliorer »
  - qui : « équipe plateforme » n&apos;est pas une personne
  - quand : « T1 » n&apos;est pas une date
A1 : 0 problème(s)
A2 : 0 problème(s)
A3 : 1 problème(s)
  - combien : « à définir » sans chiffre
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La ligne de décembre, A0, échoue sur les sept questions : quatre cases vides et
trois réponses qui n&apos;en sont pas. A1 et A2 passent. A3 échoue sur une seule
case, la plus facile à remplir pour la forme. Personne ne sait encore combien
coûte l&apos;automatisation des certificats internes, et c&apos;est une information en
soi. La correction n&apos;invente pas de chiffre : elle planifie l&apos;estimation. La
case devient « inconnu ; estimation par Dominique le 15 avril 2026 ». C&apos;est ce
que recommande la fiche de l&apos;AHRQ : prévoir comment répondre aux questions dont
on ne connaît pas encore la réponse.&lt;/p&gt;
&lt;p&gt;Cette nouvelle case passe la règle du chiffre grâce à la date, pas grâce à un
coût. Le script vérifie la forme, pas le fond. Une ligne peut passer les cinq
règles et rester une mauvaise action. C&apos;est aussi la limite de la grille
elle-même.&lt;/p&gt;
&lt;h2&gt;La grille ne priorise pas, et rien ne prouve son effet&lt;/h2&gt;
&lt;p&gt;La fiche de l&apos;AHRQ ne lui reconnaît qu&apos;un inconvénient : elle n&apos;est pas utile si
la plupart des réponses sont inconnues. Elle affirme aussi que la grille prend
en compte tous les aspects d&apos;une situation. Elle en couvre sept. La priorité, le
risque, les dépendances entre actions et la force d&apos;une action n&apos;en font pas
partie.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;La grille ne dit pas quelle action est la plus forte.&lt;/strong&gt; A2 et A3 passent le
script avec zéro problème, mais elles ne se valent pas. Le guide RCA² (Root Cause Analysis and Action (analyse des causes racines et actions))
de la &lt;a href=&quot;https://www.ihi.org/library/tools/rca2-improving-root-cause-analyses-and-actions-prevent-harm&quot;&gt;National Patient Safety Foundation&lt;/a&gt;
(2015) classe les actions correctives selon une hiérarchie. Les plus fortes
retirent l&apos;occasion de l&apos;erreur : contraintes techniques, simplification,
standardisation. Les plus faibles dépendent de quelqu&apos;un qui se souvient et
obéit : avertissements, nouvelles procédures, formation. L&apos;&lt;a href=&quot;https://www.health.mn.gov/facilities/patientsafety/adverseevents/toolkit/docs/safetytoolkit_actionhierarchy.pdf&quot;&gt;outil de hiérarchie des actions&lt;/a&gt;
demande au moins une action forte ou intermédiaire par cause. L&apos;alerte A2 est un
avertissement : elle compte sur une astreinte qui lit et agit. A3 supprime
l&apos;étape humaine. A3 est donc plus forte, et la grille ne le montre pas.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Les cases se remplissent pour la forme.&lt;/strong&gt; Dans le plan de l&apos;hôpital de Rio, la
colonne &lt;em&gt;How much&lt;/em&gt; indique « Unestimated Expense » pour les sept actions. Aucune
n&apos;a de coût. L&apos;article ne mesure pas non plus de résultat, et ses auteurs
reconnaissent l&apos;absence de cadre méthodologique structuré, puisqu&apos;il s&apos;agit d&apos;un
retour d&apos;expérience.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Aucune étude contrôlée ne mesure son effet.&lt;/strong&gt; Je n&apos;ai trouvé aucune étude qui
compare des équipes avec et sans 5W2H. La littérature disponible se compose de
retours d&apos;expérience et d&apos;études de cas uniques, comme celles de Braga et de
Mariani. La grille est sans doute utile, mais personne ne l&apos;a démontré.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;La grille ne choisit pas l&apos;objectif.&lt;/strong&gt; « Aucune panne due à un certificat
expiré » vient du postmortem et de la revue stratégique. Le 5W2H décline cet
objectif en actions. Il ne dit pas s&apos;il valait mieux investir ailleurs. Une
grille parfaite exécute aussi bien un mauvais objectif qu&apos;un bon. Pour mettre à
l&apos;épreuve les hypothèses d&apos;un objectif, le registre de prémisses de l&apos;article
sur le &lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/metodo-cientifico-en-software-y-seguridad/&quot;&gt;método científico&lt;/a&gt; (en espagnol) propose une grille voisine :
une ligne par affirmation, et le résultat qui la réfuterait.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Les réponses coûtent plus cher que les cases.&lt;/strong&gt; La fiche de Techniques de
l&apos;Ingénieur avertit de ne pas sous-estimer le temps de recueil des informations.
Écrire sept cases prend quelques minutes. Trouver le coût réel de A3 demande une
estimation, et donc une action de plus dans le plan.&lt;/p&gt;
&lt;h2&gt;Une action exécutable nomme une personne, une date et un coût&lt;/h2&gt;
&lt;p&gt;Une action est exécutable quand la personne nommée peut commencer le lendemain
sans poser de question. La ligne de décembre ne répondait de façon exploitable à
aucune des sept questions. Réécrite, elle devient trois lignes. Chacune nomme
une personne, une date et un effort, ou la date à laquelle on le connaîtra. Son
pourquoi cite une cause trouvée par l&apos;analyse. Rien ne garantit que les trois
actions réussiront. Mais chacune a désormais quelqu&apos;un à relancer, une date où
l&apos;on saura, et un coût que l&apos;on peut comparer aux autres.&lt;/p&gt;
&lt;p&gt;La question qui a trois sens est le pourquoi : l&apos;impact quand on décrit, la
cause quand on explique, la justification quand on planifie. Dans un plan, il
doit renvoyer à une cause.&lt;/p&gt;
&lt;p&gt;À retenir, et à appliquer dès la prochaine réunion : avant de la clore, relisez
chaque action et demandez qui, quand, combien, et quelle cause elle traite. Si
une réponse manque, la première action à écrire est celle qui la trouvera, avec
un nom et une date.&lt;/p&gt;
&lt;h2&gt;Références&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Origines des questions :&lt;/strong&gt; Aristote, &lt;a href=&quot;https://classics.mit.edu/Aristotle/nicomachaen.3.iii.html&quot;&gt;Éthique à Nicomaque, livre III&lt;/a&gt;,
trad. W. D. Ross ; Quintilien, &lt;a href=&quot;https://penelope.uchicago.edu/Thayer/E/Roman/Texts/Quintilian/Institutio_Oratoria/5B*.html&quot;&gt;Institution oratoire, livre V&lt;/a&gt;,
trad. H. E. Butler (Loeb, 1920-1922) ; Thomas d&apos;Aquin, &lt;a href=&quot;https://www.newadvent.org/summa/2007.htm&quot;&gt;Somme théologique, I-II, q. 7&lt;/a&gt; ;
M. C. Sloan, &lt;a href=&quot;https://doi.org/10.1086/656196&quot;&gt;« Aristotle&apos;s Nicomachean Ethics as the Original Locus for the Septem Circumstantiae »&lt;/a&gt;,
&lt;em&gt;Classical Philology&lt;/em&gt;, 2010.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kipling et le journalisme :&lt;/strong&gt; R. Kipling, &lt;a href=&quot;https://www.gutenberg.org/files/2781/2781-h/2781-h.htm&quot;&gt;Just So Stories&lt;/a&gt;
(1902) ; Kipling Society, &lt;a href=&quot;https://www.kiplingsociety.co.uk/?p=5567&quot;&gt;notes sur The Elephant&apos;s Child&lt;/a&gt; ;
W. G. Bleyer, &lt;a href=&quot;https://gutenberg.org/files/65884/65884-h/65884-h.htm&quot;&gt;Newspaper Writing and Editing&lt;/a&gt;
(1913), chapitre IV.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;La méthode :&lt;/strong&gt; AHRQ, &lt;a href=&quot;https://digital.ahrq.gov/health-it-tools-and-resources/evaluation-resources/workflow-assessment-health-it-toolkit/all-workflow-tools/5w2h&quot;&gt;5W2H&lt;/a&gt;,
d&apos;après N. Tague, &lt;em&gt;The Quality Toolbox&lt;/em&gt;, 2e éd., ASQ Quality Press, 2005 ; I.
Equixor, &lt;a href=&quot;https://www.techniques-ingenieur.fr/base-documentaire/genie-industriel-th6/methodes-de-resolution-de-problemes-42592210/la-methode-qqoqccp-un-outil-d-analyse-fic0447/&quot;&gt;La méthode QQOQCCP, un outil d&apos;analyse&lt;/a&gt;,
Techniques de l&apos;Ingénieur, fiche FIC0447 ; C. Roser, &lt;a href=&quot;https://www.allaboutlean.com/8d-d2-and-d3/&quot;&gt;The Eight Disciplines Problem Solving (8D): D2 and D3&lt;/a&gt;,
AllAboutLean (source secondaire).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Études de cas :&lt;/strong&gt; Braga, Lins, Christovam et de Souza, &lt;a href=&quot;https://pmc.ncbi.nlm.nih.gov/articles/PMC9897221/&quot;&gt;« Quality management in the COVID-19 pandemic: nursing action plan »&lt;/a&gt;,
&lt;em&gt;Revista Brasileira de Enfermagem&lt;/em&gt;, 2023 ; C. A. Mariani, &lt;a href=&quot;https://www.revistas.usp.br/rai/article/view/79051&quot;&gt;« Método PDCA e ferramentas da qualidade no gerenciamento de processos industriais »&lt;/a&gt;,
&lt;em&gt;RAI&lt;/em&gt;, 2005.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Causes, cycle et force des actions :&lt;/strong&gt; M. Ballé, &lt;a href=&quot;https://www.lean.org/the-lean-post/articles/five-whys/&quot;&gt;Five whys&lt;/a&gt;,
Lean Enterprise Institute, 2012 ; A. J. Card, &lt;a href=&quot;https://doi.org/10.1136/bmjqs-2016-005849&quot;&gt;« The problem with &apos;5 whys&apos; »&lt;/a&gt;,
&lt;em&gt;BMJ Quality &amp;amp; Safety&lt;/em&gt;, 2017 ; Lean Enterprise Institute, &lt;a href=&quot;https://www.lean.org/lexicon-terms/pdca/&quot;&gt;PDCA&lt;/a&gt; ;
norme &lt;a href=&quot;https://www.iso.org/standard/62085.html&quot;&gt;ISO 9001:2015&lt;/a&gt;,
clause 6.2.2 ; NPSF et IHI, &lt;a href=&quot;https://www.ihi.org/library/tools/rca2-improving-root-cause-analyses-and-actions-prevent-harm&quot;&gt;RCA²: Improving Root Cause Analyses and Actions to Prevent Harm&lt;/a&gt;
(2015), et son &lt;a href=&quot;https://www.health.mn.gov/facilities/patientsafety/adverseevents/toolkit/docs/safetytoolkit_actionhierarchy.pdf&quot;&gt;outil de hiérarchie des actions&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logiciel :&lt;/strong&gt; J. Lunney et S. Lueder, &lt;a href=&quot;https://sre.google/sre-book/postmortem-culture/&quot;&gt;Postmortem Culture: Learning from Failure&lt;/a&gt;
et &lt;a href=&quot;https://sre.google/sre-book/example-postmortem/&quot;&gt;Example Postmortem&lt;/a&gt;,
&lt;em&gt;Site Reliability Engineering&lt;/em&gt;, Google, 2016 ; Prometheus, &lt;a href=&quot;https://github.com/prometheus/blackbox_exporter&quot;&gt;blackbox exporter&lt;/a&gt; ;
documentation de &lt;a href=&quot;https://cert-manager.io/docs/&quot;&gt;cert-manager&lt;/a&gt; ;
R. Barnes et al., &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc8555.html&quot;&gt;RFC 8555&lt;/a&gt;
(2019).&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>tools</category></item><item><title>ASGI Event by Event: How scope, receive and send Keep a Connection Open, and Why an Async WSGI Would Not</title><link>https://yeison-liscano.github.io/portfolio/blog/asgi-event-by-event/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/asgi-event-by-event/</guid><description>A raw ASGI app traced under Uvicorn: what scope, receive and send carry, how a request body arrives in pieces, how the app notices a client leave, how a WebSocket pushes, and why wrapped WSGI code ran one request at a time.</description><pubDate>Mon, 02 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Keywords: ASGI, WSGI, scope, receive, send, Uvicorn, WebSocket, long polling,
lifespan, asgiref&lt;/p&gt;
&lt;p&gt;An order page should show &quot;shipped&quot; the moment the warehouse marks order 42 as
shipped. To do that, the server has to send a message long after the browser
asked, on a connection that stays open.
A WSGI (Web Server Gateway Interface) application
cannot do this, because it is a function that takes one request and returns
one response. Making that function &lt;code&gt;async&lt;/code&gt; would not fix it either, and
the &lt;a href=&quot;https://asgi.readthedocs.io/en/latest/introduction.html&quot;&gt;ASGI introduction&lt;/a&gt;
explains why. This post builds the feature on
raw ASGI (Asynchronous Server Gateway Interface),
with no framework, runs it under Uvicorn, and prints every event that passes
between the server and the application. You will see what &lt;code&gt;scope&lt;/code&gt;, &lt;code&gt;receive&lt;/code&gt;
and &lt;code&gt;send&lt;/code&gt; carry, how a request body arrives in pieces, and how the
application learns that a client has left. You will also see what happens to
old WSGI code on the same server, and why three one-second WSGI requests took
three seconds.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/wsgi-vs-asgi-protocols/&quot;&gt;WSGI vs ASGI post&lt;/a&gt;
showed a hello-world application for each interface, and when to choose each
one. This post goes one level down, to the messages themselves. Every trace
below is real output from Python 3.13.9, Uvicorn 0.54.0 with its h11 HTTP
implementation, asgiref 3.13.0, httpx 0.28.1 and websockets 17.4. It ran on
one machine, over localhost, with one worker process. A real deployment adds
a network, several workers and a message broker, and the sections say where
that changes the result.&lt;/p&gt;
&lt;h2&gt;WSGI hands the application one request and takes back one response&lt;/h2&gt;
&lt;p&gt;The WSGI specification, PEP (Python Enhancement Proposal) &lt;a href=&quot;https://peps.python.org/pep-3333/&quot;&gt;3333&lt;/a&gt;,
defines the application as &quot;simply a callable object that accepts two
arguments&quot;: &lt;code&gt;environ&lt;/code&gt;, a dict that describes the request, and
&lt;code&gt;start_response&lt;/code&gt;, a function that sets the status and headers. The server
&quot;invokes the application callable once for each request&quot;, and the
application returns &quot;an iterable yielding zero or more bytestrings&quot;. Every
input arrives before the call starts, and every output leaves through the
return value.&lt;/p&gt;
&lt;p&gt;The order page needs two things this shape cannot give. The first is
a long poll
over HTTP (Hypertext Transfer Protocol): the browser
asks for the next status change, and the server answers only when there is
one. In WSGI, the call must stay running for the whole wait, and
nothing in the interface tells it that the browser has given up. The second
is a WebSocket.
After the handshake, the browser sends frames whenever it wants. A call that
has already received its &lt;code&gt;environ&lt;/code&gt; has no way to get them.&lt;/p&gt;
&lt;p&gt;Would an &lt;code&gt;async&lt;/code&gt; WSGI help? Picture one:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# A hypothetical async WSGI: not a real interface.
async def application(environ):
    ...  # everything the client will send is already in environ
    return [b&quot;...&quot;]  # everything the server will send leaves here
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;async&lt;/code&gt; lets the server run other requests on
its event loop
while this one waits, but the application still has one way in and one way
out. The ASGI introduction puts it
this way: &quot;Even if we made this callable asynchronous, it still only has a
single path to provide a request, so protocols that have multiple incoming
events (like receiving WebSocket frames) can&apos;t trigger this.&quot;
The &lt;a href=&quot;https://asgi.readthedocs.io/en/latest/specs/main.html&quot;&gt;ASGI specification&lt;/a&gt;
gives the same reason in its rationale: WSGI&apos;s design &quot;is irrevocably tied to
the HTTP-style request/response cycle&quot;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: A WSGI call gets the whole request when it starts and gives back the whole response when it returns. An ASGI call lasts as long as the connection, and events cross in both directions while it runs, including a message the app sends when nothing asked for it.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/asgi-event-by-event/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;The figure raises three questions that the rest of the post answers with a
running server. How does an application read a request that is still
arriving? How does it learn that the browser has gone? And can the old WSGI
code of the same project live on the new server, at the same speed?&lt;/p&gt;
&lt;h2&gt;An ASGI application is called once per connection, with three arguments&lt;/h2&gt;
&lt;p&gt;ASGI started in Django Channels. Andrew Godwin proposed it
on &lt;a href=&quot;https://mail.python.org/pipermail/web-sig/2016-March/005437.html&quot;&gt;the Web-SIG list in March 2016&lt;/a&gt;.
He argued for &quot;a clean break from WSGI to a new structure (and NOT calling it
&apos;WSGI 2&apos;)&quot;. Version 3.0, the current one, dates from March 2019. It retired an
older style in which the application was split into two callables. Today the
whole interface is one line of the specification:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;coroutine application(scope, receive, send)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The specification defines the three arguments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;scope&lt;/code&gt;: &quot;a dictionary that contains at least a &lt;code&gt;type&lt;/code&gt; key specifying the
protocol that is incoming&quot;. It describes the connection.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;receive&lt;/code&gt;: &quot;an awaitable callable that will yield a new event dictionary
when one is available&quot;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;send&lt;/code&gt;: &quot;an awaitable callable taking a single event dictionary as a
positional argument that will return once the send has been completed or
the connection has been closed&quot;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&quot;The application is called once per &apos;connection&apos;,&quot; the specification goes on.
&quot;With HTTP it is one request, whereas for a WebSocket it is a single WebSocket
connection.&quot; To see what a server actually passes, this application prints its
arguments and answers &lt;code&gt;204 No Content&lt;/code&gt;. The output below leaves out a
readiness probe and shortens the module paths:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# scope_dump.py
from pprint import pprint


async def app(scope, receive, send):
    if scope[&quot;type&quot;] != &quot;http&quot;:
        raise RuntimeError(f&quot;unsupported scope type: {scope[&apos;type&apos;]}&quot;)
    pprint(scope)
    print(receive, send, sep=&quot;\n&quot;)
    await send({&quot;type&quot;: &quot;http.response.start&quot;, &quot;status&quot;: 204, &quot;headers&quot;: []})
    await send({&quot;type&quot;: &quot;http.response.body&quot;})
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;uvicorn scope_dump:app --port 8044
curl -H &quot;accept: application/json&quot; &quot;http://127.0.0.1:8044/orders/42?fields=status&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;INFO:     Waiting for application startup.
INFO:     ASGI &apos;lifespan&apos; protocol appears unsupported.
INFO:     Application startup complete.
{&apos;asgi&apos;: {&apos;spec_version&apos;: &apos;2.3&apos;, &apos;version&apos;: &apos;3.0&apos;},
 &apos;client&apos;: (&apos;127.0.0.1&apos;, 55899),
 &apos;headers&apos;: [(b&apos;host&apos;, b&apos;127.0.0.1:8044&apos;),
             (b&apos;user-agent&apos;, b&apos;curl/8.7.1&apos;),
             (b&apos;accept&apos;, b&apos;application/json&apos;)],
 &apos;http_version&apos;: &apos;1.1&apos;,
 &apos;method&apos;: &apos;GET&apos;,
 &apos;path&apos;: &apos;/orders/42&apos;,
 &apos;query_string&apos;: b&apos;fields=status&apos;,
 &apos;raw_path&apos;: b&apos;/orders/42&apos;,
 &apos;root_path&apos;: &apos;&apos;,
 &apos;scheme&apos;: &apos;http&apos;,
 &apos;server&apos;: (&apos;127.0.0.1&apos;, 8044),
 &apos;state&apos;: {},
 &apos;type&apos;: &apos;http&apos;}
&amp;lt;bound method RequestResponseCycle.receive of &amp;lt;...h11_impl.RequestResponseCycle object at 0x10a5356d0&amp;gt;&amp;gt;
&amp;lt;bound method RequestResponseCycle.send of &amp;lt;...h11_impl.RequestResponseCycle object at 0x10a5356d0&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Four details in that output come back later:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The body is missing.&lt;/strong&gt; The scope holds the method, the path, the query and
the headers, but the specification leaves out &quot;the HTTP request body, as
this is streamed in via events&quot;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The types are exact.&lt;/strong&gt; The path is a &lt;code&gt;str&lt;/code&gt;, decoded. The query string and
the headers are &lt;code&gt;bytes&lt;/code&gt;, as the client sent them. Header names are
lowercase.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two versions are given.&lt;/strong&gt; &lt;code&gt;version&lt;/code&gt; is the ASGI version, 3.0.
&lt;code&gt;spec_version&lt;/code&gt; is the version of the HTTP message format that this code path
of Uvicorn implements, 2.3.
The &lt;a href=&quot;https://asgi.readthedocs.io/en/latest/specs/www.html&quot;&gt;HTTP and WebSocket format&lt;/a&gt;
is at 2.5. The difference matters below, in the section on disconnects.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;receive&lt;/code&gt; and &lt;code&gt;send&lt;/code&gt; belong to one request.&lt;/strong&gt; They are two methods of the
same &lt;code&gt;RequestResponseCycle&lt;/code&gt; object, which Uvicorn creates for each request.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first log lines are a result of the &lt;code&gt;raise&lt;/code&gt;. Before the first request,
Uvicorn called the application with a &lt;code&gt;lifespan&lt;/code&gt; scope. The application
raised, as the specification asks: &quot;Applications should actively reject any
protocol that they do not understand with an Exception&quot;. Uvicorn then went on
without lifespan events. The lifespan section below accepts that scope.&lt;/p&gt;
&lt;h2&gt;Every event is a dict, and its &lt;code&gt;type&lt;/code&gt; key names its shape&lt;/h2&gt;
&lt;p&gt;Each event is a plain dict. Its &lt;code&gt;type&lt;/code&gt; is &quot;namespaced as
&lt;code&gt;protocol.message_type&lt;/code&gt;&quot;, so &lt;code&gt;http.request&lt;/code&gt; belongs to the HTTP protocol and
&lt;code&gt;websocket.send&lt;/code&gt; to the WebSocket protocol. These are the core events of the
three protocols:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;From &lt;code&gt;receive()&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;To &lt;code&gt;send()&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTTP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http.request&lt;/code&gt;, &lt;code&gt;http.disconnect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http.response.start&lt;/code&gt;, &lt;code&gt;http.response.body&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket&lt;/td&gt;
&lt;td&gt;&lt;code&gt;websocket.connect&lt;/code&gt;, &lt;code&gt;websocket.receive&lt;/code&gt;, &lt;code&gt;websocket.disconnect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;websocket.accept&lt;/code&gt;, &lt;code&gt;websocket.send&lt;/code&gt;, &lt;code&gt;websocket.close&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifespan&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lifespan.startup&lt;/code&gt;, &lt;code&gt;lifespan.shutdown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lifespan.startup.complete&lt;/code&gt; or &lt;code&gt;.failed&lt;/code&gt;, &lt;code&gt;lifespan.shutdown.complete&lt;/code&gt; or &lt;code&gt;.failed&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Because every event passes through two callables, a wrapper around them sees
all the traffic. The specification calls such a wrapper middleware: &quot;code that
plays the role of both server and application&quot;. This one replaces &lt;code&gt;receive&lt;/code&gt;
and &lt;code&gt;send&lt;/code&gt; with versions that print each event before passing it on. &lt;code&gt;&amp;lt;-&lt;/code&gt;
marks an event that &lt;code&gt;receive()&lt;/code&gt; returned, &lt;code&gt;-&amp;gt;&lt;/code&gt; an event given to &lt;code&gt;send()&lt;/code&gt;, and
&lt;code&gt;**&lt;/code&gt; a new call of the application:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# tracing.py
import threading
import time

_start = _last = time.monotonic()


def clock():
    &quot;&quot;&quot;Milliseconds since the experiment began, after a second of silence.&quot;&quot;&quot;
    global _start, _last
    now = time.monotonic()
    if now - _last &amp;gt; 1:
        _start = now
    _last = now
    return (now - _start) * 1000


def log(arrow, text):
    print(f&quot;{clock():6.1f} ms {arrow} {text}&quot;, flush=True)


def show(event):
    fields = &quot; &quot;.join(f&quot;{k}={v!r}&quot; for k, v in event.items() if k != &quot;type&quot;)
    return f&quot;{event[&apos;type&apos;]} {fields}&quot;.rstrip()


def trace(app):
    async def traced(scope, receive, send):
        async def traced_receive():
            event = await receive()
            log(&quot;&amp;lt;-&quot;, show(event))
            return event

        async def traced_send(event):
            log(&quot;-&amp;gt;&quot;, show(event))
            await send(event)

        parts = [scope[&quot;type&quot;], &quot;scope&quot;, scope.get(&quot;method&quot;), scope.get(&quot;path&quot;)]
        if scope.get(&quot;client&quot;):
            parts.append(f&quot;port {scope[&apos;client&apos;][1]}&quot;)
        parts.append(f&quot;in {threading.current_thread().name}&quot;)
        log(&quot;**&quot;, &quot; &quot;.join(p for p in parts if p))
        await app(scope, traced_receive, traced_send)

    return traced
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The orders application routes on &lt;code&gt;scope[&quot;type&quot;]&lt;/code&gt; first and on the path second.
It is about a hundred lines, and the sections below show its parts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# orders.py (routing)
async def app(scope, receive, send):
    if scope[&quot;type&quot;] == &quot;lifespan&quot;:
        await lifespan(scope, receive, send)
    elif scope[&quot;type&quot;] == &quot;websocket&quot;:
        await order_feed(scope, receive, send)
    elif scope[&quot;type&quot;] != &quot;http&quot;:
        raise RuntimeError(f&quot;unsupported scope type: {scope[&apos;type&apos;]}&quot;)
    elif scope[&quot;path&quot;] == &quot;/orders/42/status&quot;:
        await update_status(scope, receive, send)
    elif scope[&quot;path&quot;] == &quot;/orders/42/next&quot;:
        await wait_for_update(scope, receive, send)
    elif scope[&quot;path&quot;].startswith(&quot;/legacy&quot;):
        await legacy(scope, receive, send)
    else:
        await respond(send, 404, {&quot;error&quot;: &quot;not found&quot;})


traced = trace(app)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;uvicorn orders:traced --port 8042 --no-access-log
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;A body can arrive in several events, so read until &lt;code&gt;more_body&lt;/code&gt; is false&lt;/h2&gt;
&lt;p&gt;The warehouse reports a status change with a &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;/orders/42/status&lt;/code&gt;. To
show how a body arrives, the test client sends this one in three pieces, 100 ms
apart. httpx sends a body that it reads from a generator with
&lt;code&gt;Transfer-Encoding: chunked&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def body():
    for piece in (b&apos;{&quot;order&quot;: 42, &apos;, b&apos;&quot;status&quot;: &apos;, b&apos;&quot;packed&quot;}&apos;):
        yield piece
        await asyncio.sleep(0.1)

await client.post(&quot;http://127.0.0.1:8042/orders/42/status&quot;, content=body())
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;   0.0 ms ** http scope POST /orders/42/status port 55985 in MainThread
   0.1 ms &amp;lt;- http.request body=b&apos;{&quot;order&quot;: 42, &apos; more_body=True
 106.7 ms &amp;lt;- http.request body=b&apos;&quot;status&quot;: &apos; more_body=True
 211.5 ms &amp;lt;- http.request body=b&apos;&quot;packed&quot;}&apos; more_body=True
 313.6 ms &amp;lt;- http.request body=b&apos;&apos; more_body=False
 314.0 ms -&amp;gt; http.response.start status=202 headers=[(b&apos;content-type&apos;, b&apos;application/json&apos;)]
 314.4 ms -&amp;gt; http.response.body body=b&apos;{&quot;listeners&quot;: 0}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Uvicorn removed the chunk framing and passed each piece on as an
&lt;code&gt;http.request&lt;/code&gt; event. The specification gives the rule for &lt;code&gt;more_body&lt;/code&gt;: &quot;If
&lt;code&gt;True&lt;/code&gt;, the consuming application should wait until it gets a chunk with this
set to &lt;code&gt;False&lt;/code&gt;.&quot; The last event is empty. It arrived when the client&apos;s
generator finished and the client sent the final zero-length chunk, about 100
ms after the last piece. Only then does the server know that the body is
complete. The handler reads every event and joins the pieces:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def read_body(receive):
    body = b&quot;&quot;
    while True:
        event = await receive()
        if event[&quot;type&quot;] == &quot;http.disconnect&quot;:
            raise ConnectionError(&quot;client left before the body ended&quot;)
        body += event.get(&quot;body&quot;, b&quot;&quot;)
        if not event.get(&quot;more_body&quot;, False):
            return body


async def respond(send, status, payload):
    await send({
        &quot;type&quot;: &quot;http.response.start&quot;,
        &quot;status&quot;: status,
        &quot;headers&quot;: [(b&quot;content-type&quot;, b&quot;application/json&quot;)],
    })
    await send({&quot;type&quot;: &quot;http.response.body&quot;, &quot;body&quot;: json.dumps(payload).encode()})


async def update_status(scope, receive, send):
    update = json.loads(await read_body(receive))
    publish(update)
    await respond(send, 202, {&quot;listeners&quot;: len(subscribers)})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The response has the same two-part shape. &lt;code&gt;http.response.start&lt;/code&gt; carries the
status and the headers. The specification says it &quot;needs to be followed by at
least one response content message&quot;, and the server may hold it back until
the first &lt;code&gt;http.response.body&lt;/code&gt;. An &lt;code&gt;http.response.body&lt;/code&gt; without &lt;code&gt;more_body&lt;/code&gt;
ends the response. &lt;code&gt;{&quot;listeners&quot;: 0}&lt;/code&gt; means that nobody was waiting for order
42 yet. The next section adds a listener.&lt;/p&gt;
&lt;p&gt;The client port also says something. The next two requests in the lab reused
port 55985, so httpx kept the
same TCP (Transmission Control Protocol) connection
open for them. Each request
still got its own call and its own scope. The HTTP format states that an HTTP
connection scope lasts one request, &quot;even if the underlying socket is still
open and serving multiple requests&quot;.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;receive()&lt;/code&gt; is also how the application learns the client has left&lt;/h2&gt;
&lt;p&gt;The long poll is &lt;code&gt;GET /orders/42/next&lt;/code&gt;. The handler subscribes to updates and
waits for whichever comes first: the next update, or the browser leaving. The
event for the second case is &lt;code&gt;http.disconnect&lt;/code&gt;, which the format describes as
&quot;mainly useful for long-polling, where you may want to trigger cleanup code if
the connection closes early&quot;. The application gets it from &lt;code&gt;receive()&lt;/code&gt;, so the
handler waits on two tasks at once with &lt;code&gt;asyncio.wait&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;My first version did exactly that and failed. The client got
&lt;code&gt;500 Internal Server Error&lt;/code&gt;, and the trace showed why:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   0.0 ms ** http scope GET /orders/42/next in MainThread
   0.1 ms &amp;lt;- http.request body=b&apos;&apos; more_body=False
ERROR:    ASGI callable returned without starting response.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first &lt;code&gt;receive()&lt;/code&gt; of an HTTP call returns the request body, even for a
&lt;code&gt;GET&lt;/code&gt; with no body: an empty &lt;code&gt;http.request&lt;/code&gt;. That event finished the
&quot;disconnect&quot; task at once. The handler took it as the browser leaving,
returned without a response, and Uvicorn answered 500 for it. The format says
when &lt;code&gt;http.disconnect&lt;/code&gt; comes: when &quot;receive is called after a response has
been sent or after the HTTP connection has been closed&quot;. Until then,
&lt;code&gt;receive()&lt;/code&gt; hands over the body. A handler that waits for a disconnect must
first read the body to the end. The fix is one line, &lt;code&gt;read_body&lt;/code&gt; before the
wait:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def wait_for_update(scope, receive, send):
    await read_body(receive)  # the first event is the request body
    queue = asyncio.Queue()
    subscribers.add(queue)
    update = asyncio.create_task(queue.get())
    disconnect = asyncio.create_task(receive())
    try:
        done, _ = await asyncio.wait(
            {update, disconnect}, return_when=asyncio.FIRST_COMPLETED
        )
        if update in done:
            await respond(send, 200, update.result())
    finally:
        subscribers.discard(queue)
        update.cancel()
        disconnect.cancel()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;subscribers&lt;/code&gt; is a set of &lt;code&gt;asyncio.Queue&lt;/code&gt; objects, and &lt;code&gt;publish&lt;/code&gt; puts each
update on every queue in it. With the fix, two runs show both endings. In the
first, the warehouse posts &quot;shipped&quot; 300 ms after the browser started waiting.
In the second, the browser gives up after 0.5 s:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   0.0 ms ** http scope GET /orders/42/next port 55985 in MainThread
   0.2 ms &amp;lt;- http.request body=b&apos;&apos; more_body=False
 303.1 ms ** http scope POST /orders/42/status port 55986 in MainThread
 303.2 ms &amp;lt;- http.request body=b&apos;{&quot;order&quot;:42,&quot;status&quot;:&quot;shipped&quot;}&apos; more_body=False
 303.3 ms -&amp;gt; http.response.start status=202 headers=[(b&apos;content-type&apos;, b&apos;application/json&apos;)]
 303.4 ms -&amp;gt; http.response.body body=b&apos;{&quot;listeners&quot;: 1}&apos;
 303.5 ms -&amp;gt; http.response.start status=200 headers=[(b&apos;content-type&apos;, b&apos;application/json&apos;)]
 303.7 ms -&amp;gt; http.response.body body=b&apos;{&quot;order&quot;: 42, &quot;status&quot;: &quot;shipped&quot;}&apos;

   0.0 ms ** http scope GET /orders/42/next port 55985 in MainThread
  16.4 ms &amp;lt;- http.request body=b&apos;&apos; more_body=False
 502.1 ms &amp;lt;- http.disconnect
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the first run, the POST&apos;s call and the waiting GET&apos;s call ran at the same
time, on one thread, and the update crossed from one to the other in less
than a millisecond. In the second, &lt;code&gt;http.disconnect&lt;/code&gt; came 502 ms after the
request, when the client&apos;s 0.5 s timeout closed the socket. The handler
cleaned up without sending anything.&lt;/p&gt;
&lt;p&gt;Sending after a disconnect is the other half of the question. Since version
2.4 of the format, &quot;If &lt;code&gt;send()&lt;/code&gt; is called on a closed connection the server
should raise a server-specific subclass of &lt;code&gt;OSError&lt;/code&gt;&quot;. This Uvicorn path
reports 2.3, and
its &lt;a href=&quot;https://github.com/Kludex/uvicorn/blob/main/uvicorn/protocols/http/h11_impl.py&quot;&gt;h11 code&lt;/a&gt;
returns from &lt;code&gt;send&lt;/code&gt; without doing anything once the client is gone. Its
WebSocket code reports 2.4 and raises &lt;code&gt;ClientDisconnected&lt;/code&gt;, a subclass of
&lt;code&gt;OSError&lt;/code&gt;. An application that must work on any server should handle both
behaviors.&lt;/p&gt;
&lt;h2&gt;A WebSocket needs a second coroutine to listen and push at once&lt;/h2&gt;
&lt;p&gt;The long poll answers once and then the browser must ask again. A WebSocket
keeps one connection for the whole session. Its scope &quot;lives as long as the
socket itself - if the application dies the socket should be closed, and
vice-versa&quot;. The first event is &lt;code&gt;websocket.connect&lt;/code&gt;, and the application must
answer it with &lt;code&gt;websocket.accept&lt;/code&gt; or &lt;code&gt;websocket.close&lt;/code&gt; before any
&lt;code&gt;websocket.receive&lt;/code&gt; can arrive. A close at this point makes the server reject
the handshake with HTTP 403.&lt;/p&gt;
&lt;p&gt;After that, the connection has two sources of events. The browser can send a
frame at any time, which arrives through &lt;code&gt;receive()&lt;/code&gt;, and the warehouse can
change a status at any time, which arrives through the queue.
One coroutine
cannot wait on both in turn, so the handler starts a second one. The
introduction describes this pattern: ASGI &quot;allows for a background coroutine
so the application can do other things (such as listening for events on an
external trigger, like a Redis queue)&quot;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def order_feed(scope, receive, send):
    await receive()  # websocket.connect
    await send({&quot;type&quot;: &quot;websocket.accept&quot;})
    queue = asyncio.Queue()
    subscribers.add(queue)

    async def push_updates():
        while True:
            update = await queue.get()
            await send({&quot;type&quot;: &quot;websocket.send&quot;, &quot;text&quot;: json.dumps(update)})

    pusher = asyncio.create_task(push_updates())
    try:
        while True:
            event = await receive()
            if event[&quot;type&quot;] == &quot;websocket.disconnect&quot;:
                break
            await send({&quot;type&quot;: &quot;websocket.send&quot;, &quot;text&quot;: f&quot;ack {event[&apos;text&apos;]}&quot;})
    finally:
        subscribers.discard(queue)
        pusher.cancel()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The test client opens the socket, sends &lt;code&gt;hello&lt;/code&gt;, has the warehouse post
&quot;delivered&quot;, reads both messages and closes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   0.0 ms ** websocket scope /orders/42/feed port 55990 in MainThread
   0.1 ms &amp;lt;- websocket.connect
   0.1 ms -&amp;gt; websocket.accept
   1.1 ms &amp;lt;- websocket.receive text=&apos;hello&apos;
   1.1 ms -&amp;gt; websocket.send text=&apos;ack hello&apos;
   2.0 ms ** http scope POST /orders/42/status port 55986 in MainThread
   2.0 ms &amp;lt;- http.request body=b&apos;{&quot;order&quot;:42,&quot;status&quot;:&quot;delivered&quot;}&apos; more_body=False
   2.1 ms -&amp;gt; http.response.start status=202 headers=[(b&apos;content-type&apos;, b&apos;application/json&apos;)]
   2.1 ms -&amp;gt; http.response.body body=b&apos;{&quot;listeners&quot;: 1}&apos;
   2.2 ms -&amp;gt; websocket.send text=&apos;{&quot;order&quot;: 42, &quot;status&quot;: &quot;delivered&quot;}&apos;
   2.9 ms &amp;lt;- websocket.disconnect code=1000 reason=&apos;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Figure: Two calls of the same application run at once. Call A holds the browser&apos;s WebSocket, and call B serves the warehouse&apos;s POST. B puts the update on A&apos;s queue, and A&apos;s push task sends it to the browser. No request from the browser is behind that message.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/asgi-event-by-event/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;websocket.send&lt;/code&gt; at 2.2 ms is the message that WSGI has no slot for. It
came from the application with no request behind it. Code 1000 is &quot;a normal
closure&quot; in &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc6455&quot;&gt;RFC 6455&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The lab takes one shortcut here. The set of queues lives in the memory of one
process, so it works only because Uvicorn ran one worker. With
&lt;code&gt;--workers 4&lt;/code&gt;, a POST served by one worker would never reach a WebSocket held
by another. The introduction&apos;s example, a Redis queue, is what fills that gap
in production: each worker subscribes to a channel outside the process.&lt;/p&gt;
&lt;h2&gt;Lifespan events give the application a start and an end of its own&lt;/h2&gt;
&lt;p&gt;Every scope so far belonged to one request or one socket. A connection pool,
or the subscriber to that Redis channel, belongs to the whole process.
The &lt;a href=&quot;https://asgi.readthedocs.io/en/latest/specs/lifespan.html&quot;&gt;lifespan protocol&lt;/a&gt;
gives the application a call for that: its messages &quot;allow for an application
to initialise and shutdown in the context of a running event loop&quot;. Uvicorn
makes this call at startup, with a &lt;code&gt;lifespan&lt;/code&gt; scope:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def lifespan(scope, receive, send):
    while True:
        event = await receive()
        if event[&quot;type&quot;] == &quot;lifespan.startup&quot;:
            await send({&quot;type&quot;: &quot;lifespan.startup.complete&quot;})
        elif event[&quot;type&quot;] == &quot;lifespan.shutdown&quot;:
            await send({&quot;type&quot;: &quot;lifespan.shutdown.complete&quot;})
            return
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;INFO:     Waiting for application startup.
  66.1 ms ** lifespan scope in MainThread
  66.3 ms &amp;lt;- lifespan.startup
  66.5 ms -&amp;gt; lifespan.startup.complete
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8042 (Press CTRL+C to quit)
...
INFO:     Shutting down
INFO:     Waiting for application shutdown.
   0.0 ms &amp;lt;- lifespan.shutdown
   0.1 ms -&amp;gt; lifespan.shutdown.complete
INFO:     Application shutdown complete.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server &quot;must wait for this message before it starts processing
connections&quot;, so a pool opened before &lt;code&gt;lifespan.startup.complete&lt;/code&gt; is ready
for the first request. Shutdown comes &quot;when the server has stopped accepting
connections and closed all active connections&quot;. Lifespan runs &quot;once per event
loop&quot;, so with four workers each process opens its own pool.&lt;/p&gt;
&lt;p&gt;To share an object with the requests, the application puts it in
&lt;code&gt;scope[&quot;state&quot;]&lt;/code&gt;, and the server passes &quot;a shallow copy of the namespace&quot; into
each request&apos;s scope.
That is the empty &lt;code&gt;&apos;state&apos;: {}&lt;/code&gt; in the scope dump. The &lt;code&gt;scope_dump.py&lt;/code&gt; example
shows the other path: when an application rejects the lifespan scope, the
server &quot;must continue but not send any lifespan events&quot;.&lt;/p&gt;
&lt;h2&gt;WSGI code runs on a thread beside the event loop, one request at a time&lt;/h2&gt;
&lt;p&gt;The project also has reports written for WSGI years ago. The introduction
promises they can stay: ASGI &quot;is also designed to be a superset of WSGI&quot;, and
&quot;a threadpool can be used to run the synchronous WSGI applications away from
the async event loop&quot;. asgiref, the reference library of the ASGI project,
provides the wrapper &lt;code&gt;WsgiToAsgi&lt;/code&gt;, which the router above mounts under
&lt;code&gt;/legacy&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# legacy.py
import threading


def legacy_wsgi_app(environ, start_response):
    thread = threading.current_thread().name
    start_response(&quot;200 OK&quot;, [(&quot;content-type&quot;, &quot;text/plain&quot;)])
    line = f&quot;WSGI {environ[&apos;REQUEST_METHOD&apos;]} {environ[&apos;PATH_INFO&apos;]} in {thread}\n&quot;
    return [line.encode()]


# orders.py
legacy = WsgiToAsgi(legacy_wsgi_app)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;   0.0 ms ** http scope GET /legacy/orders port 55986 in MainThread
   0.3 ms &amp;lt;- http.request body=b&apos;&apos; more_body=False
   1.8 ms -&amp;gt; http.response.start status=200 headers=[(b&apos;content-type&apos;, b&apos;text/plain&apos;)]
   2.2 ms -&amp;gt; http.response.body body=b&apos;WSGI GET /legacy/orders in ThreadPoolExecutor-0_0\n&apos; more_body=True
   2.4 ms -&amp;gt; http.response.body
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The ASGI call ran on &lt;code&gt;MainThread&lt;/code&gt;, where the event loop runs. The WSGI
function ran on &lt;code&gt;ThreadPoolExecutor-0_0&lt;/code&gt;, so a blocking call inside it does
not stop the loop. The wrapper read the whole body first, then turned the
returned list into one &lt;code&gt;http.response.body&lt;/code&gt; with &lt;code&gt;more_body=True&lt;/code&gt; and a final
empty one.&lt;/p&gt;
&lt;p&gt;Then a report that takes one second, with &lt;code&gt;time.sleep(1)&lt;/code&gt; standing in for a
blocking database query, served alone through &lt;code&gt;WsgiToAsgi&lt;/code&gt;. Three browsers ask
for it at the same moment:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# slow_legacy.py
import threading
import time

from asgiref.wsgi import WsgiToAsgi


def slow_report(environ, start_response):
    started = time.monotonic()
    time.sleep(1)  # a blocking database query
    thread = threading.current_thread().name
    start_response(&quot;200 OK&quot;, [(&quot;content-type&quot;, &quot;text/plain&quot;)])
    return [f&quot;{thread} started at {started:.2f}\n&quot;.encode()]


app = WsgiToAsgi(slow_report)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;request 1: done at 1.03 s, ThreadPoolExecutor-0_0 started at 92103.07
request 2: done at 2.04 s, ThreadPoolExecutor-0_0 started at 92104.08
request 3: done at 3.05 s, ThreadPoolExecutor-0_0 started at 92105.08
all three: 3.05 s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The requests ran one after the other, on the same thread. The cause is in
asgiref&apos;s source. &lt;a href=&quot;https://github.com/django/asgiref/blob/main/asgiref/wsgi.py&quot;&gt;&lt;code&gt;wsgi.py&lt;/code&gt;&lt;/a&gt;
runs the WSGI application through &lt;code&gt;@sync_to_async&lt;/code&gt; with its defaults,
and &lt;a href=&quot;https://github.com/django/asgiref/blob/main/asgiref/sync.py&quot;&gt;&lt;code&gt;sync.py&lt;/code&gt;&lt;/a&gt;
sets &lt;code&gt;thread_sensitive=True&lt;/code&gt; by default. In an async program, its docstring
explains, thread-sensitive code runs in &quot;a dedicated single sub-thread that
all sync code runs in, one after the other&quot;. The default exists for code that
is not thread-safe, and the docstring gives SQLite connections as an example.
The &quot;threadpool&quot; of the introduction is real, but a pool of one thread.&lt;/p&gt;
&lt;p&gt;Django&apos;s own ASGI handler avoids this.
Its &lt;a href=&quot;https://github.com/django/django/blob/main/django/core/handlers/asgi.py&quot;&gt;source&lt;/a&gt;
opens an &lt;code&gt;async with ThreadSensitiveContext():&lt;/code&gt; block around each request, so
each request gets its own sync thread. The same three lines of wrapping work
for the report:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# slow_legacy_ctx.py
from asgiref.sync import ThreadSensitiveContext

from slow_legacy import app as wrapped


async def app(scope, receive, send):
    async with ThreadSensitiveContext():
        await wrapped(scope, receive, send)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;request 2: done at 1.06 s, ThreadPoolExecutor-3_0 started at 92130.82
request 3: done at 1.06 s, ThreadPoolExecutor-4_0 started at 92130.82
request 1: done at 1.06 s, ThreadPoolExecutor-2_0 started at 92130.82
all three: 1.06 s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Figure: Three requests reach a WSGI report that blocks for one second. Under WsgiToAsgi&apos;s defaults they share one thread and finish at 3.05 s. With one ThreadSensitiveContext per request, each gets its own thread, and all three finish at 1.06 s.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/asgi-event-by-event/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;The context only gives each request a thread. It does not make the WSGI code
safe to run in parallel. A report that shares a database connection between
threads, or keeps state in globals, was safe only because of the single
thread. If the report already ran on a WSGI server with several threads, it
is safe here too. If it has only ever run on one thread, keep the default, or
keep serving it with a WSGI server.&lt;/p&gt;
&lt;h2&gt;Async was never the missing piece; a way in for later events was&lt;/h2&gt;
&lt;p&gt;Back to the title. An &lt;code&gt;async&lt;/code&gt; WSGI would still receive everything in one
&lt;code&gt;environ&lt;/code&gt; at the start and give everything back in one return value. The order
page needed three things that shape has no slot for, and the traces showed
each one:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Events after the call starts.&lt;/strong&gt; The pieces of a chunked body, the frames
of a WebSocket, and the &lt;code&gt;http.disconnect&lt;/code&gt; of a browser that gave up all
arrived through &lt;code&gt;receive()&lt;/code&gt; while the call was running.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Messages with no request behind them.&lt;/strong&gt; The &lt;code&gt;websocket.send&lt;/code&gt; at 2.2 ms
came from a queue, not from the browser.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A start and an end on the event loop.&lt;/strong&gt; Lifespan let the application set
up on the running loop before the server accepted a connection, and close
down after the last one closed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;async&lt;/code&gt; changes how a server waits. &lt;code&gt;receive&lt;/code&gt; and &lt;code&gt;send&lt;/code&gt; change what an
application can hear and say while it waits. That is the reason ASGI could not
be a small update to WSGI.&lt;/p&gt;
&lt;p&gt;The takeaway fits in one small middleware. When an ASGI application, or the
framework above it, behaves in a way you cannot explain, put a tracing
middleware like the one above in front of it and read the events. Both
surprises in this post showed up in a trace before they had an explanation:
the empty &lt;code&gt;http.request&lt;/code&gt; that ended a long poll, and the single
&lt;code&gt;ThreadPoolExecutor-0_0&lt;/code&gt; that ran every WSGI request.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ASGI specification:&lt;/strong&gt; the &lt;a href=&quot;https://asgi.readthedocs.io/en/latest/introduction.html&quot;&gt;introduction&lt;/a&gt;; the &lt;a href=&quot;https://asgi.readthedocs.io/en/latest/specs/main.html&quot;&gt;ASGI specification, version 3.0&lt;/a&gt;;
the &lt;a href=&quot;https://asgi.readthedocs.io/en/latest/specs/www.html&quot;&gt;HTTP and WebSocket message format, version 2.5&lt;/a&gt;;
the &lt;a href=&quot;https://asgi.readthedocs.io/en/latest/specs/lifespan.html&quot;&gt;lifespan protocol, version 2.0&lt;/a&gt;.
The source of all four is in the &lt;a href=&quot;https://github.com/django/asgiref&quot;&gt;asgiref repository&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WSGI and WebSocket:&lt;/strong&gt; &lt;a href=&quot;https://peps.python.org/pep-3333/&quot;&gt;PEP 3333&lt;/a&gt;
(Eby, 2010); &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc6455&quot;&gt;RFC 6455&lt;/a&gt;
(Fette and Melnikov, 2011).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;History:&lt;/strong&gt; Andrew Godwin, &lt;a href=&quot;https://mail.python.org/pipermail/web-sig/2016-March/005437.html&quot;&gt;&quot;Inviting feedback on my proposed &apos;ASGI&apos; spec&quot;&lt;/a&gt;,
Web-SIG, 9 March 2016; the version history at the end of the ASGI
specification.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Source code read for this post:&lt;/strong&gt; asgiref &lt;a href=&quot;https://github.com/django/asgiref/blob/main/asgiref/wsgi.py&quot;&gt;&lt;code&gt;wsgi.py&lt;/code&gt;&lt;/a&gt;
and &lt;a href=&quot;https://github.com/django/asgiref/blob/main/asgiref/sync.py&quot;&gt;&lt;code&gt;sync.py&lt;/code&gt;&lt;/a&gt;
(version 3.13.0); Uvicorn &lt;a href=&quot;https://github.com/Kludex/uvicorn/blob/main/uvicorn/protocols/http/h11_impl.py&quot;&gt;&lt;code&gt;h11_impl.py&lt;/code&gt;&lt;/a&gt;
and &lt;a href=&quot;https://github.com/Kludex/uvicorn/blob/main/uvicorn/protocols/websockets/websockets_impl.py&quot;&gt;&lt;code&gt;websockets_impl.py&lt;/code&gt;&lt;/a&gt;
(version 0.54.0); Django&apos;s &lt;a href=&quot;https://github.com/django/django/blob/main/django/core/handlers/asgi.py&quot;&gt;ASGI handler&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Servers and frameworks:&lt;/strong&gt; the ASGI project&apos;s &lt;a href=&quot;https://asgi.readthedocs.io/en/latest/implementations.html&quot;&gt;list of implementations&lt;/a&gt;,
which includes the servers Uvicorn, Hypercorn, Daphne and Granian, and the
frameworks Django, Starlette and FastAPI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On this blog:&lt;/strong&gt; the &lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/wsgi-vs-asgi-protocols/&quot;&gt;WSGI vs ASGI post&lt;/a&gt;,
for hello-world applications and when to choose each interface, and
the &lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/concurrency-and-multithreading/&quot;&gt;concurrency post&lt;/a&gt;,
for threads and event loops.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>python</category><category>web</category></item><item><title>ROI logiciel : mesurer les gains sans surestimer les économies</title><link>https://yeison-liscano.github.io/portfolio/blog/roi-logiciel/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/roi-logiciel/</guid><description>Mesurer le retour sur investissement d’un produit ou service logiciel : coûts, marge, temps libéré et attribution des gains. Un cas chiffré et un simulateur montrent les hypothèses qui changent la décision.</description><pubDate>Mon, 02 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Mots-clés : retour sur investissement, logiciel, coûts, marge, productivité,
mesure des gains&lt;/p&gt;
&lt;p&gt;Une équipe envisage un logiciel pour automatiser son support client. Dans ce
cas fictif, la première année coûte 30 000 €. Le projet promet de libérer du
temps et de mieux servir les clients. Mais quelles améliorations deviennent
des gains financiers, et lesquelles restent de la capacité disponible ? Nous
allons mesurer le retour sur investissement, ou ROI (Return on Investment, retour sur investissement), en comptant les coûts et les gains
attribuables au logiciel. Nous comparerons aussi le point de vue du client
avec celui du fournisseur.&lt;/p&gt;
&lt;h2&gt;Le même logiciel a un ROI différent pour le client et le fournisseur&lt;/h2&gt;
&lt;p&gt;L’équipe achète un outil, mais son éditeur vend un produit. Leurs calculs
portent sur des investissements différents. Un prestataire qui déploie cet
outil pour ses clients doit encore choisir un autre périmètre.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Acteur&lt;/th&gt;
&lt;th&gt;Décision évaluée&lt;/th&gt;
&lt;th&gt;Gains à examiner&lt;/th&gt;
&lt;th&gt;Coûts à examiner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;Acheter ou développer un outil&lt;/td&gt;
&lt;td&gt;Dépenses évitées, marge supplémentaire&lt;/td&gt;
&lt;td&gt;Achat, intégration, ressources internes, exploitation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Éditeur&lt;/td&gt;
&lt;td&gt;Investir dans un produit ou une fonctionnalité&lt;/td&gt;
&lt;td&gt;Revenus supplémentaires attribuables au projet&lt;/td&gt;
&lt;td&gt;Développement, acquisition, hébergement, support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prestataire&lt;/td&gt;
&lt;td&gt;Créer une offre ou améliorer sa production&lt;/td&gt;
&lt;td&gt;Prestations supplémentaires, coûts évités&lt;/td&gt;
&lt;td&gt;Outillage, formation, réalisation, maintenance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;L’abonnement payé par le client est un coût pour lui et un revenu pour
l’éditeur. Une marge élevée par abonnement ne prouve pas que le développement
du produit a déjà été rentabilisé. Il faut aussi couvrir l’investissement et
les autres coûts du périmètre choisi.&lt;/p&gt;
&lt;p&gt;Pour l’article, nous suivrons le &lt;strong&gt;ROI du client sur les douze premiers mois&lt;/strong&gt;.
Le fournisseur pourra reprendre la méthode avec ses propres flux. Cette
distinction de points de vue est notre cadrage du cas, pas une norme comptable.&lt;/p&gt;
&lt;h2&gt;La formule exige une période et un scénario de référence&lt;/h2&gt;
&lt;p&gt;Nous utiliserons un ROI simple de projet, sans actualisation :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ROI sur la période = (B − C) / C × 100

B = gains financiers supplémentaires attribuables au changement
C = coûts supplémentaires nécessaires pour obtenir ces gains
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Les coûts doivent être positifs pour calculer ce ratio. Un gain de 45 000 €
pour un coût de 30 000 € laisse 15 000 € nets : le ROI est de 50 %.
Les 45 000 € sont les gains avant déduction des coûts du projet. Les appeler
« bénéfice net » puis retirer encore ces coûts les compterait deux fois.&lt;/p&gt;
&lt;p&gt;Le manuel &lt;a href=&quot;https://openstax.org/books/contemporary-mathematics/pages/6-6-methods-of-savings&quot;&gt;OpenStax&lt;/a&gt; présente ce principe du rendement net
rapporté à l’investissement. Notre formule précise son application au projet.
Un ROI de 50 % sur douze mois ne signifie pas 50 % par an pour toujours.&lt;/p&gt;
&lt;p&gt;L’équipe doit ensuite décrire le scénario de référence.
Ici, elle conserve son outil actuel et sous-traite une partie du support.
Le coût et les résultats de cette option servent de comparaison.
Le &lt;a href=&quot;https://www.gov.uk/government/publications/the-green-book--2/the-green-book-2026&quot;&gt;Green Book&lt;/a&gt; formalise cette comparaison avec la poursuite
de l’existant. Ses règles d’évaluation publique ne fixent pas notre seuil de
rentabilité d’entreprise.&lt;/p&gt;
&lt;p&gt;Avant de remplir la formule, écrivons donc : « Comparer l’achat à la poursuite
de l’existant, du lancement à la fin du douzième mois, du point de vue du
client. » Cette phrase empêche de comparer des gains sur trois ans à des coûts
sur un seul an.&lt;/p&gt;
&lt;h2&gt;Le coût dépasse l’achat ou le développement initial&lt;/h2&gt;
&lt;p&gt;La licence ne représente qu’une partie du changement. Pour notre équipe, le
budget fictif de la première année est le suivant :&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Poste supplémentaire par rapport à l’existant&lt;/th&gt;
&lt;th&gt;Montant&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Intégration&lt;/td&gt;
&lt;td&gt;6 000 €&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migration des données&lt;/td&gt;
&lt;td&gt;2 000 €&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Formation et temps de démarrage&lt;/td&gt;
&lt;td&gt;2 000 €&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Abonnement annuel&lt;/td&gt;
&lt;td&gt;12 000 €&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usage et hébergement supplémentaires&lt;/td&gt;
&lt;td&gt;4 000 €&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance et support supplémentaires&lt;/td&gt;
&lt;td&gt;4 000 €&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total sur douze mois&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;30 000 €&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Les trois premiers postes totalisent 10 000 € au démarrage. Les trois suivants
représentent 20 000 € sur l’année. Nous supposons que le tarif d’abonnement
n’inclut pas les postes facturés séparément. Dans un contrat réel, il faut
vérifier cette frontière.&lt;/p&gt;
&lt;p&gt;Le coût de formation inclut les ressources internes mobilisées. Même avec des
salaires inchangés, leur temps a un coût d’opportunité. Ce budget économique
ne décrit donc pas uniquement des sorties de trésorerie supplémentaires.
L’article sur les &lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/economia-b%C3%A1sica/&quot;&gt;concepts économiques&lt;/a&gt;, en espagnol, explique cette distinction et celle
entre résultat et trésorerie.&lt;/p&gt;
&lt;p&gt;Pour un développement interne, ajouter le travail de conception, de test, de
sécurité et d’exploitation nécessaire au changement. Pour une prestation,
inclure le pilotage et les reprises prévues au contrat. Prévoir aussi les
coûts de sortie si l’horizon couvre le remplacement de l’outil.&lt;/p&gt;
&lt;p&gt;Les dépenses déjà engagées et irréversibles ne changent pas entre les options.
Les factures futures évitables, elles, comptent. Déclarer les règles
d’allocation des coûts partagés rend le calcul vérifiable. Le cadre
de la &lt;a href=&quot;https://www.finops.org/framework/capabilities/unit-economics/&quot;&gt;FinOps Foundation&lt;/a&gt; aide à relier ces dépenses aux unités métier,
comme le coût par dossier résolu.&lt;/p&gt;
&lt;h2&gt;Les gains reposent sur la marge et les économies réellement obtenues&lt;/h2&gt;
&lt;p&gt;Le pilote de notre cas fictif suggère 600 heures de support libérées par an.
Multiplier ces heures par un taux salarial donne une valorisation de capacité.
Cela ne prouve pas que l’entreprise dépensera moins.&lt;/p&gt;
&lt;p&gt;Le &lt;a href=&quot;https://www.gov.uk/government/publications/the-government-efficiency-framework/the-government-efficiency-framework--2&quot;&gt;Government Efficiency Framework&lt;/a&gt; distingue précisément ces deux
résultats. L’équipe peut traiter davantage de demandes avec les mêmes salaires,
sans réduction de sa masse salariale. Ce gain peut être utile ; il faut
indiquer comment il sera utilisé.&lt;/p&gt;
&lt;p&gt;L’équipe répartit les 600 heures entre deux usages distincts :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;200 heures remplacent de la sous-traitance évitable&lt;/strong&gt;, facturée 75 €
l’heure. L’économie prévue est de 15 000 €. Elle exige une baisse réelle
des achats, sans minimum contractuel qui maintiendrait la facture.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;400 heures permettent des prestations supplémentaires de support payant&lt;/strong&gt;.
Chaque heure vendue apporte 75 € de marge après les coûts variables de
réalisation. La marge supplémentaire prévue est de 30 000 €.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Les 75 € de marge du second poste sont déjà nets des coûts variables concernés.
Ils excluent les coûts du logiciel, comptés dans les 30 000 € du projet.
Nous supposons une demande suffisante pour vendre ces prestations et une
qualité de service maintenue. Sans ventes supplémentaires, cette marge reste
une prévision.&lt;/p&gt;
&lt;p&gt;Le schéma montre où les heures deviennent des gains financiers. La somme
200 + 400 épuise les 600 heures disponibles : aucune heure n’est réutilisée
dans les deux branches.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: Les 45 000 € de gains supposent deux usages distincts des 600 heures libérées et la réalisation de chacun.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/roi-logiciel/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Une heure libérée doit avoir un usage avant d’avoir un gain financier.&lt;/strong&gt;
Ajouter une « économie salariale » pour ces mêmes 600 heures compterait à
nouveau leur valeur, alors que les salaires restent inchangés.
Une meilleure satisfaction client peut aussi compter dans la décision.
Elle restera un résultat séparé tant que sa traduction financière n’est pas
étayée.&lt;/p&gt;
&lt;h2&gt;Une amélioration doit pouvoir être attribuée au logiciel&lt;/h2&gt;
&lt;p&gt;Le tableau financier ne prouve pas que le logiciel produit les gains annoncés.
Une baisse des tickets peut venir d’un changement de clientèle. Une hausse des
prestations peut venir d’une campagne commerciale.&lt;/p&gt;
&lt;p&gt;Avant le déploiement, l’équipe définit une période d’observation et des mesures :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Temps de traitement par dossier, à complexité comparable.&lt;/li&gt;
&lt;li&gt;Dépenses de sous-traitance réellement évitées.&lt;/li&gt;
&lt;li&gt;Volume supplémentaire vendu et marge correspondante.&lt;/li&gt;
&lt;li&gt;Qualité : réouvertures, erreurs et satisfaction des clients.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Quand c’est possible, répartir aléatoirement des groupes comparables entre
l’ancien et le nouveau fonctionnement. Un test A/B aide à isoler
l’effet du changement. Les chercheurs de &lt;a href=&quot;https://www.microsoft.com/en-us/research/publication/online-experimentation-at-microsoft/&quot;&gt;Microsoft Research&lt;/a&gt; décrivent
ce principe pour les produits numériques. Il faut aussi vérifier que les
groupes ne se contaminent pas et que l’échantillon permet une conclusion utile.&lt;/p&gt;
&lt;p&gt;Certains déploiements ne permettent pas cette expérience. Une comparaison avec
une équipe témoin ou un déploiement progressif peut aider, avec ses limites.
Un simple avant/après reste vulnérable aux changements de volume, de saison
ou d’organisation. Le billet sur le &lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/metodo-cientifico-en-software-y-seguridad/&quot;&gt;raisonnement expérimental en logiciel&lt;/a&gt;,
en espagnol, explique pourquoi documenter ces autres explications.&lt;/p&gt;
&lt;p&gt;Le pilote mesure un effet opérationnel dans certaines conditions. Le ROI
prévisionnel extrapole cet effet sur douze mois. Le ROI réalisé utilise les
coûts et les gains observés sur la période écoulée. Il faut conserver ces
trois niveaux séparés : une extrapolation ne devient pas une mesure parce
qu’elle apparaît dans un tableau.&lt;/p&gt;
&lt;h2&gt;Le cas chiffré révèle les hypothèses qui font basculer le ROI&lt;/h2&gt;
&lt;p&gt;Sous les hypothèses centrales, les gains atteignent 45 000 € et les coûts
30 000 €. Le résultat économique net est de 15 000 €, soit un ROI simple
de 50 % sur douze mois. Il ne s’agit pas d’une mesure de rentabilité observée
dans une entreprise.&lt;/p&gt;
&lt;p&gt;Voici le calcul exécuté en Python. Les scénarios font varier les gains retenus
et les coûts. Ils illustrent des hypothèses, sans leur attribuer de probabilité.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;planned_benefits = 200 * 75 + 400 * 75
scenarios = [
    (&quot;Prudent&quot;, 0.40, 36_000),
    (&quot;Central&quot;, 1.00, 30_000),
    (&quot;Favorable&quot;, 1.20, 30_000),
]

for name, realization, costs in scenarios:
    benefits = planned_benefits * realization
    net = benefits - costs
    roi = net / costs * 100
    print(f&quot;{name}: gains={benefits:.0f} EUR, &quot;
          f&quot;coûts={costs:.0f} EUR, net={net:.0f} EUR, ROI={roi:.0f} %&quot;)

break_even = 30_000 / planned_benefits * 100
print(f&quot;Part des gains prévus nécessaire pour couvrir les coûts: &quot;
      f&quot;{break_even:.1f} %&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sortie obtenue :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Prudent: gains=18000 EUR, coûts=36000 EUR, net=-18000 EUR, ROI=-50 %
Central: gains=45000 EUR, coûts=30000 EUR, net=15000 EUR, ROI=50 %
Favorable: gains=54000 EUR, coûts=30000 EUR, net=24000 EUR, ROI=80 %
Part des gains prévus nécessaire pour couvrir les coûts: 66.7 %
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;À coût constant de 30 000 €, il faut réaliser environ deux tiers des gains
prévus pour couvrir les coûts. Si seules les économies de sous-traitance se
réalisent, les gains tombent à 15 000 € et le ROI à −50 %.
La demande de prestations supplémentaires devient donc une hypothèse à tester
avant de généraliser l’outil.&lt;/p&gt;
&lt;p&gt;Le simulateur permet d’explorer cette fragilité. Son taux fait varier les deux
postes de gains ensemble ; une analyse réelle doit aussi les tester séparément.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: Avec 30 000 € de coûts, réaliser 100 % des 45 000 € prévus donne un ROI de 50 % ; en réaliser 60 % donne −10 %.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/roi-logiciel/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;La décision dépend aussi du délai, du risque et des résultats observés&lt;/h2&gt;
&lt;p&gt;Le ROI décrit un rendement relatif. Il ne dit pas quand l’investissement sera
récupéré. Le délai de récupération répond à cette autre question.&lt;/p&gt;
&lt;p&gt;Dans une variante entièrement exprimée en trésorerie, supposons que les
10 000 € de démarrage soient décaissés au lancement. Les 20 000 € récurrents et
les 45 000 € de gains sont payés et encaissés régulièrement pendant douze mois,
sans délai ni variation. Le flux net mensuel est alors
&lt;code&gt;(45 000 − 20 000) / 12&lt;/code&gt;, soit
environ 2 083 €. Le délai simple est &lt;code&gt;10 000 / 2 083&lt;/code&gt;, environ 4,8 mois.&lt;/p&gt;
&lt;p&gt;Cette estimation exige des flux de trésorerie réels. Elle change si
l’abonnement est payé d’avance, si les ventes démarrent tard ou si une partie
des coûts valorise du temps interne sans décaissement supplémentaire.
Diviser les 30 000 € de coûts annuels par les gains mensuels mélangerait
investissement initial et dépenses déjà réparties dans le temps.&lt;/p&gt;
&lt;p&gt;Le délai simple ignore les gains après récupération et la valeur temporelle
de l’argent, comme le rappelle &lt;a href=&quot;https://openstax.org/books/principles-finance-2e/pages/16-1-payback-period-method&quot;&gt;OpenStax&lt;/a&gt;.
Sur plusieurs années, examiner aussi la VAN (Valeur actuelle nette : somme des flux futurs ramenés à aujourd’hui, investissement compris.).
Le billet sur la &lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/time-value-of-money/&quot;&gt;valeur temporelle de l’argent&lt;/a&gt;, en anglais,
en développe le calcul. Un ROI élevé sur un petit projet ne garantit pas
davantage de valeur totale qu’un ROI plus faible sur un projet plus grand.&lt;/p&gt;
&lt;p&gt;Notre équipe dispose maintenant d’une décision à instruire : valider la demande
de prestations, vérifier que la sous-traitance est évitable et mesurer les
coûts du pilote. Le scénario central seul ne suffit pas. Aucun pourcentage
universel ne remplace ces vérifications.&lt;/p&gt;
&lt;p&gt;Avant votre prochain achat ou développement, remplissez cette fiche :&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Réponse à écrire avant de lancer le projet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pour qui ?&lt;/td&gt;
&lt;td&gt;Client, éditeur ou prestataire&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Par rapport à quoi ?&lt;/td&gt;
&lt;td&gt;Option de référence et résultats attendus sans changement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sur quelle période ?&lt;/td&gt;
&lt;td&gt;Dates, montée en charge et durée retenue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;À quel coût ?&lt;/td&gt;
&lt;td&gt;Postes supplémentaires, ressources internes et règles d’allocation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pour quels gains ?&lt;/td&gt;
&lt;td&gt;Dépenses évitées, marge supplémentaire et capacité séparée&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avec quelles preuves ?&lt;/td&gt;
&lt;td&gt;Mesures, comparaison, responsable de chaque gain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quand réviser ?&lt;/td&gt;
&lt;td&gt;Date du bilan, hypothèses à vérifier et critères de poursuite&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Commencez par le gain le moins certain : écrivez la preuve qui vous ferait
le compter, puis cherchez cette preuve avant de financer le déploiement.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Références&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;OpenStax, Donna Kirk, 2023. Voir &lt;a href=&quot;https://openstax.org/books/contemporary-mathematics/pages/6-6-methods-of-savings&quot;&gt;Contemporary Mathematics, Methods of Savings&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;HM Treasury, édition 2026. Voir &lt;a href=&quot;https://www.gov.uk/government/publications/the-green-book--2/the-green-book-2026&quot;&gt;The Green Book&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Ronny Kohavi et ses coauteurs, Microsoft Research, 2009. Voir &lt;a href=&quot;https://www.microsoft.com/en-us/research/publication/online-experimentation-at-microsoft/&quot;&gt;Online Experimentation at Microsoft&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Government Finance Function et HM Treasury, version 2025. Voir &lt;a href=&quot;https://www.gov.uk/government/publications/the-government-efficiency-framework/the-government-efficiency-framework--2&quot;&gt;The Government Efficiency Framework&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;FinOps Foundation, consulté le 2 novembre 2026. Voir &lt;a href=&quot;https://www.finops.org/framework/capabilities/unit-economics/&quot;&gt;Unit Economics&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;OpenStax, Julie Dahlquist et Rainford Knight, 2026. Voir &lt;a href=&quot;https://openstax.org/books/principles-finance-2e/pages/16-1-payback-period-method&quot;&gt;Principles of Finance, Payback Period Method&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>finance</category><category>programming</category></item><item><title>Perfect Secrecy and Kerckhoffs&apos; Principle: How a Provably Unbreakable Cipher Was Broken</title><link>https://yeison-liscano.github.io/portfolio/blog/perfect-secrecy-kerckhoffs/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/perfect-secrecy-kerckhoffs/</guid><description>Shannon&apos;s definition of perfect secrecy, the one-time pad and the theorems that set its price, with every hypothesis stated. Then Kerckhoffs&apos; principle, and what Venona shows about breaking a cipher that cannot be broken.</description><pubDate>Mon, 26 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Keywords: perfect secrecy, one-time pad, Shannon, Kerckhoffs&apos; principle, key
reuse, Venona&lt;/p&gt;
&lt;p&gt;How do you break a cipher that is mathematically unbreakable? In February 1946,
Meredith Gardner, an analyst on the US Army&apos;s Venona project, read his first
Soviet message protected by a one-time pad. Three years later, Claude Shannon
published the proof that a one-time pad, used correctly, cannot be broken, even
with unlimited computing power. The question is not only historical: AES (Advanced Encryption Standard)
in counter mode and ChaCha20 fail the same way when a key and nonce are reused.
This post proves what perfect secrecy requires, states every hypothesis behind
the proofs, and shows which one Venona broke.&lt;/p&gt;
&lt;p&gt;The path goes from Shannon&apos;s definition to the one-time pad and the theorems
that set its price. Then it shows why Kerckhoffs&apos; principle, written in 1883, is
already part of that definition. The Python examples run in your browser.&lt;/p&gt;
&lt;h2&gt;Venona read Soviet traffic that was protected by one-time pads&lt;/h2&gt;
&lt;p&gt;Soviet services encrypted in two steps. A codebook turned words into groups of
digits. Then a clerk added key digits, taken from a page of a one-time pad, to
those groups. Each page was meant to be used for one message only.&lt;/p&gt;
&lt;p&gt;In 1942, under pressure from the German advance on Moscow, the organization that
produced the KGB (Komitet Gosudarstvennoy Bezopasnosti: the Soviet Committee for State Security)&apos;s pads printed about 35,000 pages of duplicate key. The copies
were sent to users far apart from each other, which made the reuse harder to
spot.&lt;/p&gt;
&lt;p&gt;On 1 February 1943, a small US Army team led by Gene Grabeel started working on
the traffic. Gardner read the first message three years later. The project ran
until 1 October 1980. Of the many thousands of messages sent between 1940 and
1948, about 3,000 were at least partly read. They exposed agents such as Klaus
Fuchs, Julius Rosenberg and members of the Cambridge Five. The translations
became public in 1995.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: Theory and practice of the one-time pad. A Soviet proof that the pad is secure predates the duplicate pages by a year, and Shannon&apos;s classified proof predates the first Venona break by a year. The proofs were never wrong.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/perfect-secrecy-kerckhoffs/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;The mathematics was never broken. Each page, taken alone, was as secure as
Shannon&apos;s proof says. So why is a duplicated page fatal? To answer that, we need
the exact definition, including the hypotheses that usually stay implicit.&lt;/p&gt;
&lt;h2&gt;Perfect secrecy means the ciphertext does not change the attacker&apos;s odds&lt;/h2&gt;
&lt;p&gt;An encryption scheme has three algorithms. &lt;code&gt;Gen&lt;/code&gt; picks a key &lt;code&gt;k&lt;/code&gt; from a key
space &lt;code&gt;K&lt;/code&gt;. &lt;code&gt;Enc&lt;/code&gt; turns a message &lt;code&gt;m&lt;/code&gt; from the message space &lt;code&gt;M&lt;/code&gt; into a
ciphertext &lt;code&gt;c&lt;/code&gt; from the ciphertext space &lt;code&gt;C&lt;/code&gt;. &lt;code&gt;Dec&lt;/code&gt; turns &lt;code&gt;c&lt;/code&gt; back into &lt;code&gt;m&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Every result in this post rests on the following hypotheses. I will refer to
them by name.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;H1, finite spaces.&lt;/strong&gt; &lt;code&gt;M&lt;/code&gt;, &lt;code&gt;K&lt;/code&gt; and &lt;code&gt;C&lt;/code&gt; are finite. &lt;code&gt;K&lt;/code&gt; contains only keys
that &lt;code&gt;Gen&lt;/code&gt; can output, and &lt;code&gt;C&lt;/code&gt; contains only ciphertexts that can occur.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;H2, independent key.&lt;/strong&gt; The key is chosen independently of the message.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;H3, correctness.&lt;/strong&gt; &lt;code&gt;Dec&lt;/code&gt; is deterministic and &lt;code&gt;Dec_k(Enc_k(m)) = m&lt;/code&gt; for
every key &lt;code&gt;k&lt;/code&gt; and every message &lt;code&gt;m&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;H4, known system.&lt;/strong&gt; The attacker knows &lt;code&gt;Gen&lt;/code&gt;, &lt;code&gt;Enc&lt;/code&gt;, &lt;code&gt;Dec&lt;/code&gt; and the
probability of each message. Only the value of &lt;code&gt;k&lt;/code&gt; is secret. This is
Kerckhoffs&apos; principle, and it gets its own section below.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;H5, one message per key.&lt;/strong&gt; The attacker sees a single ciphertext produced
with the key.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The attacker&apos;s computing power is unlimited. With that setting, here is
Shannon&apos;s definition (1949), in the modern form used by Katz and Lindell:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Definition (perfect secrecy).&lt;/strong&gt; Under H1–H5, a scheme is perfectly secret if
for every probability distribution over &lt;code&gt;M&lt;/code&gt;, every message &lt;code&gt;m ∈ M&lt;/code&gt; and every
ciphertext &lt;code&gt;c ∈ C&lt;/code&gt; with &lt;code&gt;Pr[C = c] &amp;gt; 0&lt;/code&gt;, we have
&lt;code&gt;Pr[M = m | C = c] = Pr[M = m]&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The right side is what the attacker believes about the message before
intercepting anything. The left side is what they believe after seeing &lt;code&gt;c&lt;/code&gt;.
Perfect secrecy says the two are equal: the ciphertext teaches nothing. The
words &quot;every probability distribution&quot; matter. The attacker may already know
that &quot;attack at dawn&quot; is far more likely than &quot;retreat&quot;, and the scheme must
still not add to that knowledge.&lt;/p&gt;
&lt;p&gt;There is an equivalent form that is easier to check. Under H1 and H2, a scheme
is perfectly secret if and only if, for every pair of messages &lt;code&gt;m0, m1 ∈ M&lt;/code&gt; and
every ciphertext &lt;code&gt;c ∈ C&lt;/code&gt;,
&lt;code&gt;Pr[Enc_K(m0) = c] = Pr[Enc_K(m1) = c]&lt;/code&gt;. The probability is over the key and
over any randomness inside &lt;code&gt;Enc&lt;/code&gt;. Every message produces every ciphertext with
the same probability, so &lt;code&gt;c&lt;/code&gt; carries no trace of which message it came from. The
two forms are linked by Bayes&apos; theorem.&lt;/p&gt;
&lt;h2&gt;The one-time pad is perfectly secret, and the proof takes one line&lt;/h2&gt;
&lt;p&gt;In the one-time pad, messages, keys and ciphertexts are all strings of &lt;code&gt;n&lt;/code&gt; bits.
&lt;code&gt;Gen&lt;/code&gt; picks &lt;code&gt;k&lt;/code&gt; uniformly at random from &lt;code&gt;{0,1}^n&lt;/code&gt;. Encryption is
&lt;code&gt;Enc_k(m) = m ⊕ k&lt;/code&gt; and decryption is &lt;code&gt;Dec_k(c) = c ⊕ k&lt;/code&gt;, where &lt;code&gt;⊕&lt;/code&gt; is
bitwise XOR (Exclusive OR: 1 when the two bits differ, 0 when they are equal). Gilbert Vernam built this cipher for teleprinters in 1917, and Joseph
Mauborgne added the requirement that the key tape be random and never reused.
Steven Bellovin later found that Frank Miller had described the same idea for
telegraph codes in 1882.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Theorem (one-time pad).&lt;/strong&gt; Assume H2 and H5, that messages are &lt;code&gt;n&lt;/code&gt;-bit
strings, and that the key is uniform over &lt;code&gt;{0,1}^n&lt;/code&gt;. Then the one-time pad is
perfectly secret.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Proof.&lt;/strong&gt; Fix a message &lt;code&gt;m&lt;/code&gt; and a ciphertext &lt;code&gt;c&lt;/code&gt;. The event &lt;code&gt;Enc_K(m) = c&lt;/code&gt;
happens exactly when &lt;code&gt;K = m ⊕ c&lt;/code&gt;, which is one specific key. The key is uniform
and independent of &lt;code&gt;m&lt;/code&gt;, so &lt;code&gt;Pr[Enc_K(m) = c] = 2^-n&lt;/code&gt; for every &lt;code&gt;m&lt;/code&gt; and every
&lt;code&gt;c&lt;/code&gt;. That probability does not depend on &lt;code&gt;m&lt;/code&gt;, which is the equivalent form of
the definition. ∎&lt;/p&gt;
&lt;p&gt;The definition can also be checked directly. The next example enumerates every
case exactly, with fractions instead of floats, for &lt;code&gt;n = 2&lt;/code&gt; and a deliberately
uneven prior. It compares the one-time pad with a weaker scheme that uses a
1-bit key twice, so its only keys are &lt;code&gt;00&lt;/code&gt; and &lt;code&gt;11&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from fractions import Fraction
from itertools import product

MESSAGES = [&quot;00&quot;, &quot;01&quot;, &quot;10&quot;, &quot;11&quot;]
# Any prior works; this one is deliberately uneven.
PRIOR = {&quot;00&quot;: Fraction(1, 2), &quot;01&quot;: Fraction(1, 4),
         &quot;10&quot;: Fraction(1, 8), &quot;11&quot;: Fraction(1, 8)}


def xor(a: str, b: str) -&amp;gt; str:
    return &quot;&quot;.join(&quot;0&quot; if x == y else &quot;1&quot; for x, y in zip(a, b))


def posterior(keys: list[str], c: str) -&amp;gt; dict[str, Fraction]:
    &quot;&quot;&quot;Pr[M = m | C = c]: uniform key, chosen independently of m.&quot;&quot;&quot;
    joint = {
        m: PRIOR[m] * Fraction(sum(xor(m, k) == c for k in keys), len(keys))
        for m in MESSAGES
    }
    total = sum(joint.values())
    return {m: p / total for m, p in joint.items()}


one_time_pad = [&quot;&quot;.join(bits) for bits in product(&quot;01&quot;, repeat=2)]  # 4 keys
short_key = [bit * 2 for bit in &quot;01&quot;]  # 2 keys: a 1-bit key used twice

print(&quot;m    prior   OTP     short key   (ciphertext c = 00)&quot;)
otp, short = posterior(one_time_pad, &quot;00&quot;), posterior(short_key, &quot;00&quot;)
for m in MESSAGES:
    print(f&quot;{m}   {str(PRIOR[m]):&amp;lt;7} {str(otp[m]):&amp;lt;7} {short[m]}&quot;)

# The OTP check holds for every ciphertext, not just 00.
assert all(posterior(one_time_pad, c) == PRIOR for c in MESSAGES)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;m    prior   OTP     short key   (ciphertext c = 00)
00   1/2     1/2     4/5
01   1/4     1/4     0
10   1/8     1/8     0
11   1/8     1/8     1/5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the one-time pad, the attacker&apos;s odds after seeing &lt;code&gt;00&lt;/code&gt; are the prior,
exactly. With the short key, seeing &lt;code&gt;00&lt;/code&gt; rules out two messages and raises the
probability of &lt;code&gt;00&lt;/code&gt; from 1/2 to 4/5. The attacker did not break anything. They
only read the odds that the scheme handed them.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: Each cell holds the key that turns message m (row) into ciphertext c (column). In the one-time pad every cell is filled and each key appears once per row and once per column. With the short key, half the cells are empty: seeing c = 00 leaves only 00 and 11.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/perfect-secrecy-kerckhoffs/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;Perfect secrecy requires at least as many keys as messages&lt;/h2&gt;
&lt;p&gt;The short key did not fail by accident. Its failure is a theorem.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Theorem 1 (key-space bound, Shannon 1949).&lt;/strong&gt; Assume H1 and H3, and let the
scheme be perfectly secret. Then &lt;code&gt;|K| ≥ |M|&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The theorem does not assume that &lt;code&gt;Enc&lt;/code&gt; is deterministic or that keys are
uniform. It only needs correctness and the definition.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Proof.&lt;/strong&gt; Suppose &lt;code&gt;|K| &amp;lt; |M|&lt;/code&gt;. Take the uniform distribution over &lt;code&gt;M&lt;/code&gt; and any
ciphertext &lt;code&gt;c&lt;/code&gt; with &lt;code&gt;Pr[C = c] &amp;gt; 0&lt;/code&gt;. Decrypting &lt;code&gt;c&lt;/code&gt; with every key gives the set
of messages that &lt;code&gt;c&lt;/code&gt; could come from. By H3 that set has at most &lt;code&gt;|K|&lt;/code&gt; elements,
so some message &lt;code&gt;m&apos;&lt;/code&gt; is missing from it. If some key could encrypt &lt;code&gt;m&apos;&lt;/code&gt; into
&lt;code&gt;c&lt;/code&gt;, decrypting &lt;code&gt;c&lt;/code&gt; with that key would give &lt;code&gt;m&apos;&lt;/code&gt;, so no key can. Then
&lt;code&gt;Pr[M = m&apos; | C = c] = 0&lt;/code&gt;, while &lt;code&gt;Pr[M = m&apos;] = 1/|M| &amp;gt; 0&lt;/code&gt;. That contradicts
perfect secrecy. ∎&lt;/p&gt;
&lt;p&gt;In bits: if every &lt;code&gt;n&lt;/code&gt;-bit string is a possible message and keys are &lt;code&gt;ℓ&lt;/code&gt;-bit
strings, then &lt;code&gt;2^ℓ ≥ 2^n&lt;/code&gt;, so &lt;code&gt;ℓ ≥ n&lt;/code&gt;. The key must be at least as long as the
message. Shannon stated the same bound in 1949: the number of keys must be at
least the number of possible messages.&lt;/p&gt;
&lt;p&gt;Shannon also characterized the schemes that sit exactly at this bound:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Theorem 2 (Shannon&apos;s theorem).&lt;/strong&gt; Assume H1–H3, that &lt;code&gt;Enc&lt;/code&gt; is
deterministic, and that &lt;code&gt;|M| = |K| = |C|&lt;/code&gt;. The scheme is perfectly secret if
and only if both conditions hold:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Gen&lt;/code&gt; chooses every key with probability &lt;code&gt;1/|K|&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For every &lt;code&gt;m ∈ M&lt;/code&gt; and every &lt;code&gt;c ∈ C&lt;/code&gt;, exactly one key &lt;code&gt;k&lt;/code&gt; satisfies
&lt;code&gt;Enc_k(m) = c&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;Condition 2 is what the left grid in the figure shows: every cell holds exactly
one key, and each key appears once in every row and once in every column. The
one-time pad is the standard example, and the proof above is the &quot;if&quot; direction
of this theorem.&lt;/p&gt;
&lt;p&gt;The equal-size hypothesis matters only for the &quot;only if&quot; direction. Without it,
a perfectly secret scheme can have extra keys that are not uniform. Take a
one-time pad and append one unused bit to the key, chosen with probability 0.9
for &lt;code&gt;0&lt;/code&gt;. The scheme is still perfectly secret, yet it has twice as many keys as
messages, the keys are not uniform, and every cell has two keys.&lt;/p&gt;
&lt;h2&gt;A reused key breaks the one-message hypothesis, not the proof&lt;/h2&gt;
&lt;p&gt;Now the opening question has an answer. A duplicated page means two messages
encrypted with the same key:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;c1 ⊕ c2 = (m1 ⊕ k) ⊕ (m2 ⊕ k) = m1 ⊕ m2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key cancels. The attacker now holds the XOR of two plaintexts, which keeps
the structure of language. Theorem 1 explains why this must leak. Encrypting two
&lt;code&gt;n&lt;/code&gt;-bit messages with one &lt;code&gt;n&lt;/code&gt;-bit key is a scheme whose messages are pairs: it
has &lt;code&gt;2^(2n)&lt;/code&gt; possible messages and only &lt;code&gt;2^n&lt;/code&gt; keys. By Theorem 1, it cannot be
perfectly secret. The short-key scheme from the first example is exactly this
case: a 1-bit pad used for two 1-bit messages.&lt;/p&gt;
&lt;p&gt;The next example shows the leak in practice with &lt;strong&gt;crib dragging&lt;/strong&gt;. Guess a word
that probably appears in one of the messages, slide it along &lt;code&gt;c1 ⊕ c2&lt;/code&gt;, and keep
the positions where the other message would read as plain text.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import secrets


def xor(a: bytes, b: bytes) -&amp;gt; bytes:
    return bytes(x ^ y for x, y in zip(a, b))


m1 = b&quot;SEND THE REPORT TO THE CONSULATE&quot;
m2 = b&quot;AGENT MEETS COURIER AT THE DOCKS&quot;
key = secrets.token_bytes(len(m1))  # perfect key... used twice

c1, c2 = xor(m1, key), xor(m2, key)
leak = xor(c1, c2)
assert leak == xor(m1, m2)  # the key has cancelled out

# Crib dragging: guess a word that is probably in one message,
# slide it along the leak, and keep positions where the other
# message would read as plain uppercase text.
crib = b&quot; THE &quot;
for i in range(len(leak) - len(crib) + 1):
    other = xor(leak[i : i + len(crib)], crib)
    if all(ch == 32 or 65 &amp;lt;= ch &amp;lt;= 90 for ch in other):
        print(i, other.decode())
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;4 T MEE
11 PXUDR
18 R AT
22  CONS
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three of the four hits are real fragments. &lt;code&gt;T MEE&lt;/code&gt; and &lt;code&gt;R AT&lt;/code&gt; come from &lt;code&gt;m2&lt;/code&gt;,
where &lt;code&gt;m1&lt;/code&gt; holds &quot; THE &quot;. &lt;code&gt;CONS&lt;/code&gt; comes from &lt;code&gt;m1&lt;/code&gt;, where &lt;code&gt;m2&lt;/code&gt; holds &quot; THE &quot;. The
attacker does not know which message contains the guess, and does not need to.
&lt;code&gt;PXUDR&lt;/code&gt; is noise that a human discards at a glance. Each confirmed fragment
suggests the next guess, and the two messages come out together.&lt;/p&gt;
&lt;p&gt;Run the example twice. The key is new each time, and the output is identical.
That is what &quot;the key cancels&quot; means.&lt;/p&gt;
&lt;p&gt;Venona worked the same way. Soviet clerks added digits instead of XORing bits,
so the analysts subtracted, and the key cancelled just the same. Their task was
harder, because they also had to rebuild the codebook before any of it read as
words. But the opening that made it possible was the reused key.&lt;/p&gt;
&lt;p&gt;The theory was not the problem. According to a 2006 account by Sergei Molotkov,
the Soviet engineer Vladimir Kotelnikov had proved the absolute security of the
one-time pad in a 1941 report that apparently remains classified. A year later,
the same state printed duplicate pages. The proof held. Production broke one of
its hypotheses.&lt;/p&gt;
&lt;p&gt;Reuse is not the only way out of the theorem. Each hypothesis has a failure mode
of its own:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A key that is not uniform (H2 and the theorem&apos;s uniform key).&lt;/strong&gt; If a 256-bit
pad is generated from a 32-bit seed, there are only &lt;code&gt;2^32&lt;/code&gt; real keys. By
Theorem 1, that is far too few for 256-bit messages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Messages of different lengths (H1).&lt;/strong&gt; The definition fixes &lt;code&gt;M&lt;/code&gt;, and the
one-time pad uses messages of one length &lt;code&gt;n&lt;/code&gt;. A ciphertext reveals the length
of its message unless messages are padded to a common length.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tampering (not covered at all).&lt;/strong&gt; Flipping bit &lt;code&gt;i&lt;/code&gt; of &lt;code&gt;c&lt;/code&gt; flips bit &lt;code&gt;i&lt;/code&gt; of
&lt;code&gt;m&lt;/code&gt;. Perfect secrecy is about what the attacker learns, not about what they
can change. Integrity needs a separate mechanism, such as
a message authentication code.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kerckhoffs&apos; principle is built into the definition&lt;/h2&gt;
&lt;p&gt;In 1883, Auguste Kerckhoffs published &lt;em&gt;La cryptographie militaire&lt;/em&gt; in the
&lt;em&gt;Journal des sciences militaires&lt;/em&gt;, in two parts, in January and February. It
lists six requirements for military field ciphers. The second one is the
principle that carries his name:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Il faut qu&apos;il n&apos;exige pas le secret, et qu&apos;il puisse sans inconvénient tomber
entre les mains de l&apos;ennemi.&quot;&lt;/p&gt;
&lt;p&gt;The system must not require secrecy, and it must be able to fall into the
hands of the enemy without inconvenience.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Shannon adopted the same assumption in 1949, and it is often quoted as Shannon&apos;s
maxim: &quot;the enemy knows the system.&quot; In this post it is hypothesis H4. The
definition of perfect secrecy takes probabilities over the key only. The
attacker knows &lt;code&gt;Gen&lt;/code&gt;, &lt;code&gt;Enc&lt;/code&gt;, &lt;code&gt;Dec&lt;/code&gt; and the prior over messages. Nothing else is
assumed secret, so the definition measures the strength of the key and nothing
else.&lt;/p&gt;
&lt;p&gt;Why build that into the definition? Because a secret algorithm is key material
with bad properties. It is copied into every device that runs it, so it leaks.
It cannot be changed quickly once it leaks. And it cannot be reviewed in public
before it is deployed. Two well-documented cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GSM&apos;s A5/1.&lt;/strong&gt; The cipher that protected GSM (Global System for Mobile Communications) calls was kept secret. Its
general design leaked in 1994, and Marc Briceno reverse-engineered it in full
from a GSM phone in 1999. Alex Biryukov, Adi Shamir and David Wagner presented
a real-time attack in 2000.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MIFARE Classic&apos;s Crypto-1.&lt;/strong&gt; NXP&apos;s contactless cards used a proprietary
cipher with a 48-bit key. In 2008, Karsten Nohl, David Evans, Starbug and
Henryk Plötz reverse-engineered it from images of the chip&apos;s silicon. The
same year, Flavio Garcia and colleagues at Radboud University recovered it
from card-reader traffic and published practical key-recovery attacks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Kerckhoffs&apos; principle does not require publishing the design. It requires that
publication cost nothing.&lt;/p&gt;
&lt;h2&gt;Kerckhoffs also accepted that perfect secrecy is too expensive&lt;/h2&gt;
&lt;p&gt;His first requirement is less famous and just as relevant:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Le système doit être matériellement, sinon mathématiquement, indéchiffrable.&quot;&lt;/p&gt;
&lt;p&gt;The system must be practically, if not mathematically, undecipherable.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Kerckhoffs settled for practical security 66 years before Theorem 1 proved why.
Mathematical security needs a key at least as long as all the traffic it
protects, generated at random, delivered in advance and never reused.&lt;/p&gt;
&lt;p&gt;Modern ciphers take the practical branch. AES uses a public design and a 128- or
256-bit key to encrypt gigabytes. By Theorem 1, it cannot be perfectly secret
for any message longer than its key. Its security is &lt;strong&gt;computational&lt;/strong&gt;: the best
known attacks would take an infeasible amount of work. That is an assumption
supported by decades of public analysis, not a theorem.&lt;/p&gt;
&lt;p&gt;Stream constructions such as AES in counter mode and ChaCha20 then imitate the
pad. They expand the key and a nonce, a value that must be used only once per
key, into a long keystream, and XOR it with the message. Reuse a key-nonce pair
and the result is the two-time pad from the example above.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hypothesis&lt;/th&gt;
&lt;th&gt;Where it is used&lt;/th&gt;
&lt;th&gt;What breaks if it fails&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Key uniform and independent (H2)&lt;/td&gt;
&lt;td&gt;One-time pad theorem&lt;/td&gt;
&lt;td&gt;The ciphertext shifts the attacker&apos;s odds&lt;/td&gt;
&lt;td&gt;A 256-bit pad generated from a 32-bit seed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;At least as many keys as messages&lt;/td&gt;
&lt;td&gt;Theorem 1&lt;/td&gt;
&lt;td&gt;Some messages are ruled out&lt;/td&gt;
&lt;td&gt;The short-key scheme in the first example&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One message per key (H5)&lt;/td&gt;
&lt;td&gt;The definition&lt;/td&gt;
&lt;td&gt;&lt;code&gt;c1 ⊕ c2 = m1 ⊕ m2&lt;/code&gt;: the key cancels&lt;/td&gt;
&lt;td&gt;Venona; a reused key-nonce pair in AES-CTR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Only the key is secret (H4)&lt;/td&gt;
&lt;td&gt;The definition, Kerckhoffs&lt;/td&gt;
&lt;td&gt;Security rests on a secret that leaks&lt;/td&gt;
&lt;td&gt;A5/1, Crypto-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Messages of one length (H1)&lt;/td&gt;
&lt;td&gt;The one-time pad&apos;s &lt;code&gt;M&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The ciphertext reveals the message length&lt;/td&gt;
&lt;td&gt;Unpadded messages of different sizes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;A provably unbreakable cipher breaks when one of its hypotheses fails&lt;/h2&gt;
&lt;p&gt;How do you break a cipher that is mathematically unbreakable? You do not attack
the proof. You find the hypothesis that the deployed system no longer meets. In
Venona, it was a single one: each key page had to be used once, and 35,000 pages
were not. Shannon&apos;s theorems and Kerckhoffs&apos; principle describe the same
boundary from two sides: the math protects you only as far as its hypotheses
go, and the key is the only secret it allows.&lt;/p&gt;
&lt;p&gt;The takeaway fits in one habit. When someone says a system is unbreakable, ask
for the theorem and read its hypotheses. In code, that habit becomes three
rules. Take keys from a cryptographically secure generator, such as Python&apos;s
&lt;code&gt;secrets&lt;/code&gt; module. Never encrypt twice with the same key and nonce. Design as if
the attacker already has your source code.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Shannon, C. E. (1949). Communication Theory of Secrecy Systems. &lt;em&gt;Bell System
Technical Journal&lt;/em&gt;, 28(4), 656–715. An earlier version circulated as the
classified Bell Labs memorandum &lt;em&gt;A Mathematical Theory of Cryptography&lt;/em&gt;
(MM 45-110-02, 1 September 1945).&lt;/li&gt;
&lt;li&gt;Kerckhoffs, A. (1883). La cryptographie militaire. &lt;em&gt;Journal des sciences
militaires&lt;/em&gt;, IX, 5–38 (January) and 161–191
(February). &lt;a href=&quot;https://www.petitcolas.net/kerckhoffs/&quot;&gt;Transcription and scans by Fabien Petitcolas&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Katz, J., &amp;amp; Lindell, Y. &lt;em&gt;Introduction to Modern Cryptography&lt;/em&gt;, chapter 2
(perfectly secret encryption). CRC Press.&lt;/li&gt;
&lt;li&gt;National Security Agency. &lt;em&gt;VENONA: An Overview&lt;/em&gt;. &lt;a href=&quot;https://media.defense.gov/2021/Jun/29/2002752012/-1/-1/0/VENONA_AN_OVERVIEW.PDF&quot;&gt;PDF&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Vernam, G. S. U.S. Patent 1,310,719, &lt;em&gt;Secret Signaling System&lt;/em&gt;, issued 22
July 1919.&lt;/li&gt;
&lt;li&gt;Bellovin, S. M. (2011). Frank Miller: Inventor of the One-Time Pad.
&lt;em&gt;Cryptologia&lt;/em&gt;, 35.&lt;/li&gt;
&lt;li&gt;Molotkov, S. N. (2006). Quantum cryptography and V. A. Kotel&apos;nikov&apos;s one-time
key and sampling theorems. &lt;em&gt;Physics-Uspekhi&lt;/em&gt;, 49(7).&lt;/li&gt;
&lt;li&gt;Biryukov, A., Shamir, A., &amp;amp; Wagner, D. Real Time Cryptanalysis of A5/1 on a
PC. &lt;em&gt;Fast Software Encryption (FSE 2000)&lt;/em&gt;, LNCS, Springer, 2001.&lt;/li&gt;
&lt;li&gt;Nohl, K., Evans, D., Starbug, &amp;amp; Plötz, H. (2008). Reverse-Engineering a
Cryptographic RFID (Radio-Frequency Identification) Tag. &lt;em&gt;USENIX Security Symposium&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Garcia, F. D., de Koning Gans, G., Muijrers, R., van Rossum, P., Verdult, R.,
Wichers Schreur, R., &amp;amp; Jacobs, B. (2008). Dismantling MIFARE Classic.
&lt;em&gt;ESORICS 2008&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>security</category><category>cryptography</category><category>math</category></item><item><title>Análisis de datos: los cuatro tipos y qué información usa cada uno</title><link>https://yeison-liscano.github.io/portfolio/blog/tipos-de-analisis-de-datos/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/tipos-de-analisis-de-datos/</guid><description>Análisis descriptivo, predictivo, diagnóstico y prescriptivo aplicados a un mismo problema: qué pregunta responde cada fase, qué datos necesita, cómo se usan y por qué existen dos formas de ordenarlas.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Palabras clave: análisis descriptivo, análisis predictivo, análisis diagnóstico,
análisis prescriptivo, prueba A/B, optimización&lt;/p&gt;
&lt;p&gt;El análisis de datos suele dividirse en cuatro tipos, y cada uno responde una
pregunta distinta: &lt;strong&gt;qué está sucediendo&lt;/strong&gt;, &lt;strong&gt;qué va a suceder&lt;/strong&gt;, &lt;strong&gt;por qué
sucede&lt;/strong&gt; y &lt;strong&gt;qué debo hacer&lt;/strong&gt;. Lo que más los diferencia no es la herramienta,
sino la información que necesitan y lo que entregan a la fase siguiente.&lt;/p&gt;
&lt;p&gt;Para que las diferencias sean concretas, este artículo sigue un solo problema de
principio a fin. Una tienda en línea con 1900 clientes activos quiere que menos
clientes dejen de comprar, lo que se conoce como &lt;strong&gt;abandono&lt;/strong&gt; o &lt;em&gt;churn&lt;/em&gt;. Con el
mismo presupuesto de $2500 en cupones, repartirlos a quienes tienen más riesgo
deja $2600 de beneficio neto, y combinar las cuatro fases deja $5600. La
diferencia está en qué información usa cada fase.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: Una tienda en línea que quiere perder menos clientes recorre las cuatro fases. Cada salida alimenta la fase siguiente, y la decisión final produce los datos con los que empieza la próxima vuelta.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/tipos-de-analisis-de-datos/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;1. Análisis descriptivo: ¿qué está sucediendo?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Qué información usa.&lt;/strong&gt; Los datos que la organización ya tiene: pedidos,
visitas, registros de soporte, logs y transacciones. Casi siempre son tablas
históricas, aunque también pueden ser eventos, texto o series de tiempo. No hace
falta etiquetar nada ni generar datos nuevos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cómo la usa.&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Limpia y perfila&lt;/strong&gt; los datos: valores nulos, duplicados, tipos y rangos
imposibles. Un análisis descriptivo sobre datos sucios describe los errores
de captura, no el negocio.&lt;/li&gt;
&lt;li&gt;Hace &lt;strong&gt;análisis exploratorio de datos (EDA)&lt;/strong&gt;: distribuciones, medias y
medianas, valores atípicos, agrupaciones por segmento y tendencias en el
tiempo.&lt;/li&gt;
&lt;li&gt;Calcula &lt;strong&gt;correlaciones&lt;/strong&gt; entre variables (Pearson para relaciones lineales,
Spearman para relaciones monótonas) para ver qué se mueve junto con qué.&lt;/li&gt;
&lt;li&gt;Comunica el resultado con tableros, reportes y gráficos.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;En la tienda, este análisis muestra que el 28 % de los clientes del trimestre
pasado no volvió a comprar en los 90 días siguientes. La cifra sube al 40 %
entre quienes registraron una queja y al 60 % entre quienes llevan más de 60
días sin comprar. Los días desde la última compra y el número de quejas se
mueven junto con el abandono.&lt;/p&gt;
&lt;p&gt;Buscamos responder a la pregunta &quot;¿qué está sucediendo y por qué?&quot;, y la
segunda mitad merece cuidado: una correlación sugiere un &quot;por qué&quot;, pero no lo
demuestra. Que los clientes con quejas se vayan más puede deberse a la queja, a
un producto defectuoso que causó ambas cosas o a cualquier otra variable que no
medimos. El descriptivo produce &lt;strong&gt;hipótesis&lt;/strong&gt;, y las fases siguientes las ponen
a prueba. Si te interesa la frontera entre describir una muestra y sacar
conclusiones sobre una población, la explico
en &lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/inferential-vs-descriptive-statistics/&quot;&gt;estadística descriptiva e inferencial&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;2. Análisis predictivo: ¿qué va a suceder?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Qué información usa.&lt;/strong&gt; Un histórico &lt;strong&gt;etiquetado&lt;/strong&gt;: cada fila es una
observación pasada con sus &lt;strong&gt;variables de entrada&lt;/strong&gt; (&lt;em&gt;features&lt;/em&gt;) y el
&lt;strong&gt;resultado que ocurrió después&lt;/strong&gt; (&lt;em&gt;target&lt;/em&gt;). En la tienda, cada fila es un
cliente en una fecha. Sus variables son los días desde la última compra, el
número de pedidos, el ticket promedio y las quejas, y su etiqueta dice si dejó
de comprar en los 90 días siguientes. La calidad, la cantidad y la representatividad de esos
datos pesan más que la elección del algoritmo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cómo la usa.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Regresión&lt;/strong&gt; cuando el resultado es un número: las ventas del próximo mes o
el margen que dejará un cliente durante el año.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clasificación&lt;/strong&gt; cuando el resultado es una categoría: se va o se queda,
fraude o no fraude.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pronóstico&lt;/strong&gt; cuando el resultado es una serie de tiempo: la demanda de cada
semana.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;El flujo típico es dividir los datos en entrenamiento, validación y prueba,
entrenar el modelo, medirlo con métricas como RMSE (Root Mean Square Error (raíz del error cuadrático medio)), precisión, &lt;em&gt;recall&lt;/em&gt; o AUC (Area Under the ROC Curve (área bajo la curva ROC)), y
desplegarlo para puntuar datos nuevos. Dos detalles importan mucho cuando la
predicción alimenta decisiones. Primero, la división debe respetar el tiempo:
entrenar con trimestres antiguos y evaluar con el más reciente, y no usar
variables que se miden después del resultado (&lt;em&gt;data leakage&lt;/em&gt;). Segundo, las
probabilidades deben estar &lt;strong&gt;calibradas&lt;/strong&gt;: si el modelo dice 40 %, cerca de 4 de
cada 10 de esos clientes deberían irse. La fase prescriptiva usará esa cifra como
probabilidad, no solo como ranking.&lt;/p&gt;
&lt;p&gt;El resultado en la tienda es un riesgo por cliente: la probabilidad de que se
vaya en los próximos 90 días. Pero predecir no es explicar. El modelo puede
acertar usando variables que el negocio no controla, y aprende asociaciones del
pasado: no dice qué pasaría si la tienda cambiara algo. Esa pregunta es la de la
fase siguiente.&lt;/p&gt;
&lt;h2&gt;3. Análisis diagnóstico: ¿por qué sucedió esto?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Qué información usa.&lt;/strong&gt; Además de los datos históricos, información &lt;strong&gt;generada
a propósito&lt;/strong&gt; para aislar una causa: un grupo de control y uno de tratamiento,
una asignación al azar entre ambos y una sola variable que cambia. También usa
las hipótesis que dejó el descriptivo y el contexto de lo que pasó en el
periodo, como campañas, cambios de precio o fallas del sitio.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cómo la usa.&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Convierte una hipótesis en algo que se puede refutar: &quot;un cupón del 15 %
reduce el abandono&quot;.&lt;/li&gt;
&lt;li&gt;Diseña un &lt;strong&gt;experimento&lt;/strong&gt; o &lt;strong&gt;prueba A/B&lt;/strong&gt;: reparte a los clientes al azar,
da el cupón solo a uno de los grupos y mide la diferencia. El azar es lo que
permite hablar de causa, porque reparte por igual entre los dos grupos todo
lo que no medimos.&lt;/li&gt;
&lt;li&gt;Decide el tamaño de la muestra &lt;strong&gt;antes&lt;/strong&gt; de empezar, con
un cálculo de potencia, y no mira los resultados a mitad de camino para detenerse cuando le
convienen.&lt;/li&gt;
&lt;li&gt;Analiza la diferencia con estadística inferencial: intervalos de confianza,
valores p y tamaño del efecto.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;En la tienda, el trimestre anterior se repartió al azar a 1800 clientes: 900
recibieron el cupón y 900 no.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: La prueba A/B del cupón. Sin cupón se fue el 28 % del grupo; con cupón, el 22 %. La diferencia es de 6 puntos porcentuales y su intervalo de confianza del 95 %, de 2 a 10 puntos, no incluye el cero, así que el efecto no se explica solo por el azar.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/tipos-de-analisis-de-datos/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;El cálculo cabe en unas pocas líneas, usando la aproximación normal para la
diferencia de dos proporciones:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from math import sqrt

n = 900
p_control, p_cupon = 252 / n, 198 / n  # 0.28 y 0.22
efecto = p_control - p_cupon  # 0.06: 6 puntos porcentuales
error = sqrt(p_control * (1 - p_control) / n + p_cupon * (1 - p_cupon) / n)
bajo, alto = efecto - 1.96 * error, efecto + 1.96 * error  # 0.020 y 0.100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cuando no se puede experimentar, por costo, por ética o porque el hecho ya
ocurrió, se usan métodos de inferencia causal sobre datos observacionales,
como diferencias en diferencias, regresión discontinua o emparejamiento. Sus
conclusiones dependen de supuestos más fuertes que los de un experimento.&lt;/p&gt;
&lt;p&gt;Hay un matiz que la fase siguiente necesita: el efecto de 6 puntos es un
&lt;strong&gt;promedio&lt;/strong&gt;. El mismo cupón puede retener a muchos clientes que acaban de
quejarse y casi a ninguno de los que ya se fueron mentalmente. Medir el efecto
por segmento exige experimentos más grandes o modelos de &lt;em&gt;uplift&lt;/em&gt;, que estiman
cuánto cambia el resultado de cada cliente si se actúa. Con 900 clientes por
grupo, los intervalos de cada segmento serían muy anchos. En el
ejemplo supongo que esos efectos ya están bien estimados.&lt;/p&gt;
&lt;h2&gt;4. Análisis prescriptivo: ¿qué debo hacer?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Qué información usa.&lt;/strong&gt; Las salidas de las fases anteriores más la descripción
del &lt;strong&gt;problema de decisión&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Predicciones&lt;/strong&gt;: el riesgo de cada segmento y el margen que dejará cada
cliente que se quede, que también es una predicción.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Efectos causales&lt;/strong&gt;: cuánto cambia el resultado si se actúa, medido en el
diagnóstico.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recursos&lt;/strong&gt;: presupuesto, personal, inventario o capacidad.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Objetivo&lt;/strong&gt;: qué se maximiza o minimiza, como el beneficio neto.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Restricciones&lt;/strong&gt;: límites de capacidad, normas, plazos y costos de cada
acción.&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Segmento&lt;/th&gt;
&lt;th&gt;Clientes&lt;/th&gt;
&lt;th&gt;Riesgo (predictivo)&lt;/th&gt;
&lt;th&gt;Efecto del cupón (diagnóstico)&lt;/th&gt;
&lt;th&gt;Margen por cliente&lt;/th&gt;
&lt;th&gt;Neto por cupón&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Queja reciente&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;40 %&lt;/td&gt;
&lt;td&gt;15 pp&lt;/td&gt;
&lt;td&gt;$150&lt;/td&gt;
&lt;td&gt;+$17,50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nuevos sin 2.ª compra&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;35 %&lt;/td&gt;
&lt;td&gt;8 pp&lt;/td&gt;
&lt;td&gt;$90&lt;/td&gt;
&lt;td&gt;+$2,20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inactivos 60+ días&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;60 %&lt;/td&gt;
&lt;td&gt;2 pp&lt;/td&gt;
&lt;td&gt;$100&lt;/td&gt;
&lt;td&gt;−$3,00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sensibles al precio&lt;/td&gt;
&lt;td&gt;350&lt;/td&gt;
&lt;td&gt;25 %&lt;/td&gt;
&lt;td&gt;10 pp&lt;/td&gt;
&lt;td&gt;$80&lt;/td&gt;
&lt;td&gt;+$3,00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mayoristas&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;15 %&lt;/td&gt;
&lt;td&gt;4 pp&lt;/td&gt;
&lt;td&gt;$400&lt;/td&gt;
&lt;td&gt;+$11,00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frecuentes satisfechos&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;5 %&lt;/td&gt;
&lt;td&gt;0,4 pp&lt;/td&gt;
&lt;td&gt;$160&lt;/td&gt;
&lt;td&gt;−$4,36&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Cada cupón cuesta $5. El neto por cupón es el margen que protege, efecto por
margen, menos ese costo: para el segmento con queja reciente,
0,15 × $150 − $5 = $17,50.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cómo la usa.&lt;/strong&gt; Formula el problema como una &lt;strong&gt;optimización&lt;/strong&gt;. Si &lt;code&gt;k_i&lt;/code&gt; es el
número de cupones para el segmento &lt;em&gt;i&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;maximizar   Σ k_i · (efecto_i · margen_i − costo)
sujeto a    Σ k_i · costo ≤ presupuesto
            0 ≤ k_i ≤ clientes_i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Es un problema de &lt;strong&gt;programación lineal&lt;/strong&gt;. Como todos los cupones cuestan lo
mismo, su solución es sencilla: llenar primero los segmentos con mayor neto por
cupón y no dar cupones donde el neto es negativo. Es la regla de la mochila
fraccionaria, que toma primero lo que más vale por unidad de peso. Con más tipos de acción, costos distintos o restricciones enteras
se usan programación entera, simulación o aprendizaje por refuerzo.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: Mueve el presupuesto y cambia de estrategia. Con $2500, la estrategia óptima retiene a 51 clientes y deja $5600 de beneficio neto; repartir por riesgo retiene a 36 y deja $2600, porque gasta $1500 en clientes inactivos a los que el cupón casi no cambia.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/tipos-de-analisis-de-datos/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;El explorable muestra la lección central de la fase: &lt;strong&gt;el riesgo dice quién se
va; el efecto dice a quién puedes retener&lt;/strong&gt;. Los inactivos tienen el riesgo más
alto, pero el cupón casi no los mueve. Los frecuentes satisfechos se quedarían
de todos modos. Una estrategia que solo usa la predicción gasta el presupuesto
en ellos. Fíjate también en que, con presupuesto de sobra, la estrategia óptima
deja dinero sin gastar: el objetivo es el beneficio neto, no repartir todos los
cupones.&lt;/p&gt;
&lt;p&gt;Esta es la fase más valiosa y también la más difícil, porque exige modelar bien
tanto los datos como el negocio. Un error en el margen, en el costo o en una
restricción produce una decisión precisa y equivocada.&lt;/p&gt;
&lt;h2&gt;El orden correcto depende de qué se quiere medir&lt;/h2&gt;
&lt;p&gt;Este artículo recorrió las fases en el orden descriptivo, predictivo,
diagnóstico y prescriptivo. El modelo de madurez analítica de Gartner pone el
&lt;strong&gt;diagnóstico en segundo lugar&lt;/strong&gt; y el &lt;strong&gt;predictivo en tercero&lt;/strong&gt;. Los dos órdenes
son coherentes, porque usan criterios distintos.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: Las mismas cuatro fases en dos órdenes. Gartner va de mirar atrás a mirar adelante. Este artículo va de observar asociaciones a intervenir y, al final, decidir.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/tipos-de-analisis-de-datos/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gartner ordena por madurez y valor para el negocio.&lt;/strong&gt; Primero se entiende el
pasado (qué pasó y por qué) y luego se mira al futuro (qué pasará y qué
hacer). En ese modelo, el diagnóstico suele ser trabajo sobre los datos que ya
existen: desglosar un indicador por segmento, comparar periodos y buscar la
causa raíz. Es más fácil que entrenar un modelo, por eso va antes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Este artículo ordena por lo que exige cada fase.&lt;/strong&gt; Aquí el diagnóstico se
define con experimentos y pruebas A/B, así que el orden sigue una escalera
parecida a la de causalidad de Judea Pearl. Primero viene la &lt;strong&gt;asociación&lt;/strong&gt;:
observar datos, que es lo que hacen la correlación y la predicción. Luego, la
&lt;strong&gt;intervención&lt;/strong&gt;: cambiar algo y medir qué pasa. Al final, la &lt;strong&gt;decisión&lt;/strong&gt;,
que necesita saber qué pasa si se interviene. Miguel Hernán y sus coautores proponen una división
similar de las tareas de la ciencia de datos: descripción, predicción e
inferencia causal.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La elección práctica depende de la pregunta. Para medir la madurez de un equipo
o planear qué capacidades construir primero, el orden de Gartner funciona bien.
Para decidir qué evidencia necesita una decisión, el orden de este artículo es
más preciso: una predicción, por buena que sea, no reemplaza a un experimento
cuando la pregunta es qué pasa si actúo.&lt;/p&gt;
&lt;h2&gt;Cada fase entrega lo que la siguiente necesita&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fase&lt;/th&gt;
&lt;th&gt;Pregunta&lt;/th&gt;
&lt;th&gt;Información que usa&lt;/th&gt;
&lt;th&gt;Cómo la usa&lt;/th&gt;
&lt;th&gt;Resultado&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Descriptivo&lt;/td&gt;
&lt;td&gt;¿Qué está sucediendo?&lt;/td&gt;
&lt;td&gt;Datos históricos que ya existen&lt;/td&gt;
&lt;td&gt;Limpieza, EDA, correlación, tableros&lt;/td&gt;
&lt;td&gt;Línea base e hipótesis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Predictivo&lt;/td&gt;
&lt;td&gt;¿Qué va a suceder?&lt;/td&gt;
&lt;td&gt;Histórico etiquetado: variables y resultado&lt;/td&gt;
&lt;td&gt;Regresión, clasificación, pronóstico&lt;/td&gt;
&lt;td&gt;Probabilidades y pronósticos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diagnóstico&lt;/td&gt;
&lt;td&gt;¿Por qué sucedió esto?&lt;/td&gt;
&lt;td&gt;Datos generados con grupos asignados al azar&lt;/td&gt;
&lt;td&gt;Experimentos, pruebas A/B, inferencia causal&lt;/td&gt;
&lt;td&gt;Efectos causales con su margen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prescriptivo&lt;/td&gt;
&lt;td&gt;¿Qué debo hacer?&lt;/td&gt;
&lt;td&gt;Predicciones, efectos, recursos, objetivos, límites&lt;/td&gt;
&lt;td&gt;Optimización: asignación, decisión, planificación&lt;/td&gt;
&lt;td&gt;Una acción concreta&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Cada fase depende de lo que entrega la anterior, y la decisión final cierra el
ciclo: al ejecutarse, produce los datos que la próxima vuelta del análisis
descriptivo va a resumir.&lt;/p&gt;
&lt;p&gt;Si te quedas con una sola idea, que sea la de la tienda: la predicción dice
quién se va, pero solo el experimento dice a quién puedes retener. Una decisión
que use solo la primera gasta el presupuesto donde el cupón no cambia nada.&lt;/p&gt;
</content:encoded><category>statistics</category><category>data-science</category></item><item><title>Método científico aplicado al desarrollo de software y la seguridad</title><link>https://yeison-liscano.github.io/portfolio/blog/metodo-cientifico-en-software-y-seguridad/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/metodo-cientifico-en-software-y-seguridad/</guid><description>Cómo tratar los supuestos de seguridad como hipótesis falsables: el ciclo del método científico en el trabajo diario, sus límites y un registro de premisas para convertir creencias en pruebas repetibles.</description><pubDate>Mon, 19 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Palabras clave: método científico, falsabilidad, hipótesis, threat modeling,
registro de premisas&lt;/p&gt;
&lt;p&gt;En seguridad tomamos decisiones apoyadas en supuestos: &quot;este servicio no es
alcanzable desde fuera de la VPC (Virtual Private Cloud (nube privada virtual))&quot;, &quot;nuestro SAST (Static Application Security Testing (pruebas estáticas de seguridad de aplicaciones)) detecta inyecciones&quot;, &quot;el token
expira en 15 minutos&quot;. Si no los verificamos, pueden convertirse en puntos
ciegos. La idea central de este artículo es sencilla: &lt;strong&gt;toda premisa es una
hipótesis hasta que la sometes a una prueba que podría refutarla&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;El ciclo, traducido al trabajo diario&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Observación&lt;/strong&gt;: un comportamiento o dato concreto, como un pico de errores
403, un hallazgo del SAST o un incidente.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hipótesis falsable&lt;/strong&gt;: una afirmación que puede resultar falsa. &quot;Este
servicio es inalcanzable desde fuera de la VPC&quot; lo es. &quot;Nuestra arquitectura
es segura&quot; no.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Predicción&lt;/strong&gt;: si la hipótesis es cierta, qué debería pasar en una prueba
concreta, y qué resultado la refutaría.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Experimento&lt;/strong&gt;: un test, un fuzzing, un escaneo, un ejercicio de red team o
un benchmark, con las variables controladas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Análisis y registro&lt;/strong&gt;: se documentan todos los resultados, incluidos los
negativos y los que contradicen lo esperado.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Revisión por pares&lt;/strong&gt;: en la práctica son el code review, la revisión
del threat model y el postmortem sin culpables.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;El criterio que separa esto de una opinión es la &lt;strong&gt;falsabilidad&lt;/strong&gt;: una
afirmación empírica que ningún resultado posible puede contradecir no ofrece un
criterio de prueba. Los requisitos normativos y las propiedades demostradas
formalmente se evalúan de otras maneras.&lt;/p&gt;
&lt;h2&gt;Dónde aparece en la práctica&lt;/h2&gt;
&lt;h3&gt;Threat models como conjuntos de hipótesis&lt;/h3&gt;
&lt;p&gt;Muchos supuestos de un threat model (&quot;el atacante no tiene acceso al bucket&quot;,
&quot;el token expira en 15 minutos&quot;) pueden convertirse en una verificación
automática sobre la infraestructura o en un test en el pipeline de CI (Continuous Integration (integración continua)). Así el
modelo deja de ser un documento que envejece y pasa a ser una batería de pruebas
que puede fallar. El test del inicio de este artículo es exactamente eso: la
hipótesis &quot;el token expira a los 15 minutos&quot; escrita de forma que pueda ser
refutada.&lt;/p&gt;
&lt;h3&gt;Medir las herramientas en lugar de creerles&lt;/h3&gt;
&lt;p&gt;&quot;Nuestro SAST detecta inyecciones&quot; es una afirmación empírica. Para evaluarla
puedes sembrar vulnerabilidades conocidas en un corpus de prueba y comprobar
cuáles detecta la herramienta y cuáles no. Lo que no detecta es información
valiosa sobre sus límites.&lt;/p&gt;
&lt;h3&gt;Cambios con línea base&lt;/h3&gt;
&lt;p&gt;Antes de afirmar que una mitigación redujo el riesgo, compara contra un
baseline. Una línea base ayuda a interpretar el cambio, pero una comparación
antes/después no basta para establecer causalidad: el tiempo medio de
remediación pudo bajar porque cambió el equipo, no porque cambió el proceso.&lt;/p&gt;
&lt;h3&gt;Postmortems y reproducibilidad&lt;/h3&gt;
&lt;p&gt;Un postmortem sólido formula hipótesis causales y busca evidencia que las
desmienta, en lugar de quedarse con la primera explicación plausible. Una prueba
de concepto reproducible fortalece la evidencia, pero un incidente también puede
establecerse con registros, trazas o evidencia forense aunque no sea posible
repetirlo. Hay que documentar la incertidumbre restante.&lt;/p&gt;
&lt;h3&gt;Un contraejemplo vale más que mil confirmaciones&lt;/h3&gt;
&lt;p&gt;Una afirmación universal como «este endpoint nunca acepta peticiones sin
autenticación» queda refutada por una petición no autenticada que sí funciona.
En cambio, una petición rechazada no prueba que ningún otro intento pueda
funcionar. El contraejemplo demuestra que esa propiedad concreta falla en las
condiciones probadas; no clasifica toda la seguridad del sistema.&lt;/p&gt;
&lt;h2&gt;Límites que conviene recordar&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Una batería finita de pruebas no suele demostrar ausencia de errores.&lt;/strong&gt; La
conclusión debe limitarse a las condiciones evaluadas. Las verificaciones
exhaustivas de modelos finitos y las pruebas formales pueden demostrar
propiedades dentro de sus modelos y supuestos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;El adversario se adapta.&lt;/strong&gt; El sistema no es estacionario: lo que fue cierto
ayer puede dejar de serlo cuando el atacante cambia de táctica, así que las
hipótesis necesitan revalidarse de forma continua.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Goodhart.&lt;/strong&gt; Cuando una métrica se vuelve objetivo, deja de ser buena
métrica. &quot;Número de vulnerabilidades cerradas&quot; es el ejemplo clásico.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sesgo de confirmación.&lt;/strong&gt; Es el riesgo principal. La mejor defensa es
escribir la hipótesis, la predicción y el criterio de refutación &lt;strong&gt;antes&lt;/strong&gt; de
ejecutar la prueba, para no ajustarlos después de ver el resultado.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Un método práctico para empezar: el registro de premisas&lt;/h2&gt;
&lt;p&gt;Crea una tabla con tus supuestos de seguridad y cuatro columnas:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Afirmación&lt;/th&gt;
&lt;th&gt;Impacto si es falsa&lt;/th&gt;
&lt;th&gt;Evidencia actual&lt;/th&gt;
&lt;th&gt;Resultado que la refutaría&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;El bucket no es accesible público&lt;/td&gt;
&lt;td&gt;Alto&lt;/td&gt;
&lt;td&gt;Revisión manual hace 6 m&lt;/td&gt;
&lt;td&gt;Leer el bucket sin autorización&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;El token expira en 15 minutos&lt;/td&gt;
&lt;td&gt;Medio&lt;/td&gt;
&lt;td&gt;Configuración leída&lt;/td&gt;
&lt;td&gt;Obtener acceso con un token vencido&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;El SAST detecta inyecciones SQL&lt;/td&gt;
&lt;td&gt;Alto&lt;/td&gt;
&lt;td&gt;Ninguna&lt;/td&gt;
&lt;td&gt;Omitir una inyección del corpus de prueba&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Prioriza las filas de alto impacto y baja evidencia, y conviértelas en pruebas
repetibles que corran de forma continua. Cada vez que una falle, habrás
aprendido algo que una creencia sin verificar nunca te habría dicho.&lt;/p&gt;
&lt;h2&gt;Resumen&lt;/h2&gt;
&lt;p&gt;El método científico es un marco simple: formular hipótesis falsables, predecir
qué debería ocurrir, ponerlas a prueba, registrar todos los resultados y
someterlos a revisión. Aplicado a la seguridad, convierte los supuestos
implícitos en pruebas repetibles. Lo que importa es el hábito: escribir qué
esperas, qué te haría cambiar de opinión, y buscar activamente esa evidencia.&lt;/p&gt;
</content:encoded><category>security</category><category>programming</category></item><item><title>Descriptive vs Inferential Statistics: What Seven Numbers Can Tell You</title><link>https://yeison-liscano.github.io/portfolio/blog/inferential-vs-descriptive-statistics/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/inferential-vs-descriptive-statistics/</guid><description>Descriptive statistics summarizes the data you have. Inferential statistics uses a sample to estimate what you do not have. One seven-number sample shows where the first ends and the second begins.</description><pubDate>Sun, 29 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Take seven measurements: 23, 45, 12, 67, 34, 89 and 21. You can say exactly what
they look like. You cannot yet say what the next measurement, or the whole
population they came from, will look like. The first job belongs to
&lt;strong&gt;descriptive statistics&lt;/strong&gt;. The second belongs to &lt;strong&gt;inferential statistics&lt;/strong&gt;,
and mixing them up is how a small sample turns into a confident, wrong
conclusion.&lt;/p&gt;
&lt;h2&gt;Descriptive statistics summarizes the data you have&lt;/h2&gt;
&lt;p&gt;The goal of descriptive statistics is to &lt;strong&gt;summarize a dataset with a small
number of measures&lt;/strong&gt;. Faced with a heap of raw numbers, it asks how we can find
the pattern and present it in a digestible way. It does this without assuming
anything beyond the dataset itself. The most common summaries are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Central tendency:&lt;/strong&gt; the mean, median, and mode identify the center of the
data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dispersion:&lt;/strong&gt; the variance, standard deviation, and interquartile range
(IQR) describe how stretched or squeezed the data are.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shape and relationship:&lt;/strong&gt; skewness, kurtosis, and the linear correlation
between two variables.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For our seven measurements, the mean is 41.57 and the standard deviation is
27.71 (dividing by n - 1). Those two numbers describe the sample completely, and
they say nothing about anyone outside it.&lt;/p&gt;
&lt;h2&gt;Inferential statistics estimates what you did not measure&lt;/h2&gt;
&lt;p&gt;Descriptive statistics describes the exact data on hand. Inferential statistics
&lt;strong&gt;generalizes from a sample to a larger population&lt;/strong&gt;. Measuring a whole
population, such as every person in a country, is often impossible, slow, or
expensive. So researchers measure a sample and use a sampling design and a
statistical model to estimate the population parameters. A large sample alone
does not remove selection bias.&lt;/p&gt;
&lt;p&gt;This works because &lt;strong&gt;inferential statistics relies on the laws of probability&lt;/strong&gt;
to reach conclusions and to state how confident we are in them. It uses
&lt;strong&gt;inductive reasoning&lt;/strong&gt;: the logic flows from the specific sample up to a
broader claim about the population.&lt;/p&gt;
&lt;p&gt;The key techniques are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Point estimation:&lt;/strong&gt; a single &quot;best guess&quot; for an unknown population
parameter.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interval estimation (confidence intervals):&lt;/strong&gt; an interval built with a
specified long-run coverage rate under the model&apos;s assumptions. A 95%
procedure covers the fixed population parameter in 95% of repeated samples.
It does not assign a 95% probability to that parameter after you observe an
interval.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hypothesis testing:&lt;/strong&gt; an assessment of how compatible the data are with a
null hypothesis and its assumptions. A &lt;em&gt;p-value&lt;/em&gt; is not the probability that
the hypothesis is true, and statistical significance does not measure effect
size.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;From seven numbers to a range for the population mean&lt;/h2&gt;
&lt;p&gt;Here is the step from description to inference. Assume the seven observations
are an independent random sample from a normal population. A Student&apos;s t
interval uses the sample standard deviation (&lt;code&gt;ddof=1&lt;/code&gt;), six degrees of freedom,
and &lt;code&gt;mean ± 2.447 * sample_std / sqrt(7)&lt;/code&gt;. With only seven observations, the
normality assumption matters.
See NIST (National Institute of Standards and Technology)&apos;s &lt;a href=&quot;https://www.itl.nist.gov/div898/handbook/eda/section3/eda352.htm&quot;&gt;confidence limits for the mean&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: The seven values have mean 41.57. Under the stated sampling assumptions, the 95% t confidence interval for the population mean μ is 15.9 to 67.2. The interval reflects both the small sample and its variability.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/inferential-vs-descriptive-statistics/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;The sample mean, 41.57, is a description. The range from 15.9 to 67.2 is an
inference. It is wide because seven observations carry little information and
the data vary a lot. A larger sample or less variable data would narrow it.&lt;/p&gt;
&lt;h2&gt;Describe first, then infer, and state your assumptions&lt;/h2&gt;
&lt;p&gt;Descriptive statistics tells you what is in your sample. Inferential statistics
uses probability to say what that sample suggests about the population, and how
uncertain that suggestion is. Always describe the data before you infer from
it. Then check that your sampling and model assumptions hold, because the
interval is only as trustworthy as they are.&lt;/p&gt;
</content:encoded><category>statistics</category><category>data-science</category></item><item><title>Classic Algorithms Explained</title><link>https://yeison-liscano.github.io/portfolio/blog/algorithms/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/algorithms/</guid><description>An exploration of essential algorithms including Karatsuba multiplication, binary search and sorting algorithms, with a link to graph traversal—plus Python implementations and complexity analysis.</description><pubDate>Sun, 22 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Let&apos;s dive into some of the most important algorithms in computer science.
Whether you&apos;re preparing for technical interviews, optimizing code, or just
curious about how software works under the hood, understanding these classics is
essential. We&apos;ll explore what each algorithm does, why it matters, and how to
implement it.&lt;/p&gt;
&lt;h2&gt;Karatsuba Multiplication&lt;/h2&gt;
&lt;p&gt;Multiplying two numbers might seem straightforward—just use the &lt;code&gt;*&lt;/code&gt; operator,
right? But when you&apos;re dealing with very large numbers (thousands or millions of
digits), the naive approach becomes inefficient. That&apos;s where Karatsuba
multiplication comes in.&lt;/p&gt;
&lt;h3&gt;What It Does&lt;/h3&gt;
&lt;p&gt;Karatsuba multiplication is a divide-and-conquer algorithm that multiplies two
n-digit numbers faster than the grade-school method. Instead of doing a single
O(n²) multiplication, it breaks the problem into smaller pieces and recombines
them cleverly.&lt;/p&gt;
&lt;p&gt;The key insight: to multiply two 2-digit numbers &lt;code&gt;xy&lt;/code&gt; and &lt;code&gt;ab&lt;/code&gt;, you can use
three multiplications instead of four:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Instead of computing all four products from scratch, you reuse intermediate
results&lt;/li&gt;
&lt;li&gt;This reduces the number of recursive multiplications needed&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Time Complexity&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Time:&lt;/strong&gt; O(n^log₂(3)) ≈ O(n^1.585) — much better than O(n²) for large numbers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Space:&lt;/strong&gt; O(n) for storing intermediate results&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Python Implementation&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def karatsuba(x, y):
    &quot;&quot;&quot;
    Multiply two integers using Karatsuba algorithm.

    Args:
        x, y: integers to multiply

    Returns:
        The product x * y
    &quot;&quot;&quot;
    # Base case: small numbers, use standard multiplication
    if x &amp;lt; 10 or y &amp;lt; 10:
        return x * y

    # Determine the number of digits
    n = max(len(str(x)), len(str(y)))
    m = n // 2

    # Split the numbers
    # x = x_high * 10^m + x_low
    # y = y_high * 10^m + y_low
    x_high, x_low = divmod(x, 10**m)
    y_high, y_low = divmod(y, 10**m)

    # Recursive multiplications
    z0 = karatsuba(x_low, y_low)
    z2 = karatsuba(x_high, y_high)
    z1 = karatsuba(x_high + x_low, y_high + y_low) - z0 - z2

    # Combine results
    return z2 * (10 ** (2 * m)) + z1 * (10 ** m) + z0

# Example usage
print(karatsuba(1234, 5678))  # Output: 7006652
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Why It Matters&lt;/h3&gt;
&lt;p&gt;For cryptography and big integer arithmetic (common in blockchain and scientific
computing), Karatsuba provides a significant speedup. Python&apos;s native &lt;code&gt;int&lt;/code&gt; type
uses algorithms like Karatsuba internally for efficient large number
multiplication.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Binary Search&lt;/h2&gt;
&lt;p&gt;If you&apos;ve ever searched through a sorted dictionary, you were doing binary
search in your head. This is one of the most elegant algorithms—simple to
understand, powerful in practice.&lt;/p&gt;
&lt;h3&gt;How Binary Search Works&lt;/h3&gt;
&lt;p&gt;Binary search finds a target value in a &lt;strong&gt;sorted array&lt;/strong&gt; by repeatedly dividing
the search interval in half. At each step, you compare the middle element to
your target and eliminate half the remaining elements.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: Searching for 23 among 11 sorted values. Each iteration keeps only the half that can still hold the target, so 4 comparisons are enough.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/algorithms/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;h3&gt;Binary Search Complexity&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Time:&lt;/strong&gt; O(log n) — incredibly fast even for huge datasets&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Space:&lt;/strong&gt; O(1) for iterative version; O(log n) for recursive (call stack)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Binary Search in Python&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def binary_search(arr, target):
    &quot;&quot;&quot;
    Find target in a sorted array.

    Args:
        arr: sorted list of comparable elements
        target: value to find

    Returns:
        Index of target if found, -1 otherwise
    &quot;&quot;&quot;
    left, right = 0, len(arr) - 1

    while left &amp;lt;= right:
        mid = (left + right) // 2

        if arr[mid] == target:
            return mid
        elif arr[mid] &amp;lt; target:
            # Target is in the right half
            left = mid + 1
        else:
            # Target is in the left half
            right = mid - 1

    return -1  # Not found

# Example usage
numbers = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
print(binary_search(numbers, 7))   # Output: 3
print(binary_search(numbers, 10))  # Output: -1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Why Binary Search Matters&lt;/h3&gt;
&lt;p&gt;Binary search is the foundation of many database and systems techniques. Any
time you need to search a sorted collection—from database indexes to finding the
insertion point in a data structure—binary search principles apply. It&apos;s O(log
n) elegance in action.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Merge Sort&lt;/h2&gt;
&lt;p&gt;Merge sort is a &lt;strong&gt;stable&lt;/strong&gt;, &lt;strong&gt;divide-and-conquer&lt;/strong&gt; sorting algorithm that
consistently delivers O(n log n) performance, regardless of input. It&apos;s widely
used in practice and forms the basis of timsort (used in Python).&lt;/p&gt;
&lt;h3&gt;How Merge Sort Works&lt;/h3&gt;
&lt;p&gt;Merge sort divides an array into single-element arrays (which are trivially
sorted), then merges them back together in sorted order. The merge step is where
the magic happens—combining two sorted arrays into one sorted array in linear
time.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: Eight values split into single elements over log₂ 8 = 3 levels, then merge back in sorted order over 3 more. Every level touches all n values, which is where O(n log n) comes from.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/algorithms/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;h3&gt;Merge Sort Complexity&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Time:&lt;/strong&gt; O(n log n) in all cases (best, average, worst)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Space:&lt;/strong&gt; O(n) — requires extra space for merging&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Merge Sort in Python&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def merge_sort(arr):
    &quot;&quot;&quot;
    Sort array using merge sort algorithm.

    Args:
        arr: list to sort

    Returns:
        Sorted list
    &quot;&quot;&quot;
    if len(arr) &amp;lt;= 1:
        return arr

    # Divide
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])

    # Conquer (merge)
    return merge(left, right)

def merge(left, right):
    &quot;&quot;&quot;
    Merge two sorted lists into one sorted list.
    &quot;&quot;&quot;
    result = []
    i = j = 0

    # Compare elements from left and right, add smaller
    while i &amp;lt; len(left) and j &amp;lt; len(right):
        if left[i] &amp;lt;= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1

    # Add remaining elements
    result.extend(left[i:])
    result.extend(right[j:])

    return result

# Example usage
data = [38, 27, 43, 3, 9, 82, 10]
print(merge_sort(data))  # Output: [3, 9, 10, 27, 38, 43, 82]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Why Merge Sort Matters&lt;/h3&gt;
&lt;p&gt;Merge sort&apos;s consistent O(n log n) performance and stability (equal elements
maintain relative order) make it ideal for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sorting linked lists (no random access needed)&lt;/li&gt;
&lt;li&gt;External sorting (sorting data larger than RAM (Random-Access Memory))&lt;/li&gt;
&lt;li&gt;Any scenario where worst-case guarantee matters&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Quicksort&lt;/h2&gt;
&lt;p&gt;Quicksort is the practical workhorse of sorting algorithms. While its worst-case
is O(n²), its average O(n log n) performance and in-place sorting make it the
default choice in many languages.&lt;/p&gt;
&lt;h3&gt;How Quicksort Works&lt;/h3&gt;
&lt;p&gt;Quicksort uses &lt;strong&gt;divide-and-conquer&lt;/strong&gt; by selecting a pivot element and
partitioning the array around it. Elements smaller than the pivot go left;
larger go right. Recursively sort each partition.&lt;/p&gt;
&lt;h3&gt;Quicksort Complexity&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Time:&lt;/strong&gt; O(n log n) average; O(n²) worst case (poor pivot selection)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auxiliary space for the in-place partition version:&lt;/strong&gt; O(log n) average
(call stack); O(n) worst case.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auxiliary space for the list-building version below:&lt;/strong&gt; O(n) for balanced
partitions; O(n²) in the worst case because recursive calls retain their lists.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Quicksort in Python&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def quicksort(arr):
    &quot;&quot;&quot;
    Return a sorted list using quicksort (allocates new lists).

    Args:
        arr: list to sort

    Returns:
        Sorted list
    &quot;&quot;&quot;
    if len(arr) &amp;lt;= 1:
        return arr

    # Choose pivot (using middle element for better average case)
    pivot = arr[len(arr) // 2]

    # Partition into three groups
    left = [x for x in arr if x &amp;lt; pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x &amp;gt; pivot]

    # Recursively sort and combine
    return quicksort(left) + middle + quicksort(right)

def quicksort_inplace(arr, low=0, high=None):
    &quot;&quot;&quot;
    In-place variant of quicksort (more memory efficient).

    Args:
        arr: list to sort
        low, high: subarray boundaries
    &quot;&quot;&quot;
    if high is None:
        high = len(arr) - 1

    if low &amp;lt; high:
        # Partition and get pivot position
        pivot_index = partition(arr, low, high)
        # Recursively sort left and right
        quicksort_inplace(arr, low, pivot_index - 1)
        quicksort_inplace(arr, pivot_index + 1, high)

    return arr

def partition(arr, low, high):
    &quot;&quot;&quot;Partition array around pivot.&quot;&quot;&quot;
    pivot = arr[high]
    i = low - 1

    for j in range(low, high):
        if arr[j] &amp;lt; pivot:
            i += 1
            arr[i], arr[j] = arr[j], arr[i]

    arr[i + 1], arr[high] = arr[high], arr[i + 1]
    return i + 1

# Example usage
data = [64, 34, 25, 12, 22, 11, 90]
print(quicksort(data))  # Output: [11, 12, 22, 25, 34, 64, 90]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Why Quicksort Matters&lt;/h3&gt;
&lt;p&gt;Quicksort&apos;s in-place partitioning and cache behavior make it useful in production
systems. Introsort combines quicksort with a worst-case fallback. Python&apos;s
Timsort belongs to the merge-sort family, not the quicksort family; CPython&apos;s
implementation uses a Powersort merge policy. See
the &lt;a href=&quot;https://github.com/python/cpython/blob/main/Objects/listsort.txt&quot;&gt;CPython sorting design notes&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Breadth-First Search (BFS)&lt;/h2&gt;
&lt;p&gt;Breadth-First Search is a graph traversal algorithm that explores all vertices
at distance k before exploring vertices at distance k+1, where distance counts
edges. It finds shortest paths in unweighted graphs (or graphs with equal edge
weights), connected components, and level-order traversals.&lt;/p&gt;
&lt;p&gt;Given the depth of BFS and graph algorithms in general, we&apos;ve covered this
thoroughly in our
&lt;strong&gt;dedicated graphs and graph algorithms article&lt;/strong&gt;, including
implementation, applications, and comparison with Depth-First Search.&lt;/p&gt;
&lt;p&gt;BFS is a foundation for more advanced techniques like Dijkstra&apos;s algorithm
(shortest path), level-order tree traversal, and multi-source shortest path
problems.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;When to Use Each Algorithm&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Algorithm&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Key Advantage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Karatsuba&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Very large integer multiplication&lt;/td&gt;
&lt;td&gt;Faster than naive O(n²) approach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Binary Search&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Searching sorted data&lt;/td&gt;
&lt;td&gt;Logarithmic O(log n) complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Merge Sort&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Need consistent O(n log n); stability matters&lt;/td&gt;
&lt;td&gt;Guaranteed performance, stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Quicksort&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;General-purpose sorting&lt;/td&gt;
&lt;td&gt;Fast average case, in-place&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BFS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shortest paths, level-order exploration&lt;/td&gt;
&lt;td&gt;Explores by distance layers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Introduction to Algorithms (CLRS)&lt;/strong&gt; — Cormen, Leiserson, Rivest, Stein. MIT
Press. The definitive algorithms textbook covering all these algorithms in
depth.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Khan Academy: Algorithms Course&lt;/strong&gt; — &lt;a href=&quot;https://www.khanacademy.org/computing/computer-science/algorithms&quot;&gt;https://www.khanacademy.org/computing/computer-science/algorithms&lt;/a&gt;. Free,
interactive explanations of classic algorithms with visualizations.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Python Documentation: Time Complexity&lt;/strong&gt; — &lt;a href=&quot;https://wiki.python.org/moin/TimeComplexity&quot;&gt;https://wiki.python.org/moin/TimeComplexity&lt;/a&gt;. Official Python performance
characteristics for built-in data structures and operations.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Visualgo: Algorithm Visualizer&lt;/strong&gt; — &lt;a href=&quot;https://visualgo.net/&quot;&gt;https://visualgo.net/&lt;/a&gt;. Interactive
visualizations of sorting, searching, and graph algorithms in action.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Big-O Cheat Sheet&lt;/strong&gt; — &lt;a href=&quot;https://www.bigocheatsheet.com/&quot;&gt;https://www.bigocheatsheet.com/&lt;/a&gt;. Quick reference for
time and space complexity of common algorithms.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>programming</category><category>algorithms</category></item><item><title>Artificial Vision and Computer Vision: From Pixels to Understanding</title><link>https://yeison-liscano.github.io/portfolio/blog/artificial-vision/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/artificial-vision/</guid><description>An introduction to computer vision and image processing. From spatial domain operations to frequency transforms, with practical Python examples using OpenCV.</description><pubDate>Sun, 22 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Computer vision, often called artificial vision, is the field of artificial
intelligence that enables computers to interpret and understand the visual
world. It processes digital images and videos to extract meaningful information,
make decisions, and take actions based on visual input. From medical diagnoses
to autonomous vehicles navigating highways, computer vision powers many of
today&apos;s most transformative technologies.&lt;/p&gt;
&lt;p&gt;At its core, computer vision asks a fundamental question: how can we teach
machines to &quot;see&quot; and understand images the way humans do? While human vision is
intuitive, the computational challenges are immense. A digital image is just a
matrix of numbers—pixel values—and extracting semantic meaning from these
numbers requires sophisticated mathematical and computational techniques.&lt;/p&gt;
&lt;p&gt;This article explores the foundational concepts of computer vision and image
processing, starting with basic spatial domain operations and progressing to
more advanced frequency domain techniques. We&apos;ll examine practical applications
and provide working code examples using OpenCV, a widely used computer vision library with Python bindings.
The examples require OpenCV, NumPy and Matplotlib, input images at the stated
paths, and a desktop GUI (Graphical User Interface) for &lt;code&gt;cv2.imshow&lt;/code&gt;. They do not run unchanged in the
browser runner. Later fragments reuse variables from earlier examples.&lt;/p&gt;
&lt;h2&gt;Understanding Visual Computing: Three Distinct Disciplines&lt;/h2&gt;
&lt;p&gt;Visual computing encompasses three closely related but distinct domains, each
with different goals and methodologies.&lt;/p&gt;
&lt;h3&gt;Image Processing: Enhancing and Analyzing Images&lt;/h3&gt;
&lt;p&gt;Image processing is the transformation of an image to enhance it or extract
useful information. It takes one image as input and produces another image (or
data about that image) as output. Image processing focuses on improving image
quality, extracting features, or preparing images for further analysis.&lt;/p&gt;
&lt;p&gt;Common image processing tasks include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Noise reduction and image denoising&lt;/li&gt;
&lt;li&gt;Contrast enhancement&lt;/li&gt;
&lt;li&gt;Feature extraction and edge detection&lt;/li&gt;
&lt;li&gt;Image compression and restoration&lt;/li&gt;
&lt;li&gt;Color space conversions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Image processing is domain-agnostic—the same techniques work across medical
images, photographs, satellite imagery, and microscopy.&lt;/p&gt;
&lt;h3&gt;Computer Graphics: From 3D Models to 2D Display&lt;/h3&gt;
&lt;p&gt;Computer graphics creates or manipulates visual content, including 2D drawings
and 3D rendering. In 3D rendering, models and scenes produce 2D images for display. Graphics rendering involves lighting calculations, texture
mapping, perspective projection, and visual effects.&lt;/p&gt;
&lt;p&gt;While computer vision asks &quot;what is in this image?&quot;, graphics asks &quot;how do I
create this image?&quot; Applications include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;3D scene rendering&lt;/li&gt;
&lt;li&gt;Video game engines&lt;/li&gt;
&lt;li&gt;Scientific visualization&lt;/li&gt;
&lt;li&gt;Virtual and augmented reality&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Computer Vision: Understanding and Interpreting Images&lt;/h3&gt;
&lt;p&gt;Computer vision goes beyond image processing to achieve understanding. It
analyzes images to recognize objects, extract semantic meaning, track motion,
reconstruct 3D scenes, and make intelligent decisions. Vision tasks include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Object detection and classification&lt;/li&gt;
&lt;li&gt;Facial recognition&lt;/li&gt;
&lt;li&gt;Scene understanding&lt;/li&gt;
&lt;li&gt;3D reconstruction&lt;/li&gt;
&lt;li&gt;Motion estimation&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Image Processing in the Spatial Domain&lt;/h2&gt;
&lt;p&gt;Spatial domain processing operates directly on pixel values and their spatial
relationships. Unlike frequency domain methods (which we&apos;ll discuss later),
spatial processing doesn&apos;t transform images into other representations.&lt;/p&gt;
&lt;h3&gt;Geometric Transformations&lt;/h3&gt;
&lt;p&gt;Geometric transformations modify the spatial arrangement of pixels. These
operations change position, size, or orientation without altering pixel
intensities (in the ideal case).&lt;/p&gt;
&lt;h4&gt;Rotation&lt;/h4&gt;
&lt;p&gt;Rotation pivots an image around a center point by a specified angle. When
rotating, we face a fundamental challenge: pixel coordinates in the rotated
image often fall between integer positions of the original image,
requiring interpolation.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import cv2
import numpy as np
import matplotlib.pyplot as plt

# Read image
image = cv2.imread(&apos;photo.jpg&apos;)
height, width = image.shape[:2]
center = (width // 2, height // 2)

# Rotate 45 degrees clockwise (negative angle)
rotation_matrix = cv2.getRotationMatrix2D(center, -45, 1.0)
rotated = cv2.warpAffine(image, rotation_matrix, (width, height))

cv2.imshow(&apos;Rotated Image&apos;, rotated)
cv2.waitKey(0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The rotation matrix is a 2×3 affine transformation matrix that OpenCV applies
using &lt;code&gt;warpAffine&lt;/code&gt;. The third parameter (1.0) is a scale factor for zooming
during rotation.&lt;/p&gt;
&lt;h4&gt;Scaling (Zoom)&lt;/h4&gt;
&lt;p&gt;Scaling resizes an image by a factor. Upscaling (enlarging) requires
interpolation. Downscaling also resamples the image; area interpolation is
often useful for reducing aliasing, but not every resize method averages neighborhoods.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Scale to 50% of original size
scaled_down = cv2.resize(image, (width // 2, height // 2),
                         interpolation=cv2.INTER_AREA)

# Scale to 200% of original size
scaled_up = cv2.resize(image, (width * 2, height * 2),
                       interpolation=cv2.INTER_CUBIC)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Different interpolation methods offer speed-quality tradeoffs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;INTER_NEAREST&lt;/code&gt;: Preserves discrete labels; may look blocky on photos.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INTER_LINEAR&lt;/code&gt;: Bilinear interpolation, a common general-purpose option.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INTER_AREA&lt;/code&gt;: Area-based resampling, often useful for shrinking.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INTER_CUBIC&lt;/code&gt; and &lt;code&gt;INTER_LANCZOS4&lt;/code&gt;: Use larger neighborhoods and may improve
some enlargements, but can introduce ringing. There is no universal best method.
See &lt;a href=&quot;https://docs.opencv.org/4.x/da/d54/group__imgproc__transform.html&quot;&gt;OpenCV geometric transforms&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Translation&lt;/h4&gt;
&lt;p&gt;Translation shifts an image horizontally and vertically using a 2×3 translation
matrix.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Translate 100 pixels right and 50 pixels down
translation_matrix = np.float32([[1, 0, 100], [0, 1, 50]])
translated = cv2.warpAffine(image, translation_matrix, (width, height))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Histogram Processing and Equalization&lt;/h3&gt;
&lt;p&gt;A histogram represents the distribution of pixel intensity values. For grayscale
8-bit images, it shows how many pixels have each intensity from 0 (black) to 255
(white).&lt;/p&gt;
&lt;p&gt;Histogram equalization redistributes pixel intensities to use the full range
more uniformly, improving contrast in low-contrast images.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import cv2
import numpy as np
import matplotlib.pyplot as plt

# Read in grayscale
gray = cv2.imread(&apos;photo.jpg&apos;, cv2.IMREAD_GRAYSCALE)

# Apply histogram equalization
equalized = cv2.equalizeHist(gray)

# For color images, convert to HSV and equalize the V channel
color_image = cv2.imread(&apos;photo.jpg&apos;)
hsv = cv2.cvtColor(color_image, cv2.COLOR_BGR2HSV)
hsv[:, :, 2] = cv2.equalizeHist(hsv[:, :, 2])
equalized_color = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)

# Compare histograms visually
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].hist(gray.ravel(), 256, [0, 256])
axes[0].set_title(&apos;Original Histogram&apos;)
axes[1].hist(equalized.ravel(), 256, [0, 256])
axes[1].set_title(&apos;Equalized Histogram&apos;)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Histogram equalization works by computing a cumulative distribution function
(CDF) and using it to map old intensity values to new ones, spreading them
across the full range. This technique is particularly valuable for medical
imaging, where subtle intensity differences may contain diagnostic information.&lt;/p&gt;
&lt;h3&gt;Arithmetic Operations on Images&lt;/h3&gt;
&lt;p&gt;Arithmetic operations combine two or more images element-wise or apply
mathematical functions to pixel values.&lt;/p&gt;
&lt;h4&gt;Addition&lt;/h4&gt;
&lt;p&gt;Adding images can blend or composite them together:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;image1 = cv2.imread(&apos;photo1.jpg&apos;).astype(np.float32)
image2 = cv2.imread(&apos;photo2.jpg&apos;).astype(np.float32)

# Weighted average: 70% image1, 30% image2
blended = cv2.addWeighted(image1, 0.7, image2, 0.3, 0)

# Convert back to uint8 for display
blended = np.clip(blended, 0, 255).astype(np.uint8)
cv2.imshow(&apos;Blended&apos;, blended)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Image addition is useful for averaging multiple noisy measurements of the same
scene to reduce noise.&lt;/p&gt;
&lt;h4&gt;Subtraction&lt;/h4&gt;
&lt;p&gt;Subtracting images reveals differences:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Detect motion between frames
frame1 = cv2.imread(&apos;frame1.jpg&apos;).astype(np.float32)
frame2 = cv2.imread(&apos;frame2.jpg&apos;).astype(np.float32)

difference = cv2.absdiff(frame1, frame2)
# Keep intensity units: frame inputs are 8-bit images converted to float32.
difference = cv2.cvtColor(difference, cv2.COLOR_BGR2GRAY)
difference = np.clip(difference, 0, 255).astype(np.uint8)

# Use threshold to identify regions of significant change
_, motion_mask = cv2.threshold(difference, 30, 255, cv2.THRESH_BINARY)
cv2.imshow(&apos;Motion&apos;, motion_mask)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Difference images highlight changes and are fundamental to motion detection and
change analysis.&lt;/p&gt;
&lt;h4&gt;Multiplication&lt;/h4&gt;
&lt;p&gt;Pixel-wise multiplication can apply gain or create masks:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Create a mask (values 0-1)
mask = (image &amp;gt; 100).astype(np.float32)

# Apply mask to isolate bright regions
masked_image = image.astype(np.float32) * mask
masked_image = np.clip(masked_image, 0, 255).astype(np.uint8)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Spatial Filtering and Convolution&lt;/h3&gt;
&lt;p&gt;Spatial filters process each pixel using information from its neighborhood.
Linear filters can be implemented using convolution or correlation. Nonlinear
filters, such as the median filter, are not convolutions.&lt;/p&gt;
&lt;h4&gt;Convolution Concept&lt;/h4&gt;
&lt;p&gt;Convolution slides a small kernel (filter matrix) over the image, computing a
weighted sum at each position. For a 2D image, the discrete convolution is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Convolution: Output[i,j] = Σu Σv Kernel[u,v] × Image[i-u, j-v]
Correlation: Output[i,j] = Σu Σv Kernel[u,v] × Image[i+u, j+v]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The indices here are offsets from the kernel center. Convolution flips the
kernel; correlation does not. &lt;code&gt;cv2.filter2D&lt;/code&gt; and the explorable below use
correlation. A symmetric blur kernel gives the same result either way; an
asymmetric derivative kernel can change sign. See
the &lt;a href=&quot;https://docs.opencv.org/4.x/d4/d86/group__imgproc__filter.html&quot;&gt;OpenCV filtering reference&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The kernel defines what features the filter detects or what effect it applies.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: A Sobel x kernel on a bright square. Pick any output pixel to see the 3×3 window it reads and the weighted sum it computes. The response is strongly positive on the square&apos;s left edge, negative on its right edge, and zero where the window sees no horizontal change.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/artificial-vision/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;h4&gt;Common Spatial Filters&lt;/h4&gt;
&lt;h5&gt;Blur (Averaging Filter)&lt;/h5&gt;
&lt;p&gt;A simple averaging kernel reduces noise but also softens edges:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Gaussian blur: weighted average with Gaussian kernel
blurred = cv2.GaussianBlur(image, (5, 5), 1.0)

# Simple box blur: equal weights
box_blurred = cv2.boxFilter(image, -1, (5, 5))

# Median filter: excellent for salt-and-pepper noise
median_filtered = cv2.medianBlur(image, 5)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Edge Detection&lt;/h5&gt;
&lt;p&gt;Edges are detected by computing image gradients. The Sobel operator approximates
derivatives:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# Compute horizontal and vertical gradients
sobel_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
sobel_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)

# Magnitude of gradient
edge_magnitude = np.sqrt(sobel_x**2 + sobel_y**2)
maximum = edge_magnitude.max()
if maximum &amp;gt; 0:
    edge_magnitude = edge_magnitude / maximum * 255
edge_magnitude = edge_magnitude.astype(np.uint8)

# Canny edge detector (more sophisticated)
smoothed = cv2.GaussianBlur(gray, (5, 5), 1.4)
edges = cv2.Canny(smoothed, 100, 200)

cv2.imshow(&apos;Edges&apos;, edges)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The conceptual Canny pipeline smooths the image, computes gradients,
performs non-maximum suppression, and uses hysteresis thresholding—it&apos;s a complete edge
detection pipeline. The example smooths explicitly before calling &lt;code&gt;cv2.Canny&lt;/code&gt;.&lt;/p&gt;
&lt;h5&gt;Sharpening&lt;/h5&gt;
&lt;p&gt;Sharpening enhances edges by subtracting a blurred version from the original:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Unsharp mask: sharpening via subtraction
blurred = cv2.GaussianBlur(gray, (0, 0), 2.0)
sharpened = cv2.addWeighted(gray, 1.5, blurred, -0.5, 0)
sharpened = np.clip(sharpened, 0, 255).astype(np.uint8)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Image Processing in the Frequency Domain&lt;/h2&gt;
&lt;p&gt;While spatial domain processing is intuitive and direct, frequency domain
analysis provides powerful insights by decomposing images into sine and cosine
waves of different frequencies.&lt;/p&gt;
&lt;h3&gt;Introduction to the Fourier Transform&lt;/h3&gt;
&lt;p&gt;The Fourier transform converts an image from the spatial domain (pixel intensity
as a function of position) to the frequency domain (amplitude and phase as a
function of frequency). This fundamental tool reveals which patterns and
frequencies dominate an image.&lt;/p&gt;
&lt;p&gt;The 2D Discrete Fourier Transform (DFT) is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;F[u,v] = Σ(x=0..M-1) Σ(y=0..N-1) f[x,y] × exp(-2πi(ux/M + vy/N))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;where f[x,y] is the pixel value and F[u,v] is the frequency component.&lt;/p&gt;
&lt;h3&gt;Why Use the Frequency Domain?&lt;/h3&gt;
&lt;p&gt;Frequency domain processing offers advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Efficient filtering&lt;/strong&gt;: Some operations are faster in the frequency domain&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Insight&lt;/strong&gt;: Visualizing the frequency spectrum reveals image properties&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compression&lt;/strong&gt;: Many compression algorithms work in the frequency domain
(e.g., JPEG (Joint Photographic Experts Group, the committee that created the format) uses DCT (Discrete Cosine Transform))&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Analysis&lt;/strong&gt;: Periodic patterns and noise characteristics become obvious&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np
import matplotlib.pyplot as plt

# Compute FFT
image_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY).astype(np.float32)
fft = np.fft.fft2(image_gray)
fft_shifted = np.fft.fftshift(fft)  # Move zero frequency to center

# Magnitude spectrum (logarithmic scale for visualization)
magnitude = 20 * np.log1p(np.abs(fft_shifted))

# Phase spectrum
phase = np.angle(fft_shifted)

# Display
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.imshow(magnitude, cmap=&apos;gray&apos;)
plt.title(&apos;Magnitude Spectrum&apos;)
plt.colorbar()

plt.subplot(1, 2, 2)
plt.imshow(phase, cmap=&apos;gray&apos;)
plt.title(&apos;Phase Spectrum&apos;)
plt.colorbar()
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The magnitude spectrum shows which frequencies are strong; the phase carries
information about where features are located. Phase-only reconstructions can
preserve recognizable structure for some images, but both magnitude and phase
are needed for exact reconstruction; this is not a universal ranking of importance.&lt;/p&gt;
&lt;h2&gt;Practical Example: Building a Complete Image Processing Pipeline&lt;/h2&gt;
&lt;p&gt;Let&apos;s combine multiple techniques in a realistic scenario: enhancing a
low-contrast grayscale photograph. This is an image-processing demonstration,
not a clinically validated medical-imaging workflow.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import cv2
import numpy as np
import matplotlib.pyplot as plt

# Read image
image = cv2.imread(&apos;photo.jpg&apos;, cv2.IMREAD_GRAYSCALE)
if image is None:
    raise FileNotFoundError(&apos;photo.jpg&apos;)

# Step 1: Denoise using bilateral filter
# (preserves edges while smoothing)
denoised = cv2.bilateralFilter(image, 9, 75, 75)

# Step 2: Enhance contrast using CLAHE
# (Contrast Limited Adaptive Histogram Equalization)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
enhanced = clahe.apply(denoised)

# Step 3: Edge enhancement using high-pass filter
blurred = cv2.GaussianBlur(enhanced, (0, 0), 2.0)
high_pass = enhanced.astype(np.float32) - blurred.astype(np.float32)
sharpened = np.clip(enhanced.astype(np.float32) + 0.5 * high_pass, 0, 255)

# Step 4: Normalize intensity
normalized = cv2.normalize(sharpened, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)

# Display results
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
axes[0, 0].imshow(image, cmap=&apos;gray&apos;)
axes[0, 0].set_title(&apos;Original&apos;)
axes[0, 1].imshow(denoised, cmap=&apos;gray&apos;)
axes[0, 1].set_title(&apos;Denoised&apos;)
axes[0, 2].imshow(enhanced, cmap=&apos;gray&apos;)
axes[0, 2].set_title(&apos;Enhanced&apos;)
axes[1, 0].imshow(high_pass, cmap=&apos;gray&apos;)
axes[1, 0].set_title(&apos;High-Pass Filter&apos;)
axes[1, 1].imshow(sharpened, cmap=&apos;gray&apos;)
axes[1, 1].set_title(&apos;Sharpened&apos;)
axes[1, 2].imshow(normalized, cmap=&apos;gray&apos;)
axes[1, 2].set_title(&apos;Normalized&apos;)

for ax in axes.flat:
    ax.axis(&apos;off&apos;)
plt.tight_layout()
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pipeline demonstrates how combining multiple techniques—denoising, contrast
enhancement, sharpening, and normalization—changes an image. Whether it helps
depends on the task; it may also amplify noise or distort useful information.&lt;/p&gt;
&lt;h2&gt;Common Applications of Computer Vision&lt;/h2&gt;
&lt;p&gt;Computer vision technology powers countless real-world applications:&lt;/p&gt;
&lt;h3&gt;Medical Imaging&lt;/h3&gt;
&lt;p&gt;Computer vision assists in diagnosis and treatment planning. Algorithms detect
tumors, segment organs, measure anatomical structures, and register images
across time or modalities (CT (Computed Tomography), MRI (Magnetic Resonance Imaging), ultrasound). Performance depends on the
specific task, data and clinical validation; success on a benchmark does not
establish safe use in a clinical workflow.&lt;/p&gt;
&lt;h3&gt;Autonomous Driving&lt;/h3&gt;
&lt;p&gt;Self-driving vehicles rely on multiple cameras and sophisticated vision
algorithms. Tasks include lane detection, vehicle and pedestrian detection,
traffic sign recognition, and 3D scene understanding from monocular or stereo
cameras. Real-time performance is critical—decisions must be made in
milliseconds.&lt;/p&gt;
&lt;h3&gt;Quality Control and Inspection&lt;/h3&gt;
&lt;p&gt;Manufacturing facilities use vision systems for automated quality inspection.
Cameras detect defects, measure dimensions, verify component assembly, and
identify damaged products at production speeds—faster and more consistently than
human inspectors.&lt;/p&gt;
&lt;h3&gt;Robotics and Manipulation&lt;/h3&gt;
&lt;p&gt;Robots use vision for navigation, obstacle avoidance, object grasping, and
hand-eye coordination. Industrial robots identify parts, calculate grasp poses,
and verify task completion using sophisticated vision systems.&lt;/p&gt;
&lt;h3&gt;Surveillance and Security&lt;/h3&gt;
&lt;p&gt;Security systems analyze video feeds for intrusion detection, crowd monitoring,
anomaly detection, and forensic investigation. Modern systems use deep learning
for person tracking, behavior analysis, and event detection.&lt;/p&gt;
&lt;h3&gt;Face Recognition and Biometrics&lt;/h3&gt;
&lt;p&gt;Face detection and recognition unlock phones, control access to buildings,
identify individuals in photos, and power law enforcement applications. Similar
techniques work for iris recognition, fingerprint analysis, and gait
recognition.&lt;/p&gt;
&lt;h3&gt;Remote Sensing and GIS&lt;/h3&gt;
&lt;p&gt;Satellite and aerial imagery analysis supports agriculture (crop monitoring,
yield prediction), urban planning, environmental monitoring, disaster response,
and climate change analysis.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Computer vision transforms raw pixels into actionable insights and intelligent
decisions. Understanding the foundational techniques—geometric transformations,
histogram processing, spatial filtering, and frequency domain analysis—provides
the conceptual foundation for more advanced methods like deep learning.&lt;/p&gt;
&lt;p&gt;Modern computer vision increasingly relies on deep neural networks that
automatically learn visual features from data. However, the classical techniques
discussed here remain important: they&apos;re computationally efficient,
interpretable, and foundational to understanding how modern methods work.&lt;/p&gt;
&lt;p&gt;Whether you&apos;re processing medical images, building autonomous systems, or
developing creative visual applications, the principles and code examples in
this article provide a practical starting point for your computer vision
journey.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;OpenCV Documentation&lt;/strong&gt;: &lt;a href=&quot;https://docs.opencv.org/&quot;&gt;https://docs.opencv.org/&lt;/a&gt; - Comprehensive reference
for the OpenCV library with tutorials and API (Application Programming Interface) documentation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Gonzalez, R. C., &amp;amp; Woods, R. E. (2018). Digital Image Processing (4th ed.).
Pearson.&lt;/strong&gt; - The definitive textbook on digital image processing, covering
both spatial and frequency domain techniques with rigorous mathematical
foundations.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Stanford CS231n: Convolutional Neural Networks for Visual Recognition&lt;/strong&gt;: &lt;a href=&quot;https://cs231n.stanford.edu/&quot;&gt;https://cs231n.stanford.edu/&lt;/a&gt; - Excellent course covering image
classification, feature extraction, and the connection between classical image
processing and modern deep learning.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Bradski, G., &amp;amp; Kaehler, A. (2008). Learning OpenCV: Computer Vision with the
OpenCV Library. O&apos;Reilly Media.&lt;/strong&gt; - Practical guide to OpenCV with real-world
examples and applications.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Forsyth, D. A., &amp;amp; Ponce, J. (2003). Computer Vision: A Modern Approach.
Pearson.&lt;/strong&gt; - Comprehensive textbook covering geometry, features, recognition,
and 3D vision.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>ai</category><category>python</category></item><item><title>Dynamic Programming</title><link>https://yeison-liscano.github.io/portfolio/blog/dynamic-programming/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/dynamic-programming/</guid><description>Learn how dynamic programming works, when to use it, and how to model solutions using top-down memoization and bottom-up tabulation.</description><pubDate>Sun, 22 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Dynamic programming is a powerful technique used to solve complex problems by
breaking them down into simpler sub-problems and reusing partial results.&lt;/p&gt;
&lt;p&gt;It is built on two ideas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Optimal substructure&lt;/strong&gt;: the best answer for a problem can be built from the
best answers of smaller sub-problems.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Overlapping sub-problems&lt;/strong&gt;: the same smaller problems appear repeatedly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These properties can make reuse effective, but polynomial running time also
requires a polynomial number of states and work per state. Some dynamic programs
remain exponential or pseudopolynomial. Counting problems such as Fibonacci use
the same approach without an optimization objective.&lt;/p&gt;
&lt;p&gt;The Fibonacci examples assume a nonnegative integer &lt;code&gt;n&lt;/code&gt;. Their O(n) time and
O(1) optimized space count arithmetic operations and stored integers; Python&apos;s
arbitrary-precision integers grow with &lt;code&gt;n&lt;/code&gt;, so bit costs are larger.&lt;/p&gt;
&lt;h2&gt;Why Dynamic Programming Matters&lt;/h2&gt;
&lt;p&gt;Many algorithms re-compute the same values over and over. Dynamic programming
stores those intermediate results so each one is computed only once.&lt;/p&gt;
&lt;p&gt;This often gives large performance gains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Time: from $O(2^n)$ to $O(n)$ or $O(n^2)$ in many classic cases.&lt;/li&gt;
&lt;li&gt;Space: additional memory is used to save computed states.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key trade-off is clear: we spend memory to save time.&lt;/p&gt;
&lt;h2&gt;A Useful Mental Model&lt;/h2&gt;
&lt;p&gt;To solve a dynamic programming problem, define the following pieces first:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;State&lt;/strong&gt;: what sub-problem are you solving?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transition&lt;/strong&gt;: how does one state depend on previous states?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Base cases&lt;/strong&gt;: what states are already known?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Answer&lt;/strong&gt;: which state represents the final solution?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you can clearly write these four parts, implementation becomes much easier.&lt;/p&gt;
&lt;h2&gt;Top-Down Approach (Memoization)&lt;/h2&gt;
&lt;p&gt;Top-down dynamic programming starts with the final question and recursively
solves smaller questions, caching answers as they are discovered.&lt;/p&gt;
&lt;h3&gt;Fibonacci with Memoization&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from functools import lru_cache


@lru_cache(maxsize=None)
def fibonacci(n: int) -&amp;gt; int:
  if n &amp;lt;= 1:
    return n
  return fibonacci(n - 1) + fibonacci(n - 2)


print(fibonacci(10))  # 55
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using a cache avoids repeated calls like &lt;code&gt;fibonacci(5)&lt;/code&gt; being computed many
times.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: The naive fibonacci(5) makes 15 calls. With @lru_cache, each value from f(0) to f(5) is computed once, the 3 repeats become cache hits, and the 6 calls below them never happen.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/dynamic-programming/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;Bottom-Up Approach (Tabulation)&lt;/h2&gt;
&lt;p&gt;Bottom-up dynamic programming starts from base cases and iteratively builds
toward the final answer.&lt;/p&gt;
&lt;h3&gt;Fibonacci with Tabulation&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def fibonacci(n: int) -&amp;gt; int:
  if n &amp;lt;= 1:
    return n

  dp = [0] * (n + 1)
  dp[1] = 1

  for i in range(2, n + 1):
    dp[i] = dp[i - 1] + dp[i - 2]

  return dp[n]


print(fibonacci(10))  # 55
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This version avoids recursion overhead and is often preferred in performance
sensitive paths.&lt;/p&gt;
&lt;h2&gt;Space Optimization Example&lt;/h2&gt;
&lt;p&gt;Some dynamic programming transitions only depend on a few previous states. In
that case, you can store only those values.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def fibonacci(n: int) -&amp;gt; int:
  if n &amp;lt;= 1:
    return n

  prev2, prev1 = 0, 1
  for _ in range(2, n + 1):
    prev2, prev1 = prev1, prev1 + prev2

  return prev1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This reduces the number of stored Fibonacci values from $O(n)$ to $O(1)$.&lt;/p&gt;
&lt;h2&gt;Another Classic Problem: Minimum Coin Change&lt;/h2&gt;
&lt;p&gt;Problem: given coin values and a target amount, find the minimum number of coins
needed to make that amount, with unlimited copies of each coin. Assume positive
integer coin values and a nonnegative integer amount. The time cost is
O(amount × number of denominations), pseudopolynomial in the amount&apos;s bit length.&lt;/p&gt;
&lt;p&gt;State definition:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[a]&lt;/code&gt; = minimum number of coins to make amount &lt;code&gt;a&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Transition:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[a] = min(dp[a], dp[a - coin] + 1)&lt;/code&gt; for each valid &lt;code&gt;coin&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;def min_coins(coins: list[int], amount: int) -&amp;gt; int:
  inf = amount + 1
  dp = [inf] * (amount + 1)
  dp[0] = 0

  for a in range(1, amount + 1):
    for coin in coins:
      if coin &amp;lt;= a:
        dp[a] = min(dp[a], dp[a - coin] + 1)

  return dp[amount] if dp[amount] != inf else -1


print(min_coins([1, 3, 4], 6))  # 2 (3 + 3)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Common Pitfalls&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Defining the wrong state.&lt;/li&gt;
&lt;li&gt;Forgetting base cases.&lt;/li&gt;
&lt;li&gt;Missing transitions.&lt;/li&gt;
&lt;li&gt;Using mutable default arguments in recursive functions.&lt;/li&gt;
&lt;li&gt;Not checking memory usage when the state space is large.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How to Decide If DP Applies&lt;/h2&gt;
&lt;p&gt;Ask these questions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Can the problem be split into smaller versions of itself?&lt;/li&gt;
&lt;li&gt;Do those smaller versions repeat?&lt;/li&gt;
&lt;li&gt;Can I combine smaller optimal answers into a larger optimal answer?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the answer is yes to all three, dynamic programming is likely a good fit.&lt;/p&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Dynamic programming is not one algorithm, but a way of thinking. Once you
practice state design and transitions, many hard problems become manageable and
systematic to solve.&lt;/p&gt;
</content:encoded><category>programming</category><category>algorithms</category></item><item><title>Economía Básica</title><link>https://yeison-liscano.github.io/portfolio/blog/economia-b%C3%A1sica/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/economia-b%C3%A1sica/</guid><description>Conceptos fundamentales de economía: transacciones, oferta y demanda, crédito, ciclos económicos, y más. Una guía práctica para entender cómo funciona la economía.</description><pubDate>Sun, 22 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Palabras claves: economía, oferta, demanda, competencia perfecta, monopolio,
dinero, tiempo, valor&lt;/p&gt;
&lt;p&gt;La economía es el conjunto de actividades (transacciones) que se realizan para
satisfacer las necesidades de las personas. Estas actividades incluyen la
producción, la distribución, el intercambio y el consumo de bienes y servicios.
Entender los principios fundamentales de la economía es esencial para tomar
decisiones financieras informadas, tanto a nivel personal como profesional.&lt;/p&gt;
&lt;h2&gt;Transacciones&lt;/h2&gt;
&lt;p&gt;Una transacción es el canje de bienes y servicios entre dos o más partes. En
esencia, toda transacción involucra un intercambio de valor donde ambas partes
esperan mejorar su situación.&lt;/p&gt;
&lt;p&gt;Las transacciones modernas se realizan utilizando dinero y/o crédito como medio
de intercambio. Estos recursos se intercambian por activos financieros,
mercancías, servicios y otros bienes de valor. Los participantes principales en
las transacciones económicas son las empresas, los gobiernos y los bancos,
aunque también incluye a individuos y consumidores.&lt;/p&gt;
&lt;p&gt;El gasto es un &lt;strong&gt;flujo durante un período&lt;/strong&gt;, mientras que el dinero y el crédito
disponible se miden como saldos en un momento. Por eso no existe la identidad
«dinero + crédito = gasto total»: una misma unidad monetaria puede gastarse
varias veces y una línea de crédito puede no utilizarse. El crédito puede
financiar gasto, pero no se convierte automáticamente en él.&lt;/p&gt;
&lt;h2&gt;Mercado&lt;/h2&gt;
&lt;p&gt;Un mercado es el lugar (físico o virtual) donde se realizan transacciones de
compra y venta del mismo producto o servicio. Los mercados son mecanismos
eficientes para que compradores y vendedores se encuentren y acuerden precios.&lt;/p&gt;
&lt;p&gt;Existen mercados para prácticamente todo: desde bienes de consumo hasta valores
financieros, servicios profesionales y activos reales. La característica
principal de un mercado es que permite la formación de precios a través de la
interacción entre oferta y demanda, creando un equilibrio entre lo que los
productores quieren vender y lo que los consumidores quieren comprar.&lt;/p&gt;
&lt;p&gt;Los mercados pueden ser locales, nacionales o internacionales. También pueden
ser más o menos regulados dependiendo del país y el tipo de bien o servicio que
se comercialice. La información disponible en el mercado, la transparencia de
precios y la facilidad para participar influyen en la eficiencia del mercado.&lt;/p&gt;
&lt;h2&gt;Crédito&lt;/h2&gt;
&lt;p&gt;El crédito es un acuerdo para recibir recursos hoy y pagarlos después. Puede
financiar una transacción, pero no es sinónimo de dinero. Al conceder un
préstamo, un banco comercial normalmente crea un depósito y un activo crediticio;
su capacidad está limitada por regulación, riesgo, rentabilidad y demanda.
Véase &lt;a href=&quot;https://www.bankofengland.co.uk/quarterly-bulletin/2014/q1/money-creation-in-the-modern-economy&quot;&gt;Money creation in the modern economy, del Banco de Inglaterra&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Los participantes en operaciones de crédito son los prestadores (o acreedores) y
los prestatarios (o deudores). El prestador proporciona recursos con la
expectativa de ser reembolsado en el futuro, generalmente con intereses. El
prestatario recibe recursos hoy a cambio de la obligación de devolver una
cantidad mayor en el futuro.&lt;/p&gt;
&lt;p&gt;El crédito actúa como un poderoso motor de crecimiento económico a corto plazo.
Permite a empresas e individuos invertir en proyectos, comprar bienes y expandir
operaciones sin esperar a acumular ahorros. Sin embargo, este crecimiento
acelerado viene con riesgos: si el crecimiento futuro no materializa o el
prestatario no puede devolver el crédito, se produce una crisis de
endeudamiento.&lt;/p&gt;
&lt;h2&gt;Tasa de Interés&lt;/h2&gt;
&lt;p&gt;La tasa de interés es el precio del dinero. Puede expresar tanto la rentabilidad
de un activo financiero como el costo de un crédito. Es uno de los instrumentos
más importantes que tiene un banco central para regular la economía.&lt;/p&gt;
&lt;p&gt;Para que una economía se mantenga estable, la inflación debe mantenerse dentro
de un rango establecido por el banco central. Esto es especialmente importante
para preservar el poder adquisitivo de la moneda. El Índice de Precios al
Consumidor (IPC) es el indicador clave que los bancos centrales monitorean.
Cuando la inflación sube demasiado, el banco central aumenta la tasa de interés
para reducir la cantidad de dinero en circulación y disminuir la demanda. Cuando
la inflación es demasiado baja, el banco central disminuye la tasa de interés
para estimular el gasto y la inversión.&lt;/p&gt;
&lt;p&gt;En un préstamo simple sin pagos intermedios ni comisiones, el importe total
a devolver es &lt;strong&gt;principal + intereses&lt;/strong&gt;. Eso no define el crédito en general. El banco central de
cada país establece la tasa de interés de referencia, que sirve como base para
todas las otras tasas en la economía. Esta tasa es una herramienta fundamental
para conservar el poder adquisitivo de la moneda y controlar tanto la inflación
como el crecimiento económico.&lt;/p&gt;
&lt;p&gt;De forma simplificada, y con rezagos y efectos que dependen del contexto:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tasa de interés baja&lt;/strong&gt; → más crédito disponible → más gasto → más producción
(activa la economía)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tasas de interés altas&lt;/strong&gt; → menos crédito disponible → menos gasto → menos
producción (frena la economía)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Un concepto importante es que el crédito se convierte en deuda. Para el
prestador es un activo (dinero que le deben), mientras que para el prestatario
es un pasivo (dinero que debe pagar).&lt;/p&gt;
&lt;h2&gt;Ciclo Económico&lt;/h2&gt;
&lt;p&gt;Los ciclos económicos son fluctuaciones de la actividad agregada. Una
clasificación habitual distingue cuatro etapas o puntos de giro:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Expansión&lt;/strong&gt;: aumenta la actividad económica.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pico&lt;/strong&gt;: nivel máximo antes de la contracción; no es la máxima tasa de crecimiento.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contracción o recesión&lt;/strong&gt;: descenso significativo de la actividad; crecer más
despacio no equivale por sí solo a una recesión.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Valle&lt;/strong&gt;: mínimo desde el que empieza la siguiente expansión.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La recuperación es parte de la expansión. Una depresión es una contracción
especialmente profunda y prolongada, no una etapa obligatoria de cada ciclo.
La duración y las causas varían: pueden intervenir crédito, demanda, política
monetaria y fiscal, tecnología o choques externos. Véanse
las &lt;a href=&quot;https://www.nber.org/research/business-cycle-dating/business-cycle-dating-procedure-frequently-asked-questions&quot;&gt;definiciones del ciclo&lt;/a&gt; de la NBER (National Bureau of Economic Research (Oficina Nacional de Investigación Económica de EE. UU.)).&lt;/p&gt;
&lt;h2&gt;Crecimiento Económico&lt;/h2&gt;
&lt;p&gt;El crecimiento de la producción depende, entre otros factores, de la cantidad
de trabajo, la acumulación de capital y la productividad. El crédito puede
financiar inversión que amplíe esa capacidad o gasto corriente; sus efectos
dependen de qué se financie y de la capacidad de pago.&lt;/p&gt;
&lt;p&gt;Productividad y deuda no son fuerzas excluyentes. Una empresa puede endeudarse
para mejorar su productividad, y tanto la innovación como el crédito pueden
acompañar fluctuaciones económicas. La productividad es especialmente importante
para el crecimiento sostenido del ingreso por persona.&lt;/p&gt;
&lt;h2&gt;Productividad&lt;/h2&gt;
&lt;p&gt;La productividad mide cuánto producimos y, en última instancia, determina cuánto
podemos consumir de forma sostenible. Es la base real del bienestar económico:
el verdadero soporte del crecimiento económico a largo plazo.&lt;/p&gt;
&lt;p&gt;Existen diferentes tipos de productividad: productividad del trabajo (cuánto
produce un trabajador por hora), productividad del capital (cuánto ingreso
genera una unidad de capital), y productividad total de factores (que mide la
eficiencia con la que se combinan trabajo, capital y otros recursos).&lt;/p&gt;
&lt;p&gt;Aumentar la productividad requiere inversión en educación, tecnología,
infraestructura y mejora de procesos. Los países con mayor productividad tienden
a tener mayores ingresos per cápita, mejores salarios y mayor calidad de vida.
La inversión en productividad es la vía sostenible hacia el bienestar económico.&lt;/p&gt;
&lt;h2&gt;Ciclo de Deuda a Corto Plazo&lt;/h2&gt;
&lt;p&gt;El ciclo de deuda a corto plazo es un mecanismo fundamental que impulsa la
economía en períodos de expansión:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Crédito → Deuda → Gasto → Producción → Ingreso → Más Crédito&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;No hay una duración fija ni una periodicidad garantizada para este mecanismo. Cuando los bancos otorgan más crédito,
las personas y empresas gastan más, lo que impulsa la producción, aumenta el
empleo y genera más ingreso. Con más ingreso, la capacidad de tomar más crédito
aumenta, alimentando el ciclo. Sin embargo, este ciclo no es infinito:
eventualmente, la deuda acumulada se vuelve demasiado pesada de servir, y el
ciclo puede invertirse y contribuir a una recesión; no toda expansión crediticia
termina necesariamente así.&lt;/p&gt;
&lt;h2&gt;Ciclo de Deuda a Largo Plazo&lt;/h2&gt;
&lt;p&gt;Mientras que el ciclo corto dura algunos años, el ciclo de deuda a largo plazo
puede extenderse durante décadas. Este ciclo refleja la acumulación sostenida de
deuda en la economía, con períodos largos de crecimiento seguidos de ajustes
severos.&lt;/p&gt;
&lt;p&gt;El ciclo largo se construye sobre múltiples ciclos cortos. Cada ciclo corto
puede dejar un residuo de deuda que no se liquida completamente, acumulándose
gradualmente. Cuando esta deuda acumulada se vuelve insostenible en relación al
ingreso, ocurre una desaceleración severa o una &quot;gran recesión&quot; que puede durar
varios años.&lt;/p&gt;
&lt;p&gt;El fin de un ciclo de deuda largo requiere generalmente de intervenciones
significativas: reducción de deuda (quitas), reestructuración de deudas, aumento
de productividad para que la economía crezca más rápido que la deuda, o una
combinación de estos factores. Comprender estos ciclos largos es esencial para
interpretar riesgos, pero no permite fechar de forma fiable la próxima crisis.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Costo de Oportunidad&lt;/h2&gt;
&lt;p&gt;El costo de oportunidad es el valor de la mejor alternativa que no elegimos.
Cuando decidimos hacer algo, renunciamos automáticamente a todas las otras cosas
que podríamos haber hecho en ese mismo tiempo o con esos mismos recursos.&lt;/p&gt;
&lt;p&gt;Por ejemplo, si inviertes tu dinero en acciones de una empresa, el costo de
oportunidad es el retorno que hubieras obtenido si lo hubieras invertido en
bonos, bienes raíces, o dejado en una cuenta de ahorros. Este concepto es
fundamental para tomar buenas decisiones económicas porque nos obliga a comparar
realmente todas las opciones disponibles.&lt;/p&gt;
&lt;p&gt;Entender el costo de oportunidad nos ayuda a ser más racionales en nuestras
decisiones. No se trata solo de si algo es bueno en términos absolutos, sino si
es mejor que las alternativas disponibles.&lt;/p&gt;
&lt;h2&gt;Distribución del Ingreso&lt;/h2&gt;
&lt;p&gt;La distribución del ingreso es la forma en que se reparte el ingreso entre los
miembros de una sociedad. Es un tema fundamental en economía porque determina la
desigualdad económica, la accesibilidad a oportunidades y la estabilidad social.&lt;/p&gt;
&lt;p&gt;Una distribución muy desigual del ingreso puede indicar una economía donde las
oportunidades están concentradas en pocas manos, lo que limita la movilidad
social y el crecimiento económico inclusivo. Por el contrario, una distribución
más equitativa tiende a promover mayor acceso a educación, salud y oportunidades
de negocio para una proporción mayor de la población.&lt;/p&gt;
&lt;p&gt;La distribución del ingreso se mide frecuentemente usando
el &lt;a href=&quot;https://en.wikipedia.org/wiki/Gini_coefficient&quot;&gt;Coeficiente de Gini&lt;/a&gt;, que varía
de 0 (igualdad perfecta) a 1 (desigualdad máxima). Los gobiernos pueden influir
en la distribución del ingreso a través de impuestos progresivos, programas de
transferencias sociales, inversión en educación pública y regulación del mercado
laboral. Es un tema de gran importancia política, social y económica.&lt;/p&gt;
&lt;h2&gt;Escasez&lt;/h2&gt;
&lt;p&gt;La escasez es la falta de recursos suficientes para satisfacer todas las
necesidades y deseos de una población. Es un problema económico fundamental que
afecta a todos los países, aunque en diferentes grados.&lt;/p&gt;
&lt;p&gt;La escasez es lo que hace que la economía sea necesaria: si tuviéramos recursos
ilimitados, no habría que tomar decisiones sobre asignación. Todos los bienes
económicos son escasos en el sentido de que hay una cantidad limitada
disponible. Esta realidad obliga a las sociedades a responder tres preguntas
fundamentales: ¿Qué producir?, ¿Cómo producir?, y ¿Para quién producir?&lt;/p&gt;
&lt;p&gt;La escasez es relativa, no absoluta. Una sociedad puede ser escasa en algunos
recursos pero abundante en otros. Además, la tecnología y la innovación pueden
reducir la escasez de ciertos bienes (por ejemplo, la energía solar reduce la
escasez de energía).&lt;/p&gt;
&lt;h2&gt;Oferta y Demanda&lt;/h2&gt;
&lt;p&gt;La oferta y la demanda son dos conceptos fundamentales que determinan los
precios y las cantidades en los mercados. La oferta representa la cantidad de
bienes o servicios que los productores están dispuestos a ofrecer en el mercado
a diferentes precios. La demanda, por otro lado, representa la cantidad de
bienes o servicios que los consumidores desean comprar a diferentes precios.&lt;/p&gt;
&lt;p&gt;Manteniendo constantes los demás factores, al subir el precio suele disminuir
la &lt;strong&gt;cantidad demandada&lt;/strong&gt; y aumentar la &lt;strong&gt;cantidad ofrecida&lt;/strong&gt;. Son movimientos
sobre las curvas. Cambios en ingresos, costos o preferencias desplazan las
curvas de demanda u oferta. El precio de
equilibrio es aquel en el que la cantidad ofrecida iguala la cantidad demandada.&lt;/p&gt;
&lt;p&gt;Cuando la oferta es menor que la demanda, hay escasez y los precios tienden a
subir. Cuando la oferta es mayor que la demanda, hay exceso de inventario y los
precios tienden a bajar. Este es un modelo de ajuste, no una garantía de equilibrio instantáneo.
Información imperfecta, poder de mercado, costos de ajuste, contratos y
regulaciones pueden influir en la respuesta.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: Mueve la demanda o la oferta y observa el nuevo equilibrio. Si la demanda sube, suben el precio y la cantidad; si la oferta sube, el precio baja y la cantidad sube. Las líneas punteadas grises son las curvas originales.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/economia-b%C3%A1sica/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;Tipos de Mercado&lt;/h2&gt;
&lt;p&gt;Existen diferentes estructuras de mercado que afectan cómo se forman los precios
y cómo se comportan las empresas:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Competencia Perfecta&lt;/strong&gt;: Muchos vendedores y compradores, productos homogéneos,
libre entrada y salida del mercado. En este tipo de mercado, ningún vendedor
individual puede influir en el precio. Los precios se determinan por la oferta y
demanda del mercado completo. Ejemplos aproximados incluyen mercados agrícolas
muy grandes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Monopolio&lt;/strong&gt;: Un único vendedor domina el mercado, con barreras significativas
a la entrada. El monopolista tiene poder de mercado y puede influir en los
precios. Sin embargo, enfrenta la restricción de que a precios más altos, menos
personas comprarán. Los monopolios pueden surgir por economías de escala (es más
eficiente que una empresa provea un servicio) o por control de recursos clave.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Oligopolio&lt;/strong&gt;: Pocos vendedores grandes dominan el mercado. Hay
interdependencia entre las empresas: cada empresa debe considerar cómo sus
decisiones de precio y cantidad afectarán a sus competidores. Ejemplos incluyen
la industria de telecomunicaciones, aviación y manufactura automotriz.&lt;/p&gt;
&lt;p&gt;Entender la estructura del mercado es importante porque afecta los precios que
pagamos, la innovación en la industria, y la rentabilidad de las empresas.&lt;/p&gt;
&lt;h2&gt;Rentabilidad y Utilidad&lt;/h2&gt;
&lt;p&gt;La rentabilidad y la utilidad son conceptos relacionados pero diferentes que
miden el éxito financiero de una inversión.&lt;/p&gt;
&lt;p&gt;La rentabilidad es la relación porcentual entre el beneficio obtenido y la
inversión realizada. Es una medida relativa que permite comparar el retorno de
diferentes inversiones de diferentes tamaños. Si inviertes 100 pesos y recibes 120 al final, tu rentabilidad es del 20 %.
Invertir un millón y recibir 1,2 millones da el mismo porcentaje.&lt;/p&gt;
&lt;p&gt;La utilidad, por otro lado, es el montante absoluto de dinero que se obtiene de
la venta de un bien, servicio o inversión después de restar los costos. Es una
medida en términos de dinero, no de porcentaje. La utilidad contable no equivale necesariamente al efectivo recibido, pero no te permite comparar fácilmente con otras
inversiones.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Rentabilidad (%) = (Valor final / Inversión − 1) × 100
Utilidad         = Valor final − Inversión inicial
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Para la toma de decisiones, generalmente la rentabilidad es más relevante porque
permite comparar oportunidades de diferentes tamaños; también hay que comparar
plazos, riesgos, costos y flujos de caja.&lt;/p&gt;
&lt;h2&gt;Riesgo&lt;/h2&gt;
&lt;p&gt;El riesgo es la probabilidad de que el resultado real de una inversión sea
significativamente diferente al resultado esperado. En otras palabras, es la
incertidumbre sobre los retornos futuros.&lt;/p&gt;
&lt;p&gt;Existen diferentes tipos de riesgo: el riesgo de mercado (el precio del activo
baja), el riesgo de crédito (el deudor no paga), el riesgo operacional (algo
sale mal en la empresa), y el riesgo sistemático (toda la economía cae). Los
inversores racionales exigen una mayor rentabilidad para asumir mayor riesgo.
Esta relación entre riesgo y retorno es fundamental en finanzas.&lt;/p&gt;
&lt;p&gt;El riesgo no puede eliminarse completamente en la mayoría de las inversiones,
pero puede gestionarse y diversificarse. Una cartera diversificada que incluya
diferentes tipos de activos tiende a tener menos riesgo que apostar todo en una
única inversión.&lt;/p&gt;
&lt;h2&gt;Flujo de Caja y Liquidez&lt;/h2&gt;
&lt;p&gt;El flujo de caja es la cantidad de dinero que entra y sale de una empresa (o
persona) durante un período específico de tiempo. Es diferente de las ganancias
contables porque se enfoca en dinero real, no en ingresos o gastos reconocidos
contablemente.&lt;/p&gt;
&lt;p&gt;El flujo de caja puede ser positivo (entra más dinero del que sale) o negativo
(sale más dinero del que entra). Una empresa muy rentable en términos contables
puede quebrar si tiene un flujo de caja negativo, porque no tendrá efectivo para
pagar sus obligaciones inmediatas.&lt;/p&gt;
&lt;p&gt;La liquidez es la capacidad de una empresa (o persona) para convertir sus
activos en efectivo rápidamente sin perder valor. Un activo muy líquido como el
efectivo puede convertirse inmediatamente, mientras que bienes raíces son menos
líquidos porque pueden tomar meses vender una propiedad. La liquidez es crítica
para sobrevivir a crisis económicas o sorpresas inesperadas.&lt;/p&gt;
&lt;h2&gt;Deudas y Gastos&lt;/h2&gt;
&lt;p&gt;Aunque a menudo se confunden, deudas y gastos son conceptos distintos que es
importante diferenciar para una buena salud financiera.&lt;/p&gt;
&lt;p&gt;Los gastos representan recursos consumidos o costos incurridos durante un
período. No todos son voluntarios ni coinciden con un pago inmediato; en
contabilidad, una depreciación es un gasto sin salida de efectivo en ese momento. Los
gastos pueden ser necesarios (alimento, vivienda) o discrecionales
(entretenimiento, lujos).&lt;/p&gt;
&lt;p&gt;Las deudas, por otro lado, son obligaciones o compromisos financieros que
tenemos con terceros. Representan dinero que nos han prestado y que debemos
devolver, generalmente con intereses. Mientras que un gasto es definitivo, una
deuda es temporal: eventualmente debe saldarse.&lt;/p&gt;
&lt;p&gt;Una deuda se vuelve problemática cuando el flujo de caja no es suficiente para
cubrirla. Si nuestro ingreso no crece al ritmo de nuestras deudas y gastos,
eventualmente seremos insolventes.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Ahorro e Inversión&lt;/h2&gt;
&lt;p&gt;El ahorro es la parte del ingreso que no se gasta inmediatamente. Es reservar
dinero para el futuro, reduciendo el consumo presente. La inversión, por otro
lado, es usar el dinero ahorrado para comprar activos que generen retornos
futuros.&lt;/p&gt;
&lt;p&gt;En una economía cerrada, el ahorro y la inversión agregados coinciden como
identidad contable ex post. En una economía abierta también interviene el
financiamiento exterior. Esto no significa que todo préstamo bancario necesite
un depósito ahorrado previamente: crear crédito y financiar inversión puede
generar ingresos y ahorro después. A nivel personal, el ahorro es la
clave para construir riqueza a largo plazo, especialmente mediante la
capitalización de intereses compuestos.&lt;/p&gt;
&lt;p&gt;La tasa de ahorro (qué porcentaje del ingreso se ahorra) varía
significativamente entre países y personas. Países con altas tasas de ahorro
tienden a tener más capital disponible para inversión, lo que impulsa el
crecimiento. Culturas que valoran el ahorro tienden a acumular más riqueza a
largo plazo. Sin embargo, demasiado ahorro puede frenar el crecimiento económico
a corto plazo si reduce el consumo excesivamente.&lt;/p&gt;
&lt;h2&gt;Política Fiscal y Monetaria&lt;/h2&gt;
&lt;p&gt;La política fiscal y monetaria son las dos herramientas principales que los
gobiernos y bancos centrales usan para regular la economía.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Política Monetaria&lt;/strong&gt;: Es la regulación de la cantidad de dinero en la economía
y de las tasas de interés, generalmente bajo control del banco central. Una
política monetaria expansiva aumenta la cantidad de dinero (tasas bajas) para
estimular la economía durante recesiones. Una política monetaria contractiva
reduce la cantidad de dinero (tasas altas) para frenar la inflación durante
períodos de sobrecalentamiento económico.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Política Fiscal&lt;/strong&gt;: Es el uso de gastos e impuestos del gobierno para regular
la economía. Una política fiscal expansiva aumenta el gasto público o reduce
impuestos para estimular el crecimiento durante recesiones. Una política fiscal
contractiva reduce el gasto o aumenta impuestos para enfriar la economía durante
inflación. Un déficit o superávit por sí solo no identifica el cambio de
orientación fiscal: hay que comparar períodos y separar los efectos del ciclo
económico de las decisiones de gasto e impuestos.&lt;/p&gt;
&lt;p&gt;Ambas políticas tienen limitaciones y efectos secundarios. La política monetaria
puede perder efectividad en depresiones muy severas. La política fiscal requiere
tiempo para implementarse y puede tener efectos adversos a largo plazo si causa
deuda excesiva. La combinación óptima de ambas políticas es un tema central en
macroeconomía.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Reflexiones Finales&lt;/h2&gt;
&lt;p&gt;El entendimiento de conceptos como oferta y demanda, ciclos de deuda,
productividad, y la diferencia entre crecimiento por crédito versus crecimiento
por productividad es esencial para navegar el mundo financiero moderno. Ya sea
como inversor, emprendedor, trabajador o simplemente ciudadano, estas ideas
afectan nuestras vidas diariamente.&lt;/p&gt;
&lt;p&gt;Lo importante es reconocer que la economía es cíclica, que la deuda tiene
costos, y que la productividad es una fuente central del crecimiento sostenido del
ingreso por persona. Las decisiones económicas son también decisiones de oportunidad:
elegimos entre alternativas escasas.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Referencias&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Comisión para el Mercado Financiero de Chile. &quot;¿Qué es la tasa de interés?&quot;.
Recuperado de &lt;a href=&quot;https://www.cmfchile.cl/educa/621/w3-article-27164.html&quot;&gt;https://www.cmfchile.cl/educa/621/w3-article-27164.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Investopedia. (2024). &quot;Economics: Definition, Topics, and Schools of Thought&quot;.
Recuperado de &lt;a href=&quot;https://www.investopedia.com/terms/e/economics.asp&quot;&gt;https://www.investopedia.com/terms/e/economics.asp&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Principios Fundamentales de la Economía: Costo y Oportunidad. Recuperado
de &lt;a href=&quot;https://platzi.com/cursos/economia-del-comportamiento/los-cuatro-principios-de-la-economia/&quot;&gt;https://platzi.com/cursos/economia-del-comportamiento/los-cuatro-principios-de-la-economia/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>finance</category></item><item><title>Indicadores Económicos: Ocho Números para Leer una Economía</title><link>https://yeison-liscano.github.io/portfolio/blog/indicadores-economicos/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/indicadores-economicos/</guid><description>Ocho indicadores económicos (PIB, inflación, desempleo, balanza comercial, tasa de interés, confianza del consumidor, deuda y tipo de cambio): qué miden, cómo se calculan y por qué ninguno cuenta solo la historia completa.</description><pubDate>Sun, 22 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Un país anuncia que su PIB (Producto Interno Bruto (Gross Domestic Product, GDP)) creció 2,35 %. ¿Es una buena noticia? Depende de
cuánto subieron los precios, de cuánta gente tiene empleo y de cuánto debe el
gobierno. Los &lt;strong&gt;indicadores económicos&lt;/strong&gt; son las estadísticas que responden esas
preguntas, y cada uno ilumina solo una parte de la economía.&lt;/p&gt;
&lt;p&gt;En este artículo veremos ocho de ellos: qué miden, cómo se calculan con un
ejemplo numérico y por qué ninguno cuenta solo la historia completa. La tabla
resume lo esencial; el resto del artículo la desarrolla.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Indicador&lt;/th&gt;
&lt;th&gt;Qué mide&lt;/th&gt;
&lt;th&gt;Ojo con&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PIB&lt;/td&gt;
&lt;td&gt;Valor de lo producido en un período&lt;/td&gt;
&lt;td&gt;Distinguir PIB nominal de PIB real&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inflación (IPC)&lt;/td&gt;
&lt;td&gt;Alza general y sostenida de los precios&lt;/td&gt;
&lt;td&gt;Desinflación no es deflación&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desempleo&lt;/td&gt;
&lt;td&gt;Desempleados sobre la población activa&lt;/td&gt;
&lt;td&gt;No refleja la calidad del empleo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Balanza comercial&lt;/td&gt;
&lt;td&gt;Exportaciones menos importaciones&lt;/td&gt;
&lt;td&gt;Un superávit no equivale a más reservas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tasa de interés&lt;/td&gt;
&lt;td&gt;El costo del dinero&lt;/td&gt;
&lt;td&gt;Su efecto depende del plazo y del riesgo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confianza del consumidor&lt;/td&gt;
&lt;td&gt;Optimismo de los hogares&lt;/td&gt;
&lt;td&gt;Anticipa el consumo, no lo garantiza&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deuda pública&lt;/td&gt;
&lt;td&gt;Lo que el gobierno debe, como % del PIB&lt;/td&gt;
&lt;td&gt;Ningún umbral la declara segura o insostenible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tipo de cambio&lt;/td&gt;
&lt;td&gt;El precio de una moneda en otra&lt;/td&gt;
&lt;td&gt;Favorece a unos y perjudica a otros&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Producto Interno Bruto (PIB): el tamaño de la economía, no su ritmo&lt;/h2&gt;
&lt;p&gt;El PIB mide el valor de los bienes y servicios finales producidos dentro de un
territorio en un período de tiempo determinado, generalmente un año. Es el
indicador más amplio de la actividad económica y, por tanto, uno de los más
utilizados para evaluar la salud de una economía.&lt;/p&gt;
&lt;p&gt;Un PIB alto describe el tamaño de la economía, no su tasa de crecimiento. Para
medir cambios en la producción se compara el &lt;strong&gt;PIB real&lt;/strong&gt;, ajustado por precios;
el PIB nominal también puede subir por inflación. No hay una tasa universal de
crecimiento «sana»: depende de la población, la productividad y el contexto.
Véase
la &lt;a href=&quot;https://www.bea.gov/resources/learning-center/what-to-know-gdp&quot;&gt;explicación del PIB&lt;/a&gt; de la BEA (Bureau of Economic Analysis (Oficina de Análisis Económico de EE. UU.)).&lt;/p&gt;
&lt;h3&gt;Cómo se calcula el PIB&lt;/h3&gt;
&lt;p&gt;La fórmula más común para calcular el PIB es el &lt;strong&gt;enfoque de gasto&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PIB = C + I + G + (X - M)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Donde:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;C&lt;/strong&gt; = Consumo (gasto de los hogares en bienes y servicios)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I&lt;/strong&gt; = Inversión privada bruta, incluida vivienda y variación de inventarios&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;G&lt;/strong&gt; = Compras de bienes y servicios e inversión del gobierno; no incluye
transferencias como pensiones, que no pagan producción corriente&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;X&lt;/strong&gt; = Exportaciones (bienes y servicios vendidos al exterior)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;M&lt;/strong&gt; = Importaciones (bienes y servicios comprados del exterior)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Ejemplo práctico&lt;/h3&gt;
&lt;p&gt;Imaginemos un país con los siguientes datos anuales:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Consumo de hogares: $500 mil millones&lt;/li&gt;
&lt;li&gt;Inversión empresarial: $150 mil millones&lt;/li&gt;
&lt;li&gt;Gasto del gobierno: $200 mil millones&lt;/li&gt;
&lt;li&gt;Exportaciones: $100 mil millones&lt;/li&gt;
&lt;li&gt;Importaciones: $80 mil millones&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;El PIB sería: $500 + $150 + $200 + ($100 - $80) = &lt;strong&gt;$870 mil millones&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Si el año anterior fue $850 mil millones, el crecimiento del PIB es ($870 -
$850) / $850 × 100 = &lt;strong&gt;2,35 %&lt;/strong&gt; anual. Esta es una variación nominal si los
valores están a precios corrientes; solo mide crecimiento real si ambos están
expresados en precios comparables.&lt;/p&gt;
&lt;h2&gt;Inflación: el dinero compra menos&lt;/h2&gt;
&lt;p&gt;La inflación es el aumento generalizado y sostenido de los precios de bienes y
servicios en una economía durante un período de tiempo. En otras palabras, la
inflación reduce el poder adquisitivo del dinero: cada unidad monetaria compra
menos productos que antes.&lt;/p&gt;
&lt;p&gt;Los objetivos de inflación varían entre bancos centrales. La inflación elevada o
imprevisible dificulta la planificación y reduce el poder de compra si los
ingresos no la acompañan. &lt;strong&gt;Desinflación&lt;/strong&gt; significa que la inflación disminuye:
si pasa del 8 % al 4 %, los precios siguen subiendo. &lt;strong&gt;Deflación&lt;/strong&gt; significa una
caída del nivel general de precios; puede aumentar la carga real de las deudas
y, si se espera que continúe, aplazar ciertas compras.&lt;/p&gt;
&lt;h3&gt;IPC (Índice de Precios al Consumidor)&lt;/h3&gt;
&lt;p&gt;El IPC es el indicador más utilizado para medir la inflación. Mide la variación
de los precios de un conjunto de bienes y servicios que adquiere un consumidor
promedio en un período determinado. Este conjunto incluye alimentos, vivienda,
transporte, salud, educación, entretenimiento, y más.&lt;/p&gt;
&lt;p&gt;En un índice simplificado de canasta fija, el costo de las cantidades del
período base se compara a precios actuales y a precios base:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IPC = 100 × Σ(precio_actual × cantidad_base) / Σ(precio_base × cantidad_base)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Si usamos participaciones del gasto base &lt;code&gt;w_i&lt;/code&gt; que suman 1, la forma equivalente
es &lt;code&gt;IPC = 100 × Σ w_i × (precio_actual_i / precio_base_i)&lt;/code&gt;. No se deben mezclar
esas participaciones con cantidades físicas. Los IPC oficiales tienen métodos de
agregación, actualización de ponderaciones y ajuste de calidad más completos;
véase
la &lt;a href=&quot;https://www.bls.gov/cpi/questions-and-answers.htm&quot;&gt;guía del IPC&lt;/a&gt; de la BLS (Bureau of Labor Statistics (Oficina de Estadísticas Laborales de EE. UU.)).&lt;/p&gt;
&lt;h3&gt;Ejemplo práctico del IPC&lt;/h3&gt;
&lt;p&gt;Supongamos estas participaciones en el gasto del período base:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bien&lt;/th&gt;
&lt;th&gt;Precio base&lt;/th&gt;
&lt;th&gt;Participación del gasto base&lt;/th&gt;
&lt;th&gt;Precio actual&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Leche (litro)&lt;/td&gt;
&lt;td&gt;$1,00&lt;/td&gt;
&lt;td&gt;10 %&lt;/td&gt;
&lt;td&gt;$1,05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pan (kilo)&lt;/td&gt;
&lt;td&gt;$3,00&lt;/td&gt;
&lt;td&gt;20 %&lt;/td&gt;
&lt;td&gt;$3,15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gasolina (litro)&lt;/td&gt;
&lt;td&gt;$2,00&lt;/td&gt;
&lt;td&gt;70 %&lt;/td&gt;
&lt;td&gt;$2,10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Cada precio subió un 5 %, así que:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IPC = 100 × (0,10 × 1,05 + 0,20 × 1,05 + 0,70 × 1,05) = 105
Inflación respecto al período base = (105 / 100 − 1) × 100 = 5 %
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Factores que producen inflación&lt;/h3&gt;
&lt;p&gt;La inflación no surge de una única causa, sino de una combinación de factores:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Incremento de la demanda&lt;/strong&gt;: Cuando hay más demanda de bienes y servicios que
oferta disponible, los precios suben. Por ejemplo, durante un auge económico,
el consumo aumenta rápidamente y los productores no pueden satisfacer toda la
demanda.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Aumento de los costos de producción&lt;/strong&gt;: Si el costo de las materias primas,
el trabajo, o la energía sube, las empresas trasladan estos costos a los
precios finales. Un ejemplo es cuando el precio del petróleo sube,
incrementando el costo de transporte y producción.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Inflación auto-generada o expectativas inflacionarias&lt;/strong&gt;: Si los agentes
económicos (trabajadores, empresas, consumidores) esperan inflación futura,
actúan en consecuencia: los trabajadores piden salarios más altos, las
empresas suben precios anticipadamente, y los consumidores compran más rápido.
Estas respuestas pueden contribuir a la persistencia de la inflación; no
implican que toda expectativa se cumpla automáticamente.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Aumento de la cantidad de dinero en circulación&lt;/strong&gt;: Si el banco central
aumenta mucho la oferta monetaria y la producción no crece igual, hay más
dinero persiguiendo los mismos productos. Eso genera presiones inflacionarias
y el dinero pierde valor relativo.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Inflación importada&lt;/strong&gt;: En economías abiertas, si las monedas extranjeras se
aprecian (o la moneda local se deprecia), los bienes importados se vuelven más
caros, lo que puede transmitirse a la inflación general.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tasa de Desempleo: cuánta gente busca trabajo y no lo encuentra&lt;/h2&gt;
&lt;p&gt;La tasa de desempleo mide el porcentaje de la población económicamente activa
que está desempleada: en general, sin empleo, disponible para trabajar y
buscando activamente, según los criterios del organismo estadístico. Es un
indicador clave de la salud del mercado laboral y tiene implicaciones sociales y
económicas significativas.&lt;/p&gt;
&lt;p&gt;Una tasa de desempleo baja generalmente indica un mercado laboral fuerte y que
las personas pueden encontrar trabajo fácilmente. Sin embargo, un desempleo muy
bajo (cercano a cero) es prácticamente imposible de lograr, pues siempre hay
fricción en el mercado (personas cambiando de trabajo, nuevos entrantes, etc.).
Una tasa de desempleo alta refleja dificultades económicas: menos oportunidades,
menor consumo, y potencialmente mayor pobreza.&lt;/p&gt;
&lt;h3&gt;Cómo se calcula&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Tasa de Desempleo = (Personas desempleadas / Población económicamente activa) × 100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La población económicamente activa suma ocupados y desempleados. La edad mínima
y las excepciones dependen de la metodología nacional. Ser estudiante, jubilado
o dedicarse al hogar &lt;strong&gt;no excluye automáticamente&lt;/strong&gt; a alguien: cuenta si trabaja
o cumple los criterios de desempleo. Quien no trabaja ni busca activamente suele
quedar fuera de la fuerza laboral. Véase
la &lt;a href=&quot;https://www.bls.gov/cps/cps_htgm.htm&quot;&gt;metodología de la BLS&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Ejemplo práctico del desempleo&lt;/h3&gt;
&lt;p&gt;Un país tiene:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Población total: 50 millones&lt;/li&gt;
&lt;li&gt;Población económicamente activa: 25 millones&lt;/li&gt;
&lt;li&gt;Personas con trabajo: 23.5 millones&lt;/li&gt;
&lt;li&gt;Personas desempleadas (buscando activamente): 1.5 millones&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tasa de Desempleo = (1.5 / 25) × 100 = &lt;strong&gt;6%&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;El 6 % corresponde a la fuerza laboral, no a toda la población. Para
interpretarlo hay que mirar su evolución, la participación y la calidad del
empleo.&lt;/p&gt;
&lt;h2&gt;Balanza Comercial: cuánto vende y cuánto compra un país&lt;/h2&gt;
&lt;p&gt;La balanza comercial mide la diferencia entre el valor de las exportaciones
(bienes vendidos al exterior) y las importaciones (bienes comprados del
exterior) en un período determinado. Es parte de la cuenta corriente de
la balanza de pagos.&lt;/p&gt;
&lt;p&gt;Una balanza comercial positiva (superávit) significa que el país exporta más de
lo que importa. Una balanza comercial negativa (déficit) significa que importa
más de lo que exporta. Ambas situaciones tienen implicaciones económicas
importantes.&lt;/p&gt;
&lt;h3&gt;Cálculo de la balanza comercial&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Balanza Comercial = Exportaciones - Importaciones
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Un resultado positivo indica superávit comercial; uno negativo, déficit.&lt;/p&gt;
&lt;h3&gt;Ejemplo práctico de balanza comercial&lt;/h3&gt;
&lt;p&gt;Un país exporta bienes por valor de $300 mil millones e importa por $250 mil
millones.&lt;/p&gt;
&lt;p&gt;Balanza Comercial = $300 - $250 = &lt;strong&gt;+$50 mil millones (superávit)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Este superávit registra más exportaciones que importaciones de bienes. No
implica por sí solo una entrada neta total de divisas ni mayores reservas:
faltan servicios, rentas, transferencias y movimientos financieros.&lt;/p&gt;
&lt;h3&gt;Importancia de la balanza comercial&lt;/h3&gt;
&lt;p&gt;Un superávit comercial es generalmente visto positivamente porque significa que
los productos del país son competitivos en el mercado global. Sin embargo, un
superávit muy grande puede generar fricciones comerciales internacionales. Un
déficit comercial significa que el país consume más de lo que produce
internamente, lo cual puede ser sostenible si va acompañado de inversión
extranjera. Países como Estados Unidos tienen déficit comerciales persistentes
pero sostienen este modelo mediante la inversión extranjera y el dólar
como moneda de reserva mundial.&lt;/p&gt;
&lt;h2&gt;Tasa de Interés: el freno y el acelerador del crédito&lt;/h2&gt;
&lt;p&gt;La tasa de interés es el costo del dinero: el precio que se paga por usar
capital ajeno durante un período determinado, expresado como un porcentaje para
un período especificado. Los bancos centrales fijan tasas de política que
influyen en las demás; las tasas de mercado y de los préstamos también dependen
del plazo, el riesgo, la competencia y la oferta y demanda de fondos.&lt;/p&gt;
&lt;p&gt;Una tasa de interés baja incentiva el consumo y la inversión (es más barato
endeudarse), pero puede generar inflación. Una tasa alta desalienta el
endeudamiento y ayuda a frenar la inflación, pero también ralentiza el
crecimiento. Los bancos centrales ajustan las tasas para mantener un equilibrio
entre crecimiento e inflación.&lt;/p&gt;
&lt;p&gt;Para una explicación más detallada sobre tasas de interés, tipos de interés, y
su impacto en economías personales, te recomendamos leer nuestro artículo
sobre &lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/economia-b%C3%A1sica/&quot;&gt;Conceptos básicos de economía&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Importancia de la tasa de interés&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Las tasas afectan hipotecas, préstamos para autos, tarjetas de crédito, y
ahorros&lt;/li&gt;
&lt;li&gt;Tasas altas benefician a ahorradores pero perjudican a deudores&lt;/li&gt;
&lt;li&gt;Los inversores siguen de cerca cambios en tasas porque afectan la rentabilidad
de inversiones&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Índice de Confianza del Consumidor: el ánimo que anticipa el gasto&lt;/h2&gt;
&lt;p&gt;El índice de confianza del consumidor (ICC) mide el grado de optimismo o
pesimismo que tienen los consumidores sobre la economía actual y futura. Se
obtiene mediante encuestas en las que se pregunta a hogares sobre su situación
económica, perspectivas de empleo, y disposición a hacer compras principales.&lt;/p&gt;
&lt;p&gt;Un ICC alto indica que los consumidores tienen confianza en que sus ingresos se
mantendrán o aumentarán, que pueden encontrar empleo, y que es buen momento para
comprar. Un ICC bajo refleja incertidumbre: las personas reducen consumo y
ahorran más como protección.&lt;/p&gt;
&lt;h3&gt;Importancia del ICC&lt;/h3&gt;
&lt;p&gt;El ICC es un indicador anticipado importante: cambios en confianza generalmente
preceden cambios en el consumo real. Si el ICC cae significativamente, los
economistas pueden anticipar una reducción futura en consumo, lo que ralentiza
la economía. Por el contrario, un ICC creciente sugiere expansión económica
próxima.&lt;/p&gt;
&lt;h3&gt;Ejemplo práctico del ICC&lt;/h3&gt;
&lt;p&gt;Un país calcula su ICC preguntando a 1,000 hogares sobre:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Situación económica actual (mejor, igual, peor)&lt;/li&gt;
&lt;li&gt;Perspectivas para los próximos 12 meses&lt;/li&gt;
&lt;li&gt;Disposición a comprar casa, auto, o hacer mejoras al hogar&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Si el 55% tiene expectativas positivas, el 25% neutrales, y el 20% negativas, el
índice podría calcularse como (55 - 20) = 35 puntos (en una escala
simplificada). Si este índice sube de 35 a 42 en el siguiente trimestre, refleja
mejora en confianza.&lt;/p&gt;
&lt;h2&gt;Deuda Pública: lo que se debe frente a lo que se produce&lt;/h2&gt;
&lt;p&gt;La deuda pública es la cantidad total de dinero que un gobierno debe a
prestamistas, que pueden ser ciudadanos, instituciones financieras, o gobiernos
extranjeros. Surge cuando los gobiernos gastan más dinero del que recaudan en
impuestos, necesitando financiar el déficit mediante endeudamiento.&lt;/p&gt;
&lt;p&gt;La deuda pública se puede medir en términos absolutos (cantidad total) o en
términos relativos (como porcentaje del PIB). El segundo es más útil para
comparaciones internacionales y para evaluar sostenibilidad.&lt;/p&gt;
&lt;h3&gt;Cálculo de la deuda pública&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Ratio Deuda Pública / PIB = (Deuda Pública Total / PIB Anual) × 100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Este ratio indica qué tan grande es la deuda respecto a la producción económica
anual.&lt;/p&gt;
&lt;h3&gt;Ejemplo de deuda pública&lt;/h3&gt;
&lt;p&gt;Un país tiene:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Deuda pública: $2 billones&lt;/li&gt;
&lt;li&gt;PIB anual: $10 billones&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ratio Deuda / PIB = ($2 / $10) × 100 = &lt;strong&gt;20%&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Un ratio del 20 % describe este ejemplo, pero no demuestra sostenibilidad.
También importan intereses, vencimientos, moneda de denominación, ingresos
fiscales, crecimiento y acceso al financiamiento. No existe un umbral único que
clasifique la deuda de todos los países como segura o insostenible.&lt;/p&gt;
&lt;h3&gt;Importancia de la deuda pública&lt;/h3&gt;
&lt;p&gt;Una deuda moderada es normal y hasta beneficiosa: permite financiar inversión
pública (infraestructura, educación). Pero deuda excesiva reduce la flexibilidad
fiscal del gobierno, aumenta gastos en intereses, y puede llevar a crisis de
refinanciamiento si los prestamistas pierden confianza. En casos extremos, un
gobierno puede tener que reestructurar su deuda (por ejemplo, extender plazos o
reducir pagos pactados) o incumplir. Una reestructuración no equivale
simplemente a refinanciar a un interés menos favorable.&lt;/p&gt;
&lt;h2&gt;Tipo de Cambio: quién gana cuando una moneda se mueve&lt;/h2&gt;
&lt;p&gt;El tipo de cambio es el precio de una moneda en términos de otra. Por ejemplo, 1
dólar estadounidense = 0,95 euros, una cotización hipotética de 0,95 EUR (Euro (código ISO 4217))
por USD (United States dollar (dólar estadounidense, código ISO 4217)). En la convención de pares de mercado se escribiría USD/EUR = 0,95; EUR/USD
indicaría cuántos dólares compra un euro. Los tipos de cambio fluctúan
constantemente en mercados abiertos basándose en oferta y demanda de monedas.&lt;/p&gt;
&lt;p&gt;El tipo de cambio es crucial para países que comercian internacionalmente. Una
moneda fuerte (aprecia respecto a otras) significa que es más cara para
extranjeros comprar productos del país, haciendo sus exportaciones menos
competitivas. Una moneda débil (deprecia) abarata las exportaciones pero
encarece las importaciones.&lt;/p&gt;
&lt;h3&gt;Importancia del tipo de cambio&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Para exportadores&lt;/strong&gt;: Una moneda débil favorece sus ventas al exterior&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Para importadores&lt;/strong&gt;: Una moneda fuerte hace más barata la compra de
productos extranjeros&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Para inversores extranjeros&lt;/strong&gt;: Necesitan considerar riesgo de cambio&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Para ahorradores&lt;/strong&gt;: La apreciación puede mejorar la capacidad de comprar
bienes extranjeros, pero no garantiza preservar el poder de compra interno&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Ejemplo práctico del tipo de cambio&lt;/h3&gt;
&lt;p&gt;Un exportador de textiles vende productos por $1 millón USD. Si el tipo de
cambio es 1 USD = 20 pesos, recibe 20 millones de pesos. Si la moneda se
deprecia a 1 USD = 25 pesos (moneda más débil), recibe 25 millones de pesos por
la misma venta. La moneda débil beneficia al exportador en este caso.&lt;/p&gt;
&lt;h2&gt;Ningún indicador cuenta solo la historia completa&lt;/h2&gt;
&lt;p&gt;Los indicadores económicos son el sistema de diagnóstico de la salud de una
economía. Ninguno por sí solo cuenta toda la historia: un PIB que crece junto
con inflación alta no es necesariamente una buena noticia. Un desempleo bajo
acompañado de deuda pública insostenible tampoco lo es.&lt;/p&gt;
&lt;p&gt;Un análisis económico completo requiere observar múltiples indicadores en
conjunto, entender sus interrelaciones, y contextualizar los datos dentro de la
historia y realidad política de cada país. Por ejemplo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Si el PIB crece pero la balanza comercial empeora, el crecimiento podría estar
basado en consumo insostenible de importaciones&lt;/li&gt;
&lt;li&gt;Si el desempleo cae pero los salarios reales caen debido a inflación, la
calidad de vida real puede estar deteriorándose&lt;/li&gt;
&lt;li&gt;Si la deuda pública crece más rápido que el PIB, el país puede estar en una
trayectoria insostenible&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Por eso, la próxima vez que veas un titular con una sola cifra, pregúntate qué
indicador falta para completar la historia.&lt;/p&gt;
&lt;h2&gt;Referencias&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Banco Mundial (World Bank): &lt;a href=&quot;https://www.worldbank.org/&quot;&gt;https://www.worldbank.org/&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Proporciona datos sobre PIB, comercio, inflación, y desarrollo de todos los
países&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fondo Monetario Internacional (FMI): &lt;a href=&quot;https://www.imf.org/en/home&quot;&gt;https://www.imf.org/en/home&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Análisis económicos y bases de datos sobre indicadores macroeconómicos
globales&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Investopedia - Economic Indicators: &lt;a href=&quot;https://www.investopedia.com/&quot;&gt;https://www.investopedia.com/&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Definiciones detalladas y ejemplos prácticos de indicadores económicos&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OCDE (Organización para la Cooperación y Desarrollo
Económicos): &lt;a href=&quot;https://www.oecd.org/&quot;&gt;https://www.oecd.org/&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Estadísticas y análisis de economías avanzadas&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Curso de Indicadores
Económicos &lt;a href=&quot;https://platzi.com/cursos/indicadores-economicos/&quot;&gt;https://platzi.com/cursos/indicadores-economicos/&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Curso en español que explica en detalle cada indicador y su interpretación&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>finance</category></item><item><title>Building a Production-Ready Remote MCP Server</title><link>https://yeison-liscano.github.io/portfolio/blog/mcp/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/mcp/</guid><description>Lessons from building a remote MCP server in Python. Challenges with stateful sessions, input validation, scoped tool access, and authorization in horizontally scaled production environments.</description><pubDate>Sun, 22 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The Model Context Protocol (MCP) is an open standard for connecting LLMs (Large Language Models) to
external tools and data sources via a JSON-RPC (JavaScript Object Notation Remote Procedure Call) interface. The key insight is
that MCP gives you a proper, discoverable API (Application Programming Interface): a model can list available tools,
inspect input schemas and optional output schemas, and request tool calls
through a host application. MCP defines separate primitives for tools, resources,
and prompts; retrieving a prompt is not itself execution of a function.&lt;/p&gt;
&lt;p&gt;I was in charge of creating a remote MCP server to allow our customers to use
LLMs to access the data they already have on our platform. What seemed
straightforward turned into a deep dive into protocol design, session
management, and authorization at scale.&lt;/p&gt;
&lt;h2&gt;Session Problems in Our April 2025 Deployment&lt;/h2&gt;
&lt;p&gt;There was one problem: in April 2025, the Python MCP ecosystem was still very
young. I needed a library that I could embed directly in a Starlette/FastAPI
application — one that was lightweight, type-safe, and simple to reason about.
The official SDK (Software Development Kit) had significant issues with our infrastructure, particularly
around session management in horizontally scaled environments:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Image: GitHub discussion about MCP session loss in horizontally scaled environments&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/modelcontextprotocol/python-sdk/issues/520#issuecomment-2808158583&quot;&gt;GitHub Discussion Link&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Rather than working around these limitations, I decided to build a library from
scratch with the characteristics mentioned above — and without the problems
encountered in that deployment. This is a historical account, not a claim that
current SDK releases require the same workaround. The official SDK now
documents &lt;a href=&quot;https://py.sdk.modelcontextprotocol.io/run/deploy/&quot;&gt;deployment and scaling options&lt;/a&gt;,
including stateless operation.&lt;/p&gt;
&lt;h2&gt;Building http-mcp&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/yeison-liscano/http_mcp&quot;&gt;&lt;code&gt;http-mcp&lt;/code&gt;&lt;/a&gt; (published on PyPI (Python Package Index) as
&lt;code&gt;http-mcp&lt;/code&gt;) provides tools and prompts over HTTP (Hypertext Transfer Protocol) and stdio. Its supported
features and protocol versions should be checked against the client in use. I
designed it around a few principles:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Pydantic-first, schema automatically generated.&lt;/strong&gt; Tools are declared by
wrapping a Python function in a &lt;code&gt;Tool&lt;/code&gt; dataclass and pointing at its Pydantic
input/output models. The JSON (JavaScript Object Notation) schema that MCP clients use for discovery is
derived automatically from those models:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from http_mcp.types import Arguments, Tool
from pydantic import BaseModel, Field, UUID4


class FixMetadataInput(BaseModel):
    vulnerability_id: UUID4 = Field(description=&quot;Vulnerability uuid4 id&quot;)


class FixMetadataOutput(BaseModel):
    version: str = Field(
        description=&quot;Safe version of the dependency&quot;,
    )
    is_breaking_change: bool = Field(
        description=&quot;Whether upgrading to safe version is a breaking change&quot;,
    )


def get_fix_metadata(
    arguments: Arguments[FixMetadataInput],
) -&amp;gt; FixMetadataOutput:
    return FixMetadataOutput(
        version=&quot;2.3.4&quot;,
        is_breaking_change=False,
    )


tool = Tool(
    inputs=FixMetadataInput,
    output=FixMetadataOutput,
    func=get_fix_metadata,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With inputs as Pydantic models validation happens automatically before the data
reaches the tool function. Inputs are rejected with structured errors if they do
not match the schema. Another advantage is that every field is self-documenting
— by using &lt;code&gt;Field(description=&quot;...&quot;)&lt;/code&gt;, both the input and output schemas carry
human-readable documentation that MCP clients can surface to users and models
alike.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Starlette-native auth.&lt;/strong&gt; Rather than inventing a new auth model, the
library plugs into Starlette&apos;s &lt;code&gt;AuthenticationMiddleware&lt;/code&gt;. Scope-based access
control just works: tools declare which scopes they require, and the framework
filters them before listing or invocation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Tool errors as model feedback.&lt;/strong&gt; The &lt;code&gt;http-mcp&lt;/code&gt; documentation describes
&lt;code&gt;return_error_message=True&lt;/code&gt; as converting &lt;code&gt;ToolInvocationError&lt;/code&gt; exceptions into
&lt;code&gt;ErrorMessage&lt;/code&gt; results. This is a library feature, not a general guarantee about
all Pydantic failures or HTTP status codes. A useful error can explain what the
caller should correct, provided the client exposes that result to the model.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Tool(
    inputs=FixMetadataInput,
    output=FixMetadataOutput,
    func=get_fix_metadata,
    return_error_message=True,  # Return supported tool invocation errors as ErrorMessage results
    scopes=(&quot;mcp:read&quot;,),
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This can make recoverable failures actionable. Whether a retry succeeds, or
saves tokens, depends on the error and the client.&lt;/p&gt;
&lt;h2&gt;Stateful Sessions: A Scaling Problem&lt;/h2&gt;
&lt;p&gt;The early versions of the MCP Python SDK maintained session state across
requests. This creates a critical problem in horizontally scaled environments:
if your MCP server runs on multiple workers behind a load balancer, each request
might hit a different worker instance. Session data stored on Worker A is
invisible to Worker B, causing a session lookup to fail. That can interrupt tool access, but it does
not inherently erase the model&apos;s conversation history, which the host manages.&lt;/p&gt;
&lt;p&gt;The difficulty is coordinating state, not the mere existence of state:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Requests can reach a worker that does not own an in-memory session.&lt;/li&gt;
&lt;li&gt;Session affinity is one option, with failover trade-offs. Shared state and
routing to the owning worker are other possible designs.&lt;/li&gt;
&lt;li&gt;Stateless request handling simplifies horizontal scaling, but stateful
services can also scale with appropriate coordination and recovery.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For our workload, avoiding per-worker session state was a useful trade-off.&lt;/p&gt;
&lt;p&gt;The solution was to implement a &lt;strong&gt;stateless HTTP transport&lt;/strong&gt; that treats each
request as independent. Instead of maintaining session objects in memory, the
transport sends all necessary context in each request. This stateless design
became the foundation for &lt;code&gt;http-mcp&lt;/code&gt;: &lt;a href=&quot;https://pypi.org/project/http-mcp/&quot;&gt;https://pypi.org/project/http-mcp/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: With the session in worker memory, the second request fails as soon as the balancer picks another worker. When each request carries its context, any worker can answer.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/mcp/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;Remote MCP Architecture: Streamable HTTP vs stdio&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://modelcontextprotocol.io/specification/2025-11-25/basic/transports&quot;&gt;2025-11-25 MCP specification&lt;/a&gt;
defines two standard transports and permits custom ones:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stdio Transport&lt;/strong&gt;: The MCP server runs as a subprocess on the same machine,
communicating via standard input/output. This is simple for local integrations
but limits you to a single host.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Streamable HTTP&lt;/strong&gt;: The MCP server runs as a separate service (often remote),
communicating over HTTP. This enables true separation of concerns and
horizontal scaling.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For production systems, HTTP transport is the practical choice. It allows
separate deployment and scaling of the MCP server, easier monitoring and
logging, integration with load balancers and service meshes, and
language-agnostic communication between components.&lt;/p&gt;
&lt;p&gt;The trade-off is complexity: handle network errors, timeouts, authentication,
and any chosen session lifecycle. Streamable HTTP permits optional sessions;
it does not require every server to be stateless.&lt;/p&gt;
&lt;h2&gt;Challenge 1: Errors the Caller Can Act On&lt;/h2&gt;
&lt;p&gt;Input-schema validation and tool execution are different stages. Malformed
protocol requests can produce JSON-RPC errors; recoverable execution failures can
be represented as tool results. MCP defines an &lt;code&gt;isError&lt;/code&gt; flag for tool execution
errors. See the &lt;a href=&quot;https://modelcontextprotocol.io/specification/2025-11-25/server/tools#error-handling&quot;&gt;tool error rules&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;http-mcp&lt;/code&gt;, the documented &lt;code&gt;return_error_message&lt;/code&gt; option handles supported
&lt;code&gt;ToolInvocationError&lt;/code&gt; exceptions. For example, a handler can report that a
requested record is unavailable, with enough context for the caller to change
its arguments. Do not assume that every client displays errors identically or
that a retry will succeed.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: A recoverable tool error can guide a revised request when the client passes its details to the model. The outcome depends on the client and the error.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/mcp/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;Challenge 2: Context-Based Tool Exposure&lt;/h2&gt;
&lt;p&gt;Not all tools should be available to all callers. You might want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Public tools&lt;/strong&gt; (no auth required): A &quot;get_server_status&quot; tool anyone can use&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Private tools&lt;/strong&gt; (auth required): A &quot;delete_user&quot; tool only admins can access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scoped tools&lt;/strong&gt;: Different tools for different contexts or user roles&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I solved this by leveraging Starlette&apos;s scope system. Each HTTP request carries
scope metadata (user, role, permissions), and tools declare which scopes they
require. With the same models as before, the only addition is &lt;code&gt;scopes&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Tool(
    inputs=FixMetadataInput,
    output=FixMetadataOutput,
    func=get_fix_metadata,
    scopes=(&quot;mcp:read&quot;,),  # Only callers with &quot;mcp:read&quot; scope can use this tool
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach has several benefits: tools are self-documenting about their
requirements, authorization is centralized and consistent, you can change
permissions without modifying tool code, and the AI (Artificial Intelligence) agent can query which tools
are available in its current context — which reduces suggestions for unavailable tools. A client can still submit
a hidden or stale tool name, so the server must authorize every invocation.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: Scope filtering limits discovery. The server must also reject unauthorized tools/call requests, even if the client knows a hidden tool name.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/mcp/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;Lessons Learned&lt;/h2&gt;
&lt;p&gt;Building a production MCP server taught me several things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Stateless by default.&lt;/strong&gt; Design your server assuming instances are
ephemeral. This simplifies request routing and avoids some
session-related bugs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Error messages are feedback loops.&lt;/strong&gt; The AI agent can adjust subsequent calls using error
messages. Make them helpful and specific — generic errors cause retries and
wasted tokens. Some MCP clients do not even pass error responses to the LLM;
supported tool errors can instead be returned as results
via &lt;code&gt;return_error_message=True&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Explicit permissions matter.&lt;/strong&gt; Making scope requirements visible in tool
definitions makes it easier to reason about security and to conditionally
expose tools and prompts based on the caller&apos;s identity.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Official MCP Specification&lt;/strong&gt;: &lt;a href=&quot;https://modelcontextprotocol.io&quot;&gt;modelcontextprotocol.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;My HTTP Transport Implementation&lt;/strong&gt;: &lt;a href=&quot;https://pypi.org/project/http-mcp/&quot;&gt;http-mcp on PyPI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stateless Sessions Discussion&lt;/strong&gt;: &lt;a href=&quot;https://github.com/modelcontextprotocol/python-sdk/issues/520#issuecomment-2808158583&quot;&gt;Python SDK GitHub Issue #520&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>ai</category><category>python</category><category>mcp</category></item><item><title>SOLID Python</title><link>https://yeison-liscano.github.io/portfolio/blog/solid-python/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/solid-python/</guid><description>The five SOLID design principles applied to Python. Practical examples of SRP, OCP, LSP, ISP, and DIP to write cleaner, more maintainable code.</description><pubDate>Sun, 22 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The SOLID (Single responsibility, Open/closed, Liskov substitution, Interface segregation, Dependency inversion) principles are five design guidelines that help developers write code
that is more understandable, flexible, and maintainable. Popularized by Robert C.
Martin, these principles bring together ideas from several authors and are
applicable across programming languages, including Python. This article explores
each principle with practical Python examples that demonstrate how to apply them
in real-world scenarios.&lt;/p&gt;
&lt;h2&gt;SRP: Single Responsibility Principle&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;A class should have only one reason to change.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A reason to change corresponds to a responsibility to an actor or stakeholder
group. It does not mean a class must have only one method. This
principle helps to keep classes small, focused, and maintainable. When a class
has multiple responsibilities, changes to one responsibility can inadvertently
break another, making the code harder to test and understand.&lt;/p&gt;
&lt;p&gt;Consider a class that handles both user authentication and sending email
notifications. If the authentication logic needs to change, and the email logic
also needs to change, the class becomes a point of conflict. By separating these
concerns into distinct classes, each can evolve independently.&lt;/p&gt;
&lt;p&gt;Here&apos;s a before-and-after example. The password comparison is a deliberately
simplified stand-in for authentication, not a password-storage design. Real
applications should use a password-hashing library&apos;s verification function and
store salted password hashes, as described in
the OWASP (Open Worldwide Application Security Project) &lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html&quot;&gt;Password Storage Cheat Sheet&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# BEFORE: Violates SRP
class User:
    def __init__(self, email: str, password: str):
        self.email = email
        self.password = password

    def authenticate(self, provided_password: str) -&amp;gt; bool:
        # Authentication logic
        return self.password == provided_password

    def send_welcome_email(self):
        # Email logic mixed with user logic
        print(f&quot;Sending welcome email to {self.email}&quot;)
        return True


# AFTER: Follows SRP
class User:
    def __init__(self, email: str, password: str):
        self.email = email
        self.password = password

    def authenticate(self, provided_password: str) -&amp;gt; bool:
        return self.password == provided_password


class EmailService:
    @staticmethod
    def send_welcome_email(user: User):
        print(f&quot;Sending welcome email to {user.email}&quot;)
        return True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The refactored version gives each class a single, clear responsibility. User
handles authentication, while EmailService handles email operations. This makes
testing easier (you can mock the email service) and allows either responsibility
to change independently.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Single responsibility makes code easier to test, reuse, and
modify without unintended side effects.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;OCP: Open/Closed Principle&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Software entities should be open for extension but closed for modification.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This principle encourages designing systems so that new functionality can be
added without changing existing code. Instead of modifying a class to support
new behavior, you extend it through inheritance, composition, or abstraction.
This reduces the risk of breaking existing functionality and makes codebases
more stable.&lt;/p&gt;
&lt;p&gt;In Python, the Open/Closed Principle is elegantly expressed through abstract
base classes (ABC) and polymorphism. By defining an interface (abstract class),
you allow different implementations to be &quot;plugged in&quot; without modifying the
core logic that uses them.&lt;/p&gt;
&lt;p&gt;Consider a payment processing system that needs to support multiple payment
methods:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from abc import ABC, abstractmethod


# BEFORE: Violates OCP - requires modification for each new payment type
class PaymentProcessor:
    def process(self, payment_type: str, amount: float) -&amp;gt; bool:
        if payment_type == &quot;credit_card&quot;:
            return self._process_credit_card(amount)
        elif payment_type == &quot;paypal&quot;:
            return self._process_paypal(amount)
        elif payment_type == &quot;bitcoin&quot;:
            return self._process_bitcoin(amount)
        else:
            raise ValueError(f&quot;Unknown payment type: {payment_type}&quot;)

    def _process_credit_card(self, amount: float) -&amp;gt; bool:
        print(f&quot;Processing credit card payment of ${amount}&quot;)
        return True

    def _process_paypal(self, amount: float) -&amp;gt; bool:
        print(f&quot;Processing PayPal payment of ${amount}&quot;)
        return True

    def _process_bitcoin(self, amount: float) -&amp;gt; bool:
        print(f&quot;Processing Bitcoin payment of ${amount}&quot;)
        return True


# AFTER: Follows OCP - extensible without modification
class PaymentMethod(ABC):
    @abstractmethod
    def process(self, amount: float) -&amp;gt; bool:
        pass


class CreditCardPayment(PaymentMethod):
    def process(self, amount: float) -&amp;gt; bool:
        print(f&quot;Processing credit card payment of ${amount}&quot;)
        return True


class PayPalPayment(PaymentMethod):
    def process(self, amount: float) -&amp;gt; bool:
        print(f&quot;Processing PayPal payment of ${amount}&quot;)
        return True


class BitcoinPayment(PaymentMethod):
    def process(self, amount: float) -&amp;gt; bool:
        print(f&quot;Processing Bitcoin payment of ${amount}&quot;)
        return True


class PaymentProcessor:
    def __init__(self, payment_method: PaymentMethod):
        self.payment_method = payment_method

    def process(self, amount: float) -&amp;gt; bool:
        return self.payment_method.process(amount)


# Using it:
processor = PaymentProcessor(CreditCardPayment())
processor.process(100.00)

# To add a new payment method, simply create a new class:
class ApplePayPayment(PaymentMethod):
    def process(self, amount: float) -&amp;gt; bool:
        print(f&quot;Processing Apple Pay payment of ${amount}&quot;)
        return True

processor = PaymentProcessor(ApplePayPayment())
processor.process(50.00)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The refactored version is closed for modification—the PaymentProcessor class
doesn&apos;t change when you add new payment methods—and open for extension through
the PaymentMethod abstraction. This follows the Strategy pattern, a classic way
to implement OCP.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Open/Closed Principle reduces regression risks and allows
your codebase to grow without constant refactoring of existing code.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;LSP: Liskov Substitution Principle&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Subtypes must be substitutable for their base types without breaking the
application.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This principle ensures that derived classes can be used in place of their base
classes without causing errors or unexpected behavior. If a function expects a
base class, passing a derived class should work seamlessly. Violating LSP
typically results in type checking or conditional logic that defeats the purpose
of polymorphism.&lt;/p&gt;
&lt;p&gt;A classic example involves geometric shapes. While a square is technically a
rectangle mathematically, in object-oriented design, if Rectangle allows
independent width and height changes but Square requires them to be equal,
substituting a Square for a Rectangle will cause problems.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# BEFORE: Violates LSP
class Rectangle:
    def __init__(self, width: float, height: float):
        self._width = width
        self._height = height

    def set_width(self, width: float):
        self._width = width

    def set_height(self, height: float):
        self._height = height

    def get_area(self) -&amp;gt; float:
        return self._width * self._height


class Square(Rectangle):
    def set_width(self, width: float):
        self._width = width
        self._height = width  # Enforce square property

    def set_height(self, height: float):
        self._width = height
        self._height = height  # Enforce square property


# This breaks LSP:
def calculate_area(shape: Rectangle) -&amp;gt; float:
    shape.set_width(5)
    shape.set_height(4)
    expected_area = 20
    actual_area = shape.get_area()
    assert actual_area == expected_area  # This fails if shape is a Square!
    return actual_area


# AFTER: Follows LSP
from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def get_area(self) -&amp;gt; float:
        pass


class Rectangle(Shape):
    def __init__(self, width: float, height: float):
        self._width = width
        self._height = height

    def set_width(self, width: float):
        self._width = width

    def set_height(self, height: float):
        self._height = height

    def get_area(self) -&amp;gt; float:
        return self._width * self._height


class Square(Shape):
    def __init__(self, side: float):
        self._side = side

    def set_side(self, side: float):
        self._side = side

    def get_area(self) -&amp;gt; float:
        return self._side ** 2


def calculate_area(shape: Shape) -&amp;gt; float:
    area = shape.get_area()
    return area


# Now both can be used safely:
rect = Rectangle(5, 4)
rect.set_width(5)
rect.set_height(4)
assert rect.get_area() == 20

square = Square(5)
square.set_side(5)
assert square.get_area() == 25
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the corrected version, Rectangle and Square are both concrete implementations
of Shape. They don&apos;t try to substitute for each other inappropriately; each is
designed to work correctly with its own interface. This allows polymorphic usage
without breaking contracts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; LSP ensures that inheritance hierarchies are logically sound
and that polymorphism works as intended without surprises.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;ISP: Interface Segregation Principle&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Clients should not depend on interfaces they do not use.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This principle suggests creating many focused, client-specific interfaces rather
than one monolithic interface. If a class is forced to implement methods it
doesn&apos;t need, the implementation becomes bloated and changes in unrelated
methods can affect unrelated clients.&lt;/p&gt;
&lt;p&gt;In Python, this is best expressed using abstract base classes (ABC) or Protocols
that define narrow, specific contracts. Each implementation only depends on the
methods it actually uses.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# BEFORE: Violates ISP
from abc import ABC, abstractmethod


class Worker(ABC):
    @abstractmethod
    def work(self):
        pass

    @abstractmethod
    def eat_lunch(self):
        pass


class HumanWorker(Worker):
    def work(self):
        print(&quot;Human working...&quot;)

    def eat_lunch(self):
        print(&quot;Human eating lunch...&quot;)


class RobotWorker(Worker):
    def work(self):
        print(&quot;Robot working...&quot;)

    def eat_lunch(self):
        # Robots don&apos;t eat! But they&apos;re forced to implement this.
        raise NotImplementedError(&quot;Robots don&apos;t eat&quot;)


# AFTER: Follows ISP
class Workable(ABC):
    @abstractmethod
    def work(self):
        pass


class Eatable(ABC):
    @abstractmethod
    def eat_lunch(self):
        pass


class HumanWorker(Workable, Eatable):
    def work(self):
        print(&quot;Human working...&quot;)

    def eat_lunch(self):
        print(&quot;Human eating lunch...&quot;)


class RobotWorker(Workable):
    def work(self):
        print(&quot;Robot working...&quot;)
    # No need to implement eat_lunch


# Using them:
def start_work(worker: Workable):
    worker.work()


def lunch_break(eater: Eatable):
    eater.eat_lunch()


human = HumanWorker()
start_work(human)
lunch_break(human)

robot = RobotWorker()
start_work(robot)
# lunch_break(robot)  # Type error - robot is not Eatable, which is correct!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By segregating the Worker interface into smaller, focused interfaces (Workable
and Eatable), each class only implements what it needs. This prevents robot
implementations from being forced to handle eating, and it makes the code&apos;s
intent clearer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Interface Segregation keeps implementations lean and
prevents classes from being burdened with methods they don&apos;t use.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;DIP: Dependency Inversion Principle&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;High-level modules should depend on abstractions, not low-level modules. Both
should depend on abstractions.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This principle reverses the typical dependency direction in traditional layered
architectures. Instead of high-level business logic depending on low-level
utility classes, both depend on shared abstractions. This reduces coupling and
makes swapping implementations easier, especially for testing.&lt;/p&gt;
&lt;p&gt;Dependency injection is the primary technique for implementing DIP in Python.
Rather than a class creating its own dependencies, they&apos;re provided (injected)
from outside.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# BEFORE: Violates DIP
class EmailService:
    def send(self, contact: str, message: str):
        print(f&quot;Sending email to {contact}: {message}&quot;)


class SMSService:
    def send(self, contact: str, message: str):
        print(f&quot;Sending SMS to {contact}: {message}&quot;)


class Notifier:
    def __init__(self):
        # High-level module depends directly on low-level modules
        self.email_service = EmailService()
        self.sms_service = SMSService()

    def notify_user(self, user_type: str, contact: str, message: str):
        if user_type == &quot;email&quot;:
            self.email_service.send(contact, message)
        elif user_type == &quot;sms&quot;:
            self.sms_service.send(contact, message)


# AFTER: Follows DIP
from abc import ABC, abstractmethod


class NotificationService(ABC):
    @abstractmethod
    def send(self, contact: str, message: str):
        pass


class EmailService(NotificationService):
    def send(self, contact: str, message: str):
        print(f&quot;Sending email to {contact}: {message}&quot;)


class SMSService(NotificationService):
    def send(self, contact: str, message: str):
        print(f&quot;Sending SMS to {contact}: {message}&quot;)


class PushNotificationService(NotificationService):
    def send(self, contact: str, message: str):
        print(f&quot;Sending push notification to {contact}: {message}&quot;)


class Notifier:
    def __init__(self, service: NotificationService):
        # Depend on abstraction, not concrete class
        self.service = service

    def notify_user(self, contact: str, message: str):
        self.service.send(contact, message)


# Using it:
email_notifier = Notifier(EmailService())
email_notifier.notify_user(&quot;user@example.com&quot;, &quot;Hello!&quot;)

sms_notifier = Notifier(SMSService())
sms_notifier.notify_user(&quot;+1234567890&quot;, &quot;Hello!&quot;)

push_notifier = Notifier(PushNotificationService())
push_notifier.notify_user(&quot;device123&quot;, &quot;Hello!&quot;)


# Testing is now easy - inject a mock:
class MockNotificationService(NotificationService):
    def __init__(self):
        self.messages = []

    def send(self, contact: str, message: str):
        self.messages.append((contact, message))


mock_service = MockNotificationService()
test_notifier = Notifier(mock_service)
test_notifier.notify_user(&quot;test@example.com&quot;, &quot;Test message&quot;)
assert len(mock_service.messages) == 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The refactored version inverts dependencies. The Notifier class (high-level
module) doesn&apos;t create or depend on concrete EmailService or SMSService;
instead, it depends on the NotificationService abstraction. This allows you to
inject any NotificationService implementation, making the code flexible and
testable.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: Before, Notifier points down at the concrete services it creates. After, every arrow ends at NotificationService: Notifier uses it, and each service implements it, so adding PushNotificationService does not touch Notifier.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/solid-python/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Dependency Inversion decouples high-level business logic
from low-level implementation details, making your code more testable, flexible,
and resilient to change.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Applying SOLID Together&lt;/h2&gt;
&lt;p&gt;These five principles work best as a cohesive set. A project that follows all of
them tends to be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Modular&lt;/strong&gt;: Each class has a clear, focused purpose (SRP).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extensible&lt;/strong&gt;: New features can be added without modifying existing code
(OCP).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Predictable&lt;/strong&gt;: Derived classes behave as their base classes expect (LSP).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Focused&lt;/strong&gt;: Classes only depend on methods they use (ISP).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decoupled&lt;/strong&gt;: High-level logic doesn&apos;t depend on implementation details
(DIP).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While strict adherence to SOLID isn&apos;t always necessary for small scripts or
prototypes, investing in these principles early pays dividends in larger,
evolving codebases. Python&apos;s flexibility and support for abstractions (ABC,
Protocols, duck typing) make implementing SOLID principles straightforward.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Python Software Foundation. &quot;abc — Abstract Base Classes.&quot; Python
Documentation. &lt;a href=&quot;https://docs.python.org/3/library/abc.html&quot;&gt;https://docs.python.org/3/library/abc.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Curso de Patrones de Diseño y SOLID en Python.
Platzi. &lt;a href=&quot;https://platzi.com/cursos/solid-python/&quot;&gt;https://platzi.com/cursos/solid-python/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>programming</category><category>python</category></item><item><title>Time Value of Money: Why a Dollar Today Beats a Dollar Tomorrow</title><link>https://yeison-liscano.github.io/portfolio/blog/time-value-of-money/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/time-value-of-money/</guid><description>The time value of money explained: compound interest, present and future value, annuities, and net present value. With formulas, worked examples, a chart, and Python code.</description><pubDate>Sun, 22 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Save $500 a month for 30 years at 8%, and you end up with about $745,180, even
though you only deposited $180,000. Borrow $300,000 for 30 years at 5%, and you
pay about $279,767 in interest on top of the loan. Both numbers come from the
same idea: &lt;strong&gt;with a positive rate of return, a dollar today is worth more than
the same dollar received later.&lt;/strong&gt; That idea is the time value of money, and it
underlies nearly every financial decision, from personal savings goals to
corporate investments.&lt;/p&gt;
&lt;p&gt;Money has a time value for three reasons. First, money can earn returns: a
dollar invested today at 5% a year grows to $1.05 a year from now. Second,
inflation erodes purchasing power, so future dollars buy less than today&apos;s.
Third, the future is uncertain: a promised payment next year may not arrive.&lt;/p&gt;
&lt;p&gt;This article walks through the formulas behind those two opening numbers and
the decisions they support: choosing between a lump sum and installments,
evaluating an investment, planning for retirement, and comparing mortgages. All
rates and cash flows below are hypothetical, constant unless stated otherwise,
and exclude fees and taxes.
See &lt;a href=&quot;https://openstax.org/books/principles-finance/pages/7-1-now-versus-later-concepts&quot;&gt;OpenStax Principles of Finance, time value of money&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Compound interest earns interest on interest&lt;/h2&gt;
&lt;p&gt;When you invest, you earn interest on the principal. Over time, the interest you
earn also earns interest. This process of earning returns on your returns is
called &lt;strong&gt;compounding&lt;/strong&gt;, and it makes growth exponential.&lt;/p&gt;
&lt;p&gt;The fundamental formula for compound interest is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FV = PV × (1 + r)^n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FV&lt;/code&gt; is the future value of the investment&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PV&lt;/code&gt; is the present value (initial amount) of the investment&lt;/li&gt;
&lt;li&gt;&lt;code&gt;r&lt;/code&gt; is the interest rate per period (expressed as a decimal)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt; is the number of periods&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, if you invest $1,000 at a 6% annual interest rate for 10 years,
your future value is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FV = 1000 × (1.06)^10 = 1000 × 1.791 = $1,791
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your investment grew by almost 80%. The whole $791 gain is interest, and $191 of
it is interest earned on earlier interest.&lt;/p&gt;
&lt;p&gt;We can rearrange this formula to work backwards. If you know the future value of
an investment and want its present value, use the present value formula:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PV = FV / (1 + r)^n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells you how much money you need to invest today to reach a specific
financial goal in the future.&lt;/p&gt;
&lt;h2&gt;The gap between simple and compound interest widens with time&lt;/h2&gt;
&lt;p&gt;To see what compounding adds, compare it with simple interest. With &lt;strong&gt;simple
interest&lt;/strong&gt;, you earn returns only on the original principal. The formula is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FV = PV × (1 + r × n)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using the previous example, $1,000 at 6% simple interest for 10 years grows to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FV = 1000 × (1 + 0.06 × 10) = 1000 × 1.6 = $1,600
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With &lt;strong&gt;compound interest&lt;/strong&gt;, you earn returns on the principal and on all
accumulated interest:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FV = 1000 × (1.06)^10 = $1,791
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The difference is $191 in favor of compound interest. Simple interest adds the
same $60 every year, while compound interest adds a little more each year, so
the gap widens with longer periods and higher rates. After 30 years, simple
interest reaches $2,800 and compound interest reaches $5,743.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: $1,000 at 6% a year. Simple interest adds the same $60 every year; compound interest adds more each year, so compounding is worth $191 after 10 years and $2,943 after 30.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/time-value-of-money/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Savings accounts and most investments compound, but loans apply interest in
different ways. Loan interest may accrue on the outstanding balance without
unpaid interest being capitalized. Check the actual contract rather than
assuming every product uses the same formula.&lt;/p&gt;
&lt;h2&gt;Every cash flow is a lump sum, an annuity, or an irregular series&lt;/h2&gt;
&lt;p&gt;Financial situations involve three main types of cash flows: lump sums,
annuities, and irregular series of cash flows. Each requires its own valuation
approach.&lt;/p&gt;
&lt;h3&gt;Lump sum&lt;/h3&gt;
&lt;p&gt;A lump sum is a single amount of money received or paid at one point in time.
It uses the two formulas above: &lt;code&gt;FV = PV × (1 + r)^n&lt;/code&gt; to move it forward and
&lt;code&gt;PV = FV / (1 + r)^n&lt;/code&gt; to bring it back.&lt;/p&gt;
&lt;h4&gt;College savings example&lt;/h4&gt;
&lt;p&gt;Suppose you want to save for your child&apos;s college education and estimate you&apos;ll
need $150,000 in 15 years. If you can invest in a fund earning 7% annually, how
much do you need to deposit today?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PV = 150,000 / (1.07)^15 = 150,000 / 2.759032 = $54,367
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You would need to deposit approximately $54,367 today to reach your goal of
$150,000 in 15 years, assuming a consistent 7% annual return.&lt;/p&gt;
&lt;h3&gt;Annuity&lt;/h3&gt;
&lt;p&gt;An annuity is a series of equal payments made at regular intervals over time.
Common examples include mortgage payments, retirement withdrawals, and savings
contributions.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;future value of an ordinary annuity&lt;/strong&gt; (payments at the end of each period)
is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FV = PMT × ((1 + r)^n - 1) / r
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;strong&gt;present value of an ordinary annuity&lt;/strong&gt; is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PV = PMT × (1 - (1 + r)^(-n)) / r
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FV&lt;/code&gt; is the future value of the annuity&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PV&lt;/code&gt; is the present value of the annuity&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PMT&lt;/code&gt; is the payment amount per period&lt;/li&gt;
&lt;li&gt;&lt;code&gt;r&lt;/code&gt; is the interest rate per period&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt; is the number of periods&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Monthly savings plan example&lt;/h4&gt;
&lt;p&gt;You decide to save $500 per month for retirement in an investment account with a
hypothetical nominal annual rate of 8%, compounded monthly. How much will you
have accumulated after 30 years?&lt;/p&gt;
&lt;p&gt;Use the exact monthly rate &lt;code&gt;r = 0.08 / 12&lt;/code&gt; (approximately 0.6667%). If 8% were
an effective annual rate instead, the monthly rate would be &lt;code&gt;(1.08)^(1/12) - 1&lt;/code&gt;.
These are different assumptions.&lt;/p&gt;
&lt;p&gt;Number of periods: 30 years × 12 months = 360&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;r = 0.08 / 12
FV = 500 × ((1 + r)^360 - 1) / r ≈ $745,179.72
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By saving $500 per month for 30 years, you accumulate approximately $745,179.72.
Your total contributions are $500 × 360 = $180,000, so $565,179.72 comes from
investment returns: more than three times what you put in.&lt;/p&gt;
&lt;p&gt;The formula also runs in reverse. If you want $500,000 at retirement and need to
know the monthly payment, solve for &lt;code&gt;PMT&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PMT = FV × r / ((1 + r)^n - 1)
PMT = 500,000 × r / ((1 + r)^360 - 1) ≈ $335.49
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At the same 8% rate, about $335.49 a month for 30 years reaches $500,000.&lt;/p&gt;
&lt;h3&gt;Series of cash flows and net present value&lt;/h3&gt;
&lt;p&gt;Many real-world investments involve irregular cash flows: different amounts at
different times. To evaluate these, we use &lt;strong&gt;net present value (NPV)&lt;/strong&gt;, which
discounts every future cash flow back to the present and sums them.&lt;/p&gt;
&lt;p&gt;The NPV formula is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NPV = Σ(t=0..n) CF_t / (1 + r)^t
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CF_t&lt;/code&gt; is the cash flow in period &lt;code&gt;t&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;r&lt;/code&gt; is the discount rate (required rate of return)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;t&lt;/code&gt; is the time period&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt; is the total number of periods&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A positive NPV means the modeled cash flows exceed the required return under the
chosen assumptions. For mutually exclusive, comparable projects without capital
constraints, higher NPV is preferred. Risk, unequal project lifetimes and
funding constraints can require additional analysis.&lt;/p&gt;
&lt;h4&gt;Investment project evaluation example&lt;/h4&gt;
&lt;p&gt;Your company is considering a project with the following cash flows over 5
years. The required rate of return is 10%.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Year 0: -$100,000 (initial investment)&lt;/li&gt;
&lt;li&gt;Year 1: $30,000&lt;/li&gt;
&lt;li&gt;Year 2: $35,000&lt;/li&gt;
&lt;li&gt;Year 3: $40,000&lt;/li&gt;
&lt;li&gt;Year 4: $25,000&lt;/li&gt;
&lt;li&gt;Year 5: $20,000&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Calculate the NPV:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NPV = -100,000 / 1.10^0 + 30,000 / 1.10^1
    + 35,000 / 1.10^2 + 40,000 / 1.10^3
    + 25,000 / 1.10^4 + 20,000 / 1.10^5
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;NPV ≈ -100,000 + 27,272.73 + 28,925.62 + 30,052.59
      + 17,075.34 + 12,418.43 = $15,744.70
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The modeled NPV is $15,744.70, positive at the assumed 10% discount rate. The
project returns $150,000 in total, but in today&apos;s money those returns are worth
only $115,744.70. A positive NPV supports accepting the project if the forecasts, risk
adjustment and other constraints are appropriate; it is not a guarantee of the
actual outcome.&lt;/p&gt;
&lt;h2&gt;Inflation shrinks what your returns can buy&lt;/h2&gt;
&lt;p&gt;When evaluating investments, distinguish between nominal returns and real
returns.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Nominal returns&lt;/strong&gt; are the stated, unadjusted returns. If your savings account
earns 2% annually, that&apos;s the nominal rate.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Real returns&lt;/strong&gt; account for inflation. If the nominal rate is 2% but inflation
is 3%, your real return is negative: your purchasing power declined.&lt;/p&gt;
&lt;p&gt;The exact purchasing-power adjustment for the same period is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(1 + r_real) = (1 + r_nominal) / (1 + i)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where &lt;code&gt;i&lt;/code&gt; is the inflation rate. The commonly used approximation is
&lt;code&gt;real rate ≈ nominal rate − inflation&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For example, if your investment earns 6% (nominal) and inflation is 2%, your
real return is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;r_real = (1.06) / (1.02) - 1 = 0.0392 = 3.92%
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your purchasing power increased by about 3.92%, not 6%. Over long periods, the
gap between nominal and real returns compounds too. To build real wealth, you
need returns that exceed inflation.&lt;/p&gt;
&lt;h2&gt;The same formulas price mortgages and retirement&lt;/h2&gt;
&lt;h3&gt;Mortgage payments&lt;/h3&gt;
&lt;p&gt;When you take out a mortgage, the lender uses the present value of an annuity
formula to calculate your monthly payment. Take a $300,000 mortgage for 30 years
at a nominal annual rate of 5%, compounded monthly: &lt;code&gt;r = 0.05 / 12&lt;/code&gt;, with 360
end-of-month payments. This excludes fees, insurance and taxes.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PMT = 300,000 × r / (1 - (1 + r)^(-360)) ≈ $1,610.46
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using the unrounded payment, total principal and interest is approximately
$579,767.35, including $279,767.35 in interest: almost as much again as the
loan itself. Actual cent-rounded payments require a small final-payment
adjustment. Because interest accrues on the outstanding balance, paying extra
principal early shrinks the balance on which every later month&apos;s interest is
charged.&lt;/p&gt;
&lt;h3&gt;Retirement planning&lt;/h3&gt;
&lt;p&gt;Retirement planning is fundamentally a time value of money problem. You need to
estimate:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;How much you&apos;ll need annually in retirement&lt;/li&gt;
&lt;li&gt;How many years you&apos;ll need the money&lt;/li&gt;
&lt;li&gt;What return your investments will earn&lt;/li&gt;
&lt;li&gt;How much you need to save now&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you need $50,000 annually in retirement for 30 years and expect 6% returns,
the present value of your retirement needs is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PV = 50,000 × (1 - (1.06)^(-30)) / 0.06 ≈ $688,241.56
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Under this simplified model, the initial amount is $688,241.56: less than half
of the $1,500,000 you will withdraw, because the balance keeps earning while you
draw it down. It assumes fixed nominal withdrawals at year-end and a constant 6%
return, with no taxes, fees or inflation increases. Variable returns
introduce sequence-of-returns risk, so this is not a safe-withdrawal guarantee. Working
backward with the annuity formula, you can determine how much to save monthly to
reach this goal.&lt;/p&gt;
&lt;h2&gt;Run the calculations in Python&lt;/h2&gt;
&lt;p&gt;These functions reproduce the examples above. Change the inputs to model your
own scenarios:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def future_value(pv, rate, periods):
    &quot;&quot;&quot;Calculate future value of a lump sum.&quot;&quot;&quot;
    return pv * (1 + rate) ** periods

def present_value(fv, rate, periods):
    &quot;&quot;&quot;Calculate present value of a future amount.&quot;&quot;&quot;
    return fv / (1 + rate) ** periods

def annuity_future_value(pmt, rate, periods):
    &quot;&quot;&quot;Calculate future value of an ordinary annuity.&quot;&quot;&quot;
    if rate == 0:
        return pmt * periods
    return pmt * (((1 + rate) ** periods - 1) / rate)

def annuity_present_value(pmt, rate, periods):
    &quot;&quot;&quot;Calculate present value of an ordinary annuity.&quot;&quot;&quot;
    if rate == 0:
        return pmt * periods
    return pmt * ((1 - (1 + rate) ** (-periods)) / rate)

def net_present_value(cash_flows, discount_rate):
    &quot;&quot;&quot;Calculate NPV of a series of cash flows.&quot;&quot;&quot;
    npv = 0
    for t, cf in enumerate(cash_flows):
        npv += cf / (1 + discount_rate) ** t
    return npv

# Example: College savings, 150,000 needed in 15 years at 7%
deposit = present_value(150_000, 0.07, 15)
print(f&quot;Deposit needed today: ${deposit:,.2f}&quot;)

# Example: End-of-month deposits; nominal 8% annually, compounded monthly
monthly_rate = 0.08 / 12
months = 30 * 12
monthly_payment = 500

fv = annuity_future_value(monthly_payment, monthly_rate, months)
print(f&quot;Future value of retirement savings: ${fv:,.2f}&quot;)

# Example: Evaluate a project
project_cashflows = [-100000, 30000, 35000, 40000, 25000, 20000]
npv = net_present_value(project_cashflows, 0.10)
print(f&quot;Net present value of project: ${npv:,.2f}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Move every cash flow to the same date before you compare&lt;/h2&gt;
&lt;p&gt;Money today has options. You can spend it, invest it, or lend it and earn
returns. Future money offers no such flexibility, because it is locked in the
future. Time value calculations translate future cash flows into their
equivalent present value so you can compare opportunities fairly.&lt;/p&gt;
&lt;p&gt;The formulas are only as good as their inputs. Small changes in the interest
rate, inflation, or time period can significantly alter the results, so treat
them as guides and question your assumptions.&lt;/p&gt;
&lt;p&gt;The practical rule is short: before you compare a lump sum with installments,
two investments, or two loans, bring every cash flow to the same date at the
same rate. Only then are you comparing like with like.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Brealey, R. A., Myers, S. C., &amp;amp; Allen, F. (2020). &quot;Principles of Corporate
Finance&quot; (13th ed.). McGraw-Hill Education.&lt;/li&gt;
&lt;li&gt;Investopedia. &lt;a href=&quot;https://www.investopedia.com/terms/t/timevalueofmoney.asp&quot;&gt;&quot;Time Value of Money (TVM).&quot;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Khan
Academy. &lt;a href=&quot;https://www.khanacademy.org/economics-finance-domain/core-finance&quot;&gt;&quot;Finance and Capital Markets.&quot;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>finance</category></item><item><title>JSON-RPC V2.0</title><link>https://yeison-liscano.github.io/portfolio/blog/json-rpc/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/json-rpc/</guid><description>JSON-RPC is a stateless, light-weight remote procedure call (RPC) protocol</description><pubDate>Sun, 15 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This article is based on
the &lt;a href=&quot;https://www.jsonrpc.org/specification&quot;&gt;JSON‑RPC 2.0 Specification&lt;/a&gt; by
the JSON‑RPC (JavaScript Object Notation Remote Procedure Call) Working Group, Copyright (C) 2007–2010 by the JSON‑RPC Working Group,
used under the permissions granted in the specification.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;JSON-RPC is a stateless, lightweight remote procedure call (RPC) protocol.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Request Object&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;jsonrpc&quot;: &quot;2.0&quot;,
  &quot;method&quot;: &quot;call_function&quot;,
  &quot;params&quot;: {
    &quot;name&quot;: &quot;function_name&quot;,
    &quot;arguments&quot;: {
      &quot;user_name&quot;: &quot;jondoe&quot;
    }
  },
  &quot;id&quot;: 2
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;jsonrpc&lt;/code&gt;: Version of the JSON-RPC protocol (String, required, must be
&lt;code&gt;&quot;2.0&quot;&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;method&lt;/code&gt;: Name of the method to be invoked (String, required).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;params&lt;/code&gt;: Structured value that holds parameter values for invocation (Array
or Object, optional).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;: Identifier defined by the client (String, Number, or Null, optional). If
omitted, the message is assumed to be a notification. Otherwise, the server
must reply with the same value in the response object.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Notifications&lt;/h3&gt;
&lt;p&gt;Messages with no &lt;code&gt;id&lt;/code&gt;. A Request object that is a notification signifies the
client&apos;s lack of interest in the Response object. The server must not reply to a
notification.&lt;/p&gt;
&lt;h3&gt;Parameters Structure&lt;/h3&gt;
&lt;p&gt;Parameters must be provided as structured values, either by position through an
array or by name through an object.&lt;/p&gt;
&lt;h2&gt;Response Object&lt;/h2&gt;
&lt;p&gt;The response is expressed as a single JSON object with the following members.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;jsonrpc&lt;/code&gt;: Version of the JSON-RPC protocol (String, required, must be
&lt;code&gt;&quot;2.0&quot;&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;result&lt;/code&gt;: Required on success. It must not exist if there was an error
invoking the method.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;error&lt;/code&gt;: Required on error. It must not exist if there was no error during
invocation.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;: Required. If there was an error detecting the &lt;code&gt;id&lt;/code&gt; in the Request
object, it must be &lt;code&gt;null&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Either the &lt;code&gt;result&lt;/code&gt; member or &lt;code&gt;error&lt;/code&gt; member must be included, but not both.&lt;/p&gt;
&lt;h3&gt;Error Object&lt;/h3&gt;
&lt;p&gt;Object with the following members:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;code&lt;/code&gt;: An integer that indicates the error type that occurred.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;message&lt;/code&gt;: Description of the error (String). Concise single sentence.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data&lt;/code&gt;: A primitive or structured value containing additional information
about the error (optional).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;See
the &lt;a href=&quot;https://www.jsonrpc.org/specification#error_object&quot;&gt;JSON-RPC error object specification&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Batch&lt;/h3&gt;
&lt;p&gt;The client sends an array filled with Request objects. The server responds with
an array containing the corresponding Response objects. A response object should
exist for each request object except for notification requests.&lt;/p&gt;
&lt;p&gt;The responses may be returned in any order in the responses array. The server
must not return an empty array and should return nothing at all for batches
containing only valid notifications. An empty batch (&lt;code&gt;[]&lt;/code&gt;) is invalid and
receives a single Invalid Request error object, not an empty response array.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: A batch of three requests gets two responses: the notification has no id, so nothing answers it, and the responses may come back in any order. A batch of only notifications gets no reply at all.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/json-rpc/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;Attribution and Copyright Notice&lt;/h2&gt;
&lt;p&gt;This article includes content derived from the JSON-RPC 2.0 specification.&lt;/p&gt;
&lt;p&gt;Copyright (C) 2007-2010 by the JSON-RPC Working Group&lt;/p&gt;
&lt;p&gt;This document and translations of it may be used to implement JSON-RPC, it may
be copied and furnished to others, and derivative works that comment on or
otherwise explain it or assist in its implementation may be prepared, copied,
published and distributed, in whole or in part, without restriction of any kind,
provided that the above copyright notice and this paragraph are included on all
such copies and derivative works. However, this document itself may not be
modified in any way.&lt;/p&gt;
&lt;p&gt;The limited permissions granted above are perpetual and will not be revoked.&lt;/p&gt;
&lt;p&gt;This document and the information contained herein is provided &quot;AS IS&quot; and ALL
WARRANTIES, EXPRESS OR IMPLIED are DISCLAIMED, INCLUDING BUT NOT LIMITED TO ANY
WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR
ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.&lt;/p&gt;
</content:encoded><category>programming</category><category>web</category></item><item><title>Most Useful Git Commands</title><link>https://yeison-liscano.github.io/portfolio/blog/git-commands/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/git-commands/</guid><description>A practical reference for the git commands I use the most. From stash tricks and interactive rebase to worktrees, bisect, and recovery with reflog.</description><pubDate>Sat, 14 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Git is one of those tools you use every day, but most people only scratch the
surface. Here are the commands I find myself reaching for the most.&lt;/p&gt;
&lt;h2&gt;The Essentials&lt;/h2&gt;
&lt;h3&gt;git log --oneline --graph --all&lt;/h3&gt;
&lt;p&gt;The default &lt;code&gt;git log&lt;/code&gt; output is verbose. This combination gives you a compact,
visual history of all branches.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git log --oneline --graph --all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can alias this to something short like &lt;code&gt;git lg&lt;/code&gt; by adding it to your
&lt;code&gt;.gitconfig&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git config --global alias.lg &quot;log --oneline --graph --all&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;git diff --staged&lt;/h3&gt;
&lt;p&gt;Before committing, I always check what is actually staged. &lt;code&gt;git diff&lt;/code&gt; alone
shows unstaged changes, but &lt;code&gt;--staged&lt;/code&gt; shows exactly what will go into the next
commit.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git diff --staged
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;git stash&lt;/h3&gt;
&lt;p&gt;When you need to quickly switch context without committing half-done work,
&lt;code&gt;stash&lt;/code&gt; saves your working directory and index state.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git stash
git checkout other-branch
# do some work
git checkout -
git stash pop
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Naming stashes&lt;/h4&gt;
&lt;p&gt;By default stashes get a generic name based on the branch and commit. Use &lt;code&gt;-m&lt;/code&gt;
to give them a meaningful description so you can identify them later.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git stash push -m &quot;wip: auth refactor&quot;
git stash list
# stash@{0}: On main: wip: auth refactor
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Stashing specific files&lt;/h4&gt;
&lt;p&gt;You do not have to stash everything. Use &lt;code&gt;push&lt;/code&gt; with file paths to stash only
certain files.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# stash only specific files
git stash push -m &quot;just the config&quot; src/config.ts src/env.ts

# stash only staged changes (leaves unstaged changes in the working tree)
git stash push --staged -m &quot;staged only&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Stashing untracked files&lt;/h4&gt;
&lt;p&gt;By default &lt;code&gt;git stash&lt;/code&gt; only saves tracked files. Use &lt;code&gt;-u&lt;/code&gt; to include untracked
files, or &lt;code&gt;-a&lt;/code&gt; to also include ignored files.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include untracked files
git stash push -u -m &quot;with new files&quot;

# include untracked AND ignored files
git stash push -a -m &quot;everything&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Stashing interactively&lt;/h4&gt;
&lt;p&gt;Use &lt;code&gt;-p&lt;/code&gt; (patch) to interactively choose which hunks to stash, similar to
&lt;code&gt;git add -p&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git stash push -p -m &quot;partial changes&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Listing and inspecting stashes&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# list all stashes
git stash list

# show the diff of a specific stash
git stash show stash@{1}

# show the full diff with file contents
git stash show -p stash@{1}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Applying and removing stashes&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# apply the most recent stash; remove it only if application succeeds
git stash pop

# apply without removing (keeps the stash for reuse)
git stash apply

# apply a specific stash
git stash apply stash@{2}

# drop a specific stash
git stash drop stash@{1}

# clear all stashes
git stash clear
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Creating a branch from a stash&lt;/h4&gt;
&lt;p&gt;If your stashed changes conflict with work done since the stash was created, you
can apply the stash onto a new branch starting from the commit where the stash
was originally made.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git stash branch new-feature-branch stash@{0}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Rewriting History&lt;/h2&gt;
&lt;h3&gt;git rebase -i HEAD~n&lt;/h3&gt;
&lt;p&gt;Interactive rebase is the most powerful command for cleaning up commits before
pushing. You can squash, reorder, reword, or drop commits.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git rebase -i HEAD~3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This opens your editor with the last 3 commits. Common operations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;squash&lt;/strong&gt;: merge a commit into the previous one&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;reword&lt;/strong&gt;: change the commit message&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;drop&lt;/strong&gt;: remove the commit entirely&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;git commit --amend&lt;/h3&gt;
&lt;p&gt;Quick fix for the last commit, whether you forgot a file or made a typo in the
message.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add forgotten-file.ts
git commit --amend
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;--no-edit&lt;/code&gt; if you only want to add files without changing the message.&lt;/p&gt;
&lt;h2&gt;Debugging&lt;/h2&gt;
&lt;h3&gt;git bisect&lt;/h3&gt;
&lt;p&gt;When a bug was introduced somewhere in the history and you have no idea where,
&lt;code&gt;bisect&lt;/code&gt; performs a binary search through your commits.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git bisect start
git bisect bad          # current commit is broken
git bisect good v1.0.0  # this tag was working
# git checks out a middle commit, you test it
git bisect good         # or git bisect bad
# repeat until git finds the first bad commit
git bisect reset
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;git blame -L start,end file&lt;/h3&gt;
&lt;p&gt;Shows who last modified each line. The &lt;code&gt;-L&lt;/code&gt; flag lets you focus on a specific
range instead of the entire file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git blame -L 10,30 src/auth/login.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;git log -S &quot;search_term&quot;&lt;/h3&gt;
&lt;p&gt;Finds commits that change the number of occurrences of a string. Use &lt;code&gt;-G&lt;/code&gt;
instead to match added or removed lines with a regular expression. See
the &lt;a href=&quot;https://git-scm.com/docs/git-log&quot;&gt;&lt;code&gt;git log&lt;/code&gt; documentation&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git log -S &quot;validateToken&quot; --oneline
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Working with Remotes&lt;/h2&gt;
&lt;h3&gt;git fetch --prune&lt;/h3&gt;
&lt;p&gt;Fetches updates from the remote and removes references to branches that no
longer exist on the remote.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git fetch --prune
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;git push -u origin branch-name&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;-u&lt;/code&gt; flag sets the upstream tracking reference. After this, you can just use
&lt;code&gt;git push&lt;/code&gt; and &lt;code&gt;git pull&lt;/code&gt; without specifying the remote and branch.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git push -u origin feature/new-auth
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;git pull --rebase&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git pull&lt;/code&gt; fetches and then integrates changes using the selected strategy.
With &lt;code&gt;--rebase&lt;/code&gt;, it replays local commits on top of the upstream history. A
merge-based pull only needs a merge commit when histories diverge; a fast-forward
needs none. Defaults depend on version and configuration.
See &lt;a href=&quot;https://git-scm.com/docs/git-pull&quot;&gt;&lt;code&gt;git pull&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: Your local commits X and Y diverged from origin/main at B. A pull using --no-rebase joins the diverged lines with merge commit M. A rebase pull replays them after D as new commits X′ and Y′, so history stays one straight line.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/git-commands/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git pull --rebase
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can make this the default behavior:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git config --global pull.rebase true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Recovery&lt;/h2&gt;
&lt;h3&gt;git reflog&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;reflog&lt;/code&gt; records local reference updates, including recent &lt;code&gt;HEAD&lt;/code&gt; movements
that &lt;code&gt;git log&lt;/code&gt; may not show. It can help recover committed work after a reset or
rebase while the entries and objects remain available. It is not a backup of
uncommitted files, and entries expire.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git reflog
git checkout HEAD@{3}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;git cherry-pick&lt;/h3&gt;
&lt;p&gt;Applies a specific commit from another branch onto your current branch. Useful
when you need one particular fix without merging an entire branch.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git cherry-pick abc1234
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;git branch from a commit&lt;/h3&gt;
&lt;p&gt;You can create a new branch starting from any commit in the history. This is
useful when you realize an older commit would be a better starting point for a
fix or feature, or when you need to recover work from a specific point in time.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# create a branch from a specific commit hash
git branch hotfix/login abc1234

# create and switch to it in one step
git checkout -b hotfix/login abc1234

# same thing using the newer switch command
git switch -c hotfix/login abc1234
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also use references like tags, &lt;code&gt;HEAD~n&lt;/code&gt;, or branch names instead of a
commit hash:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# branch from 3 commits ago
git switch -c experiment HEAD~3

# branch from a tag
git switch -c patch-v2 v2.0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;git restore --source origin/branch file&lt;/h3&gt;
&lt;p&gt;Restores a file to the version that exists on the remote. Useful when you have
made local changes to a file and want to discard them in favor of the remote
state.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# restore a single file to the remote main version
git restore --source origin/main -- src/auth/login.ts

# restore multiple files
git restore --source origin/main -- src/auth/login.ts src/auth/utils.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make sure to &lt;code&gt;git fetch&lt;/code&gt; first so your remote references are up to date:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git fetch origin
git restore --source origin/main -- path/to/file
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Worktrees&lt;/h2&gt;
&lt;p&gt;A worktree lets you check out multiple branches of the same repository at the
same time, each in its own directory on disk. Instead of stashing or committing
unfinished work to switch branches, you create a new worktree and work on both
branches simultaneously. Worktrees share the object database and most references, while each has its
own index and &lt;code&gt;HEAD&lt;/code&gt;. Git normally prevents checking out the same branch in two
worktrees at once.&lt;/p&gt;
&lt;p&gt;This is especially useful when you need to review a pull request while working
on a feature, reproduce a bug on &lt;code&gt;main&lt;/code&gt; without losing your current state, or
run tests on one branch while developing on another.&lt;/p&gt;
&lt;h3&gt;git worktree add&lt;/h3&gt;
&lt;p&gt;Creates a new worktree linked to your repository. You can specify an existing
branch or create a new one.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# check out an existing branch in a new directory
git worktree add ../hotfix-auth hotfix/auth

# create a new branch and check it out in a new directory
git worktree add -b feature/new-api ../new-api
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each worktree is a regular directory with a working tree. You can &lt;code&gt;cd&lt;/code&gt; into it,
run builds, tests, and commits independently.&lt;/p&gt;
&lt;h3&gt;git worktree list&lt;/h3&gt;
&lt;p&gt;Shows all worktrees linked to the repository.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git worktree list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/home/user/project         abc1234 [main]
/home/user/hotfix-auth     def5678 [hotfix/auth]
/home/user/new-api         abc9012 [feature/new-api]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;git worktree remove&lt;/h3&gt;
&lt;p&gt;Removes a worktree when you are done with it. The branch is kept, only the
directory is cleaned up.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git worktree remove ../hotfix-auth
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the worktree has uncommitted changes, git will refuse to remove it unless you
pass &lt;code&gt;--force&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;git worktree prune&lt;/h3&gt;
&lt;p&gt;Cleans up stale worktree references, for example when a worktree directory was
deleted manually instead of using &lt;code&gt;git worktree remove&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git worktree prune
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Useful Aliases&lt;/h2&gt;
&lt;p&gt;Here are the aliases I keep in my &lt;code&gt;.gitconfig&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[alias]
  st = status -sb
  lg = log --oneline --graph --all --decorate
  co = checkout
  br = branch -vv
  unstage = reset HEAD --
  last = log -1 HEAD --stat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These save keystrokes and make the output more readable. The small investment of
setting them up pays off every day.&lt;/p&gt;
</content:encoded><category>tools</category></item><item><title>WSGI vs ASGI Protocols</title><link>https://yeison-liscano.github.io/portfolio/blog/wsgi-vs-asgi-protocols/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/wsgi-vs-asgi-protocols/</guid><description>Comprehensive guide comparing WSGI and ASGI protocols in Python web development. Learn key differences, implementation examples, and when to use each protocol for optimal web application performance.</description><pubDate>Tue, 28 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Python web applications require protocols to communicate between web servers and
application code. WSGI (Web Server Gateway Interface) and ASGI (Asynchronous
Server Gateway Interface) are two such protocols, each with distinct
characteristics and use cases.&lt;/p&gt;
&lt;h2&gt;WSGI: Synchronous Web Applications&lt;/h2&gt;
&lt;p&gt;WSGI is the traditional protocol for Python web applications, introduced
in PEP (Python Enhancement Proposal) &lt;a href=&quot;https://peps.python.org/pep-0333/&quot;&gt;333&lt;/a&gt; and updated for Python 3
in &lt;a href=&quot;https://peps.python.org/pep-3333/&quot;&gt;PEP 3333&lt;/a&gt;. Its application interface is
synchronous; a server can still handle concurrent requests using threads,
processes, or compatible worker implementations.&lt;/p&gt;
&lt;h3&gt;Core Concepts&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Single callable function (application object)&lt;/li&gt;
&lt;li&gt;Synchronous request-response cycle&lt;/li&gt;
&lt;li&gt;Compatible with most traditional web frameworks&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Example WSGI Application&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# wsgi_app.py
def simple_wsgi_app(environ, start_response):
    status = &apos;200 OK&apos;
    headers = [(&apos;Content-type&apos;, &apos;text/plain&apos;)]
    start_response(status, headers)

    return [b&quot;Hello from WSGI!&quot;]

# Test the WSGI app
def mock_start_response(status, headers):
    print(f&quot;Status: {status}&quot;)
    print(f&quot;Headers: {headers}&quot;)

response = simple_wsgi_app({}, mock_start_response)
print(response[0].decode())
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;gunicorn --workers=2 --bind 127.0.0.1:8000 wsgi_app:simple_wsgi_app
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ASGI: Modern Async Applications&lt;/h2&gt;
&lt;p&gt;ASGI is a separate asynchronous interface designed as a successor to WSGI.
Its protocol specifications cover HTTP (Hypertext Transfer Protocol) and WebSockets; actual HTTP/2 support
depends on the server. See the &lt;a href=&quot;https://asgi.readthedocs.io/en/latest/specs/main.html&quot;&gt;ASGI specification&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Async-first design&lt;/li&gt;
&lt;li&gt;WebSocket support&lt;/li&gt;
&lt;li&gt;HTTP/2 compatibility&lt;/li&gt;
&lt;li&gt;Better handling of long-lived connections&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Example ASGI Application&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# asgi_app.py
import asyncio

async def simple_asgi_app(scope, receive, send):
    assert scope[&apos;type&apos;] == &apos;http&apos;

    await send({
        &apos;type&apos;: &apos;http.response.start&apos;,
        &apos;status&apos;: 200,
        &apos;headers&apos;: [
            [b&apos;content-type&apos;, b&apos;text/plain&apos;],
        ],
    })

    await send({
        &apos;type&apos;: &apos;http.response.body&apos;,
        &apos;body&apos;: b&quot;Hello from ASGI!&quot;,
    })

# Test the ASGI app
responses = []
async def mock_send(message):
    responses.append(message)

async def main():
    await simple_asgi_app({&apos;type&apos;: &apos;http&apos;}, None, mock_send)
    print(f&quot;Status: {responses[0][&apos;status&apos;]}&quot;)
    print(responses[1][&apos;body&apos;].decode())

if __name__ == &apos;__main__&apos;:
    asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;uvicorn asgi_app:simple_asgi_app --lifespan off
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;When to Use Each&lt;/h2&gt;
&lt;p&gt;WSGI:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Simple request-response applications&lt;/li&gt;
&lt;li&gt;Legacy systems integration&lt;/li&gt;
&lt;li&gt;Applications built around synchronous libraries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ASGI:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Real-time applications&lt;/li&gt;
&lt;li&gt;WebSocket requirements&lt;/li&gt;
&lt;li&gt;High-concurrency needs&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://fluidattacks.github.io/aioextensions/#io-bound-tasks&quot;&gt;I/O-bound&lt;/a&gt;
applications&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Performance Considerations&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;ASGI can overlap I/O (Input/Output) efficiently when the application uses nonblocking libraries.&lt;/li&gt;
&lt;li&gt;Blocking calls inside an async handler block its event-loop thread unless offloaded.&lt;/li&gt;
&lt;li&gt;CPU-bound work needs an appropriate execution strategy with either interface,
such as worker processes. Benchmark the actual application and server settings.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The following timeline is an idealized example with two synchronous WSGI
workers, nonblocking database calls for ASGI, and no scheduling overhead; it is
not a benchmark of the interfaces.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: Three requests arrive at once. Each needs 5 ms of Python, an 80 ms database call, then 5 ms more. With two WSGI workers, the third request waits for a free worker and finishes at 180 ms. One ASGI worker overlaps all three database waits and finishes at 100 ms.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/wsgi-vs-asgi-protocols/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
</content:encoded><category>python</category><category>web</category></item><item><title>Réflexions 2024</title><link>https://yeison-liscano.github.io/portfolio/blog/r%C3%A9flexions-2024/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/r%C3%A9flexions-2024/</guid><description>Un résumé des livres que j&apos;ai lus et des choses que j&apos;ai apprises en 2024.</description><pubDate>Thu, 09 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;La solution la plus simple est généralement la meilleure. C&apos;est pourquoi je
cherche presque toujours à simplifier les choses.&lt;/p&gt;
&lt;p&gt;Cette post est résumé des idées exposé dans les livres héritage Leçons d&apos;hommes
puissants du Humberto Montesinos M. and Père riche, père pauvre de Robert. En
plus, j&apos;ai ajouté quelques réflexions personnelles. En suivant les droits
d&apos;auteur, je voudrais mentionner que la plus part de idées ne sont pas les
miennes, mais celles des auteurs mentionnés.&lt;/p&gt;
&lt;h2&gt;FOCUS&lt;/h2&gt;
&lt;p&gt;Souvent, nous avons tendance de perdre le focus on se concentre sur la solution
en laissant le problème de côté. Focus en ce que vous pouvez contrôler, ne vous
inquiétez pas de ce que vous ne pouvez pas contrôler o de ce qui est externe à
vous.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Foyer! et rappelez-vous toujours FOCUS (Follow One Course Until Successful).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Ma philosophie de vie&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;pourquoi pas? imagine-toi ce qu&apos;on pourrait accomplir.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!--  --&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Des opportunités augmentent à mesure qu&apos;elles sont saisies. - Sun Tzu&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Le peur d&apos;agir ce qui nous empêche de réaliser nos rêves. C&apos;est important de
réfléchir, mais le plus important c&apos;est d&apos;agir. L&apos;action est la clé du succès.
Prend des risques c&apos;est partie de la vie. L&apos;impossible met seulement un peu plus
de temps à arriver, autant vous insistez pour essayer et essayer.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Admirez ceux qui essaient, même s&apos;ils échouent. - Seneca&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!--  --&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Il n&apos;y a rien d&apos;impossible pour celui qui essaie. - Alexandre le Grand&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!--  --&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Impossible est un mot que l&apos;on ne trouve que dans le dictionnaire des
imbéciles. - Napoléon Bonaparte&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Je veux mettre l&apos;accent sur l&apos;importance d&apos;agir consciencieusement.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Realize chaque action comme si elle était la dernière de votre vie. Marcus
Aurelius&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!--  --&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Certainement, les plans de ceux qui sont diligents auront de bons résultats;
D&apos;un autre côté, tous ceux qui se précipitent finiront dans la pauvreté - Rey
Salomon&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Reconnaître les limites de votre connaissance est la première étape pour
apprendre.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Savoir que l&apos;on sait ce que l&apos;on sait, et que l&apos;on ne sait pas ce que l&apos;on ne
sait pas, voilà la vraie sagesse. - Confucius&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!--  --&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Les sages sont plus puissants que les forts, et ceux qui recherchent la
connaissance deviennent de plus en plus forts - Rey Salomon&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Objectifs&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Prenez des décisions tous les jours. - Robert Kiyosaki&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!--  --&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Il m&apos;a fallu 10 ans pour avoir un succès du jour au lendemain - Woody Allen&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Se fixer des objectifs est une partie importante de la vie. Soyez ambitieux!.
être ambitieux est une qualité qui vous permettra de réaliser vos rêves. Il ne
s&apos;agit pas de faire plus, mais de faire le plus important. Priorisez vos
objectifs et concentrez-vous sur eux.&lt;/p&gt;
&lt;p&gt;&amp;lt;!--  --&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Trouve une raison qui soi plus grande que la réalité. Payez-vous d&apos;abord.
Robert Kiyosaki, Père riche, père pauvre&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Une raison ou un but est ce qui vous pousse à agir. Rappelle toi toujours
pourquoi tu fais ce que tu fais. Une raison ou un but est la combinaison des
désirs et no désirs de l&apos;individu.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Du riz pour vivre et des fleurs pour avoir quelque chose pour quoi vivre.
Nourrissez votre âme avec quelque chose de beau, un but. - Confucius&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Avoir de gros problèmes&lt;/h2&gt;
&lt;p&gt;Considérer des questions triviales et ridicules comme problématiques témoigne
d&apos;une mentalité très limitée. Ce qui détermine si c&apos;est bon ou mauvais, c&apos;est
votre interprétation.&lt;/p&gt;
&lt;p&gt;À retenir : les problèmes sont des opportunités. Qu&apos;est-ce que je dis problèmes,
ce n&apos;est pas un problème, c&apos;est un défi. Un défi doit être abordé une étape à la
fois.&lt;/p&gt;
&lt;h2&gt;Être un homme de parole&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Quand on dit oui, que ce soit oui, et quand on dit non, que ce soit non, car
tout ce qui est dit en plus de cela vient du mal. - Jésus-Christ&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Normes&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Vous êtes no seulement responsable de ce que vous fait, mais aussi de ce que
vous ne fait pas. - Lao-Tse&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Amis des Processus&lt;/h2&gt;
&lt;p&gt;Que est-ce que je dois me demander? Comment puis-je améliorer ce processus?. Oú
est-ce que tu crois je suis en train de faire une erreur?. Avec quip pensez-vous
que je devrais parler?.&lt;/p&gt;
</content:encoded><category>personal</category></item><item><title>Concurrency and Multithreading</title><link>https://yeison-liscano.github.io/portfolio/blog/concurrency-and-multithreading/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/concurrency-and-multithreading/</guid><description>Understanding concurrency, parallelism, and multithreading. How threads share memory, why concurrency matters for performance, and practical patterns with async Python.</description><pubDate>Mon, 04 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Concurrency is the ability of a system or program to make progress on multiple
tasks over overlapping periods. Parallelism means executing work at the same
instant, for example on multiple CPU (Central Processing Unit) cores. Concurrency can allow better resource use and
improve the overall performance of a system or program. In a concurrent system,
tasks or processes may appear to be executing simultaneously, even if they are
actually being interleaved or executed in parallel by multiple threads,
processes, or coroutines. The main goal of concurrency is to improve the
responsiveness, throughput, and scalability of a system or program.&lt;/p&gt;
&lt;h2&gt;What is Concurrency?&lt;/h2&gt;
&lt;p&gt;Concurrency means that one task can begin before another finishes. Tasks can
take turns on one thread, overlap I/O (Input/Output) waits, or run in parallel when the runtime
and hardware allow it.&lt;/p&gt;
&lt;h2&gt;What is Multithreading?&lt;/h2&gt;
&lt;p&gt;Multithreading is a specific form of concurrency where a single process can have
multiple threads executing concurrently. A thread is not a subprocess. Unlike processes,
which don&apos;t share memory by default and communicate through explicit IPC (Inter-Process Communication)
mechanisms (pipes, shared memory, sockets), threads share the same memory space
and can interact with each other more efficiently.&lt;/p&gt;
&lt;h2&gt;Why is Concurrency Important?&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Efficiency&lt;/strong&gt;: Concurrency helps applications handle multiple requests at
the same time, making it faster and more responsive.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Scalability&lt;/strong&gt;: As your application grows, you&apos;ll need to handle increasing
amounts of data and tasks. Concurrent designs can overlap waits; parallel
workers can also distribute suitable work across multiple cores.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Resource Utilization&lt;/strong&gt;: Overlapping waits can keep a CPU busy. Multiple
cores and simultaneous multithreading are different hardware features;
concurrency alone guarantees neither full utilization nor lower energy use.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;User Experience&lt;/strong&gt;: For applications that require real-time interaction,
such as gaming or video streaming, concurrency is critical for smooth
performance.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;How Does Concurrency Work in Different Environments?&lt;/h2&gt;
&lt;h3&gt;1. In a Single-Threaded Environment&lt;/h3&gt;
&lt;p&gt;Node.js normally runs JavaScript callbacks on one event-loop thread, while
the OS (Operating System) and a worker pool handle many I/O operations. Nonblocking I/O is well suited
to this model. Long-running JavaScript and synchronous I/O can block the loop.
See &lt;a href=&quot;https://nodejs.org/en/learn/asynchronous-work/dont-block-the-event-loop&quot;&gt;Node.js&apos;s event-loop guide&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;2. In a Multithreaded Environment&lt;/h3&gt;
&lt;p&gt;In multithreaded environments, you can create multiple threads to handle
different tasks concurrently. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One thread can be dedicated to accepting new connections (e.g., in a web
server).&lt;/li&gt;
&lt;li&gt;Another thread can process the request.&lt;/li&gt;
&lt;li&gt;Yet another thread can handle writing the response.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Figure: Three tasks that each compute for 20 ms, wait 60 ms on I/O, then compute 10 ms more. One thread running them in turn takes 270 ms. An event loop fills the waits with other work and finishes in 130 ms. Three threads on three cores finish in 90 ms.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/concurrency-and-multithreading/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;Why is Multithreading Important?&lt;/h2&gt;
&lt;p&gt;Multithreading is particularly important when dealing with I/O-bound operations,
such as network requests or file operations. A blocking call makes its thread
wait, but independent I/O operations need not be sequential. Threads or
asynchronous I/O allow those waits to &lt;strong&gt;overlap&lt;/strong&gt; with other work.&lt;/p&gt;
&lt;h2&gt;How to Implement Concurrency and Multithreading?&lt;/h2&gt;
&lt;h3&gt;1. &lt;strong&gt;In JavaScript/Node.js&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Node.js handles concurrency natively through its &lt;strong&gt;event loop&lt;/strong&gt; and non-blocking
async I/O. The event loop allows Node.js to perform I/O operations without
blocking the main thread, delegating them to the system kernel or a thread pool
(libuv) when possible.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use Promises and async/await patterns to handle multiple asynchronous
operations concurrently.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;Worker Threads&lt;/code&gt; for CPU-bound tasks that would otherwise block the event
loop.&lt;/li&gt;
&lt;li&gt;Use frameworks like &lt;code&gt;Express&lt;/code&gt; or &lt;code&gt;Koa&lt;/code&gt; for building web applications on top of
Node.js&apos;s concurrency model.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. &lt;strong&gt;In Python&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Python supports multithreading and multiprocessing to achieve concurrency.
However, in a GIL (Global Interpreter Lock)-enabled CPython interpreter, only one thread can
execute Python bytecode at a time. This means multithreading is effective for
I/O-bound tasks but offers limited performance gains for CPU-bound tasks.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use the &lt;code&gt;threading&lt;/code&gt; module for basic multithreading (best for I/O-bound
tasks).&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;multiprocessing&lt;/code&gt; if you need to run CPU-bound tasks concurrently,
bypassing the GIL by using separate processes.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;asyncio&lt;/code&gt; with async/await patterns to handle multiple asynchronous
operations concurrently.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Starting with Python 3.13, an experimental &lt;strong&gt;free-threaded mode&lt;/strong&gt; (no-GIL) was
introduced via the &lt;code&gt;--disable-gil&lt;/code&gt; build flag (PEP (Python Enhancement Proposal) 703). In Python 3.14, this
free-threaded build became officially supported while remaining optional (PEP
779). It permits parallel Python execution, but speedups depend on the workload
and synchronization costs. Some extension modules can re-enable the GIL. Native
extensions that release the GIL can also run in parallel in a GIL-enabled build.
See &lt;a href=&quot;https://docs.python.org/3/howto/free-threading-python.html&quot;&gt;Python&apos;s free-threading guide&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Practical Examples of Concurrency&lt;/h2&gt;
&lt;h3&gt;Web Servers&lt;/h3&gt;
&lt;p&gt;Imagine you&apos;re running a web server that handles multiple requests
simultaneously. Without concurrency, each request would have to wait for the
previous one to complete, leading to slow response times and high latency.&lt;/p&gt;
&lt;p&gt;With concurrency:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One thread accepts new connections.&lt;/li&gt;
&lt;li&gt;Another thread processes the request (e.g., reads data from the request and
sends a response).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;File Uploads&lt;/h3&gt;
&lt;p&gt;If you&apos;re uploading files to a server, each upload can be handled by a different
thread. This ensures that multiple uploads happen at the same time without
waiting for one another.&lt;/p&gt;
&lt;h3&gt;Machine Learning Models&lt;/h3&gt;
&lt;p&gt;In machine learning applications, you might want to train multiple models
simultaneously on different cores. Native numerical libraries may release the
GIL and use their own threads; processes or accelerators are other options.
Benchmark the workload, since competing thread pools can reduce performance.&lt;/p&gt;
&lt;h2&gt;Common Challenges in Concurrency&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Race Conditions&lt;/strong&gt;: When multiple threads access shared data concurrently
and at least one modifies it, the outcome depends on the timing of execution,
leading to unpredictable results.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deadlocks&lt;/strong&gt;: When two or more threads are each waiting for a resource held
by the other, none of them can proceed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Starvation&lt;/strong&gt;: A thread is perpetually denied access to resources because
other threads are continuously prioritized over it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context Switching&lt;/strong&gt;: In multithreaded environments, switching between
threads (context switching) introduces overhead at the OS level due to
saving/restoring thread state and CPU cache invalidation.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Key Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Concurrency is about making progress on tasks over overlapping periods;
parallelism executes work simultaneously.&lt;/li&gt;
&lt;li&gt;Multithreading is a specific implementation of concurrency where a single
process has multiple threads.&lt;/li&gt;
&lt;li&gt;In JavaScript/Node.js, concurrency is handled natively through the event loop
and async I/O.&lt;/li&gt;
&lt;li&gt;In Python, you can use &lt;code&gt;threading&lt;/code&gt; or &lt;code&gt;multiprocessing&lt;/code&gt; for basic concurrency.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Resources to Get Started&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;JavaScript/Node.js&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://expressjs.com/en/advanced/best-practice-performance.html&quot;&gt;Express.js Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://javascript.info/async-await&quot;&gt;Understanding Async/Await in Node.js&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://realpython.com/intro-to-python-threading/&quot;&gt;Python Threading Tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@write2bishwarup/asyncio-the-underrated-weapon-for-ml-11a37f315355&quot;&gt;Asyncio&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Concurrency and Multithreading&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://fastapi.tiangolo.com/async/?h=conc#concurrency-and-async-await&quot;&gt;Concurrency and Async Await&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python-guide.org/scenarios/speed/&quot;&gt;The Hitchhiker&apos;s Guide to Python: Concurrency&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>programming</category><category>python</category></item><item><title>Functional Programming</title><link>https://yeison-liscano.github.io/portfolio/blog/functional-programming/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/functional-programming/</guid><description>A practical introduction to functional programming in Python: pure functions, immutability, composition, and common patterns.</description><pubDate>Mon, 04 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Functional programming (FP) is a style that models programs as transformations
of data. Instead of mutating state step by step, we build small functions and
combine them to express intent.&lt;/p&gt;
&lt;p&gt;Python is multi-paradigm, so we can use FP ideas without writing purely
functional code all the time. In practice, this gives us readable pipelines,
testable logic, and fewer side effects.&lt;/p&gt;
&lt;h2&gt;Why Use FP Concepts&lt;/h2&gt;
&lt;p&gt;FP principles are useful because they improve predictability.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pure functions are easier to test.&lt;/li&gt;
&lt;li&gt;Immutable data reduces accidental bugs.&lt;/li&gt;
&lt;li&gt;Composition helps build reusable logic.&lt;/li&gt;
&lt;li&gt;Stateless functions are easier to parallelize.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Core Concepts&lt;/h2&gt;
&lt;h3&gt;Pure Functions&lt;/h3&gt;
&lt;p&gt;A pure function returns the same output for the same input and does not change
external state.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def celsius_to_fahrenheit(celsius: float) -&amp;gt; float:
  return celsius * 9 / 5 + 32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result depends only on &lt;code&gt;celsius&lt;/code&gt;, and the function has no observable side
effects. Avoiding global writes alone does not guarantee determinism: reading a
clock or mutable global state can also change a result.&lt;/p&gt;
&lt;h3&gt;Immutability&lt;/h3&gt;
&lt;p&gt;Immutability means creating new values instead of mutating existing ones.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def add_item(items: tuple[str, ...], item: str) -&amp;gt; tuple[str, ...]:
  return items + (item,)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using immutable structures makes data flow easier to reason about.&lt;/p&gt;
&lt;h3&gt;Higher-Order Functions&lt;/h3&gt;
&lt;p&gt;Higher-order functions receive functions as arguments or return functions.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def apply_twice(fn, value):
  return fn(fn(value))


print(apply_twice(lambda x: x + 3, 10))  # 16
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern lets us abstract behavior, not just data.&lt;/p&gt;
&lt;h3&gt;Referential Transparency&lt;/h3&gt;
&lt;p&gt;If an expression can be replaced by its value without changing behavior, it is
referentially transparent. This simplifies debugging and refactoring.&lt;/p&gt;
&lt;p&gt;For example, replacing &lt;code&gt;double(5)&lt;/code&gt; with &lt;code&gt;10&lt;/code&gt; is always safe if &lt;code&gt;double&lt;/code&gt; is pure.&lt;/p&gt;
&lt;h3&gt;Function Composition&lt;/h3&gt;
&lt;p&gt;Composition means combining small functions into larger ones.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def compose(f, g):
  return lambda x: f(g(x))


strip_and_lower = compose(str.lower, str.strip)
print(strip_and_lower(&quot;  Hello  &quot;))  # hello
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Useful FP Tools in Python&lt;/h2&gt;
&lt;p&gt;Python includes many features that support FP style.&lt;/p&gt;
&lt;h3&gt;map, filter, and reduce&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from functools import reduce

numbers = [1, 2, 3, 4, 5]

doubled = list(map(lambda x: x * 2, numbers))
evens = list(filter(lambda x: x % 2 == 0, doubled))
total = reduce(lambda acc, x: acc + x, evens, 0)

print(doubled)  # [2, 4, 6, 8, 10]
print(evens)    # [2, 4, 6, 8, 10]
print(total)    # 30
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;List comprehensions are often more readable than &lt;code&gt;map&lt;/code&gt; and &lt;code&gt;filter&lt;/code&gt;, but both
approaches are valid.&lt;/p&gt;
&lt;h3&gt;partial for Function Specialization&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from functools import partial


def power(base: int, exponent: int) -&amp;gt; int:
  return base ** exponent


square = partial(power, exponent=2)
cube = partial(power, exponent=3)

print(square(5))  # 25
print(cube(3))    # 27
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is partial application: fixing some arguments to obtain a new callable.
Currying instead transforms a function of several arguments into a sequence of
functions taking one argument each.
See &lt;a href=&quot;https://docs.python.org/3/library/functools.html#functools.partial&quot;&gt;&lt;code&gt;functools.partial&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Practical Pipeline Example&lt;/h2&gt;
&lt;p&gt;Suppose we receive numeric strings and blank lines and want the sum of positive
amounts. This example skips blanks; malformed nonblank strings raise
&lt;code&gt;ValueError&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from typing import Iterable


def parse_amounts(lines: Iterable[str]) -&amp;gt; list[float]:
  return [float(line) for line in lines if line.strip()]


def only_positive(values: Iterable[float]) -&amp;gt; list[float]:
  return [v for v in values if v &amp;gt; 0]


def total(values: Iterable[float]) -&amp;gt; float:
  return sum(values)


raw = [&quot;100&quot;, &quot;-40&quot;, &quot;&quot;, &quot;18.5&quot;, &quot;-2&quot;, &quot;7.5&quot;]
result = total(only_positive(parse_amounts(raw)))
print(result)  # 126.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each step is small, focused, and easy to test in isolation.&lt;/p&gt;
&lt;h2&gt;Recursion and Python&lt;/h2&gt;
&lt;p&gt;Recursion is common in FP, but Python does not optimize tail recursion. For
large inputs, iterative solutions are often safer.&lt;/p&gt;
&lt;p&gt;Use recursion when it improves clarity, but keep Python&apos;s recursion limit in
mind.&lt;/p&gt;
&lt;h2&gt;Common Pitfalls&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Overusing lambdas and making code harder to read.&lt;/li&gt;
&lt;li&gt;Forcing &lt;code&gt;reduce&lt;/code&gt; when &lt;code&gt;sum&lt;/code&gt; or a loop is clearer.&lt;/li&gt;
&lt;li&gt;Mixing pure and impure logic in the same function.&lt;/li&gt;
&lt;li&gt;Ignoring readability in the name of style purity.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;FP is most valuable when it improves maintainability, not when it is applied as
a strict rule.&lt;/p&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Functional programming in Python is about disciplined data transformation. You
can gradually adopt it by writing pure helper functions, limiting mutation, and
composing operations into clear pipelines.&lt;/p&gt;
&lt;p&gt;Used well, FP makes code easier to test, reason about, and evolve.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://wiki.haskell.org/index.php?title=Functional_programming&quot;&gt;HaskellWiki: Functional programming&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.haskell.org/&quot;&gt;Haskell Language&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>programming</category><category>python</category></item><item><title>Python Great Features</title><link>https://yeison-liscano.github.io/portfolio/blog/python-great-features/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/python-great-features/</guid><description>Exploring duck typing, Abstract Base Classes, Protocols, and caching decorators. The features that make Python flexible and expressive beyond the basics.</description><pubDate>Mon, 04 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Duck Typing, ABCs, and Protocols&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;If it looks like a duck, swims like a duck, and quacks like a duck, then it
probably is a duck.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The idea behind duck typing is that you don&apos;t need to check the type of an
object before using it. Instead you care about what methods and properties the
object has (API (Application Programming Interface)). If it has the methods and properties you need, you can use it.&lt;/p&gt;
&lt;p&gt;Abstract Base Classes (ABCs) and Protocols enable us to keep the flexibility of
duck typing, but also have some kind of type checking.&lt;/p&gt;
&lt;h3&gt;Abstract Base Classes (ABCs)&lt;/h3&gt;
&lt;p&gt;ABCs provide a way to enforce a contract between classes.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from abc import ABC, abstractmethod

class Animal(ABC):
    def __init__(self, name):
        self.name = name

    @abstractmethod
    def speak(self):
        raise NotImplementedError(&quot;speak() must be implemented&quot;)

class Dog(Animal):
    def speak(self):
        return &apos;Woof!&apos;

dog = Dog(&quot;Rex&quot;)
print(dog.speak())
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Protocols&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;The term protocols is used for some types supporting structural subtyping.&lt;/p&gt;
&lt;p&gt;A structural type defines a set of values not by their &lt;strong&gt;class&lt;/strong&gt;, but by their
properties (e.g. attributes, methods, dictionary key/value types)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Protocols are a way to define a set of methods that a class must implement in
order to satisfy that protocol during static type checking. Python does not
automatically enforce it at runtime. &lt;code&gt;@runtime_checkable&lt;/code&gt; permits limited
attribute-presence checks, not validation of method signatures.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from typing import Protocol

class Animal(Protocol):
    def speak(self) -&amp;gt; str: ...

class Dog:
    def speak(self) -&amp;gt; str:
        return &apos;Woof!&apos;

def speak(pet: Animal) -&amp;gt; str:
    return pet.speak()

print(speak(Dog()))  # Passes static type check
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Type Hints&lt;/h2&gt;
&lt;p&gt;Type hints are a way to add type information to your code. They are not enforced
by Python, but they can be used by tools like &lt;strong&gt;MyPy&lt;/strong&gt; to check your code for
type errors.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def add(x: int, y: int) -&amp;gt; int:
    return x + y

print(add(3, 5))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I will use type hints in the examples I provide in this post.&lt;/p&gt;
&lt;h2&gt;Functional Features&lt;/h2&gt;
&lt;h3&gt;Closures&lt;/h3&gt;
&lt;p&gt;Functions can read or mutate state through globals, arguments, or enclosing
scopes. A closure retains access to bindings in an enclosing function after that
function returns; this can preserve state between calls.&lt;/p&gt;
&lt;p&gt;A closure is a function object that remembers values in enclosing scopes. An
outer function can return such a function, which continues to refer to its
enclosing bindings after the outer call finishes. It means variables in the
enclosing scope are kept in memory even after the function has finished
executing.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from collections.abc import Callable

def request_token() -&amp;gt; str:
    return &quot;my-secret-token&quot;

def get_token() -&amp;gt; Callable[[], str]:
    _token: str | None = None
    def _get():
        nonlocal _token
        if _token:
            print(&apos;Using cached token&apos;)
            return _token

        print(&apos;Fetching new token&apos;)
        # call some API to get the token
        _token = request_token()
        return _token
    return _get

token = get_token()
print(token())
print(token())
print(token())
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Fetching new token
my-secret-token
Using cached token
my-secret-token
Using cached token
my-secret-token
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The next example uses a mock HTTP (Hypertext Transfer Protocol) client to illustrate token caching. A real
client also needs token expiry handling, failed-response handling, and
synchronization if concurrent callers may refresh the same token. The examples
with top-level &lt;code&gt;await&lt;/code&gt; run in an async-capable notebook or the page runner; in a
regular script, put the async work inside &lt;code&gt;main()&lt;/code&gt; and use
&lt;code&gt;asyncio.run(main())&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
import base64
from collections.abc import (
    Callable,
    Awaitable,
)
import os

# Mock for browser execution (aiohttp requires server environment)
class _MockResponse:
    async def json(self):
        return {&quot;token&quot;: &quot;mock-jwt-token&quot;}
    async def __aenter__(self):
        return self
    async def __aexit__(self, *a):
        pass

class ClientSession:
    def __init__(self, base_url=&quot;&quot;):
        pass
    def post(self, url, **kw):
        return _MockResponse()
    async def __aenter__(self):
        return self
    async def __aexit__(self, *a):
        pass

COMMON_HEADERS = {&quot;Content-Type&quot;: &quot;application/json&quot;}
os.environ[&quot;PUBLIC_KEY&quot;] = &quot;test_public_key&quot;
os.environ[&quot;SECRET_KEY&quot;] = &quot;test_secret_key&quot;


def _get_access_token() -&amp;gt; Callable[[ClientSession], Awaitable[str]]:
    current_token: str | None = None

    async def _access_token(session: ClientSession) -&amp;gt; str:
        nonlocal current_token

        if current_token:
            print(&apos;Using cached token&apos;)
            return current_token

        print(&apos;Fetching new token&apos;)
        PUBLIC_KEY = os.getenv(&quot;PUBLIC_KEY&quot;)
        SECRET_KEY = os.getenv(&quot;SECRET_KEY&quot;)
        text = f&quot;{PUBLIC_KEY}:{SECRET_KEY}&quot;
        encode = base64.b64encode(text.encode(&quot;utf-8&quot;))
        token = str(encode, &quot;utf-8&quot;)
        authorization = f&quot;Basic {token}&quot;
        headers = {**COMMON_HEADERS, &quot;Authorization&quot;: authorization}
        async with session.post(
            &quot;/login&quot;,
            headers=headers,
        ) as response:
            content = await response.json()
            if content and content.get(&quot;token&quot;):
                current_token = content[&quot;token&quot;]

        if current_token is None:
            raise ValueError(&quot;Token not found&quot;)

        return current_token

    return _access_token


_get_access_token = _get_access_token()

async def main() -&amp;gt; None:
    async with ClientSession(&quot;https://api.example.com/&quot;) as session:
        token = await _get_access_token(session)
        token = await _get_access_token(session)
        token = await _get_access_token(session)
        token = await _get_access_token(session)

await main()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Fetching new token
Using cached token
Using cached token
Using cached token
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Decorators (Higher order functions)&lt;/h3&gt;
&lt;p&gt;Decorators are a way to wrap a function and add extra functionalities to such a
function.&lt;/p&gt;
&lt;p&gt;In the following example we define a decorator that adds an access token to the
headers of a request. Note that the decorator receives a function that receives
a &lt;code&gt;ClientSession&lt;/code&gt; instance as first argument, that instance will be used to get
the access token without having to open a new session for each request.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from collections.abc import (
    Awaitable,
)
from functools import wraps
from typing import (
    Callable,
    Concatenate,
    ParamSpec,
    TypeVar,
)

# Mock for browser execution (aiohttp requires server environment)
class _MockResponse:
    def __init__(self, data):
        self._data = data
    async def json(self):
        return self._data
    async def __aenter__(self):
        return self
    async def __aexit__(self, *a):
        pass

class ClientSession:
    def __init__(self, base_url=&quot;&quot;):
        pass
    def get(self, url, **kw):
        return _MockResponse({&quot;id&quot;: 1, &quot;name&quot;: &quot;Alice&quot;})
    async def __aenter__(self):
        return self
    async def __aexit__(self, *a):
        pass

async def _get_access_token(session):
    return &quot;mock-token&quot;

T = TypeVar(&apos;T&apos;)
P = ParamSpec(&apos;P&apos;)


def add_headers(
    func: Callable[Concatenate[ClientSession, P], Awaitable[T]],
) -&amp;gt; Callable[Concatenate[ClientSession, P], Awaitable[T]]:
    @wraps(func)
    async def decorated(
        session: ClientSession, /, *args: P.args, **kwargs: P.kwargs
    ) -&amp;gt; T:
        token = await _get_access_token(session)
        headers = {
            &quot;Content-Type&quot;: &quot;application/json&quot;,
            &quot;Accept&quot;: &quot;application/json&quot;,
            &quot;Authorization&quot;: f&quot;Bearer {token}&quot;,
        }
        kwargs[&quot;headers&quot;] = headers

        return await func(session, *args, **kwargs)

    return decorated


@add_headers
async def get_user(
    session: ClientSession,
    user_id: int,
    *,
    headers: dict[str, str] | None = None
) -&amp;gt; dict[str, object]:
    async with session.get(f&quot;users/{user_id}&quot;, headers=headers) as resp:
        return await resp.json()

async with ClientSession(&quot;https://api.example.com/&quot;) as session:
    user = await get_user(session, 1) # headers will be added by the decorator
    print(user)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;wraps&lt;/h4&gt;
&lt;p&gt;without &lt;code&gt;@wraps&lt;/code&gt; the name of the function will be &lt;code&gt;wrapper_function&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def mock_decorator(original_function):
    def wrapper_function(*args, **kwargs):
        return original_function(*args, **kwargs)
    return wrapper_function

@mock_decorator
def f(x):
    &apos;&apos;&apos;Docstring&apos;&apos;&apos;
    return x ** 2

print(f.__name__)
print(f.__doc__)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;wrapper_function
None
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;from functools import wraps

def mock_decorator(original_function):
    @wraps(original_function)
    def wrapper_function(*args, **kwargs):
        return original_function(*args, **kwargs)
    return wrapper_function

@mock_decorator
def f(x):
    &apos;&apos;&apos;Docstring&apos;&apos;&apos;
    return x ** 2

print(f.__name__)
print(f.__doc__)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;f
Docstring
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;def stateful_function(func):
    cache = {}
    def wrapper_function(*args, **kwargs):
        key = (args, tuple(sorted(kwargs.items())))
        if key not in cache:
            cache[key] = func(*args, **kwargs)
        return cache[key]
    return wrapper_function

@stateful_function
def fibonacci(n):
    if n &amp;lt; 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(10))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The manual cache above requires hashable arguments and keyword values; it is
only a teaching example. Stringifying arguments is not a reliable cache key,
because different objects can have the same string representation. For pure functions,
prefer &lt;a href=&quot;https://docs.python.org/3/library/functools.html&quot;&gt;&lt;code&gt;functools.cache&lt;/code&gt; or &lt;code&gt;lru_cache&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;cache&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;import time
from functools import cache

def timer(function):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = function(*args, **kwargs)
        end = time.time()
        print(f&apos;Elapsed time: {end - start}&apos;)
        return result
    return wrapper

@cache
def fibonacci(n):
    if n &amp;lt; 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

@timer
def fibonacci_timer(n):
    return fibonacci(n)

print(fibonacci_timer(10))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is kinda like a cache (&lt;code&gt;memoization&lt;/code&gt;), so that the function doesn&apos;t have to
be called again if the arguments are the same.&lt;/p&gt;
&lt;h2&gt;Working with Files&lt;/h2&gt;
&lt;h3&gt;Generators&lt;/h3&gt;
&lt;p&gt;Generators are a special class of functions that simplify the task of writing
iterators. Generators are a simple and powerful tool for creating iterators.
They are written like regular functions but use the &lt;code&gt;yield&lt;/code&gt; statement whenever
they want to return data. Each time &lt;code&gt;next()&lt;/code&gt; is called on it, the generator
resumes where it left off (it remembers all the data values and which statement
was last executed).&lt;/p&gt;
&lt;p&gt;Generators are iterators, a kind of iterable you can only iterate over once.
It’s because they do not store all the values in memory, they generate the
values on the fly, that allows processing of large amounts of data efficiently.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Create a sample file for demonstration
with open(&apos;file.txt&apos;, &apos;w&apos;) as f:
    f.write(&apos;Hello from a large file!\n&apos; * 10)

def read_large_file(filename):
    with open(filename) as file:
        while True:
            chunk = file.read(1000)
            if not chunk:
                break
            yield chunk

for chunk in read_large_file(&apos;file.txt&apos;):
    print(chunk)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;def fibonacci(n):
    a, b = 0, 1
    for _ in range(n):
        yield a
        a, b = b, a + b

for i in fibonacci(10):
    print(i)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;0
1
1
2
3
5
8
13
21
34
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Context Managers&lt;/h3&gt;
&lt;p&gt;Context managers are used to allocate and release resources precisely when you
want to. They are very useful when you are working with external resources like
files, network connections, etc. Context managers are normally implemented using
a class that implements the special methods &lt;code&gt;__enter__()&lt;/code&gt; and &lt;code&gt;__exit__()&lt;/code&gt;. The
&lt;code&gt;__enter__()&lt;/code&gt; method is invoked when the &lt;code&gt;with&lt;/code&gt; statement is encountered. The
&lt;code&gt;__exit__()&lt;/code&gt; method is invoked at the end of the &lt;code&gt;with&lt;/code&gt; block.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class OpenFile:
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode

    def __enter__(self):
        self.file = open(self.filename, self.mode)
        return self.file

    def __exit__(self, exc_type, exc_val, traceback):
        self.file.close()

with OpenFile(&apos;sample.txt&apos;, &apos;w&apos;) as f:
    f.write(&apos;Testing&apos;)

with open(&apos;sample.txt&apos;, &apos;r&apos;) as f:
    print(f.read())
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import time

class Timer:
    def __init__(self):
        self.start = None
        self.end = None

    def __enter__(self):
        self.start = time.time()
        return self

    def __exit__(self, exc_type, exc_val, traceback):
        self.end = time.time()

    def elapsed_time(self):
        return self.end - self.start

with Timer() as timer:
    print(&apos;This should take approximately 2 seconds&apos;)
    time.sleep(2)

print(&apos;Elapsed time: {}&apos;.format(timer.elapsed_time()))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that the &lt;code&gt;__exit__()&lt;/code&gt; method can optionally return a Boolean value. If it
returns &lt;code&gt;True&lt;/code&gt;, any exception raised within the &lt;code&gt;with&lt;/code&gt; block is suppressed and
execution proceeds as if no exception had occurred. If it returns &lt;code&gt;False&lt;/code&gt;, any
exception raised within the &lt;code&gt;with&lt;/code&gt; block propagates to the surrounding code. A
return value of &lt;code&gt;None&lt;/code&gt; also leaves the exception unsuppressed.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import threading

lock = threading.Lock()

class LockedContext:
    def __init__(self, lock):
        self.lock = lock

    def __enter__(self):
        print(&apos;acquiring lock&apos;)
        self.lock.acquire()

    def __exit__(self, exc_type, exc_val, traceback):
        print(&apos;releasing lock&apos;)
        self.lock.release()

with LockedContext(lock):
    print(&apos;Lock acquired&apos;)

# The lock is automatically released when the with block ends,
# even if an error occurs
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>programming</category><category>python</category></item><item><title>Python Virtual Environments</title><link>https://yeison-liscano.github.io/portfolio/blog/python-virtual-environments/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/python-virtual-environments/</guid><description>How to isolate project dependencies with virtual environments. Creating, activating, freezing requirements, and packaging Python projects with Docker.</description><pubDate>Mon, 04 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Virtual environments are used to create an isolated environment for Python
projects. This means that each project can have its own dependencies, regardless
of what dependencies every other project has. First of all let&apos;s review how to
install, update, and uninstall packages in a virtual environment with pip. Note:
The shell examples below target Linux and macOS. Create and activate an
environment first (&lt;code&gt;python3 -m venv .venv&lt;/code&gt;, then &lt;code&gt;source .venv/bin/activate&lt;/code&gt;);
&lt;code&gt;python -m pip&lt;/code&gt; then targets that environment. Braced names are placeholders.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Contains a specific Python interpreter and software libraries and binaries
which are needed to support a project (library or application). These are by
default isolated from software in other virtual environments and Python
interpreters and libraries installed in the operating
system. &lt;a href=&quot;https://docs.python.org/3/library/venv.html&quot;&gt;Python docs&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Freezing packages (PIP)&lt;/h2&gt;
&lt;p&gt;To freeze the packages installed in the virtual environment, you can use the
&lt;code&gt;freeze&lt;/code&gt; command. This will create a &lt;code&gt;requirements.txt&lt;/code&gt; file with all the
installed distributions and their versions (some packaging tools are omitted by
default). This is an environment snapshot, not a dependency resolver or a
cross-platform lockfile.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python -m pip freeze &amp;gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Installing packages (PIP)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;python -m pip install {package_name}
python -m pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Uninstalling packages&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;python -m pip uninstall {package_name}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Updating packages&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;python -m pip install --upgrade {package_name}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Creating a virtual environment&lt;/h2&gt;
&lt;p&gt;To create a virtual environment, you could use the &lt;code&gt;venv&lt;/code&gt; module that comes with
Python, this is the recommended way to create virtual environments.&lt;/p&gt;
&lt;h3&gt;venv (built-in module in python 3.3+)&lt;/h3&gt;
&lt;p&gt;Extracted from &lt;a href=&quot;https://docs.python.org/3/library/venv.html&quot;&gt;python venv docs&lt;/a&gt; A
virtual environment is (amongst other things):&lt;/p&gt;
&lt;p&gt;Used to contain a specific Python interpreter and software libraries and
binaries which are needed to support a project (library or application). These
are by default isolated from software in other virtual environments and Python
interpreters and libraries installed in the operating system.&lt;/p&gt;
&lt;p&gt;Contained in a directory, conventionally named .venv or venv in the project
directory, or under a container directory for lots of virtual environments, such
as ~/.virtualenvs.&lt;/p&gt;
&lt;p&gt;Not checked into source control systems such as Git.&lt;/p&gt;
&lt;p&gt;Considered as disposable – it should be simple to delete and recreate it from
scratch. You don&apos;t place any project code in the environment.&lt;/p&gt;
&lt;p&gt;Not considered as movable or copyable – you just recreate the same environment
in the target location.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Run the venv module as a script with the -m option, passing the route
# where the virtual environment info will be store (environment name).
python3 -m venv {env_name}
source {env_name}/bin/activate
python -m pip install -r requirements.txt
python -m pip freeze &amp;gt; requirements.txt
deactivate
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;pipenv&lt;/h3&gt;
&lt;p&gt;Extracted from &lt;a href=&quot;https://pipenv.pypa.io/en/latest/&quot;&gt;pipenv docs&lt;/a&gt; The problems
that Pipenv seeks to solve are multi-faceted:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;You no longer need to use pip and virtualenv separately: they work together.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Managing a requirements.txt file with package hashes can be problematic.
Pipenv uses Pipfile and Pipfile.lock to separate abstract dependency
declarations from the last tested combination.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Hashes are documented in the lock file which are verified during install.
Security considerations are put first.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Strongly encourage the use of the latest versions of dependencies to minimize
security risks arising from outdated components.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Gives you insight into your dependency graph (e.g. $ pipenv graph).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Streamline development workflow by supporting local customizations with .env
files.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;python -m pip install pipenv
cd {project_dir}
pipenv install -r requirements.txt
pipenv uninstall {package_name}
pipenv install -e git+repo_url@branch#egg=package_name
# Run a script using pipenv run
pipenv run python main.py
# install a package from a git repo
pipenv shell
pipenv lock # creates a Pipfile.lock file
exit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install all the packages in the Pipfile.lock file (replicates the environment)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pipenv install --ignore-pipfile
pipenv graph # shows the dependency graph
pipenv check --scan # vulnerability scan; may require service credentials
exit
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;virtualenv (Lower level)&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;virtualenv can help you. It creates an environment that has its own
installation directories, that doesn&apos;t share libraries with other virtualenv
environments (and optionally doesn&apos;t access the globally installed libraries
either). &lt;a href=&quot;https://virtualenv.pypa.io/en/latest/&quot;&gt;virtualenv docs&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;# basic usage
python -m pip install virtualenv
cd project_folder
virtualenv venv
source venv/bin/activate
pip install requests
deactivate

# specify the python version, note that the python version must be installed
# in the system.
virtualenv --python=python3.14 {env_name}
# lsvirtualenv belongs to the separate virtualenvwrapper package
source {env_name}/bin/activate
pip install -r requirements.txt # All other pip commands work as usual
deactivate
rm -rf {env_name} # remove the virtual environment
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To delete a virtual environment, just delete its folder. (In this case, it would
be rm -rf venv.)&lt;/p&gt;
&lt;h2&gt;Isolating the versions of Python&lt;/h2&gt;
&lt;p&gt;Different projects may require different versions of Python. To isolate the
versions of Python, you can use &lt;code&gt;pyenv&lt;/code&gt; or &lt;code&gt;pyenv-virtualenv&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;pyenv&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;pyenv&lt;/code&gt; installs and selects Python interpreters; it does not create virtual
environments by itself. Install it using
the &lt;a href=&quot;https://github.com/pyenv/pyenv#installation&quot;&gt;official instructions&lt;/a&gt;, including
shell initialization and build dependencies. It is not installed with pip.
Upstream pyenv targets Unix-like systems; Windows users can use WSL (Windows Subsystem for Linux) or the
separate pyenv-win project.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pyenv install --list
pyenv install {version}
pyenv versions
pyenv local {version}
python -m venv .venv
source .venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pyenv global&lt;/code&gt; sets the user-wide default, while &lt;code&gt;pyenv local&lt;/code&gt; writes the
project&apos;s &lt;code&gt;.python-version&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;pyenv-virtualenv&lt;/h3&gt;
&lt;p&gt;The
optional &lt;a href=&quot;https://github.com/pyenv/pyenv-virtualenv&quot;&gt;pyenv-virtualenv plugin&lt;/a&gt; adds
&lt;code&gt;pyenv virtualenv&lt;/code&gt;, &lt;code&gt;pyenv virtualenvs&lt;/code&gt;, and activation commands. Install and
initialize that plugin before using these commands.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pyenv install {version}
pyenv virtualenv {version} {env_name}
pyenv local {env_name}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Anaconda&lt;/h3&gt;
&lt;p&gt;Anaconda Distribution bundles Python and scientific-computing packages. Conda is
an open-source environment and package manager, but Anaconda Distribution and
its hosted repositories have separate terms; free use depends on eligibility.
Check
the &lt;a href=&quot;https://www.anaconda.com/legal/terms/terms-of-service&quot;&gt;Anaconda terms&lt;/a&gt;. Install
the current distribution for your operating system from the official site before
running the following Conda examples.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conda info
conda create --name {env_name} python={version}
conda activate {env_name}
conda install {package_name}
conda list
conda update {package_name}
conda remove {package_name}
conda deactivate
conda remove --name {env_name} --all
conda create --name {new_env} --clone {source_env} # clone an environment
conda env export &amp;gt; environment.yml # export an environment
conda env create -f environment.yml # create an environment from a yml file
conda env list
conda env remove -n {env_name} # remove an environment
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;some useful commands&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conda list # list all the packages installed in the current environment
# list all the packages installed in the current environment with their versions
conda list --explicit
# install a package from a specific channel
conda install --channel conda-forge {package_name}
conda list --revisions # list all the revisions of the environment
 # install a specific revision of the environment
conda install --revision {revision_number}
# export the environment to a yml file
conda env export --from-history --file environment.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Mamba&lt;/h3&gt;
&lt;p&gt;Mamba is a fast, drop-in replacement for the conda package manager. It is a C++
implementation of the conda package manager, that uses multi-threading and a
sophisticated dependency solver to achieve maximum performance.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conda install -c conda-forge mamba
mamba --help
mamba create --name {env_name} python={version}
mamba activate {env_name}
mamba install {package_name}
mamba list
mamba update {package_name}
mamba remove {package_name}
mamba deactivate
mamba env create -f environment.yml # create an environment from a yml file
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Docker&lt;/h3&gt;
&lt;p&gt;Docker is a containerization platform that allows you to create and run
containers. Docker containers are isolated environments that run on a single
host. To install docker on ubuntu,
follow &lt;a href=&quot;https://docs.docker.com/engine/install/ubuntu/&quot;&gt;this&lt;/a&gt; tutorial.&lt;/p&gt;
&lt;h4&gt;Containerizing a Python script&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# Dockerfile
FROM python:3.14-slim
WORKDIR /app
COPY [&quot;requirements.txt&quot;, &quot;/app&quot;]
RUN python -m pip install --no-cache-dir -r requirements.txt
COPY app.py /app/app.py
CMD [&quot;python&quot;, &quot;app.py&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alternatively, you can use docker compose to build and run the docker container.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# compose.yaml
services:
  app-csv:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - .:/app
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;docker compose build
docker compose up -d
docker compose ps
docker compose logs
docker compose exec app-csv bash
cd /app
python3 app.py
exit
docker compose down
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Containerizing an ASGI app&lt;/h4&gt;
&lt;p&gt;This assumes &lt;code&gt;app.py&lt;/code&gt; exports an ASGI (Asynchronous Server Gateway Interface) application named &lt;code&gt;app&lt;/code&gt; (for example,
FastAPI) and &lt;code&gt;requirements.txt&lt;/code&gt; includes Uvicorn and the framework. A Flask app
uses WSGI (Web Server Gateway Interface) and needs a WSGI server or an ASGI adapter. The Dockerfile syntax is
documented in
the &lt;a href=&quot;https://docs.docker.com/reference/dockerfile/&quot;&gt;Dockerfile reference&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Dockerfile
FROM python:3.14-slim
WORKDIR /app
COPY [&quot;requirements.txt&quot;, &quot;/app&quot;]
RUN python -m pip install --no-cache-dir -r requirements.txt
COPY . /app
CMD [&quot;uvicorn&quot;, &quot;app:app&quot;, &quot;--host&quot;, &quot;0.0.0.0&quot;, &quot;--port&quot;, &quot;80&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# compose.yaml
services:
  app-asgi:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - .:/app
    ports:
      - &quot;80:80&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;docker compose build
docker compose up -d
docker compose ps
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Choosing a tool&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;venv&lt;/code&gt; and pip for a simple Python-only project.&lt;/li&gt;
&lt;li&gt;Use Pipenv when you want a Pipfile and lockfile workflow.&lt;/li&gt;
&lt;li&gt;Use Conda or Mamba when managing Python together with native dependencies.&lt;/li&gt;
&lt;li&gt;Use pyenv when you need to install and select multiple Python interpreters.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are alternative workflows; you do not need to stack all of them.&lt;/p&gt;
&lt;h4&gt;Divide and conquer&lt;/h4&gt;
&lt;p&gt;more info in
the &lt;a href=&quot;https://snakemake.readthedocs.io/en/stable/snakefiles/deployment.html#integrated-package-management&quot;&gt;Snakemake documentation&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>python</category><category>tools</category></item><item><title>Terminal Basics</title><link>https://yeison-liscano.github.io/portfolio/blog/terminal-basics/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/terminal-basics/</guid><description>A beginner-friendly guide to the command line. What shells, terminals, and commands are, plus essential operations for navigating and managing files.</description><pubDate>Mon, 04 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The examples target Bash on GNU (GNU&apos;s Not Unix)/Linux unless stated otherwise. macOS uses BSD (Berkeley Software Distribution)
versions of several utilities, and flags can differ. Replace angle-bracketed
placeholders before running commands; quote paths containing spaces.&lt;/p&gt;
&lt;h2&gt;What is a command?&lt;/h2&gt;
&lt;p&gt;A command is a program that you can run from the terminal, it is also a utility
of the shell, a shell function, or an alias. A command can receive arguments and
options.&lt;/p&gt;
&lt;h2&gt;What is a shell?&lt;/h2&gt;
&lt;p&gt;A shell is a command line interpreter, it is a program that reads commands from
the keyboard and executes them.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bourne shell (sh)&lt;/li&gt;
&lt;li&gt;Bash shell (bash)&lt;/li&gt;
&lt;li&gt;C shell (csh)&lt;/li&gt;
&lt;li&gt;Korn shell (ksh)&lt;/li&gt;
&lt;li&gt;Z shell (zsh)&lt;/li&gt;
&lt;li&gt;Fish shell (fish)&lt;/li&gt;
&lt;li&gt;PowerShell (powershell)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What is a terminal?&lt;/h2&gt;
&lt;p&gt;A terminal emulator provides a text interface to a shell or another program. It
often runs inside a graphical desktop window; the shell is the command
interpreter running within it.&lt;/p&gt;
&lt;h2&gt;Commands&lt;/h2&gt;
&lt;h3&gt;ls&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ls -al&lt;/code&gt; : list files in long format, including hidden files&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -lrt&lt;/code&gt; : list files in long format, sorted by modification time, oldest
first&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -lh&lt;/code&gt; : list files in long format, with human readable sizes&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;tree : list files in a tree format&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tree -L 2&lt;/code&gt; : list files in a tree format, 2 levels deep.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;cd [path]&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cd&lt;/code&gt; : move to home directory&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd -&lt;/code&gt;: change directory to previous directory&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd ..&lt;/code&gt; : change directory to parent directory&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd /&lt;/code&gt; : change directory to root directory&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd &amp;lt;path&amp;gt;&lt;/code&gt;: change directory to the specified path&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd ~&amp;lt;user&amp;gt;&lt;/code&gt;: change directory to user&apos;s home directory&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd ~&lt;/code&gt;: change directory to current user&apos;s home directory&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;pwd&lt;/strong&gt; (print working directory)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pwd&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;managing files&lt;/h2&gt;
&lt;h3&gt;touch&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;touch file_name&lt;/code&gt;: create an empty file if absent; otherwise update its
timestamps.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;touch file_1_name file_2_name&lt;/code&gt; : create file1 and file2&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;gt; file_name&lt;/code&gt; : create an empty file or truncate an existing file to zero
bytes&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;cp&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cp file_name file_name_copy&lt;/code&gt;: copy file_name to file_name_copy.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cp -r dir_name dir_name_copy&lt;/code&gt;: copy dir_name to dir_name_copy.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cp -p&lt;/code&gt; : copy files preserving attributes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cp -a&lt;/code&gt; : copy files preserving attributes and links&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cp -i&lt;/code&gt; : prompt before overwriting existing files&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cp -f&lt;/code&gt; : force overwrite existing files&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;mv&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mv &amp;lt;file_name&amp;gt; &amp;lt;path&amp;gt;&lt;/code&gt; : move file to specified path&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mv &amp;lt;file1&amp;gt; &amp;lt;file2&amp;gt;&lt;/code&gt; : rename file1 to file2&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mv -i&lt;/code&gt; : prompt before overwriting existing files&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mv -f&lt;/code&gt; : force overwrite existing files&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mv &amp;lt;dir1&amp;gt; &amp;lt;path&amp;gt;&lt;/code&gt;: move dir1 to specified path&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;To include hidden entries in Bash, use &lt;code&gt;dotglob&lt;/code&gt; and &lt;code&gt;nullglob&lt;/code&gt;, then pass the
expanded array to &lt;code&gt;mv&lt;/code&gt;. The destination must be outside the source:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(
  shopt -s dotglob nullglob
  files=(source_dir/*)
  if ((${#files[@]})); then
    mv -- &quot;${files[@]}&quot; destination_dir/
  fi
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;mkdir&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mkdir &amp;lt;dir_name&amp;gt;&lt;/code&gt;: create dir_name directory.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mkdir &amp;lt;dir1&amp;gt; &amp;lt;dir2&amp;gt; &amp;lt;dir3&amp;gt;&lt;/code&gt; : create directories dir1, dir2, dir3&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mkdir -p&lt;/code&gt; : create parent directories as needed&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;cat head tail less&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cat &amp;lt;file_name&amp;gt;&lt;/code&gt;: print file_name contents.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cat &amp;gt; &amp;lt;file_name&amp;gt;&lt;/code&gt; : create file and write to it.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cat &amp;lt;file_name1&amp;gt; &amp;lt;file_name2&amp;gt;&lt;/code&gt; : print file1 and file2 contents.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;head &amp;lt;file_name&amp;gt;&lt;/code&gt;: print the first 10 lines of file_name.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;head -n 20 &amp;lt;file_name&amp;gt;&lt;/code&gt;: print the first 20 lines of file_name.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tail &amp;lt;file_name&amp;gt;&lt;/code&gt;: print the last 10 lines of file_name.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tail -n 20 &amp;lt;file_name&amp;gt;&lt;/code&gt;: print the last 20 lines of file_name.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;less &amp;lt;file_name&amp;gt;&lt;/code&gt;: print file_name contents in less.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;less +20 &amp;lt;file_name&amp;gt;&lt;/code&gt;: print file_name contents in less starting at line 20.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;less +/text &amp;lt;file_name&amp;gt;&lt;/code&gt;: print file_name contents in less starting at the
first occurrence of text.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;less -N &amp;lt;file_name&amp;gt;&lt;/code&gt;: print file_name contents in less with line numbers.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;less -S &amp;lt;file_name&amp;gt;&lt;/code&gt;: print file_name contents in less without wrapping
lines.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;less -F &amp;lt;file_name&amp;gt;&lt;/code&gt;: exit automatically if the contents fit on one screen.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;less -R &amp;lt;file_name&amp;gt;&lt;/code&gt;: display supported color escape sequences.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;less -X &amp;lt;file_name&amp;gt;&lt;/code&gt;: skip terminal initialization/deinitialization
sequences.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;less -Q &amp;lt;file_name&amp;gt;&lt;/code&gt;: suppress the terminal bell.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;less -K &amp;lt;file_name&amp;gt;&lt;/code&gt;: exit on an interrupt, such as Ctrl-C.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;less -M &amp;lt;file_name&amp;gt;&lt;/code&gt;: use a more detailed prompt.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;less -i &amp;lt;file_name&amp;gt;&lt;/code&gt;: case-insensitive search unless the pattern has
uppercase letters.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;less -I &amp;lt;file_name&amp;gt;&lt;/code&gt;: always ignore case in searches.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;less -I -N -S &amp;lt;file_name&amp;gt;&lt;/code&gt;: combine case-insensitive search, line numbers and
unwrapped lines.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;less&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;less file_name&lt;/code&gt;: print file contents, one page at a time.
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/word_to_search_for&lt;/code&gt; : allow you to search for a word in the file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;q&lt;/code&gt; : quit.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;file&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;file &amp;lt;path&amp;gt;&lt;/code&gt; : print file type&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;rm&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rm &amp;lt;file_name&amp;gt;&lt;/code&gt;: delete file_name.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rm -i &amp;lt;file&amp;gt;&lt;/code&gt; : prompt before removing a file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rm -f &amp;lt;file&amp;gt;&lt;/code&gt; : remove without prompting; ignore missing files&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rm -r &amp;lt;dir&amp;gt;&lt;/code&gt; : remove directories recursively&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rmdir &amp;lt;dir_name&amp;gt;&lt;/code&gt;: remove an empty directory.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rm -rf &amp;lt;dir_name&amp;gt;&lt;/code&gt;: delete dir_name and its contents.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;xdg-open / open&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;xdg-open file_name&lt;/code&gt;: open with the default desktop application on Linux.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;open file_name&lt;/code&gt;: use the default application on macOS.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;nautilus&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nautilus [path_to_folder]&lt;/code&gt; : open file manager&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;type&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;type command&lt;/code&gt;: print command type&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;alias&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;alias&lt;/code&gt;: list aliases&lt;/li&gt;
&lt;li&gt;&lt;code&gt;alias foo=&quot;bar&quot;&lt;/code&gt;: create alias foo for bar&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;help&lt;/h3&gt;
&lt;p&gt;In Bash, &lt;code&gt;help&lt;/code&gt; is a built-in that documents Bash built-ins; it does not provide
the manual for arbitrary external programs.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;help command&lt;/code&gt;: print command help&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;man&lt;/h3&gt;
&lt;p&gt;user manual for commands&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;man command&lt;/code&gt;: print command manual&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;info&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;info command&lt;/code&gt;: print command info&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;whatis&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;whatis command&lt;/code&gt;: print command description&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;wildcards&lt;/h3&gt;
&lt;p&gt;Allows us to match patterns in filenames.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;*&lt;/code&gt;: matches any number of characters.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls *.txt&lt;/code&gt;: matches any file with .txt extension.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls data*.txt&lt;/code&gt;: list files with .txt extension starting with data.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?&lt;/code&gt;: matches any single character.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;??&lt;/code&gt;: matches any two characters and so on.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls data?.txt&lt;/code&gt;: list files with .txt extension starting with data and a single
character.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[abc]&lt;/code&gt;: matches a, b, or c.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[!abc]&lt;/code&gt;: matches anything but a, b, or c.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls [[:upper:]]*&lt;/code&gt;: list matching uppercase-leading names; for matching
directories, &lt;code&gt;ls&lt;/code&gt; lists their contents unless &lt;code&gt;-d&lt;/code&gt; is used.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -d [[:upper:]]*/&lt;/code&gt;: list matching directories starting with an uppercase
letter.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -d [[:lower:]]*/&lt;/code&gt;: list matching directories starting with a lowercase
letter.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls [ad]*&lt;/code&gt;: list files starting with a or d followed by any number of
characters.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -l [2-9]*&lt;/code&gt;: list files starting with a number between 2 and 9.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -l [0-9][0-9]*&lt;/code&gt;: list files starting with two numbers.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;redirection&lt;/h3&gt;
&lt;p&gt;A redirection is a way to redirect the output of a command to a file or another
command. file descriptor (0-1-2).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;standard input (stdin)&lt;/em&gt; : 0&lt;/li&gt;
&lt;li&gt;&lt;em&gt;standard output (stdout)&lt;/em&gt; : 1&lt;/li&gt;
&lt;li&gt;&lt;em&gt;standard error (stderr)&lt;/em&gt; : 2&lt;/li&gt;
&lt;li&gt;&lt;code&gt;command &amp;lt; file_name&lt;/code&gt;: read from file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;command &amp;gt; file_name&lt;/code&gt;: write stdout of the specified command to file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;command &amp;gt;&amp;gt; file.txt&lt;/code&gt; : append stdout to file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls 2&amp;gt; file_name&lt;/code&gt;: write stderr to file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls 2&amp;gt;&amp;gt; file_name&lt;/code&gt;: append standard error to file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls &amp;gt; file_name 2&amp;gt;&amp;amp;1&lt;/code&gt;: write standard output and standard error to file.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;pipes&lt;/h3&gt;
&lt;p&gt;A pipe is a way to connect the output of one command to the input of another
command.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;echo &quot;foo&quot; | grep &quot;foo&quot;&lt;/code&gt;: print &quot;foo&quot; and grep for &quot;foo&quot;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cat file1 file2 | grep &quot;foo&quot;&lt;/code&gt;: print file1 and file2 and grep for &quot;foo&quot;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -lh | tee file_name | less&lt;/code&gt;: list files in long format with human readable
sizes, write to file, and print one page at a time.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -lh folder | sort | tee file_name | less&lt;/code&gt;: list files in long format with
human readable sizes, sort, write to file, and print one page at a time.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cowsay &quot;foo&quot; | lolcat&lt;/code&gt;: print &quot;foo&quot; with cowsay and lolcat.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;echo &quot;foo&quot; | cowsay | lolcat&lt;/code&gt;: print &quot;foo&quot; with cowsay and lolcat.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;control operators&lt;/h3&gt;
&lt;p&gt;Control operators determine sequencing and conditions. &lt;code&gt;;&lt;/code&gt;, &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, and &lt;code&gt;||&lt;/code&gt; do
not by themselves create subshells; &lt;code&gt;&amp;amp;&lt;/code&gt; starts an asynchronous job in Bash.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ls; echo &quot;foo&quot;&lt;/code&gt;: list files and print &quot;foo&quot;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls; mkdir dir; cal&lt;/code&gt;: list files, create directory, and print calendar.
(asynchronous &quot;&amp;amp;&quot; creates a background process).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls &amp;amp; echo &quot;foo&quot;&lt;/code&gt;: run &lt;code&gt;ls&lt;/code&gt; in the background and &lt;code&gt;echo&lt;/code&gt; in the foreground.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls &amp;amp;&amp;amp; echo &quot;foo&quot;&lt;/code&gt;: list files and print &quot;foo&quot; if ls succeeds.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls || echo &quot;foo&quot;&lt;/code&gt;: list files and print &quot;foo&quot; if ls fails.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;permissions&lt;/h3&gt;
&lt;p&gt;A permission is a way to control access to a file or directory.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ls -l&lt;/code&gt; begins with a type character: &lt;code&gt;-&lt;/code&gt; for a regular file, &lt;code&gt;d&lt;/code&gt; for a
directory, &lt;code&gt;l&lt;/code&gt; for a symbolic link, and &lt;code&gt;b&lt;/code&gt; for a block device. A block device
is a device that is accessed in fixed-size blocks.&lt;/p&gt;
&lt;h3&gt;Kind of permissions&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;r : read&lt;/li&gt;
&lt;li&gt;w : write&lt;/li&gt;
&lt;li&gt;x : execute&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;owner&lt;/th&gt;
&lt;th&gt;group&lt;/th&gt;
&lt;th&gt;other&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rwx&lt;/td&gt;
&lt;td&gt;r-x&lt;/td&gt;
&lt;td&gt;r-x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;111&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Symbolic mode&lt;/h3&gt;
&lt;p&gt;u : user g : group o : other a : all chmod u=rwx,g=rwx,o=rwx file : set
permissions to 777&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;root user: a root user has full access to the system.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod&lt;/code&gt; : change file permissions (change mode)(octal).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod 755 file_name&lt;/code&gt;: set permissions to 755. (owner: rwx, group: rx, other:
rx).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod u=rwx,g=rwx,o=rwx file_name&lt;/code&gt;: set permissions to 777. (owner: rwx,
group: rwx, other: rwx).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod u-rwx,g-rwx,o-rwx file_name&lt;/code&gt;: set permissions to 000. (owner: ---,
group: ---, other: ---).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod g+w file_name&lt;/code&gt;: add group write permission, preserving all other bits.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;whoami&lt;/code&gt;: print current user.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;: print current user and group.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;su root&lt;/code&gt;: switch to root user.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;su user&lt;/code&gt;: switch to user.&lt;/li&gt;
&lt;li&gt;in ubuntu: &lt;code&gt;sudo su&lt;/code&gt; :switch to root user.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;passwd&lt;/code&gt;: change password.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chown -R &quot;$(id -un):$(id -gn)&quot; folder_name&lt;/code&gt;: recursively set owner and
primary group to the current user; sufficient privileges are required.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;change password&lt;/h3&gt;
&lt;p&gt;passwd : change password&lt;/p&gt;
&lt;h3&gt;managing permissions&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;chmod u=rwx,g=rwx,o=rwx file_name&lt;/code&gt;: set permissions to 777&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod u=rwx,g=rwx,o=rx file_name&lt;/code&gt;: set permissions to 775&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod u=rwx,g=rwx,o= file_name&lt;/code&gt;: set permissions to 770&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod u=rwx,g=rwx file_name&lt;/code&gt;: set user and group to rwx; leave other bits
unchanged&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod u=rwx,go=w file_name&lt;/code&gt;: set ordinary permission bits to 722&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod 777 file_name&lt;/code&gt;: change file_name permissions to 777.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod 755 file_name&lt;/code&gt;: change file_name permissions to 755.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod 644 file_name&lt;/code&gt;: change file_name permissions to 644.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod 600 file_name&lt;/code&gt;: change file_name permissions to 600.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod 400 file_name&lt;/code&gt;: change file_name permissions to 400.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod 000 file_name&lt;/code&gt;: change file_name permissions to 000.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod u+x file_name&lt;/code&gt;: add execute permission to file_name for user.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod u-x file_name&lt;/code&gt;: remove execute permission to file_name for user.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod u+r file_name&lt;/code&gt;: add read permission to file_name for user.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod u-r file_name&lt;/code&gt;: remove read permission to file_name for user.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod u+w file_name&lt;/code&gt;: add write permission to file_name for user.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod u-w file_name&lt;/code&gt;: remove write permission to file_name for user.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod u+x,g+x,o+x file_name&lt;/code&gt;: add execute permission to file_name for user,
group and others.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod u-x,g-x,o-x file_name&lt;/code&gt;: remove execute permission to file_name for
user, group and others.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod u+r,g+r,o+r file_name&lt;/code&gt;: add read permission to file_name for user,
group and others.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod u-r,g-r,o-r file_name&lt;/code&gt;: remove read permission to file_name for user,
group and others.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod u+w,g+w,o+w file_name&lt;/code&gt;: add write permission to file_name for user,
group and others.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod u-w,g-w,o-w file_name&lt;/code&gt;: remove write permission to file_name for user,
group and others.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod u+x,g+x,o+x,u-r,g-r,o-r,u-w,g-w,o-w file_name&lt;/code&gt;: add execute permission
to file_name for user, group and others and remove read and write permission
to file_name for user, group and others.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;environment variables&lt;/h3&gt;
&lt;p&gt;Environment variables are name/value pairs inherited by child processes. In
Bash, assignment creates a shell variable; &lt;code&gt;export&lt;/code&gt; makes it part of the
environment passed to subsequent commands.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ln -s /home/user/dir link_name&lt;/code&gt;: create symbolic link to dir named link_name.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;printenv&lt;/code&gt;: print environment variables&lt;/li&gt;
&lt;li&gt;&lt;code&gt;echo $VARIABLE_NAME&lt;/code&gt;: print VARIABLE_NAME environment variable&lt;/li&gt;
&lt;li&gt;&lt;code&gt;echo $PATH&lt;/code&gt;: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:
/sbin:/bin:/usr/games:/usr/local/games:/snap/bin --&amp;gt; &lt;em&gt;the directories where
the shell looks for commands&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;echo $HOME&lt;/code&gt;: print HOME environment variable -- /home/user -- the home
directory of the current user&lt;/li&gt;
&lt;li&gt;&lt;code&gt;echo $USER&lt;/code&gt; : print USER environment variable -- user -- the current user&lt;/li&gt;
&lt;li&gt;&lt;code&gt;echo $SHELL&lt;/code&gt; : print SHELL environment variable -- /bin/bash -- the shell
program&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;how to modify environment variables?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;export PATH=$PATH:/home/user/bin&lt;/code&gt;: add /home/user/bin to PATH environment
variable&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;look for the file &lt;code&gt;.bashrc&lt;/code&gt; (bash) in the home directory of the user.&lt;/li&gt;
&lt;li&gt;open the file &lt;code&gt;.bashrc&lt;/code&gt; with a text editor to who is using bash. In mac
&lt;code&gt;.zshrc&lt;/code&gt;. That&apos;s could vary depending on the shell you are using.&lt;/li&gt;
&lt;li&gt;add the line export PATH=$PATH:/home/user/bin to the file .bashrc.&lt;/li&gt;
&lt;li&gt;to add a environment variable, add the line &lt;code&gt;export VARIABLE_NAME=&apos;value&apos;&lt;/code&gt; to
the file .bashrc.&lt;/li&gt;
&lt;li&gt;to add an alias, add the line &lt;code&gt;alias alias_name=&apos;command&apos;&lt;/code&gt; to the file
.bashrc.&lt;/li&gt;
&lt;li&gt;to add bins to the path, add the line &lt;code&gt;export PATH=$PATH:/home/user/bin&lt;/code&gt; to
the file .bashrc.&lt;/li&gt;
&lt;li&gt;save the file .bashrc.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Searching command&lt;/h3&gt;
&lt;p&gt;A search command is a way to search for a command.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;which &amp;lt;command&amp;gt;&lt;/code&gt; : print command path. If the command is not found, nothing
is printed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;which code&lt;/code&gt;: /usr/bin/code --&amp;gt; &lt;em&gt;the path to the command code&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find path -name &quot;file_name&quot;&lt;/code&gt;: search for file_name in path. &lt;strong&gt;segment by name
(wildcards and find):&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find path -name &quot;*.txt&quot;&lt;/code&gt;: search for files with .txt extension in current
directory. &lt;strong&gt;segment by type:&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find path -type f -name &quot;*.txt&quot;&lt;/code&gt; : search for files with .txt extension in
the specified path.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find ./ -type f -name &quot;*.log&quot;&lt;/code&gt;: search for files with .log extension in
current directory.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find ./ -type d -name dir&lt;/code&gt;: search for directories with name dir in current
directory.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find ./ -type d -empty&lt;/code&gt;: search for empty directories in current directory.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find ./ -maxdepth 2 -type d&lt;/code&gt;: search for directories in current directory
with a maximum depth of 2&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find ./ -mindepth 2 -type d&lt;/code&gt;: search for directories in current directory
with a minimum depth of 2 &lt;strong&gt;segment by size:&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find ./ -type f -size +100k&lt;/code&gt;: search for files in current directory with a
size greater than 100k.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find ./ -size +100M&lt;/code&gt;: search for files larger than 100M in current directory.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find ./ | less&lt;/code&gt;: search for files in current directory and print the results
in less. &lt;strong&gt;segment by date:&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find ./ -type f -mtime +1&lt;/code&gt;: search for files in current directory that were
modified at least two full 24-hour periods ago (&lt;code&gt;-mtime&lt;/code&gt; rounds down).
&lt;strong&gt;segment by permission:&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find ./ -type f -perm 777&lt;/code&gt;: search for files in current directory with
permissions 777.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find ./ -type f -perm -777&lt;/code&gt;: search for files in current directory with all
of the listed permission bits set (additional special bits may be set).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find ./ -type f -perm /777&lt;/code&gt;: search for files in current directory with at
least one listed permission bit set (GNU find syntax). &lt;strong&gt;segment by owner:&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find ./ -type f -user user&lt;/code&gt;: search for files in current directory with owner
user.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find ./ -type f -group group&lt;/code&gt;: search for files in current directory with
group group. &lt;strong&gt;segment by content:&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find ./ -type f -exec grep -l &apos;text&apos; {} \;&lt;/code&gt;: search for files in current
directory with the text text and print the file name.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find ./ -type f -exec grep -q &apos;text&apos; {} \; -ok rm {} \;&lt;/code&gt;: find matching files
and ask before deleting each; this preserves filenames containing spaces.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find ./ -type f -size +100k -exec rm {} \;&lt;/code&gt;: search for files in current
directory with a size greater than 100k and delete them.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;managing directories&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;A directory is a way to manage directories.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pwd&lt;/code&gt;: print working directory.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd dir_name&lt;/code&gt;: change directory to dir_name.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd ..&lt;/code&gt;: change directory to parent directory.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd ~&lt;/code&gt;: change directory to home directory.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd -&lt;/code&gt;: change directory to previous directory.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls&lt;/code&gt;: list directory contents.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -a&lt;/code&gt;: list directory contents including hidden files.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -l&lt;/code&gt;: list directory contents in long format.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -la&lt;/code&gt;: list directory contents in long format including hidden files.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -lh&lt;/code&gt;: list directory contents in long format with human readable sizes.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -lah&lt;/code&gt;: list directory contents in long format with human readable sizes
including hidden files.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -lt&lt;/code&gt;: list directory contents in long format sorted by modification time.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -lat&lt;/code&gt;: list directory contents in long format sorted by modification time
including hidden files.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -lS&lt;/code&gt;: list directory contents in long format sorted by size.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -lSr&lt;/code&gt;: list directory contents in long format sorted by size in reverse.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -lX&lt;/code&gt;: list directory contents in long format sorted by extension.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -lXr&lt;/code&gt;: list directory contents in long format sorted by extension in
reverse.&lt;/li&gt;
&lt;li&gt;GNU &lt;code&gt;ls -lU&lt;/code&gt;: use directory order, without sorting. On macOS, &lt;code&gt;-U&lt;/code&gt; has
different timestamp semantics; check the local &lt;code&gt;man ls&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls -lF&lt;/code&gt;: list directory contents in long format with a slash after
directories.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;whereis&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;whereis&lt;/code&gt; locates binaries, manual pages and source locations for command names;
it is not a general file search.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;whereis &amp;lt;file_name&amp;gt;&lt;/code&gt;: find file_name.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;whereis -b &amp;lt;file_name&amp;gt;&lt;/code&gt;: find file_name binaries.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;whereis -m &amp;lt;file_name&amp;gt;&lt;/code&gt;: find file_name manuals.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;whereis -s &amp;lt;file_name&amp;gt;&lt;/code&gt;: find file_name sources.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;ln&lt;/h2&gt;
&lt;p&gt;ln -s : create symbolic links&lt;/p&gt;
&lt;h2&gt;find&lt;/h2&gt;
&lt;p&gt;Quote patterns so &lt;code&gt;find&lt;/code&gt;, rather than the shell, interprets them:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;find . -type f -name &apos;*.txt&apos;
find . -type f -name &apos;*.txt&apos; -exec grep -n &apos;foo&apos; {} +
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first command finds text files recursively. The second searches their
contents and safely handles filenames containing spaces. Permission predicates
are bit tests, not numeric comparisons; see
the &lt;a href=&quot;https://www.gnu.org/software/findutils/manual/find.html#Mode-Bits&quot;&gt;GNU find mode-bit reference&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;grep&lt;/h3&gt;
&lt;p&gt;A grep is a way to search for a pattern in a file.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;grep &amp;lt;regex&amp;gt; &amp;lt;file_name&amp;gt;&lt;/code&gt;: search for regex in file_name.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep -r &amp;lt;regex&amp;gt; &amp;lt;file_name&amp;gt;&lt;/code&gt;: search for regex in file_name recursively.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep -i &amp;lt;regex&amp;gt; &amp;lt;file_name&amp;gt;&lt;/code&gt;: search for &quot;foo&quot; in files, case insensitive.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep -v &amp;lt;regex&amp;gt; &amp;lt;file_name&amp;gt;&lt;/code&gt; : invert match, print non-matching lines.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep -l &amp;lt;regex&amp;gt; &amp;lt;file_name&amp;gt;&lt;/code&gt;: print names of files with at least one matching
line&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep -c &amp;lt;regex&amp;gt; &amp;lt;file_name&amp;gt;&lt;/code&gt;: count matching lines, not individual
occurrences.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep -n &amp;lt;regex&amp;gt; &amp;lt;file_name&amp;gt;&lt;/code&gt;: print line numbers.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep -A 2 &amp;lt;regex&amp;gt; &amp;lt;file_name&amp;gt;&lt;/code&gt;: search for regex in files, print 2 lines
after match&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep -B 2 &amp;lt;regex&amp;gt; &amp;lt;file_name&amp;gt;&lt;/code&gt;: search for regex in files, print 2 lines
before match&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep -C 2 &amp;lt;regex&amp;gt; &amp;lt;file_name&amp;gt;&lt;/code&gt;: search for regex in files, print 2 lines
before and after match&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wc &amp;lt;file_name&amp;gt;&lt;/code&gt;: count newline characters, words and bytes.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wc -l &amp;lt;file_name&amp;gt;&lt;/code&gt;: count lines in file_name.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wc -w &amp;lt;file_name&amp;gt;&lt;/code&gt;: count words in file_name.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wc -c &amp;lt;file_name&amp;gt;&lt;/code&gt;: count bytes. Use &lt;code&gt;wc -m&lt;/code&gt; to count characters.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Network utilities&lt;/h2&gt;
&lt;p&gt;These commands inspect interfaces or communicate with network services.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ifconfig&lt;/code&gt;: print network interfaces.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ping &amp;lt;host-or-ip&amp;gt;&lt;/code&gt;: send ICMP (Internet Control Message Protocol) echo requests; a missing reply can reflect
filtering&lt;/li&gt;
&lt;li&gt;&lt;code&gt;curl &amp;lt;url&amp;gt;&lt;/code&gt;: print url contents.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;curl &amp;lt;url&amp;gt; &amp;gt; index.html&lt;/code&gt;: write url contents (stdout) to index.html.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wget &amp;lt;url&amp;gt;&lt;/code&gt;: download url. get form url.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;traceroute &amp;lt;host-or-ip&amp;gt;&lt;/code&gt;: probe hops toward a destination; some may not
reply.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;netstat -tulpn&lt;/code&gt;: print listening ports.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;netstat -i&lt;/code&gt;: print networks interfaces.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;compression&lt;/h2&gt;
&lt;p&gt;A compression is a way to compress files.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gzip &amp;lt;file_name&amp;gt;&lt;/code&gt;: compress a regular file; use tar with gzip for a directory
tree.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tar -cvf &amp;lt;file_name.tar&amp;gt; &amp;lt;file_name/dir&amp;gt;&lt;/code&gt;: create tar file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tar -cvzf &amp;lt;file_name.tar.gz&amp;gt; &amp;lt;file_name/dir&amp;gt;&lt;/code&gt;: create tar.gz file (gzip).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tar -xvzf &amp;lt;file_name.tar.gz&amp;gt;&lt;/code&gt;: extract tar file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;zip -r &amp;lt;file_name.zip&amp;gt; &amp;lt;file/dir&amp;gt;&lt;/code&gt;: create zip file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unzip &amp;lt;file_name.zip&amp;gt;&lt;/code&gt; : extract zip file.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Process management&lt;/h2&gt;
&lt;p&gt;A process is a running instance of a program.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ps&lt;/code&gt;: show processes associated with the current terminal by default.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ps -ef&lt;/code&gt;: show a broader process listing on common Unix systems.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kill &amp;lt;pid&amp;gt;&lt;/code&gt;: send SIGTERM, requesting termination; it is not guaranteed to
stop the process.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;top&lt;/code&gt;: inspect processes interactively; keys vary by platform. Read its help
or &lt;code&gt;man top&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;htop&lt;/code&gt;: an optional interactive terminal process viewer.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Background jobs&lt;/h2&gt;
&lt;p&gt;Bash job control manages commands started in the current interactive shell:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sleep 1000 &amp;amp;&lt;/code&gt;: start a background job.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jobs&lt;/code&gt;: list this shell&apos;s jobs.&lt;/li&gt;
&lt;li&gt;Ctrl-Z: suspend the foreground job, rather than terminate it.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bg %1&lt;/code&gt;: continue job 1 in the background.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fg %1&lt;/code&gt;: continue job 1 in the foreground.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kill %1&lt;/code&gt;: send SIGTERM to job 1.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kill %%&lt;/code&gt;: signal the current job, not all background jobs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A background command that tries to read from the terminal normally stops
with SIGTTIN. Use input from a file or run it in the foreground instead.&lt;/p&gt;
&lt;h2&gt;vim&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vim &amp;lt;file_name&amp;gt;&lt;/code&gt;: open file_name.
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;i&lt;/code&gt;: insert mode.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;esc&lt;/code&gt;: normal mode.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:w&lt;/code&gt;: write file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:q&lt;/code&gt;: quit file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:wq&lt;/code&gt;: write and quit file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:q!&lt;/code&gt;: quit without saving.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:w!&lt;/code&gt;: write over file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:wq!&lt;/code&gt;: write over file and quit.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:w &amp;lt;file_name&amp;gt;&lt;/code&gt;: write to file_name.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/regex&lt;/code&gt;: search for regex.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dd&lt;/code&gt;: delete line.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yy&lt;/code&gt;: copy line.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p&lt;/code&gt;: paste line.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;u&lt;/code&gt;: undo.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ctrl + r&lt;/code&gt;: redo.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gg&lt;/code&gt;: go to first line.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;G&lt;/code&gt;: go to last line.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt;: next match.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;N&lt;/code&gt;: previous match.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:%s/foo/bar/g&lt;/code&gt;: replace every match of foo with bar throughout the buffer.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vim -R &amp;lt;file_name&amp;gt;&lt;/code&gt;: open file_name in read-only mode.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vim -O file1 file2&lt;/code&gt;: open files in vertical splits.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vim -o &amp;lt;file_name&amp;gt;&lt;/code&gt;: open file_name in horizontal split mode.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vim -p &amp;lt;file_name&amp;gt;&lt;/code&gt;: open file_name in tab mode.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vim -c &amp;lt;command&amp;gt;&lt;/code&gt;: execute command.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vim -c &quot;set number&quot; &amp;lt;file_name&amp;gt;&lt;/code&gt;: open file_name with line numbers.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vim -c &quot;set number&quot; -c &quot;set hlsearch&quot; &amp;lt;file_name&amp;gt;&lt;/code&gt;: open file_name with line
numbers and highlight search.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;customize the terminal&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ohmybash.github.io/&quot;&gt;bash&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ohmyz.sh&quot;&gt;zsh&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://platzi.com/cursos/terminal/&quot;&gt;Platzi: Curso de Introducción a la Terminal y Línea de Comandos&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>tools</category></item><item><title>Python Basics</title><link>https://yeison-liscano.github.io/portfolio/blog/python-basics/</link><guid isPermaLink="true">https://yeison-liscano.github.io/portfolio/blog/python-basics/</guid><description>Python is a great language for beginners and experts alike. Let&apos;s explore the basics of Python. From modules, packages, scope, and more. Let&apos;s dive in!</description><pubDate>Sat, 26 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The simplicity of Python makes the language a great option for almost everyone
aiming to use programming in any field. This is the reason Python is widely used
in the industry, from web development, data science, machine learning,
automation, scientific computing, and more. Having a huge community, and a vast
number of libraries, Python is a great choice for beginners and experts alike.&lt;/p&gt;
&lt;p&gt;In this post, we will explore some of the best features of Python, and how you
can use them to your advantage.&lt;/p&gt;
&lt;h2&gt;Modules&lt;/h2&gt;
&lt;p&gt;A module is a file containing Python definitions and statements. The file name
is the module name with the suffix &lt;code&gt;.py&lt;/code&gt; appended. Within a module, the module&apos;s
name (as a string) is available as the value of the global variable &lt;code&gt;__name__&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A module can be imported by another program to make use of its functionality. We
can define our most used functions in a module and import it, instead of copying
their definitions into different programs.&lt;/p&gt;
&lt;h3&gt;if &lt;code&gt;__name__ == &quot;__main__&quot;&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;You will often see this construct in Python files. This is used to execute some
code only if the file was run directly, and not imported. That is, if the file
is imported, the code is not run. This is because when you import a module, the
code in the module is executed, just like any other script. So if we want to
have some code that we only want to run when the module is run directly, we can
use this construct.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def main():
    print(&quot;Running as script!&quot;)

# execute only if run as a script
if __name__ == &quot;__main__&quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Packages&lt;/h2&gt;
&lt;p&gt;A package organizes modules and subpackages under a dotted namespace.
A &lt;strong&gt;regular package&lt;/strong&gt; contains &lt;code&gt;__init__.py&lt;/code&gt;, which may be empty or contain
initialization code. &lt;strong&gt;Namespace packages&lt;/strong&gt; can omit that file and span multiple
directories; they have been supported since Python 3.3. See
the &lt;a href=&quot;https://docs.python.org/3/reference/import.html#packages&quot;&gt;Python import reference&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# __init__.py
# In a package directory, you would use relative imports:
# from . import module1, module2, module3
print(&quot;Package initialized!&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Doing this will allow us to use namespaced modules, such as &lt;code&gt;package.module1&lt;/code&gt;,
&lt;code&gt;package.module2&lt;/code&gt;, etc.&lt;/p&gt;
&lt;h2&gt;Scope&lt;/h2&gt;
&lt;p&gt;Scopes are the contexts in which names are looked up. There are four different
scopes in Python: &lt;code&gt;local&lt;/code&gt;, &lt;code&gt;enclosing&lt;/code&gt;, &lt;code&gt;global&lt;/code&gt;, and &lt;code&gt;built-in&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The scope of a name defines the area of the program where you can unambiguously
access that name, such as variables, functions, objects, and so on. It is
determined by the place where it is declared.&lt;/p&gt;
&lt;p&gt;Names that are declared outside of all functions are in the &lt;code&gt;global&lt;/code&gt; scope. This
means that those names can be accessed inside or outside of functions.&lt;/p&gt;
&lt;p&gt;Names that are declared inside a function are in the &lt;code&gt;local&lt;/code&gt; scope, and can only
be accessed inside that function.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;enclosing&lt;/code&gt; scope is a special scope that only exists for nested functions.
If the local scope is an inner or nested function, then the enclosing scope is
the scope of the outer or enclosing function.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;built-in&lt;/code&gt; scope is the outermost scope in Python, and it is the scope that
contains all of the built-in names in Python. The built-in scope is searched
last, after the local, enclosing, and global scopes (LEGB (Local, Enclosing, Global, Built-in: the order in which Python looks up names)).&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure: To resolve x inside inner(), Python checks the local scope, then the enclosing outer(), and finds x = 10 in the global scope. The search stops at the first match, so built-in is never checked.&lt;/em&gt; (&lt;a href=&quot;https://yeison-liscano.github.io/portfolio/blog/python-basics/&quot;&gt;interactive version in the post&lt;/a&gt;)&lt;/p&gt;
&lt;h3&gt;Example Global Scope&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;x = 10

def my_func():
    print(x)

my_func()
print(x)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;10
10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Example Local Scope&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def my_func():
    x_ = 10
    print(x_)

my_func()
try:
    print(x_)
except NameError as e:
    print(e)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;10
name &apos;x_&apos; is not defined
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Example Enclosing Scope&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def outer():
    x = &apos;local&apos;

    def inner():
        nonlocal x
        x = &apos;nonlocal&apos;
        print(&apos;inner:&apos;, x)

    inner()
    print(&apos;outer:&apos;, x)

outer()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;inner: nonlocal
outer: nonlocal
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that the &lt;code&gt;nonlocal&lt;/code&gt; keyword is used to declare that &lt;code&gt;x&lt;/code&gt; is not a local
variable. Hence, when we assign a value to &lt;code&gt;x&lt;/code&gt; inside the nested function, that
change is reflected in the local variable in the enclosing function.&lt;/p&gt;
&lt;h3&gt;Example: A Local Name Shadows a Global Name&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;x = 10

def my_func():
    x = 20
    print(x)

my_func()
print(x)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;20
10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Example Keyword &lt;code&gt;global&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;x = 10

def my_func():
    global x
    x = 20
    print(x)

my_func()
print(x)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;20
20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that the &lt;code&gt;global&lt;/code&gt; keyword is used to declare that &lt;code&gt;x&lt;/code&gt; is a global
variable - hence, when we assign a value to &lt;code&gt;x&lt;/code&gt; inside the function, that change
is reflected when we use the value of &lt;code&gt;x&lt;/code&gt; in the main block.&lt;/p&gt;
&lt;h3&gt;Built-in Scope&lt;/h3&gt;
&lt;p&gt;These are the names in the pre-defined built-in modules. These are always
available in your Python programs. You can see the list of built-in names by
typing &lt;code&gt;import builtins; dir(builtins)&lt;/code&gt; in the Python interpreter.&lt;/p&gt;
&lt;h2&gt;Splat Operator &lt;code&gt;*&lt;/code&gt; and Double Splat Operator &lt;code&gt;**&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Splat operator is a kind of unpacking operator (destructuring in JS (JavaScript)). It can be
used to allow an iterable to be unpacked into positional arguments in a function
call, or into a list, tuple, or set display. &lt;code&gt;**&lt;/code&gt; unpacks a mapping into keyword
arguments or a dictionary display. These are often called the splat (&lt;code&gt;*&lt;/code&gt;) and
double splat (&lt;code&gt;**&lt;/code&gt;) operators.&lt;/p&gt;
&lt;h3&gt;Splat Operator&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def add(x, y):
    return x + y

numbers = [3, 5]
print(add(*numbers)) # 8
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Double Splat Operator&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def display_names(first, second):
    print(f&apos;{first} says hello to {second}&apos;)

names = {&quot;first&quot;: &quot;John&quot;, &quot;second&quot;: &quot;Bob&quot;}
display_names(**names) # John says hello to Bob
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Multiple Arguments&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;def print_everything(*args):
    for count, thing in enumerate(args):
        print(&apos;{0}. {1}&apos;.format(count, thing))

print_everything(&apos;apple&apos;, &apos;banana&apos;, &apos;cabbage&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;0. apple
1. banana
2. cabbage
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Keyword Arguments&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;def table_things(**kwargs):
    for name, value in kwargs.items():
        print(&apos;{0} = {1}&apos;.format(name, value))

table_things(apple=&apos;fruit&apos;, cabbage=&apos;vegetable&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;apple = fruit
cabbage = vegetable
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Unpacking Arguments&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;def print_three_things(a, b, c):
    print(&apos;a = {0}, b = {1}, c = {2}&apos;.format(a, b, c))

my_list = [&apos;aardvark&apos;, &apos;baboon&apos;, &apos;cat&apos;]
print_three_things(*my_list)

my_dict = {&apos;a&apos;: &apos;apple&apos;, &apos;b&apos;: &apos;banana&apos;, &apos;c&apos;: &apos;cherry&apos;}
print_three_things(**my_dict)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;a = aardvark, b = baboon, c = cat
a = apple, b = banana, c = cherry
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Positional and keyword arguments (*args, **kwargs)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;def func(*args, **kwargs):
    print(args)
    print(kwargs)

func(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, a=1, b=2, c=3)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
{&apos;a&apos;: 1, &apos;b&apos;: 2, &apos;c&apos;: 3}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>programming</category><category>python</category></item></channel></rss>