!pip install -q numba
import random
import time
import random
import numpy as np
from numba import jit
from tabulate import tabulate
import matplotlib.pyplot as plt
1. - Implementación de una Función de Cómputo Intensivo:¶
random.seed(42) # Fijo la semilla para reproducibilidad
# Generamos dos arreglos grandes con números aleatorios
N = 10_000_000
a = [random.random() for _ in range(N)]
b = [random.random() for _ in range(N)]
# Función de cómputo intensivo: suma de productos usando bucles nativo.
def suma_de_productos(a, b):
total = 0
for i in range(len(a)):
total += a[i] * b[i]
return total
# Medición del tiempo de ejecución
start_time = time.time()
resultado = suma_de_productos(a, b)
end_time = time.time()
print(f"Resultado: {resultado:.4f}")
print(f"Tiempo de ejecución: {end_time - start_time:.4f} segundos")
Resultado: 2498856.5773 Tiempo de ejecución: 1.7173 segundos
2.- Optimización con Operaciones Vectorizadas¶
# Generamos arreglos NumPy usando los mismos datos que el modelo con computo intensivo (para comparar)
a_np = np.array(a)
b_np = np.array(b)
# Medición del tiempo de ejecución usando operaciones vectorizadas
start_time_np = time.time()
resultado_np = np.sum(a_np * b_np)
end_time_np = time.time()
print(f"Resultado (NumPy): {resultado_np:.4f}")
print(f"Tiempo de ejecución (NumPy): {end_time_np - start_time_np:.4f} segundos")
Resultado (NumPy): 2498856.5773 Tiempo de ejecución (NumPy): 0.0713 segundos
3. - Optimización con Precompilación¶
# Función optimizada con Numba (seguimos usando las listas originales para comparar)
@jit(nopython=True)
def suma_de_productos_numba(a, b):
total = 0.0
for i in range(len(a)):
total += a[i] * b[i]
return total
# Warm up (compilación)
_ = suma_de_productos_numba(a_np, b_np)
# Medición
start_time_numba = time.time()
resultado_numba = suma_de_productos_numba(a_np, b_np)
end_time_numba = time.time()
print(f"Resultado (Numba): {resultado_numba:.4f}")
print(f"Tiempo de ejecución (Numba): {end_time_numba - start_time_numba:.4f} segundos")
Resultado (Numba): 2498856.5773 Tiempo de ejecución (Numba): 0.0228 segundos
4. - Uso Eficiente de Context Managers¶
class Timer:
def __init__(self, label="Bloque"):
self.label = label
def __enter__(self):
self.start = time.time()
return self # por si quieres acceder al tiempo desde fuera
def __exit__(self, exc_type, exc_value, traceback):
self.end = time.time()
self.elapsed = self.end - self.start
print(f"{self.label} ejecutado en {self.elapsed:.6f} segundos")
with Timer("Cálculo con bucles"):
total = 0
for i in range(10_000_000):
total += i * i
with Timer("Cálculo con NumPy"):
import numpy as np
arr = np.arange(10_000_000)
total_np = np.sum(arr * arr)
Cálculo con bucles ejecutado en 5.804788 segundos Cálculo con NumPy ejecutado en 0.128219 segundos
5. - Compilado de Los resultados de rendimiento obtenidos¶
# Creamos la tabla con tus resultados ya calculados para comparar
tabla = [
["Bucles nativos", f"{resultado:.4f}", f"{end_time - start_time:.4f} s"],
["NumPy vectorizado", f"{resultado_np:.4f}", f"{end_time_np - start_time_np:.4f} s"],
["Numba precompilado", f"{resultado_numba:.4f}", f"{end_time_numba - start_time_numba:.4f} s"]
]
# Encabezados
headers = ["Implementación", "Resultado", "Tiempo de ejecución"]
# Mostrar tabla
print(tabulate(tabla, headers=headers, tablefmt="fancy_grid"))
╒════════════════════╤═════════════╤═══════════════════════╕ │ Implementación │ Resultado │ Tiempo de ejecución │ ╞════════════════════╪═════════════╪═══════════════════════╡ │ Bucles nativos │ 2.49886e+06 │ 1.7173 s │ ├────────────────────┼─────────────┼───────────────────────┤ │ NumPy vectorizado │ 2.49886e+06 │ 0.0713 s │ ├────────────────────┼─────────────┼───────────────────────┤ │ Numba precompilado │ 2.49886e+06 │ 0.0228 s │ ╘════════════════════╧═════════════╧═══════════════════════╛
# Nombres de los métodos utilizados
metodos = ["Bucles nativos", "NumPy vectorizado", "Numba precompilado"]
# Tiempos previamente calculados
tiempos = [
end_time - start_time,
end_time_np - start_time_np,
end_time_numba - start_time_numba
]
# Colores personalizados
colores = ["#ff9999", "#99ccff", "#99ff99"]
# Crear gráfico
plt.figure(figsize=(7, 5))
barras = plt.bar(metodos, tiempos, color=colores)
# Añadir etiquetas de tiempo en las barras
for barra, tiempo in zip(barras, tiempos):
plt.text(barra.get_x() + barra.get_width() / 2, barra.get_height() + 0.01,
f"{tiempo:.4f} s", ha='center', va='bottom', fontsize=10)
# Título y etiquetas
plt.title("Comparación de Tiempos de Ejecución", fontsize=14)
plt.ylabel("Tiempo (segundos)")
plt.grid(axis='y', linestyle='--', alpha=0.5)
plt.tight_layout()
# Mostrar
plt.show()
6.- Informe de Optimización de Código en Python¶
1. Estructura del Código¶
El desarrollo se organiza en cinco secciones principales:
Función de Cómputo Intensivo (Bucles nativos)
Se implementa una función que calcula la suma de productos entre dos arreglos grandes, usando un buclefor
estándar.Optimización con Operaciones Vectorizadas (NumPy)
Se reescribe la misma operación utilizando arrays de NumPy para aprovechar la vectorización y mejorar el rendimiento.Optimización con Precompilación (Numba)
Se utiliza el decorador@jit(nopython=True)
de Numba para precompilar la función original y acelerar su ejecución.Uso Eficiente de Context Managers
Se implementa una claseTimer
como administrador de contexto personalizado para medir tiempos de ejecución en bloques críticos.Comparación de Rendimiento
Se presenta una tabla y un gráfico de barras que comparan los tiempos de ejecución entre las tres versiones implementadas.
2. Resultados de Rendimiento¶
Implementación | Resultado | Tiempo de ejecución |
---|---|---|
Bucles nativos | 2.49959e+06 | 1.7173 s |
NumPy vectorizado | 2.49959e+06 | 0.0713 s |
Numba precompilado | 2.49959e+06 | 0.0228 s |
Interpretación¶
- La implementación con bucles nativos es la más lenta, debido al uso de operaciones iterativas en Python puro.
- NumPy ofrece una mejora sustancial (~30 veces más rápido) gracias a su motor vectorizado en C.
- Numba, al compilar la función a código máquina, ofrece un rendimiento ligeramente superior al de NumPy.
También se incluye un gráfico de barras que ilustra esta comparación de manera visual, destacando las diferencias de rendimiento.
3. Justificación de las Técnicas de Optimización¶
- Bucles nativos se utilizan como base para comparar y entender el costo computacional de las operaciones sin optimización.
- NumPy se emplea por su eficiencia en el manejo de arrays grandes mediante operaciones vectorizadas, que reducen la sobrecarga de los bucles de Python.
- Numba se introduce como una forma de acelerar aún más el código, compilando funciones numéricas a código máquina con JIT (Just-In-Time), ideal para tareas intensivas que no pueden vectorizarse fácilmente.
- Context Managers personalizados permiten medir de forma precisa y legible los tiempos de ejecución de segmentos específicos del código, mejorando la trazabilidad y la identificación de cuellos de botella.
Autor¶
José Julián Gómez Brizuela
Junio 2025
BONUS TRACK: Utilicé la librería Polars para realizar comparaciones de rendimiento.¶
!pip install polars
import polars as pl
import random
import time
# Crear DataFrame en Polars
df = pl.DataFrame({
"a": a,
"b": b
})
# Medir el tiempo de ejecución usando Polars (eager)
start_time_polars = time.time()
resultado_polars = df.with_columns(
(pl.col("a") * pl.col("b")).alias("producto")
).select(
pl.sum("producto")
).item()
end_time_polars = time.time()
print(f"Resultado (Polars): {resultado_polars:.4f}")
print(f"Tiempo de ejecución (Polars): {end_time_polars - start_time_polars:.4f} segundos")
Requirement already satisfied: polars in /usr/local/lib/python3.11/dist-packages (1.21.0) Resultado (Polars): 2498856.5773 Tiempo de ejecución (Polars): 0.0770 segundos
# Creamos la tabla con todos los resultados ya calculados para comparar incluyendo polars
tabla = [
["Bucles nativos", f"{resultado:.4f}", f"{end_time - start_time:.4f} s"],
["NumPy vectorizado", f"{resultado_np:.4f}", f"{end_time_np - start_time_np:.4f} s"],
["Numba precompilado", f"{resultado_numba:.4f}", f"{end_time_numba - start_time_numba:.4f} s"],
["Polars optimizado", f"{resultado_polars:.4f}", f"{end_time_polars - start_time_polars:.4f} s"]
]
# Encabezados
headers = ["Implementación", "Resultado", "Tiempo de ejecución"]
# Mostrar tabla con estilo
print(tabulate(tabla, headers=headers, tablefmt="fancy_grid"))
╒════════════════════╤═════════════╤═══════════════════════╕ │ Implementación │ Resultado │ Tiempo de ejecución │ ╞════════════════════╪═════════════╪═══════════════════════╡ │ Bucles nativos │ 2.49886e+06 │ 1.7173 s │ ├────────────────────┼─────────────┼───────────────────────┤ │ NumPy vectorizado │ 2.49886e+06 │ 0.0713 s │ ├────────────────────┼─────────────┼───────────────────────┤ │ Numba precompilado │ 2.49886e+06 │ 0.0228 s │ ├────────────────────┼─────────────┼───────────────────────┤ │ Polars optimizado │ 2.49886e+06 │ 0.0770 s │ ╘════════════════════╧═════════════╧═══════════════════════╛
Los resultados obtenidos con Polars utilizando el mismo conjunto de datos y un bucle nativo muestran un rendimiento ligeramente inferior al de NumPy vectorizado. Esto resulta interesante, ya que sugiere que Polars puede integrarse eficazmente en ciertos flujos de trabajo donde la manipulación tabular es prioritaria, aunque no siempre sea la opción más rápida para operaciones numéricas específicas. 👈🏼🧐