02. Datos multidimensionales con xarray y un análisis reproducible¶
Mini-curso de Introducción a herramientas computacionales para Oceanografía Física¶
Oceanografía Física, CICESE¶
21 de septiembre de 2026
La introducción a NetCDF y
xarrayde este notebook está basada en una versión traducida y modificada de los tutoriales de fundamentos dexarrayde Anderson Banihirwe, disponibles en xarray-contrib/xarray-tutorial. La serie original también cuenta con un video tutorial. Adaptación al proyecto del curso y modificaciones por Karina Ramos Musalem.
Pregunta de este notebook: ¿qué estaba ocurriendo en la temperatura superficial del mar alrededor de Baja California mientras variaba el índice ONI, y cómo transformamos una exploración en notebook en código que podamos volver a ejecutar?
Antes de empezar¶
Al terminar este notebook:
- reconocerán la estructura de un archivo NetCDF;
- distinguirán un
xarray.Datasetde unxarray.DataArray; - seleccionarán datos por posición y por coordenadas;
- reducirán dimensiones usando operaciones como
mean; - seleccionarán una región geográfica;
- convertirán una serie de xarray a pandas;
- combinarán datos provenientes de NetCDF y CSV;
- reutilizarán una función para repetir un análisis en distintas regiones;
- separarán código reutilizable en un módulo;
- entenderán la diferencia entre notebook, función y script.
Este notebook es autocontenido: vuelve a leer ONI y precipitación aunque ya hayas ejecutado el notebook 1. Esto permite cerrar el kernel, volver otro día y reproducir el análisis desde cero.
1. Archivos NetCDF y xarray¶
En Oceanografía y Ciencias de la Tierra trabajamos frecuentemente con datos de varias dimensiones: por ejemplo, una variable puede depender simultáneamente de tiempo, profundidad, latitud y longitud. Guardar estos campos como tablas de texto es poco práctico.
El formato NetCDF permite almacenar:
- variables: los datos que queremos analizar;
- dimensiones/coordenadas: por ejemplo
time,latylon; - atributos: unidades, descripción, fuente y otra información sobre los datos.
xarray extiende el modelo de arreglos N-dimensionales de NumPy añadiendo etiquetas,
coordenadas y metadatos. Sus dos estructuras fundamentales son:
DataArray: una variable multidimensional junto con sus dimensiones, coordenadas y atributos;Dataset: una colección de variables que pueden compartir coordenadas.
Nuestro archivo contiene SST y SSTA mensuales de NOAA OISST V2.1 en una región que incluye Baja California y el Pacífico adyacente. Datos propocionado por la Dra. Marisol García Reyes.
import xarray as xr
import pandas as pd
import matplotlib.pyplot as plt
Abrimos el NetCDF con xr.open_dataset(). Xarray lee la estructura y los metadatos del
archivo y nos entrega un Dataset.
ds = xr.open_dataset("../data/sst_baja_1981_2026.nc")
ds
<xarray.Dataset> Size: 17MB
Dimensions: (time: 540, lat: 58, lon: 66)
Coordinates:
* time (time) datetime64[ns] 4kB 1981-09-01 1981-10-01 ... 2026-08-01
* lat (lat) float32 232B 21.12 21.38 21.62 21.88 ... 34.88 35.12 35.38
* lon (lon) float32 264B -124.9 -124.6 -124.4 ... -109.1 -108.9 -108.6
Data variables:
sst (time, lat, lon) float32 8MB ...
ssta (time, lat, lon) float32 8MB ...
Attributes: (12/15)
description: ENSO related variables. Monthly. Oceanic Nino...
title: Monthly sea surface temperature and SST anoma...
summary: Monthly sea surface temperature (SST) and sea...
source: NOAA Optimum Interpolation Sea Surface Temper...
institution: CICESE
Conventions: CF-1.8
... ...
geospatial_lat_min: 21.125
geospatial_lat_max: 35.375
geospatial_lon_min: -124.875
geospatial_lon_max: -108.625
geospatial_lat_resolution: 0.25
geospatial_lon_resolution: 0.25Antes de continuar, lee la representación que muestra Jupyter e identifica:
- ¿cuáles son las dimensiones?
- ¿cuáles son las coordenadas?
- ¿cuáles son las variables?
- ¿qué periodo temporal cubre el archivo?
También podemos revisar los atributos globales:
ds.attrs
{'description': 'ENSO related variables. Monthly. Oceanic Nino (ONI), NOAA OISST, Rain Ensenada Dam',
'title': 'Monthly sea surface temperature and SST anomaly around Baja California',
'summary': 'Monthly sea surface temperature (SST) and sea surface temperature anomaly (SSTA) for a regional domain around Baja California. SST data are from NOAA OISST V2.1. Prepared for the 2026 CICESE Physical Oceanography introductory scientific computing minicourse.',
'source': 'NOAA Optimum Interpolation Sea Surface Temperature (OISST) Version 2.1',
'institution': 'CICESE',
'Conventions': 'CF-1.8',
'history': '2026-09-16: Course dataset created from Datos_cursoPython_2026.nc; ONI and precipitation were exported to separate CSV files.',
'time_coverage_start': '1981-09-01',
'time_coverage_end': '2026-08-01',
'geospatial_lat_min': np.float64(21.125),
'geospatial_lat_max': np.float64(35.375),
'geospatial_lon_min': np.float64(-124.875),
'geospatial_lon_max': np.float64(-108.625),
'geospatial_lat_resolution': np.float64(0.25),
'geospatial_lon_resolution': np.float64(0.25)}
2. De Dataset a DataArray¶
Podemos seleccionar una variable del Dataset mediante su nombre:
ds.sst
<xarray.DataArray 'sst' (time: 540, lat: 58, lon: 66)> Size: 8MB
[2067120 values with dtype=float32]
Coordinates:
* time (time) datetime64[ns] 4kB 1981-09-01 1981-10-01 ... 2026-08-01
* lat (lat) float32 232B 21.12 21.38 21.62 21.88 ... 34.88 35.12 35.38
* lon (lon) float32 264B -124.9 -124.6 -124.4 ... -109.1 -108.9 -108.6
Attributes:
long_name: Monthly mean sea surface temperature
standard_name: sea_surface_temperature
units: degree_Celsius
source: NOAA OISST V2.1
cell_methods: time: meanCompáremos los tipos de los dos objetos:
type(ds)
xarray.core.dataset.Dataset
type(ds.sst)
xarray.core.dataarray.DataArray
Un Dataset puede contener varias variables; ds.sst es un solo DataArray.
Podemos preguntar explícitamente por sus dimensiones, forma y atributos:
ds.sst.dims
('time', 'lat', 'lon')
ds.sst.shape
(540, 58, 66)
ds.sst.attrs
{'long_name': 'Monthly mean sea surface temperature',
'standard_name': 'sea_surface_temperature',
'units': 'degree_Celsius',
'source': 'NOAA OISST V2.1',
'cell_methods': 'time: mean'}
Un modelo mental útil es:
Dataset
├── DataArray: sst
└── DataArray: ssta
↓
valores + dimensiones + coordenadas + atributos
A diferencia de un arreglo NumPy, no necesitamos recordar que "el eje 0 es tiempo y el eje 1 es latitud": xarray conserva esos nombres.
SST y SSTA¶
El archivo contiene:
sst: temperatura superficial del mar mensual, en °C;ssta: anomalía mensual de temperatura superficial del mar, en °C.
Consulta también los metadatos de ssta:
ds.ssta.attrs
{'long_name': 'Monthly sea surface temperature anomaly',
'units': 'degree_Celsius',
'source': 'NOAA OISST V2.1',
'cell_methods': 'time: mean',
'comment': 'Anomaly field retained from the original course dataset. The climatological reference period is not documented in the source file.'}
El archivo preparado para el curso conserva la anomalía del conjunto original. El
periodo climatológico de referencia de ssta no está documentado en el archivo fuente,
así que no debemos asumir uno.
Esta es una buena regla general: si un metadato importante no está documentado, no lo inventamos.
3. Seleccionar por posición y por coordenadas¶
Preguntas a responder¶
- ¿Cómo seleccionamos el primer elemento de una dimensión?
- ¿Cómo seleccionamos usando una fecha o una coordenada con significado físico?
xarray ofrece dos formas principales:
.isel()selecciona por posición del índice;.sel()selecciona por valor de coordenada.
Por posición:
ds.sst.isel(time=0)
<xarray.DataArray 'sst' (lat: 58, lon: 66)> Size: 15kB
[3828 values with dtype=float32]
Coordinates:
* lat (lat) float32 232B 21.12 21.38 21.62 21.88 ... 34.88 35.12 35.38
* lon (lon) float32 264B -124.9 -124.6 -124.4 ... -109.1 -108.9 -108.6
time datetime64[ns] 8B 1981-09-01
Attributes:
long_name: Monthly mean sea surface temperature
standard_name: sea_surface_temperature
units: degree_Celsius
source: NOAA OISST V2.1
cell_methods: time: meanPor coordenada temporal:
ds.sst.sel(time="1997-12")
<xarray.DataArray 'sst' (time: 1, lat: 58, lon: 66)> Size: 15kB
[3828 values with dtype=float32]
Coordinates:
* time (time) datetime64[ns] 8B 1997-12-01
* lat (lat) float32 232B 21.12 21.38 21.62 21.88 ... 34.88 35.12 35.38
* lon (lon) float32 264B -124.9 -124.6 -124.4 ... -109.1 -108.9 -108.6
Attributes:
long_name: Monthly mean sea surface temperature
standard_name: sea_surface_temperature
units: degree_Celsius
source: NOAA OISST V2.1
cell_methods: time: meanLa segunda forma suele ser mucho más legible: "1997-12" expresa directamente qué
queremos seleccionar.
Como xarray conoce las coordenadas espaciales, puede producir una figura rápidamente:
ds.ssta.sel(time="1997-12").plot()
plt.title("SSTA - diciembre de 1997")
plt.show()
Pon en práctica lo que aprendiste¶
Usa el archivo de ONI para identificar otro periodo con valores positivos grandes y haz un mapa de SSTA para uno de esos meses.
Pista: primero lee oni_mensual_1981_2026.csv con pandas, como hicimos en el notebook 1.
No busques todavía una respuesta "correcta": el objetivo es practicar cómo conectar una
pregunta con una selección temporal.
4. Reducir dimensiones: de un campo a una serie¶
Una de las ventajas de trabajar con dimensiones etiquetadas es que podemos decirle a xarray qué dimensiones queremos reducir por nombre.
Calculemos la anomalía media sobre todo el dominio:
ssta_mean = ds.ssta.mean(
dim=("lat", "lon"),
skipna=True
)
ssta_mean.plot(figsize=(10, 3))
plt.ylabel("SSTA (°C)")
plt.title("SSTA media en el dominio")
plt.show()
Compáremos las dimensiones antes y después:
ds.ssta.dims
('time', 'lat', 'lon')
ssta_mean.dims
('time',)
Pasamos de:
(time, lat, lon)
a:
(time,)
porque promediamos lat y lon. Ésta es una operación muy común en análisis
oceanográfico: reducir un campo espacial para construir una serie de tiempo.
skipna=True indica que el promedio debe ignorar valores faltantes, por ejemplo los
puntos en tierra.
5. Seleccionar una región geográfica¶
El dominio completo no necesariamente corresponde a la región física que queremos estudiar. Podemos seleccionar una caja usando las coordenadas de latitud y longitud.
Usaremos una región de ejemplo al norte de la península:
baja_norte = ds.ssta.sel(
lat=slice(28, 33),
lon=slice(-120, -115)
)
baja_norte
<xarray.DataArray 'ssta' (time: 540, lat: 20, lon: 20)> Size: 864kB
array([[[-1.050777, -0.928625, ..., -0.545403, -0.608767],
[-0.830544, -0.698408, ..., -0.568657, -0.788465],
...,
[-1.60812 , -1.423265, ..., nan, nan],
[-1.275999, -1.140823, ..., nan, nan]],
[[-0.895227, -0.816608, ..., -1.253399, -1.431128],
[-0.853249, -0.721119, ..., -1.365362, -1.658245],
...,
[-1.708464, -1.767599, ..., nan, nan],
[-1.361904, -1.368387, ..., nan, nan]],
...,
[[ 1.492355, 1.580235, ..., 1.319828, 1.373085],
[ 1.526945, 1.60129 , ..., 1.364742, 1.339455],
...,
[ 1.297667, 1.237612, ..., nan, nan],
[ 1.119097, 0.969063, ..., nan, nan]],
[[ 1.567741, 1.662237, ..., 2.600471, 2.729153],
[ 1.525042, 1.563175, ..., 2.469732, 2.469902],
...,
[ 1.946615, 2.153955, ..., nan, nan],
[ 1.932486, 2.19413 , ..., nan, nan]]],
shape=(540, 20, 20), dtype=float32)
Coordinates:
* time (time) datetime64[ns] 4kB 1981-09-01 1981-10-01 ... 2026-08-01
* lat (lat) float32 80B 28.12 28.38 28.62 28.88 ... 32.38 32.62 32.88
* lon (lon) float32 80B -119.9 -119.6 -119.4 ... -115.6 -115.4 -115.1
Attributes:
long_name: Monthly sea surface temperature anomaly
units: degree_Celsius
source: NOAA OISST V2.1
cell_methods: time: mean
comment: Anomaly field retained from the original course dataset. T...Visualicemos primero qué seleccionamos:
baja_norte.isel(time=0).plot()
plt.title("Región seleccionada")
plt.show()
Y después calculemos su media espacial:
baja_norte.mean(("lat", "lon"), skipna=True).plot(figsize=(10, 3))
plt.ylabel("SSTA (°C)")
plt.title("SSTA media - región Baja Norte")
plt.show()
La selección de la caja es una decisión científica, no una decisión de Python.
El código nos permite seleccionar cualquier región, pero debemos justificar por qué esa región responde a nuestra pregunta.
Esta libertad será importante para el proyecto final.
6. El puente entre xarray y pandas¶
Hasta ahora tenemos dos tipos de datos:
NetCDF → xarray → campo multidimensional de SST/SSTA
CSV → pandas → series ONI y precipitación
Para compararlos podemos reducir el campo de SSTA a una serie y convertirla a un objeto de pandas.
sst_series = baja_norte.mean(
dim=("lat", "lon"),
skipna=True
)
sst_series
<xarray.DataArray 'ssta' (time: 540)> Size: 2kB
array([ 1.74564160e-02, -8.01558197e-01, -5.48438787e-01, -2.10056201e-01,
-3.00844878e-01, -3.91321152e-01, -3.51450324e-01, 2.18306631e-01,
2.36718208e-01, -8.96373689e-01, -8.26660514e-01, -7.74362922e-01,
-8.32917541e-02, 2.16435254e-01, 5.55345178e-01, 5.58817208e-01,
7.67568827e-01, 8.17435920e-01, 5.92509389e-01, 3.92195582e-01,
-8.96406993e-02, -4.53420550e-01, -4.91134137e-01, 4.46056247e-01,
1.47094560e+00, 1.31415939e+00, 1.16202915e+00, 3.07478607e-01,
1.01089582e-01, 2.49819666e-01, 9.59187672e-02, -7.72193447e-02,
-3.22561711e-01, -2.27311581e-01, 1.01847410e+00, 8.41802597e-01,
1.46675611e+00, 4.73688900e-01, -5.48028052e-01, -7.17218101e-01,
-8.69918585e-01, -8.71838868e-01, -9.00755703e-01, -7.64285564e-01,
-8.32358479e-01, -5.95172942e-01, 7.39920318e-01, 1.02240585e-01,
1.37973234e-01, 2.48553261e-01, -5.81110537e-01, -8.74161959e-01,
-5.98022267e-02, 1.37858000e-02, 1.45766810e-01, 1.06481969e-01,
1.07414640e-01, -3.40419650e-01, -3.23842049e-01, -4.98340487e-01,
-1.37972903e+00, -8.75574112e-01, -4.53465253e-01, 3.95062685e-01,
-4.71810848e-02, -4.68279608e-02, -2.77384013e-01, -1.24894202e-01,
-6.47406606e-03, -6.33568466e-01, -1.23914349e+00, -7.22683966e-01,
-5.93612790e-01, 3.84082384e-02, 4.53453034e-01, -1.12486862e-01,
-9.98680532e-01, -6.73011720e-01, -2.05730632e-01, 5.48982501e-01,
...
9.45368707e-01, 1.02782607e+00, 1.04618251e+00, 8.91335964e-01,
1.31337094e+00, 1.97421849e+00, 9.42699850e-01, 7.82236397e-01,
2.48114347e-01, -1.15848340e-01, -2.80837715e-01, -1.76299915e-01,
-2.12682039e-01, 8.36434774e-03, 4.68366623e-01, 1.26603842e+00,
9.86229181e-01, 4.25968647e-01, -1.18900582e-01, 2.46720091e-02,
-3.01908590e-02, 4.35215198e-02, 2.52360944e-02, 1.03148624e-01,
-4.52163190e-01, 5.62318325e-01, 6.03779495e-01, 1.29020143e+00,
1.66140044e+00, 9.86985683e-01, 3.67624372e-01, 3.21864456e-01,
1.28840417e-01, -2.97220111e-01, -4.33409423e-01, -1.06257606e+00,
-6.39319062e-01, -2.51341134e-01, -7.72708714e-01, 2.68216729e-01,
5.86270988e-01, 1.81181297e-01, 6.09207451e-01, 9.90591884e-01,
8.24549317e-01, 6.18465662e-01, 4.67631251e-01, 1.33606359e-01,
-4.22421023e-02, -2.48348311e-01, 9.76257995e-02, 3.48739564e-01,
-7.58259296e-02, -3.32571298e-01, -5.47031045e-01, -1.91240966e-01,
-2.62534738e-01, -5.87881923e-01, -6.90237880e-01, 1.99982464e-01,
1.95412084e-01, 2.33372405e-01, -7.88965728e-04, -3.45169932e-01,
7.07026303e-01, 7.82383800e-01, 8.04951608e-01, 1.49029386e+00,
1.71222687e+00, 1.74768174e+00, 2.01705718e+00, 2.44491124e+00,
1.82997346e+00, 1.98151922e+00, 1.38823926e+00, 1.72411776e+00],
dtype=float32)
Coordinates:
* time (time) datetime64[ns] 4kB 1981-09-01 1981-10-01 ... 2026-08-01
Attributes:
long_name: Monthly sea surface temperature anomaly
units: degree_Celsius
source: NOAA OISST V2.1
cell_methods: time: mean
comment: Anomaly field retained from the original course dataset. T...sst_series = sst_series.to_series()
type(sst_series)
pandas.Series
Ahora sst_series es una pandas.Series cuyo índice es el tiempo.
Para que este notebook pueda ejecutarse de principio a fin de manera independiente, volvemos a leer aquí los dos CSV:
oni = pd.read_csv(
"../data/oni_mensual_1981_2026.csv",
parse_dates=["time"]
)
rain = pd.read_csv(
"../data/precipitacion_ensenada_1981_2026.csv",
parse_dates=["time"]
)
Podemos convertir time en el índice de cada tabla. Esto facilita alinear las tres
series usando sus fechas:
oni = oni.set_index("time")
rain = rain.set_index("time")
analysis = pd.concat(
[
sst_series.rename("ssta_degC"),
oni["oni_degC"],
rain["precip_mm"]
],
axis=1
)
analysis.head()
| ssta_degC | oni_degC | precip_mm | |
|---|---|---|---|
| time | |||
| 1981-09-01 | 0.017456 | -0.25 | 2.0 |
| 1981-10-01 | -0.801558 | -0.16 | 5.0 |
| 1981-11-01 | -0.548439 | -0.13 | 68.5 |
| 1981-12-01 | -0.210056 | -0.15 | 20.0 |
| 1982-01-01 | -0.300845 | -0.08 | 54.8 |
pd.concat(..., axis=1) coloca las series como columnas y las alinea usando el índice
temporal.
Conceptualmente acabamos de hacer:
NetCDF ──xarray──┐
├── pandas DataFrame → análisis conjunto
CSV ────pandas───┘
Éste es un patrón muy común: distintas herramientas son convenientes para distintas estructuras de datos, y podemos movernos entre ellas cuando la pregunta lo requiere.
analysis.info()
<class 'pandas.DataFrame'> DatetimeIndex: 540 entries, 1981-09-01 to 2026-08-01 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 ssta_degC 540 non-null float32 1 oni_degC 540 non-null float64 2 precip_mm 511 non-null float64 dtypes: float32(1), float64(2) memory usage: 14.8 KB
Observa que la columna de precipitación tiene menos valores válidos. concat no "rellena"
las observaciones que faltan: conserva NaN, como debe ser.
7. Una primera comparación¶
Podemos empezar con una comparación visual entre ONI y la SSTA regional:
analysis[["ssta_degC", "oni_degC"]].plot(figsize=(10, 4))
plt.ylabel("Anomalía de temperatura (°C)")
plt.xlabel("Tiempo")
plt.title("SSTA regional y ONI")
plt.show()
Otra forma de explorar la relación es mediante un diagrama de dispersión:
analysis.plot.scatter(
x="oni_degC",
y="ssta_degC",
figsize=(5, 5)
)
plt.xlabel("ONI (°C)")
plt.ylabel("SSTA regional (°C)")
plt.show()
Y podemos calcular una correlación lineal:
analysis[["oni_degC", "ssta_degC"]].corr()
| oni_degC | ssta_degC | |
|---|---|---|
| oni_degC | 1.000000 | 0.390292 |
| ssta_degC | 0.390292 | 1.000000 |
Esto es exploración, no un análisis estadístico completo. Estas series tienen estructura temporal, persistencia y posibles desfases; además, correlación no implica causalidad. El objetivo aquí es aprender a preparar y combinar los datos de manera transparente. En el proyecto final podrán decidir qué comparación tiene sentido investigar y qué limitaciones deben mencionar.
Pon en práctica lo que aprendiste¶
Elige una de estas exploraciones:
- Grafica ONI y precipitación para un intervalo de tiempo que te interese.
- Cambia la región de SSTA y repite la comparación con ONI.
- Calcula el promedio de precipitación para las fases
"El Niño","La Niña"y"Neutral"usando la funciónenso_phase()del notebook 1. - Explora visualmente si parece existir un desfase entre ONI y SSTA regional.
No es necesario llegar a una conclusión definitiva. Escribe debajo de tu figura dos o tres frases separando claramente lo que observas de lo que interpretas.
8. El problema con un notebook largo¶
Hasta aquí nuestra exploración funciona, pero imaginemos que mañana queremos repetir el mismo análisis para otra región.
Podríamos buscar todas las celdas donde escribimos:
lat=slice(28, 33)
lon=slice(-120, -115)
y cambiarlas a mano. Eso tiene varios problemas:
- repetimos código;
- podemos olvidar modificar una celda;
- es difícil mantener versiones consistentes;
- reutilizar el análisis en otro proyecto requiere copiar y pegar.
Como vimos en el notebook 1, una solución es empaquetar una tarea con una función.
9. Convertir una operación en una función¶
Primero, recordemos la operación que queremos reutilizar:
region = ds.ssta.sel(
lat=slice(28, 33),
lon=slice(-120, -115)
)
regional_mean_example = region.mean(
dim=("lat", "lon"),
skipna=True
)
Ahora generalizamos la idea. En vez de fijar la variable y las coordenadas dentro del código, las recibimos como parámetros:
def regional_mean(da, lat_range, lon_range):
"""Calcula la media espacial de un DataArray en una región rectangular.
Parameters
----------
da : xarray.DataArray
Campo con dimensiones lat y lon.
lat_range : tuple
(lat_min, lat_max)
lon_range : tuple
(lon_min, lon_max)
Returns
-------
xarray.DataArray
Serie resultante después de promediar lat y lon.
"""
region = da.sel(
lat=slice(*lat_range),
lon=slice(*lon_range)
)
return region.mean(
dim=("lat", "lon"),
skipna=True
)
El operador * en slice(*lat_range) "desempaca" la tupla. Por ejemplo:
lat_range = (28, 33)
slice(*lat_range)
equivale a:
slice(28, 33)
Ahora la misma función sirve para muchas regiones:
baja_series = regional_mean(ds.ssta, (28, 33), (-120, -115))
baja_series
<xarray.DataArray 'ssta' (time: 540)> Size: 2kB
array([ 1.74564160e-02, -8.01558197e-01, -5.48438787e-01, -2.10056201e-01,
-3.00844878e-01, -3.91321152e-01, -3.51450324e-01, 2.18306631e-01,
2.36718208e-01, -8.96373689e-01, -8.26660514e-01, -7.74362922e-01,
-8.32917541e-02, 2.16435254e-01, 5.55345178e-01, 5.58817208e-01,
7.67568827e-01, 8.17435920e-01, 5.92509389e-01, 3.92195582e-01,
-8.96406993e-02, -4.53420550e-01, -4.91134137e-01, 4.46056247e-01,
1.47094560e+00, 1.31415939e+00, 1.16202915e+00, 3.07478607e-01,
1.01089582e-01, 2.49819666e-01, 9.59187672e-02, -7.72193447e-02,
-3.22561711e-01, -2.27311581e-01, 1.01847410e+00, 8.41802597e-01,
1.46675611e+00, 4.73688900e-01, -5.48028052e-01, -7.17218101e-01,
-8.69918585e-01, -8.71838868e-01, -9.00755703e-01, -7.64285564e-01,
-8.32358479e-01, -5.95172942e-01, 7.39920318e-01, 1.02240585e-01,
1.37973234e-01, 2.48553261e-01, -5.81110537e-01, -8.74161959e-01,
-5.98022267e-02, 1.37858000e-02, 1.45766810e-01, 1.06481969e-01,
1.07414640e-01, -3.40419650e-01, -3.23842049e-01, -4.98340487e-01,
-1.37972903e+00, -8.75574112e-01, -4.53465253e-01, 3.95062685e-01,
-4.71810848e-02, -4.68279608e-02, -2.77384013e-01, -1.24894202e-01,
-6.47406606e-03, -6.33568466e-01, -1.23914349e+00, -7.22683966e-01,
-5.93612790e-01, 3.84082384e-02, 4.53453034e-01, -1.12486862e-01,
-9.98680532e-01, -6.73011720e-01, -2.05730632e-01, 5.48982501e-01,
...
9.45368707e-01, 1.02782607e+00, 1.04618251e+00, 8.91335964e-01,
1.31337094e+00, 1.97421849e+00, 9.42699850e-01, 7.82236397e-01,
2.48114347e-01, -1.15848340e-01, -2.80837715e-01, -1.76299915e-01,
-2.12682039e-01, 8.36434774e-03, 4.68366623e-01, 1.26603842e+00,
9.86229181e-01, 4.25968647e-01, -1.18900582e-01, 2.46720091e-02,
-3.01908590e-02, 4.35215198e-02, 2.52360944e-02, 1.03148624e-01,
-4.52163190e-01, 5.62318325e-01, 6.03779495e-01, 1.29020143e+00,
1.66140044e+00, 9.86985683e-01, 3.67624372e-01, 3.21864456e-01,
1.28840417e-01, -2.97220111e-01, -4.33409423e-01, -1.06257606e+00,
-6.39319062e-01, -2.51341134e-01, -7.72708714e-01, 2.68216729e-01,
5.86270988e-01, 1.81181297e-01, 6.09207451e-01, 9.90591884e-01,
8.24549317e-01, 6.18465662e-01, 4.67631251e-01, 1.33606359e-01,
-4.22421023e-02, -2.48348311e-01, 9.76257995e-02, 3.48739564e-01,
-7.58259296e-02, -3.32571298e-01, -5.47031045e-01, -1.91240966e-01,
-2.62534738e-01, -5.87881923e-01, -6.90237880e-01, 1.99982464e-01,
1.95412084e-01, 2.33372405e-01, -7.88965728e-04, -3.45169932e-01,
7.07026303e-01, 7.82383800e-01, 8.04951608e-01, 1.49029386e+00,
1.71222687e+00, 1.74768174e+00, 2.01705718e+00, 2.44491124e+00,
1.82997346e+00, 1.98151922e+00, 1.38823926e+00, 1.72411776e+00],
dtype=float32)
Coordinates:
* time (time) datetime64[ns] 4kB 1981-09-01 1981-10-01 ... 2026-08-01
Attributes:
long_name: Monthly sea surface temperature anomaly
units: degree_Celsius
source: NOAA OISST V2.1
cell_methods: time: mean
comment: Anomaly field retained from the original course dataset. T...ensenada_region = regional_mean(
ds.ssta,
(30, 33),
(-118, -115)
)
ensenada_region
<xarray.DataArray 'ssta' (time: 540)> Size: 2kB
array([ 3.62946577e-02, -1.18687034e+00, -8.29025805e-01, -2.63829947e-01,
-6.14233315e-01, -4.53669995e-01, -3.65795195e-01, -1.95580438e-01,
3.03255720e-03, -1.08906245e+00, -4.80314136e-01, -3.65696073e-01,
3.73925269e-02, 2.40376964e-01, 3.90374213e-01, 5.25056124e-01,
7.56639898e-01, 5.97896636e-01, 5.39421976e-01, 8.96685570e-02,
-1.39169604e-01, -5.67946732e-01, -2.86197990e-01, 5.50798476e-01,
1.31498802e+00, 1.07315552e+00, 1.13743627e+00, 5.64195998e-02,
4.49751914e-02, 3.80425066e-01, 5.66140294e-01, 1.58508494e-01,
1.61651731e-01, 3.13639730e-01, 1.56470954e+00, 1.39395130e+00,
1.90803671e+00, 6.26028538e-01, -6.39585853e-01, -9.62161064e-01,
-9.88233328e-01, -1.00671279e+00, -1.13069832e+00, -8.40362608e-01,
-6.94967628e-01, -7.48120248e-01, 5.78929007e-01, 6.28157035e-02,
-1.84798717e-01, 2.59546906e-01, -5.99936903e-01, -1.08487940e+00,
-9.07322690e-02, 8.78633782e-02, 1.91181138e-01, -5.46693690e-02,
1.80477574e-01, -3.66395891e-01, -5.42671025e-01, -7.76732564e-01,
-1.59355414e+00, -9.48474705e-01, -3.37785810e-01, 5.67610860e-01,
1.04841895e-01, -1.40318554e-02, -1.85128435e-01, 2.13046208e-01,
5.84474578e-02, -6.95351243e-01, -1.53275263e+00, -1.05274558e+00,
-6.88185334e-01, -2.81184256e-01, 3.84503007e-01, -3.54109436e-01,
-1.31950200e+00, -8.33082914e-01, 6.34780377e-02, 9.08944011e-01,
...
1.22340226e+00, 8.16568673e-01, 1.08476114e+00, 8.58179212e-01,
1.38246810e+00, 2.30985236e+00, 1.21432519e+00, 1.10700023e+00,
5.14360249e-01, 1.27596632e-01, 3.53533030e-02, 1.60148427e-01,
1.54361382e-01, 4.25053149e-01, 1.13496768e+00, 1.11815774e+00,
1.02193010e+00, 6.82282388e-01, -1.41372532e-01, -3.92025011e-03,
5.13838306e-02, 2.96215773e-01, 2.63938159e-01, 7.80684799e-02,
-4.89832044e-01, 8.86821926e-01, 5.01251400e-01, 1.07486308e+00,
1.68024135e+00, 8.64592075e-01, 1.35845184e-01, 3.72110233e-02,
2.24161148e-02, -3.63212854e-01, -5.41945577e-01, -1.08120263e+00,
-8.59260023e-01, -4.89191324e-01, -8.57845247e-01, -3.16693872e-01,
4.38857526e-02, -1.74302474e-01, 3.28329742e-01, 9.31417465e-01,
6.15147293e-01, 4.01500970e-01, 3.16131830e-01, -2.70038217e-01,
-1.34662330e-01, -3.93551320e-01, -1.70417383e-01, 1.54962003e-01,
-3.46843123e-01, -6.64969206e-01, -9.71608102e-01, -1.20594180e+00,
-9.34022546e-01, -7.57179499e-01, -8.82939160e-01, -1.67060375e-01,
4.83270139e-02, -8.95957723e-02, -3.67742926e-02, -3.92212301e-01,
3.48263562e-01, 2.09312444e-03, 1.92956388e-01, 1.36520243e+00,
1.57076025e+00, 1.70194900e+00, 2.09488440e+00, 2.50352168e+00,
1.67587101e+00, 1.76184404e+00, 1.15060616e+00, 1.51067805e+00],
dtype=float32)
Coordinates:
* time (time) datetime64[ns] 4kB 1981-09-01 1981-10-01 ... 2026-08-01
Attributes:
long_name: Monthly sea surface temperature anomaly
units: degree_Celsius
source: NOAA OISST V2.1
cell_methods: time: mean
comment: Anomaly field retained from the original course dataset. T...Mismo código, distinta región. Ésa es una de las razones principales para escribir funciones: separar la lógica general de los valores particulares de un análisis.
En programas más grandes construimos funciones pequeñas, con propósitos específicos, y las combinamos para formar tareas más complejas.
10. Del notebook a código reutilizable¶
Un notebook es muy útil para:
- explorar datos;
- probar ideas;
- combinar explicación, figuras y código;
- documentar decisiones.
Pero no queremos que una función útil exista únicamente en una celda de un notebook. Podemos moverla a un archivo de Python que otros programas puedan importar.
En JupyterLab crea:
scripts/
└── analysis_tools.py
y guarda ahí, por ejemplo:
def regional_mean(da, lat_range, lon_range):
"""Calcula la media espacial de un DataArray en una región rectangular."""
region = da.sel(
lat=slice(*lat_range),
lon=slice(*lon_range)
)
return region.mean(
dim=("lat", "lon"),
skipna=True
)
def enso_phase(oni_value):
"""Clasifica de manera simplificada un valor individual de ONI."""
if oni_value >= 0.5:
return "El Niño"
elif oni_value <= -0.5:
return "La Niña"
else:
return "Neutral"
Ahora tenemos un módulo con herramientas que podemos reutilizar desde más de un análisis.
11. Del notebook a un script reproducible¶
El siguiente paso es separar una tarea completa de exploración del notebook.
Crea:
scripts/
└── make_summary_figure.py
con el siguiente contenido:
import xarray as xr
import pandas as pd
import matplotlib.pyplot as plt
from analysis_tools import regional_mean
# 1. Leer datos
ds = xr.open_dataset("data/sst_baja_1981_2026.nc")
oni = pd.read_csv(
"data/oni_mensual_1981_2026.csv",
parse_dates=["time"]
).set_index("time")
# 2. Calcular una serie regional de SSTA
regional_ssta = regional_mean(
ds.ssta,
(28, 33),
(-120, -115)
).to_series()
# 3. Alinear las series
analysis = pd.concat(
[
regional_ssta.rename("ssta_degC"),
oni["oni_degC"]
],
axis=1
)
# 4. Crear la figura
fig, axes = plt.subplots(
nrows=2,
figsize=(10, 6),
sharex=True
)
analysis["ssta_degC"].plot(ax=axes[0])
axes[0].set_ylabel("SSTA (°C)")
axes[0].set_title("SSTA regional")
analysis["oni_degC"].plot(ax=axes[1])
axes[1].set_ylabel("ONI (°C)")
axes[1].set_xlabel("Tiempo")
axes[1].set_title("Oceanic Niño Index")
fig.tight_layout()
fig.savefig("figures/ssta_oni_summary.png", dpi=150)
Vuelve a la raíz del proyecto en la terminal y ejecuta:
python scripts/make_summary_figure.py
Si todo funciona, el script debe generar:
figures/
└── ssta_oni_summary.png
¿Qué ganamos?¶
Notebook
↓
explorar, probar, explicar
Función
↓
encapsular una tarea para reutilizarla
Script
↓
repetir un análisis completo
El notebook sigue siendo valioso: documenta la exploración y el razonamiento. El script nos permite volver a producir el resultado de forma consistente.
Reproducibilidad no significa sólo conservar una figura final: significa conservar los pasos necesarios para volver a crearla.
12. En resumen¶
En este notebook:
- abrimos un NetCDF con
xr.open_dataset(); - distinguimos
DatasetyDataArray; - usamos dimensiones, coordenadas y atributos;
- seleccionamos por posición con
.isel()y por coordenada con.sel(); - redujimos dimensiones con
.mean(); - seleccionamos una región geográfica;
- convertimos una serie de xarray a pandas;
- alineamos datos provenientes de NetCDF y CSV;
- hicimos una exploración gráfica y una correlación sencilla;
- convertimos una operación repetida en la función
regional_mean(); - separamos funciones reutilizables en
analysis_tools.py; - diseñamos un script que puede regenerar una figura.
Siguiente paso¶
Ahora tenemos archivos que queremos conservar, modificar y mejorar:
notebooks/
scripts/
figures/
README.md
environment.yml
En la sesión 3 usaremos Git para registrar la historia de esos cambios y GitHub para respaldar y compartir el proyecto.