Saltar a contenido

Pandas

Pandas es la librería principal para manejo de datos tabulares. Su estructura central es el DataFrame: una tabla con filas y columnas etiquetadas, similar a una hoja de Excel pero operable desde código.

En el procesamiento de datos oceanográficos, los datos de corrientes, viento y oleaje se manejan como DataFrames de Pandas.

import pandas as pd

Series y DataFrames

Una Series es una columna con índice. Un DataFrame es una tabla de Series con el mismo índice.

# Series
velocidad = pd.Series([0.08, 0.09, 0.6, 0.07], name='velocidad')

# DataFrame desde diccionario
df = pd.DataFrame({
    'velocidad': [0.08, 0.09, 0.60, 0.07],
    'direccion': [  45,   90,  352,  180],
    'profundidad': [  3,    3,    7,    3],
})

Explorar un DataFrame

df.head()           # primeras 5 filas
df.tail(10)         # últimas 10 filas
df.shape            # (filas, columnas)
df.columns          # nombres de columnas
df.dtypes           # tipo de cada columna
df.describe()       # estadísticas básicas
df.info()           # resumen general: tipos, NaN, memoria

Acceder a datos

# Columna por nombre
df['velocidad']
df[['velocidad', 'direccion']]   # varias columnas

loc vs iloc — etiqueta vs posición

Esta es la distinción que más confunde al principio. La diferencia es simple:

  • iloc — accede por posición numérica, como los índices de una lista (0, 1, 2…)
  • loc — accede por etiqueta del índice, que puede ser un número, una fecha, un string
# Si el índice del DataFrame es 0, 1, 2... ambos parecen iguales
df.iloc[0]      # primera fila (por posición)
df.loc[0]       # fila con etiqueta 0 (por etiqueta)

# La diferencia importa cuando el índice es una fecha
df = df.set_index('tiempo')   # índice es ahora DatetimeIndex

df.iloc[0]                           # primera fila del DataFrame
df.loc['2025-10-01']                 # fila con esa fecha exacta
df.loc['2025-10-01':'2025-10-31']    # rango de fechas — solo funciona con loc

Regla práctica: si trabajas con series temporales (índice de fechas), usa loc. Si solo necesitas "las primeras N filas" o "la fila en la posición X", usa iloc.

# Combinando filas y columnas
df.iloc[0:5, 1:3]                          # filas 0-4, columnas 1-2 (posición)
df.loc['2025-10', ['velocidad', 'dir']]    # octubre, columnas por nombre

Filtrado

# Condición simple
df[df['velocidad'] > 0.5]

# Múltiples condiciones
df[(df['velocidad'] > 0.1) & (df['profundidad'] == 3)]
df[(df['direccion'] < 45) | (df['direccion'] > 315)]

# isin — valores en una lista
df[df['profundidad'].isin([3, 7, 15])]

# notna / isna
df[df['velocidad'].notna()]      # excluir NaN
df[df['velocidad'].isna()]       # solo NaN

Series temporales

En oceanografía, el índice del DataFrame suele ser un DatetimeIndex. Esto permite filtrar y agrupar por tiempo de forma muy eficiente.

# Crear índice temporal desde una columna
df['tiempo'] = pd.to_datetime(df['tiempo'])
df = df.set_index('tiempo')

# Filtrar por rango de fechas
df['2025-10':'2026-03']
df.loc['2025-10-01':'2026-03-30']

# Resamplear: promedios horarios, diarios, mensuales
df.resample('1h').mean()    # promedio cada hora
df.resample('1D').max()     # máximo diario
df.resample('1ME').mean()   # promedio mensual

Timezone

Los datos del ADCP y la boya están en UTC. Para convertir a hora local (UTC-3):

from datetime import timezone, timedelta

df.index = df.index.tz_localize('UTC')
df.index = df.index.tz_convert('America/Santiago')

Operaciones por columna

# Crear nuevas columnas
df['vel_nudos'] = df['velocidad'] * 1.944

# Operaciones sobre columnas existentes
df['u'] = df['velocidad'] * np.sin(np.radians(df['direccion']))
df['v'] = df['velocidad'] * np.cos(np.radians(df['direccion']))

# Reemplazar valores
df['velocidad'] = df['velocidad'].replace(-9999, np.nan)

# Aplicar función personalizada
def clasificar_beaufort(vel):
    if vel < 0.3:   return "calma"
    elif vel < 1.5: return "ventolina"
    elif vel < 3.3: return "brisa leve"
    else:           return "brisa moderada o más"

df['beaufort'] = df['velocidad'].apply(clasificar_beaufort)

Estadísticas

df['velocidad'].mean()
df['velocidad'].max()
df['velocidad'].std()
df['velocidad'].quantile(0.95)   # percentil 95

# Por columna
df.mean()
df.describe()

# Contar valores no nulos
df['velocidad'].count()
df['velocidad'].isna().sum()    # cantidad de NaN

groupby — estadísticas por categoría

groupby divide el DataFrame en grupos según el valor de una columna, calcula algo dentro de cada grupo, y devuelve los resultados combinados. Es el equivalente en código de "agrupar por X en una tabla pivot de Excel".

DataFrame original          Después de groupby('profundidad')
──────────────────          ─────────────────────────────────
profundidad  velocidad       profundidad  velocidad_media
3            0.08            3            0.085
3            0.09            7            0.335
7            0.60            ← promedio dentro de cada grupo
7            0.07
# Estadísticas por profundidad
df.groupby('profundidad')['velocidad'].mean()
df.groupby('profundidad')['velocidad'].agg(['mean', 'max', 'std'])

# Estadísticas por mes
df.groupby(df.index.month)['velocidad'].mean()

# Por hora del día (patrón diurno)
df.groupby(df.index.hour)['velocidad'].mean()

Ejemplo real: patrón diurno del viento

patron_diurno = df.groupby(df.index.hour)['velocidad'].mean()
patron_diurno.index.name = 'hora'

groupby con múltiples agregaciones

Cuando se necesita calcular varias estadísticas a la vez, agg acepta un diccionario con nombre de salida y función:

resumen = df.groupby('profundidad')['velocidad'].agg(
    media='mean',
    maxima='max',
    p95=lambda x: x.quantile(0.95),
    n_datos='count'
)

Esto devuelve un DataFrame con una columna por estadística, nombrada explícitamente — más claro que encadenar varias llamadas separadas.

# Agrupar por dos columnas a la vez
df.groupby(['profundidad', df.index.month])['velocidad'].mean()
# → media por cada combinación (profundidad, mes)

Rolling — ventana deslizante

rolling aplica una función sobre una ventana móvil de N filas. Es la forma estándar de suavizar series temporales y calcular estadísticas en un intervalo de tiempo deslizante.

# Suavizado: media móvil de 1 hora (datos cada 10 min → ventana de 6 puntos)
df['vel_suavizada'] = df['velocidad'].rolling(window=6).mean()

# La ventana es centrada por defecto hacia atrás:
# el valor en t es la media de [t-5, t-4, t-3, t-2, t-1, t]
# Las primeras N-1 filas serán NaN porque no hay suficientes datos previos

Con series temporales de frecuencia regular, es más claro usar el tamaño de ventana en tiempo:

df['vel_suavizada'] = df['velocidad'].rolling('1h').mean()    # media de 1 hora
df['vel_suavizada'] = df['velocidad'].rolling('6h').mean()    # media de 6 horas
df['std_movil']     = df['velocidad'].rolling('1D').std()     # std diaria móvil
# Percentil 95 móvil — más pesado pero válido
df['p95_movil'] = df['velocidad'].rolling('7D').quantile(0.95)

Cuándo suavizar

Los datos de ADCP tienen ruido acústico y efectos de ondas superficiales. Una media móvil de 10–60 minutos elimina variabilidad de alta frecuencia sin afectar la señal de corriente. No suavizar antes de hacer estadísticas mensuales — eso puede sesgar los resultados.

pd.cut — crear intervalos

pd.cut divide una columna continua en intervalos (bins) y asigna una etiqueta a cada valor. Se usa para construir tablas de incidencia (velocidad × dirección) y estadísticas por rango de profundidad.

# Crear intervalos de velocidad: 0-0.1, 0.1-0.25, 0.25-0.5, 0.5-1.0 m/s
bins   = [0, 0.1, 0.25, 0.5, 1.0]
labels = ['calma', 'leve', 'moderada', 'fuerte']

df['intervalo_vel'] = pd.cut(df['velocidad'], bins=bins, labels=labels)
# Número de intervalos iguales — pandas elige los límites automáticamente
df['quintil_vel'] = pd.cut(df['velocidad'], bins=5)

# pd.qcut — misma cantidad de datos en cada intervalo (cuantiles)
df['cuartil_vel'] = pd.qcut(df['velocidad'], q=4, labels=['Q1','Q2','Q3','Q4'])

Una vez que cada dato tiene su etiqueta de intervalo, se puede agrupar:

df.groupby('intervalo_vel')['velocidad'].count()   # frecuencia por intervalo
df.groupby('intervalo_vel')['velocidad'].mean()    # media dentro de cada rango

pivot_table — tabla de frecuencias

Las tablas de incidencia (velocidad × dirección) del informe se generan con pivot_table:

# Tabla de frecuencia: velocidad × dirección
tabla = pd.pivot_table(
    df,
    values='velocidad',
    index='intervalo_vel',
    columns='octante',
    aggfunc='count',
    fill_value=0
)

# Normalizar a porcentaje
tabla_pct = tabla / tabla.values.sum() * 100

merge y concat — combinar DataFrames

concat — apilar filas

# Misma estructura, distintos períodos — apilar verticalmente
df_total = pd.concat([df_septiembre, df_octubre, df_noviembre])

# Si los índices originales se repiten, resetear
df_total = pd.concat([df_sep, df_oct, df_nov], ignore_index=True)

# Para saber de qué DataFrame vino cada fila
df_total = pd.concat(
    [df_sep, df_oct, df_nov],
    keys=['sep', 'oct', 'nov']
)

merge — combinar por columna común

merge une dos DataFrames que comparten una columna clave. El parámetro how controla qué filas se conservan cuando no hay match:

# inner (default): solo las filas que existen en ambos
df = pd.merge(df_corrientes, df_oleaje, on='tiempo', how='inner')

# left: todas las filas de df_corrientes, NaN donde no hay oleaje
df = pd.merge(df_corrientes, df_oleaje, on='tiempo', how='left')

# outer: todas las filas de ambos, NaN donde falta alguno
df = pd.merge(df_corrientes, df_oleaje, on='tiempo', how='outer')
df_corrientes    df_oleaje        inner merge       left merge
─────────────    ──────────       ────────────      ──────────
tiempo  vel      tiempo  Hm0      tiempo  vel  Hm0  tiempo  vel  Hm0
10:00   0.3      10:00   1.2      10:00   0.3  1.2  10:00   0.3  1.2
10:10   0.4      10:20   0.8      10:20   0.5  0.8  10:10   0.4  NaN
10:20   0.5                                         10:20   0.5  0.8
# Si las columnas clave tienen distinto nombre
df = pd.merge(df_corrientes, df_viento,
              left_on='tiempo_adcp', right_on='tiempo_boya',
              how='left')

Regla práctica: usar left cuando el DataFrame izquierdo es el principal y el derecho agrega información complementaria que puede no estar para todos los instantes. Usar inner cuando solo interesan los instantes donde hay datos de ambos instrumentos.

Manejar NaN

df.dropna()                          # eliminar filas con cualquier NaN
df.dropna(subset=['velocidad'])      # solo si velocidad es NaN
df.fillna(0)                         # rellenar con 0
df['velocidad'].interpolate()        # interpolación lineal
df.ffill()                           # propagar último valor válido hacia adelante

Leer y guardar datos

# Leer
df = pd.read_csv('corrientes.csv', sep=';', parse_dates=['tiempo'])
df = pd.read_excel('corrientes.xlsx', sheet_name='VELOCIDAD')

# Guardar
df.to_csv('resultado.csv', index=False)
df.to_excel('resultado.xlsx', sheet_name='Datos', index=False)

Variable Explorer en Spyder

Hacer doble clic en un DataFrame en el Variable Explorer de Spyder abre una vista de tabla interactiva donde se pueden ordenar columnas y buscar valores, sin escribir ningún código adicional.