NumPy
NumPy es la librería fundamental para cálculo numérico en Python. Su estructura central es el array: un arreglo multidimensional de valores del mismo tipo, mucho más eficiente que una lista de Python para operaciones matemáticas.
Arrays vs listas de Python
La pregunta más común al empezar con NumPy es: ¿cuándo usar un array y cuándo una lista?
| Lista de Python | Array de NumPy | |
|---|---|---|
| Tipos de elementos | Mixtos ([1, "texto", True]) |
Todos del mismo tipo |
| Operaciones matemáticas | Requieren un loop | Directas sobre todo el array |
| Velocidad | Lenta para cálculos | 10-100× más rápida |
| Uso típico | Colecciones generales, texto | Series numéricas, matrices |
# Lista — la operación no funciona como se espera
velocidades_lista = [0.08, 0.09, 0.6, 0.07]
velocidades_lista * 1.944 # repite la lista, no multiplica cada elemento
# Array — la operación se aplica a cada elemento
velocidades = np.array([0.08, 0.09, 0.6, 0.07])
velocidades * 1.944 # [0.156, 0.175, 1.166, 0.136] — correcto
Regla práctica: si vas a hacer cálculos (sumas, medias, trigonometría), usa arrays. Si solo necesitas guardar una colección de cosas para iterar sobre ellas, una lista está bien.
Arrays
# Desde una lista
velocidades = np.array([0.08, 0.09, 0.6, 0.07, 0.08, 0.09, 0.10])
# Array de ceros o unos
np.zeros(12) # [0. 0. 0. ... 0.]
np.ones((3, 4)) # matriz 3×4 de unos
# Secuencias
np.arange(0, 24, 2) # [0, 2, 4, ..., 22]
np.linspace(0, 1, 50) # 50 puntos equiespaciados entre 0 y 1
dtype — el tipo de los elementos
Cada array tiene un dtype que determina qué tipo de número almacena y cuántos bits usa:
| dtype | Bits | Precisión | Uso típico |
|---|---|---|---|
float64 |
64 | ~15 dígitos | Default de NumPy, máxima precisión |
float32 |
32 | ~7 dígitos | Archivos NetCDF, ahorra memoria |
int32 |
32 | enteros ±2.1×10⁹ | Índices, contadores |
int64 |
64 | enteros grandes | Timestamps en nanosegundos |
uint8 |
8 | enteros 0–255 | Imágenes (píxeles) |
Por qué importa: los archivos NetCDF y muchos instrumentos oceanográficos almacenan datos en float32 para ahorrar espacio. Cuando los lees con xarray o NumPy, los datos ya vienen como float32. Si mezclas float32 con float64 en una operación, NumPy promueve todo a float64 — lo cual está bien, pero puede sorprender.
# Verificar
vel.dtype # float64
# Especificar al crear
vel32 = np.array([0.08, 0.09, 0.6], dtype=np.float32)
vel32.dtype # float32
# Convertir
vel32 = vel.astype(np.float32) # float64 → float32
vel64 = vel32.astype(np.float64) # float32 → float64
# Leer desde NetCDF (xarray) — frecuentemente viene como float32
import xarray as xr
ds = xr.open_dataset('corrientes.nc')
ds['velocidad'].dtype # float32
# Convertir si necesitas precisión para análisis espectral
vel64 = ds['velocidad'].values.astype(np.float64)
float32 en análisis espectral
La pérdida de precisión de float32 (~7 dígitos) rara vez importa en estadísticas descriptivas. Sí puede importar en análisis espectral o cuando se hacen muchas operaciones encadenadas sobre los mismos datos. En esos casos conviene convertir a float64 antes de calcular.
Arrays 2D — matrices
En corrientes se trabaja frecuentemente con matrices de (tiempo × profundidad):
# Matriz de velocidades: 5 ensembles × 11 profundidades
datos = np.array([
[0.08, 0.09, 0.6, 0.07, 0.08, 0.09, 0.10, 0.09, 0.08, 0.07, 0.06],
[0.07, 0.08, 0.55, 0.06, 0.07, 0.08, 0.09, 0.08, 0.07, 0.06, 0.05],
# ...
])
datos.shape # (5, 11) — filas × columnas
datos.ndim # 2
datos.size # 55 — total de elementos
Operaciones vectorizadas y broadcasting
La ventaja de NumPy es que las operaciones se aplican a todo el array sin necesidad de un loop. A esto se le llama vectorización:
vel = np.array([0.08, 0.09, 0.6, 0.07])
# Operaciones elemento a elemento
vel * 1.944 # convertir m/s a nudos
vel ** 2 # cuadrado de cada elemento
np.sqrt(vel) # raíz cuadrada
np.log(vel) # logaritmo natural
# Comparación — devuelve array de booleanos
vel > 0.5 # [False, False, True, False]
Broadcasting es la regla que permite operar un array con un escalar (o con arrays de distinta forma compatible). NumPy "expande" el escalar para que coincida con el tamaño del array:
vel = np.array([0.08, 0.09, 0.6, 0.07])
# En vez de hacer un loop para convertir cada elemento:
# for i in range(len(vel)):
# vel[i] = vel[i] * 1.944
# NumPy lo hace solo — aplica el *1.944 a cada elemento:
vel * 1.944 # [0.156, 0.175, 1.166, 0.136]
# Sumar dos arrays del mismo tamaño — suma elemento a elemento:
u = np.array([0.1, 0.2, 0.3])
v = np.array([0.4, 0.5, 0.6])
magnitud = np.sqrt(u**2 + v**2) # calcula raíz de (u²+v²) para cada par
Conversión velocidad/dirección ↔ componentes U, V
Esta operación es fundamental en oceanografía y aparece en múltiples scripts del pipeline:
def uv_desde_vel_dir(velocidad, direccion_grad):
"""Convierte (velocidad, dirección) a componentes (U, V)."""
dir_rad = np.radians(direccion_grad)
u = velocidad * np.sin(dir_rad) # componente Este
v = velocidad * np.cos(dir_rad) # componente Norte
return u, v
def vel_dir_desde_uv(u, v):
"""Convierte (U, V) a (velocidad, dirección)."""
velocidad = np.sqrt(u**2 + v**2)
direccion = np.degrees(np.arctan2(u, v)) % 360
return velocidad, direccion
Indexación y slicing
prof = np.array([3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23])
prof[0] # 3 — primer elemento
prof[-1] # 23 — último
prof[2:5] # [7, 9, 11]
prof[::2] # [3, 7, 11, 15, 19, 23] — cada dos
# En 2D: [fila, columna]
datos[0, :] # primera fila completa (ensemble 0, todas las profundidades)
datos[:, 2] # columna 2 completa (todos los ensembles, profundidad 7 m)
datos[1:3, 3:6] # submatriz
Indexación booleana — filtrado
vel = np.array([0.08, 0.09, 0.6, 0.07, 0.55, 0.09])
# Seleccionar solo los valores > 0.5
vel[vel > 0.5] # [0.6, 0.55]
# Reemplazar valores fuera de rango con NaN
vel[vel > 0.5] = np.nan
Estadísticas
vel = np.array([0.08, 0.09, 0.60, 0.07, 0.08])
np.mean(vel) # media
np.median(vel) # mediana
np.std(vel) # desviación estándar
np.max(vel) # máximo
np.min(vel) # mínimo
np.percentile(vel, 95) # percentil 95
np.argmax(vel) # índice del máximo → 2
Funciones que ignoran NaN
Cuando los datos tienen valores faltantes (NaN), usar las versiones nan*:
Estadísticas por eje en matrices
datos.mean(axis=0) # media por profundidad (a lo largo del tiempo)
datos.mean(axis=1) # media por ensemble (a lo largo de profundidades)
datos.max(axis=0) # máximo por profundidad
Funciones trigonométricas
NumPy trabaja en radianes. Para datos de dirección oceánica (en grados):
np.radians(180) # π
np.degrees(np.pi) # 180.0
np.sin(np.radians(90)) # 1.0
np.cos(np.radians(0)) # 1.0
# arctan2 — dirección del vector (U, V)
u, v = 0.5, 0.5
direccion = np.degrees(np.arctan2(u, v)) % 360 # 45.0°
NaN — valores faltantes
np.nan # valor especial "Not a Number"
np.isnan(vel) # array booleano: True donde hay NaN
np.isnan(vel).sum() # cantidad de NaN
~np.isnan(vel) # máscara de datos válidos
# Contar datos válidos
datos_validos = vel[~np.isnan(vel)]
Operaciones útiles
# Diferencia entre elementos consecutivos
np.diff(np.array([1, 3, 6, 10])) # [2, 3, 4]
# Concatenar arrays
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
np.concatenate([a, b]) # [1, 2, 3, 4, 5, 6]
# Apilar matrices verticalmente
np.vstack([datos[:2, :], datos[3:, :]])
# Ordenar
np.sort(vel) # copia ordenada
vel.argsort() # índices que ordenarían el array
# Redondear
np.round(3.14159, 2) # 3.14
NumPy vs listas de Python
Para operaciones matemáticas sobre grandes conjuntos de datos, NumPy es entre 10 y 100 veces más rápido que un loop sobre una lista Python. En series temporales de 6 meses con datos cada 10 minutos (~26.000 registros), esa diferencia es significativa.