Semana 15 — Examinar la solución obtenida: analizar su eficiencia en tiempo y espacio, medir antes de optimizar, aplicar una mejora justificada con las pruebas como red de seguridad, y construir la trazabilidad diseño→código→medición que sostiene la defensa oral
La Semana 14 cerró con un programa que corre: módulos integrados por contrato, errores acotados a la frontera, ejecutado contra todos los casos de análisis de la Fase 1. Esta semana ese programa se pone bajo la lupa. No se introduce ningún concepto algorítmico nuevo —la materia prima sigue siendo el repertorio de las doce primeras semanas—; lo que cambia es la pregunta. Hasta ahora preguntamos si el programa es correcto; ahora preguntamos si además es bueno: eficiente, justificado y defendible.
Como las dos semanas anteriores, esta clase es un taller: el docente evalúa y optimiza sobre el caso modelo, y cada equipo traslada el método a su proyecto. El producto de hoy es el código final optimizado —con al menos una mejora medida y justificada— y la trazabilidad que permitirá defender cada decisión en la Semana 16.
En el marco de George Pólya (How to Solve It, 1945), esta semana ocupa la cuarta y última fase: examinar la solución obtenida (la visión retrospectiva). Pólya sostenía que mirar atrás la solución terminada —preguntarse si el resultado es correcto, si podría obtenerse de otro modo, si el método sirve para otros problemas— es la fase que más consolida el aprendizaje y la que con más frecuencia se omite. Trasladado al proyecto, esa mirada retrospectiva tiene un instrumento preciso: medir. La figura sitúa la semana en el ciclo.
La fase anterior fue la del encuentro con la realidad; esta es la del juicio sobre lo construido. Aquí aparecen tres tareas que el programa funcionando no resuelve por sí solo: saber cuánto cuesta (análisis de eficiencia), saber dónde cuesta (profiling) y decidir qué conviene mejorar (optimización justificada). Las tres preparan la cuarta: poder explicarlo en la defensa.
Un programa que pasa todos los casos de análisis ya demostró que es correcto. Evaluar no pone en duda esa corrección: la complementa con una pregunta que las pruebas no responden —¿a qué costo?—. Y la optimización que sigue se apoya, paradoja útil, en esas mismas pruebas: son la red que permite cambiar el cómo sin alterar el qué.
El primer paso de la retrospectiva es analizar el costo sin ejecutar nada: razonar, función por función, cuántas operaciones realiza en términos del tamaño de su entrada (complejidad temporal) y cuánta memoria reserva (complejidad espacial). Es el análisis asintótico de la Semana 5 y 11, aplicado al caso modelo: el analizador de corpus textual que venimos construyendo desde la Semana 13.
Fijemos la notación del corpus: sea C el total de caracteres del corpus, t los tokens (palabras) de un archivo, T los tokens de todo el corpus, d las palabras distintas (el tamaño del vocabulario) y v el ancho de la ventana de concordancias. La tabla resume el análisis de cada función del diseño.
| Función | Tiempo | Espacio | Justificación |
|---|---|---|---|
leer_tokens(ruta) |
O(c) | O(c) | Abre el archivo con with, lo lee y lo normaliza (lower + translate + split) en pasadas proporcionales a los c caracteres del archivo; la lista de tokens es proporcional al texto. |
acumular_frecuencias(frecuencias, tokens) |
O(t) | O(d) | Un frecuencias.get(palabra, 0) + 1 por token; el acceso al diccionario es O(1) amortizado (Semana 10). La memoria crece con las palabras distintas, no con el total. |
buscar_concordancias(...) |
O(t·v) | O(a·v) | Recorre los t tokens; por cada una de las a apariciones recorta y une una ventana de tamaño O(v). Con v pequeño, es en la práctica O(t). |
ordenar_desc_por_valor(frec) |
O(d log d) | O(d) | sorted ordena los d pares (palabra, cantidad); el ordenamiento comparativo es Θ(d log d) en el caso general (Semana 6). |
analizar_corpus(...) (orquestadora) |
O(C + d log d) | O(cmáx + d) | Recorre el corpus una sola vez (O(C)) y al final ordena el ranking (O(d log d)). El término dominante depende de qué tan grande sea el vocabulario frente al corpus. |
El análisis temporal arroja una cota agregada de O(C + d log d): lineal en el tamaño del corpus, más el costo de ordenar el vocabulario al final. No hay ningún término cuadrático: el diseño de la Fase 1 lo evitó deliberadamente al elegir el diccionario para contar (un set o una lista habrían reintroducido recorridos innecesarios —ese matiz se retoma en la sección 4—).
La columna de espacio merece una lectura aparte, porque es donde una decisión de diseño de la Semana 13 paga dividendos. La orquestadora no carga todo el corpus en memoria: gracias al generador que rinde un par (ruta, tokens) por archivo (la lectura perezosa de la Semana 11), en cada instante hay en memoria los tokens de un solo archivo —el más grande, O(cmáx)— más el diccionario de frecuencias que crece con el vocabulario, O(d). Si en cambio se hubieran aplanado todos los archivos en una única lista de tokens, el pico habría sido O(C + d), proporcional al corpus completo.
C caracteres), pero recorta drásticamente el espacio: cambia un pico proporcional a todo el corpus por uno proporcional al archivo más grande. Esta es la complejidad espacial en su forma más concreta: el pico de memoria que el sistema operativo debe reservar para que el programa no falle con corpus grandes.
El caso modelo es un corpus textual, pero el método —identificar la operación dominante y acotar su costo en tiempo y espacio— es el mismo para los cinco dominios. La tabla orienta dónde mirar en cada uno; son cotas analíticas orientativas, no medidas: cada equipo debe verificarlas sobre su propio diseño con las herramientas de la sección 3. En la notación, n es la cantidad de registros y m la cantidad de movimientos o eventos transaccionales.
| Dominio | Operación dominante típica | Costo orientativo | Dónde suele estar la mejora |
|---|---|---|---|
| A — Biblioteca | Estado derivado (qué ejemplares están prestados) a partir de los préstamos; reportes ordenados. | O(m) recorrer movimientos + O(n log n) ordenar reportes | Búsquedas por clave natural con diccionario (O(1)) en vez de recorrer listas. |
| B — Depósito | Stock actual derivado de los movimientos de entrada/salida; reportes. | O(m) una pasada por movimientos + O(n log n) reportes | Acumular el stock en un diccionario por clave de producto, no recalcular por consulta. |
| C — Horarios | Backtracking: explorar asignaciones válidas evitando conflictos. | O(bd) en el peor caso (b = opciones por paso, d = profundidad) | La poda: descartar ramas inviables temprano reduce drásticamente los nodos explorados (ver recuadro). |
| D — Logs | Consumo de un registro externo: leer y agregar por categoría. | O(L) una pasada por las L líneas; espacio O(claves distintas) | Lectura perezosa (línea a línea) para no cargar el archivo entero en memoria. |
| E — Turnos | Detección de solapamientos entre turnos asignados. | O(t²) comparando todos contra todos, o O(t log t) ordenando por horario | Ordenar por hora de inicio y comparar solo con el turno previo, en vez de cada par. |
Tres de los dominios (A, B y E) comparten un patrón que conviene nombrar en la defensa: el estado derivado (stock, disponibilidad, ocupación) no se almacena, se recalcula a partir de los registros transaccionales en una sola pasada O(m). Esa es la decisión que mantiene el costo lineal y evita inconsistencias —el mismo principio que en el caso modelo hace que la riqueza léxica se derive de las frecuencias y no se guarde por separado—.
El análisis de la sección 2 dice qué pasa cuando el corpus crece (el comportamiento asintótico). Pero no dice dónde se va el tiempo hoy, con el corpus real, donde mandan las constantes que la notación O grande esconde. Para eso no se razona: se mide. Y medir, casi siempre, desmiente la intuición.
Hay dos herramientas estándar de la biblioteca de Python, con propósitos distintos:
timeit es un cronómetro para microbenchmarks: ejecuta un fragmento muchas veces y reporta el mejor tiempo, minimizando el ruido del sistema. Responde «¿cuál de estas dos versiones es más rápida?».cProfile es un perfilador determinista: ejecuta el programa una vez registrando cuánto tiempo se pasa en cada función. Responde la pregunta que importa antes de optimizar: «¿dónde se va el tiempo?».El perfilador se usa así sobre el caso modelo —sin tocar el código del programa, solo envolviéndolo—:
¿Dónde supondría un equipo que su analizador gasta el tiempo? La respuesta intuitiva suele ser «en el ordenamiento» (la única operación con un término log) o «en el conteo» (el bucle que recorre todas las palabras). El perfilador dice otra cosa.
random.seed(42)). Perfilando el código de la Semana 14 tal cual, el costo se concentra en las operaciones de limpieza de cadenas de leer_tokens: str.translate sola se lleva el ~41 % del tiempo, str.lower el ~13 % y str.split el ~10 % —juntas, cerca del 64 %—. Los bucles a nivel Python (contar frecuencias y buscar concordancias) suman ~25 %. Y el sorted que la intuición señalaba como sospechoso resulta despreciable. El cuello de botella no estaba donde la intuición lo ponía. Cada número de esta guía se obtuvo ejecutando el código, no estimándolo.
¿Por qué str.translate —una operación implementada en C— es el costo dominante? Por una razón que el análisis asintótico no muestra: el corpus está en español y contiene caracteres anchos (el guion largo «—», las comillas tipográficas, los puntos suspensivos «…»). Por la forma en que Python representa las cadenas (PEP 393), basta con que aparezca uno de esos caracteres para que toda la cadena pase a una representación más ancha, y sobre esa representación str.translate pierde su camino rápido y se vuelve mucho más lento. El ordenamiento, en cambio, opera solo sobre las d palabras distintas —un conjunto mucho menor—. La lección es general y transferible a los cinco dominios: la operación que procesa más datos, no la que más «suena», suele ser el cuello de botella —y a veces el costo depende de cómo están representados esos datos, no del algoritmo—.
Con el perfilador señalando la limpieza de cadenas de leer_tokens, la optimización tiene una dirección clara. La consigna de la Fase 3 pide al menos una mejora documentada y justificada; aquí «justificada» significa exactamente esto: respaldada por una medición previa que muestra que vale la pena, y por una medición posterior que confirma que sirvió.
La versión de la Semana 14 limpia todo el texto antes de separarlo: aplica lower y translate sobre la cadena completa —el millón de caracteres— y recién entonces hace split. Ese translate sobre el texto entero es justamente el cuello de botella que mostró el perfilador.
La idea de la mejora es invertir el orden: separar primero con split y limpiar después solo los bordes de cada palabra con strip. Así el costoso translate sobre el texto completo desaparece: en vez de recorrer el millón de caracteres, se tocan apenas los pocos signos pegados al inicio o al final de cada token. Y todo se hace con tres métodos de cadena que ya se conocen —lower, split y strip—, sin módulos ni herramientas nuevas.
La medición confirma que la mejora vale la pena, y muestra una distinción importante entre optimizar una parte y optimizar el todo:
| Medición (corpus de 3 archivos, ≈1,36 M caracteres) | Original (S14) | Optimizada (split + strip) |
Aceleración |
|---|---|---|---|
Normalización sola (leer_tokens) | 0,1999 s | 0,0520 s | 3,85× |
Programa completo (analizar_corpus) | 0,2485 s | 0,0909 s | 2,73× |
La normalización por sí sola mejora 3,85×; el programa completo, 2,73×. La diferencia es esperable: como la limpieza era ~64 % del tiempo, acelerarla casi 4 veces deja al descubierto el 36 % restante, que no cambió. Es la cara empírica de una idea simple: acelerar una parte mejora el todo solo en la medida en que esa parte pesaba.
leer_tokens_original(ruta) == leer_tokens_optimizada(ruta) para cada archivo de prueba. Verificado sobre el corpus, las dos versiones dan tokens idénticos, y el programa completo arroja el mismo ranking, la misma riqueza y las mismas concordancias. Esa igualdad es la prueba de regresión que autoriza el cambio. Optimizar sin esa red es cambiar el cómo a ciegas, con riesgo de alterar el qué.
"U.S.A", la versión original (que borra en todo el texto) da usa, mientras que la optimizada (que solo limpia bordes) da u.s.a. Por eso la prueba de regresión no es un trámite: es lo que verifica que esa suposición —«los signos están en los bordes»— se cumple en los datos reales del dominio. Y explica, de paso, por qué el problema no se veía en una prueba pequeña: sobre el corpus de 32 palabras de la Semana 14 —corto y casi sin caracteres anchos— translate es instantáneo; el costo solo aparece al medir sobre un corpus representativo.
Conviene contrastar la mejora anterior con la tentación de optimizar lo que la intuición creía caro. El reporte solo muestra el top 5, pero ordenar_desc_por_valor ordena todo el vocabulario y luego recorta con [:5]. Existe una mejora algorítmica genuina —seleccionar solo los 5 mayores en vez de ordenar los d, que la biblioteca estándar ofrece como heapq.nlargest, cambiando O(d log d) por O(d log 5)—. Pero el profiling de la sección 3 ya dijo que el ordenamiento es una fracción despreciable del tiempo: mejorarlo casi no mueve la aguja del programa. Es el caso de Knuth en estado puro —una optimización correcta sobre algo que no es el cuello de botella—.
sorted es más claro) o aplicarla si prevé vocabularios enormes. Lo que no es legítimo es elegir sin medir. La defensa oral no premia haber optimizado todo, sino haber sabido qué valía la pena optimizar y por qué: en este proyecto, la limpieza de cadenas sí (era el 64 %), el ordenamiento no (era despreciable).
palabra in lista cuesta O(d) por consulta, de modo que construir el vocabulario así es O(t·d) —entre O(t) y O(t²)—. ¿Dónde cae en ese rango? Lo decide la ley de Heaps (Heaps, 1978): en un texto natural el vocabulario crece de forma sublineal con la extensión, aproximadamente d ∝ tβ con β ≈ 0,4–0,6. Es decir: ni O(t) (vocabulario fijo) ni O(t²) (todas distintas), sino un punto intermedio que empeora con corpus grandes. El diccionario sortea todo esto: las palabras distintas ya son sus claves y len(frecuencias) las cuenta en O(1). La decisión de estructura de la Fase 1 era, vista desde la eficiencia, exactamente la correcta.
Todo lo anterior converge aquí. La defensa oral de la Semana 16 no es un resumen del código: es la demostración de que el equipo entiende y puede justificar cada decisión. La materia prima de esa defensa es la trazabilidad que se viene construyendo desde la Semana 13: problema → análisis → diseño → código → medición.
Antes de preparar qué decir, conviene asegurar que el código que se va a mostrar está presentable. Esta revisión —la actividad de cierre del taller— recorre, una última vez, las convenciones que el curso sostuvo desde el comienzo. No es maquillaje: un código coherente y legible es más fácil de defender, porque cada decisión queda a la vista.
main distingue Prólogo, Resolución y Epílogo.for/range para recorridos determinados, while para los indeterminados; condiciones booleanas compuestas en lugar de break o continue.with; los errores se manejan en la frontera (E/S) con excepciones específicas (FileNotFoundError, UnicodeDecodeError), nunca con un except desnudo.docs/diseno.md documentó; si algo cambió durante la codificación, el diseño se actualizó en consecuencia.Este recorrido no introduce nada nuevo: reúne, en una sola lista verificable, las convenciones de las Semanas 1 a 14. Que el código final las cumpla es la señal de que el equipo no solo resolvió el problema, sino que lo hizo con el oficio que la materia se propuso formar.
Una presentación de 15–20 minutos se sostiene mejor si sigue, precisamente, las fases de Pólya que estructuraron el proyecto. Cada bloque responde una pregunta:
| Bloque | Pregunta que responde | Apoyo (de qué semana) |
|---|---|---|
| El problema | ¿Qué entra, qué sale, qué transformación ocurre? | Enunciado acotado (S13) |
| El análisis | ¿Qué casos consideramos, incluidos los límite y extremos? | Casos de análisis (S13) |
| El diseño | ¿Por qué estas estructuras y estos algoritmos? | Decisiones justificadas (S13) |
| La implementación | ¿Cómo se integran los módulos y se manejan los errores? | Código integrado (S14) |
| La evaluación | ¿Cuánto cuesta, dónde, qué optimizamos y por qué? | Análisis + profiling (S15) |
| La IA generativa | ¿Cómo la usamos y cómo verificamos lo que produjo? | Registro de IA (S13–S15) |
El núcleo de la defensa es poder responder, sobre cualquier decisión, dos preguntas: «¿por qué así?» y «¿qué pasaría si fuera de otra manera?». Una decisión bien defendida no se justifica por gusto, sino por consecuencia. Tres ejemplos tomados de este mismo caso modelo:
sorted habría sido trabajo sin efecto.»translate sobre todo el texto por separar y limpiar bordes con strip: el profiling reveló que translate se volvía lento por los caracteres anchos del español, y la nueva versión es 2,7× más rápida produciendo los mismos resultados.»open no encuentra un archivo (Semana 14)? ¿por qué un mismo algoritmo cuesta distinto según cómo se representan los datos en memoria (Semana 7)? Saber el qué del proyecto es la mitad; el por qué que lo conecta con la máquina es la otra.
El informe final incluye el análisis crítico del uso de IA generativa, apoyado en el registro_ia.md que el equipo viene completando desde la Semana 13. Cerrarlo es responder, con honestidad, tres preguntas por cada interacción relevante: qué se pidió, qué se obtuvo y, sobre todo, qué se verificó o corrigió.
str.translate de la sección 3 es un caso de estudio perfecto, y no por casualidad: tanto una herramienta de IA como un manual pueden recomendar con total seguridad «usá str.translate, es lo más rápido para limpiar texto» —un consejo que, medido sobre el corpus real en español, resulta ser el cuello de botella—. La frontera ética no está en usar o no la herramienta, sino en quién responde por el resultado: el código que se entrega y se defiende es del equipo, lo haya sugerido quien lo haya sugerido. Medir, verificar sobre datos representativos y poder explicar el porqué es lo que convierte una sugerencia ajena en una decisión propia. Esa es, exactamente, la competencia que la materia evalúa.
Un buen análisis crítico no se mide por la cantidad de IA usada ni por su ausencia, sino por la lucidez con que el equipo distingue lo que la herramienta resolvió bien, lo que resolvió mal y cómo lo detectaron. Un registro que solo dice «la IA escribió esta función y funcionó» es pobre; uno que dice «la IA propuso esta optimización, la medimos, no rendía sobre nuestro corpus y entendimos por qué» demuestra el pensamiento crítico que se busca formar.
La eficiencia conecta el programa con los niveles holísticos del curso de un modo directo. La complejidad espacial de la sección 2 no es una abstracción: es el pico de memoria que el programa le pide al sistema operativo, la misma memoria que en la arquitectura de Von Neumann comparten datos e instrucciones. Razonar el espacio es razonar cuánta de esa memoria física necesita el proceso para no fallar —y por eso el generador de la Semana 11, que mantiene el pico en O(cmáx) en vez de O(C), es una decisión de arquitectura tanto como de algoritmo—.
str.translate —rápido sobre texto angosto, lento sobre texto ancho— ilustra la pila completa del sistema computacional: hardware → SO → lenguaje → programa. El mismo trabajo lógico —quitar la puntuación— cuesta muy distinto según cómo se haga (recorrer todo el texto o solo los bordes de cada palabra) y, sobre todo, según cómo se represente la cadena en memoria: un byte por carácter o varios, según PEP 393, cambia qué atajos puede tomar el intérprete. La eficiencia de un programa no vive solo en su notación O grande: vive también en cómo el lenguaje y la máquina materializan cada operación sobre los datos tal como están representados.
Esta semana el programa que corría se volvió un programa examinado: con su costo en tiempo y espacio analizado, su cuello de botella medido, una mejora aplicada y justificada con evidencia, y la trazabilidad lista para la defensa. El producto a entregar es el código final optimizado, el informe escrito —con el análisis de complejidad y el análisis crítico del uso de IA— y la presentación preparada.
str.translate, invisible en una prueba pequeña, solo se reveló al perfilar sobre un corpus español real.
versión_original(x) == versión_optimizada(x) es lo que convierte un cambio arriesgado en una mejora segura: las pruebas de la Fase 2 son la red que hace posible la optimización de la Fase 3.
Una solución buena no es solo una que funciona: es una que su autor entiende lo bastante como para decir cuánto cuesta, dónde, y por qué decidió lo que decidió. Medir, justificar y poder explicar son la misma actitud vista en tres momentos —y es esa actitud, más que cualquier línea de código, la que distingue a quien resuelve problemas de quien solo escribe programas—.
Proyecto Final — Fase 3: Evaluación, optimización y preparación de la defensa
Semana 15 — Desarrollo de Contenidos — Fundamentos de Programación — Carrera de Informática — FIUBA