
Qué son las ufuncs
Una función universal (ufunc) es una función de NumPy que opera elemento a elemento sobre uno o más arrays. A diferencia de una función de Python convencional que procesaría los elementos con un bucle, las ufuncs ejecutan la operación en código C compilado, lo que produce un rendimiento muy superior.
Cada operador aritmético de NumPy (+, -, *, /) es en realidad un wrapper sobre una ufunc. Por ejemplo, el operador + invoca np.add, y * invoca np.multiply. Esto significa que las operaciones vectorizadas que se escriben de forma natural ya están utilizando ufuncs.
import numpy as np
a = np.array([1, 2, 3, 4])
b = np.array([10, 20, 30, 40])
# Estas dos lineas son equivalentes
suma_operador = a + b
suma_ufunc = np.add(a, b)
print(np.array_equal(suma_operador, suma_ufunc)) # True
Las ufuncs se clasifican en dos tipos según el número de arrays de entrada:
- Unarias: reciben un solo array. Ejemplos:
np.sqrt,np.sin,np.exp. - Binarias: reciben dos arrays. Ejemplos:
np.add,np.multiply,np.maximum.
Las ufuncs son el mecanismo central de rendimiento en NumPy. Cualquier operación que se pueda expresar como una ufunc evita el bucle del intérprete de Python y se ejecuta directamente en C.
flowchart LR
A["Array NumPy<br>shape, dtype, buffer"] --> B["Broadcasting<br>alinea ejes compatibles"]
B --> C["Dispatcher de la ufunc<br>selecciona kernel por dtype"]
C --> D["Loop C compilado<br>SIMD cuando procede"]
D --> E["Escribe en array de salida<br>out= o asignado"]
F["Parámetros comunes<br>out, where, axis, dtype, casting"] -.-> C
Los argumentos out=, where=, axis= y dtype= son comunes a casi todas las ufuncs y permiten reutilizar buffers, aplicar condiciones por elemento y controlar la promoción numérica sin copias intermedias.
Ufuncs trigonométricas
NumPy proporciona las funciones trigonométricas estándar, que trabajan con ángulos en radianes.
import numpy as np
angulos = np.array([0, np.pi/6, np.pi/4, np.pi/3, np.pi/2])
seno = np.sin(angulos)
coseno = np.cos(angulos)
tangente = np.tan(angulos)
print(np.round(seno, 4)) # [0. 0.5 0.7071 0.866 1. ]
print(np.round(coseno, 4)) # [1. 0.866 0.7071 0.5 0. ]
print(np.round(tangente, 4)) # [0. 0.5774 1. 1.7321 inf ]
Para convertir entre grados y radianes se dispone de np.deg2rad y np.rad2deg:
import numpy as np
grados = np.array([0, 30, 45, 60, 90])
radianes = np.deg2rad(grados)
print(np.round(np.sin(radianes), 4)) # [0. 0.5 0.7071 0.866 1.]
Las funciones inversas np.arcsin, np.arccos y np.arctan devuelven el ángulo en radianes a partir del valor trigonométrico:
import numpy as np
valores = np.array([0, 0.5, 1.0])
angulos = np.arcsin(valores)
print(np.round(np.rad2deg(angulos), 1)) # [ 0. 30. 90.]
A partir de NumPy 2.0, se añadieron aliases compatibles con C99 y con el estándar de arrays de Python: np.asin, np.acos y np.atan. Son equivalentes a np.arcsin, np.arccos y np.arctan respectivamente, y se pueden usar de forma indistinta.
# Aliases C99 (NumPy 2.0+)
print(np.asin(np.array([0, 0.5, 1.0]))) # Equivalente a np.arcsin
print(np.acos(np.array([1.0, 0.5, 0.0]))) # Equivalente a np.arccos
Ufuncs exponenciales y logarítmicas
Las funciones exponenciales y logarítmicas son fundamentales en estadística, machine learning y procesamiento de señales.
import numpy as np
x = np.array([0, 1, 2, 3])
# Exponencial: e^x
exponencial = np.exp(x)
print(exponencial) # [ 1. 2.71828183 7.3890561 20.08553692]
# Exponencial base 2
exp2 = np.exp2(x)
print(exp2) # [1. 2. 4. 8.]
Para logaritmos, NumPy ofrece varias bases:
import numpy as np
x = np.array([1, np.e, 10, 100])
# Logaritmo natural (base e)
log_natural = np.log(x)
print(np.round(log_natural, 4)) # [0. 1. 2.3026 4.6052]
# Logaritmo base 10
log10 = np.log10(x)
print(np.round(log10, 4)) # [0. 0.4343 1. 2. ]
# Logaritmo base 2
log2 = np.log2(np.array([1, 2, 4, 8, 16]))
print(log2) # [0. 1. 2. 3. 4.]
La función np.log1p calcula log(1 + x) con mayor precisión para valores pequeños de x, algo habitual en cálculos financieros y de probabilidad:
import numpy as np
x_pequeno = np.array([1e-10, 1e-15, 1e-20])
# log(1 + x) pierde precision con valores muy pequenos
print(np.log(1 + x_pequeno)) # Puede dar 0.0 por error de punto flotante
print(np.log1p(x_pequeno)) # Mantiene la precision
Ufuncs de redondeo
NumPy incluye varias ufuncs para controlar el redondeo de valores decimales:
import numpy as np
x = np.array([-2.7, -1.5, -0.3, 0.5, 1.2, 2.8])
# Redondeo al entero mas cercano
print(np.round(x)) # [-3. -2. -0. 0. 1. 3.]
# Redondeo hacia abajo (suelo)
print(np.floor(x)) # [-3. -2. -1. 0. 1. 2.]
# Redondeo hacia arriba (techo)
print(np.ceil(x)) # [-2. -1. -0. 1. 2. 3.]
# Truncar la parte decimal
print(np.trunc(x)) # [-2. -1. -0. 0. 1. 2.]
np.round acepta un segundo parámetro para especificar el número de decimales:
import numpy as np
precios = np.array([19.995, 24.444, 31.678])
print(np.round(precios, 2)) # [20. 24.44 31.68]
print(np.round(precios, 1)) # [20. 24.4 31.7]
print(np.round(precios, 0)) # [20. 24. 32.]
Ufuncs de comparación
Las ufuncs de comparación devuelven arrays booleanos y permiten obtener valores extremos elemento a elemento:
import numpy as np
a = np.array([3, 7, 2, 9, 5])
b = np.array([5, 4, 8, 1, 5])
# Maximo y minimo elemento a elemento
print(np.maximum(a, b)) # [5 7 8 9 5]
print(np.minimum(a, b)) # [3 4 2 1 5]
np.maximum y np.minimum no deben confundirse con np.max y np.min. Los primeros son ufuncs que comparan dos arrays elemento a elemento; los segundos son funciones de agregación que devuelven un solo valor.
import numpy as np
notas_parcial1 = np.array([6.5, 8.0, 4.2, 7.1])
notas_parcial2 = np.array([7.0, 7.5, 5.8, 6.9])
# Mejor nota de cada alumno (elemento a elemento)
mejor_nota = np.maximum(notas_parcial1, notas_parcial2)
print(mejor_nota) # [7. 8. 5.8 7.1]
# Nota mas alta global (agregación)
print(np.max(mejor_nota)) # 8.0
Métodos reduce y accumulate
Todas las ufuncs binarias disponen de métodos adicionales que permiten aplicar la operación de forma acumulativa o reducida sobre un eje del array.
reduce
El método reduce aplica la ufunc de forma secuencial sobre los elementos de un eje, reduciendo la dimensión. Es equivalente a insertar el operador entre todos los elementos.
import numpy as np
a = np.array([1, 2, 3, 4, 5])
# np.add.reduce equivale a 1 + 2 + 3 + 4 + 5
print(np.add.reduce(a)) # 15
# np.multiply.reduce equivale a 1 * 2 * 3 * 4 * 5
print(np.multiply.reduce(a)) # 120
Con arrays 2D se puede especificar el eje de reducción:
import numpy as np
m = np.array([[1, 2, 3],
[4, 5, 6]])
# Suma por columnas (reducir eje 0)
print(np.add.reduce(m, axis=0)) # [5 7 9]
# Suma por filas (reducir eje 1)
print(np.add.reduce(m, axis=1)) # [ 6 15]
accumulate
El método accumulate es similar a reduce, pero conserva todos los resultados intermedios. Es el equivalente vectorizado de un bucle que va acumulando resultados.
import numpy as np
ventas_diarias = np.array([120, 85, 200, 150, 95])
# Ventas acumuladas
ventas_acumuladas = np.add.accumulate(ventas_diarias)
print(ventas_acumuladas) # [120 205 405 555 650]
# Producto acumulado
factores = np.array([1.05, 0.98, 1.02, 1.01])
crecimiento = np.multiply.accumulate(factores)
print(np.round(crecimiento, 4)) # [1.05 1.029 1.0496 1.0601]
reduceyaccumulateestán disponibles en cualquier ufunc binaria. Por ejemplo,np.maximum.accumulatecalcula el máximo acumulado, útil para calcular el drawdown máximo en series financieras.
Método outer
El método outer aplica la ufunc a todas las combinaciones posibles entre los elementos de dos arrays, generando una matriz de resultados. Es el equivalente a un producto cartesiano con la operación.
import numpy as np
a = np.array([1, 2, 3])
b = np.array([10, 20, 30, 40])
# Tabla de multiplicacion
tabla = np.multiply.outer(a, b)
print(tabla)
# [[ 10 20 30 40]
# [ 20 40 60 80]
# [ 30 60 90 120]]
print(tabla.shape) # (3, 4)
Un caso de uso práctico es calcular las diferencias entre todos los pares de elementos:
import numpy as np
puntos = np.array([2.0, 5.0, 8.0, 11.0])
# Matriz de distancias entre todos los pares
distancias = np.abs(np.subtract.outer(puntos, puntos))
print(distancias)
# [[0. 3. 6. 9.]
# [3. 0. 3. 6.]
# [6. 3. 0. 3.]
# [9. 6. 3. 0.]]
Crear ufuncs personalizadas
Cuando las ufuncs integradas no cubren una necesidad concreta, NumPy permite convertir funciones de Python en ufuncs. Existen dos mecanismos principales.
np.frompyfunc
Convierte una función de Python en una ufunc que devuelve arrays de tipo object:
import numpy as np
def aplicar_iva(precio, tipo_iva):
return round(precio * (1 + tipo_iva), 2)
# Crear ufunc con 2 entradas y 1 salida
ufunc_iva = np.frompyfunc(aplicar_iva, 2, 1)
precios = np.array([10.0, 25.0, 50.0, 100.0])
resultado = ufunc_iva(precios, 0.21)
print(resultado) # [12.1 30.25 60.5 121.0]
print(resultado.dtype) # object
np.vectorize
np.vectorize es un wrapper más completo que permite especificar el tipo de dato de salida:
import numpy as np
def clasificar_nota(nota):
if nota >= 9:
return "Sobresaliente"
elif nota >= 7:
return "Notable"
elif nota >= 5:
return "Aprobado"
else:
return "Suspenso"
clasificar = np.vectorize(clasificar_nota)
notas = np.array([4.5, 6.2, 7.8, 9.1, 3.0])
etiquetas = clasificar(notas)
print(etiquetas) # ['Suspenso' 'Aprobado' 'Notable' 'Sobresaliente' 'Suspenso']
np.vectorizeynp.frompyfuncno proporcionan la misma velocidad que una ufunc nativa de C. Son herramientas de conveniencia que simplifican el código, pero internamente siguen ejecutando un bucle de Python. Para rendimiento crítico es preferible usar las ufuncs integradas o reescribir la lógica con operaciones vectorizadas puras.
Rendimiento: ufuncs frente a bucles de Python
La diferencia de rendimiento entre una ufunc y un bucle equivalente en Python es significativa. El siguiente ejemplo compara ambos enfoques para calcular la raíz cuadrada de un millón de elementos.
import numpy as np
import time
n = 1_000_000
datos = np.random.default_rng(42).random(n) * 1000
# Bucle de Python con math.sqrt
import math
inicio = time.perf_counter()
resultado_bucle = [math.sqrt(x) for x in datos]
tiempo_bucle = time.perf_counter() - inicio
# Ufunc np.sqrt
inicio = time.perf_counter()
resultado_ufunc = np.sqrt(datos)
tiempo_ufunc = time.perf_counter() - inicio
print(f"Bucle Python: {tiempo_bucle:.4f} s")
print(f"Ufunc NumPy: {tiempo_ufunc:.4f} s")
print(f"Ufunc es {tiempo_bucle / tiempo_ufunc:.0f}x mas rapida")
En una ejecución típica, la ufunc np.sqrt es entre 50x y 200x más rápida que el bucle equivalente. Esta diferencia se debe a que la ufunc opera directamente sobre el bloque de memoria del array sin pasar por el intérprete de Python en cada iteración.
La recomendación general es evitar bucles for sobre arrays de NumPy siempre que exista una ufunc o combinación de ufuncs que exprese la misma operación. Cuando no existe una ufunc integrada adecuada, es preferible combinar varias ufuncs con broadcasting antes de recurrir a np.vectorize.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, NumPy es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de NumPy
Explora más contenido relacionado con NumPy y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Comprender qué son las funciones universales (ufuncs), utilizar las ufuncs integradas de NumPy para operaciones trigonométricas, exponenciales, de redondeo y comparación, aplicar los métodos reduce, accumulate y outer, y crear ufuncs personalizadas.
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje