Notebook 1: Conceptos básicos de Python y series de tiempo con pandas¶

Mini-curso de Introducción a herramientas computacionales para Oceanografía Física¶

Oceanografía Física, CICESE¶

21 de septiembre de 2026

La parte de fundamentos de Python de este notebook está basada en la lección Programming with Python de Software Carpentry, publicada bajo la Creative Commons - Attribution License. Traducción, adaptación al proyecto del curso y modificaciones por Karina Ramos Musalem.

Para el trabajo con datos tabulados usaremos pandas, una biblioteca de Python para trabajar con datos organizados en tablas y series de tiempo.

Volviendo al caso del curso, María Luisa ya organizó los archivos del proyecto desde la terminal. Ahora puede comenzar a trabajar con los datos. Tenemos dos series de tiempo en archivos CSV: el índice oceánico de El Niño (ONI) y precipitación mensual acumulada en Ensenada. Estos datos fueron proporcionados por la Dra. Marisol García Reyes.

Pregunta de este notebook: ¿qué contienen estos archivos y cómo podemos empezar a explorarlos de una forma que después podamos reutilizar?

Antes de empezar¶

Al terminar este notebook:

  • recordarán qué es una variable y reconocerán algunos tipos de objetos de Python;
  • importarán bibliotecas y usarán funciones que viven dentro de ellas;
  • leerán archivos CSV con pandas;
  • seleccionarán columnas, filas y datos que cumplen una condición;
  • usarán if, elif y else para tomar decisiones;
  • escribirán y usarán una función;
  • usarán un ciclo for para repetir una operación;
  • identificarán datos faltantes (NaN);
  • producirán una primera visualización de las series de tiempo.

JupyterLab¶

Los Jupyter notebooks combinan celdas de código, figuras, ecuaciones y texto. Una celda de código se ejecuta con Shift + Enter. El orden de ejecución sí importa: una variable sólo existe después de ejecutar la celda que la define.

Antes de abrir este notebook, desde la raíz del proyecto:

cd enso_baja
conda activate oceanografia
jupyter lab

Este notebook debe estar en notebooks/ y los datos en data/. Por eso las rutas que usaremos comienzan con ../data/: .. significa subir un directorio, como vimos en la sesión de terminal.

1. Bibliotecas, objetos y variables¶

Preguntas a responder¶

  1. ¿Qué es una biblioteca de Python y cómo la usamos?
  2. ¿Qué guarda una variable?
  3. ¿Cómo podemos averiguar qué tipo de objeto tenemos?

Objetivos¶

  • Importar bibliotecas usando nombres cortos convencionales.
  • Asignar objetos a variables.
  • Usar type() para inspeccionar objetos.

Python incluye herramientas básicas, pero gran parte de la programación científica se apoya en bibliotecas: colecciones de funciones, clases y otros objetos diseñados para realizar tareas relacionadas.

En este proyecto usaremos:

  • numpy para operaciones numéricas;
  • pandas para tablas y series de tiempo;
  • matplotlib para visualización.
In [1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

La instrucción import pandas as pd importa la biblioteca pandas y le asigna el nombre corto pd. Es una convención muy común: otras personas reconocerán inmediatamente qué biblioteca estamos usando.

pd también es un objeto de Python:

In [2]:
type(pd)
Out[2]:
module

Una variable es un nombre que asociamos con un objeto. Por ejemplo, podemos guardar la ruta de nuestro archivo ONI en una variable:

In [3]:
filename = "../data/oni_mensual_1981_2026.csv"

type(filename)
Out[3]:
str

filename es un str (string o cadena de caracteres). El signo = asigna el objeto de la derecha al nombre de la izquierda.

Los nombres de variables pueden contener letras, números y _, pero no pueden comenzar con un número y distinguen entre mayúsculas y minúsculas.

Ayuda: en Jupyter puedes consultar la documentación de muchas funciones escribiendo ?funcion, por ejemplo ?pd.read_csv, o usando help(funcion).

2. Leer nuestro primer archivo CSV¶

Pregunta a responder¶

¿Cómo representamos en Python una tabla con fechas y valores?

pandas utiliza principalmente dos estructuras:

  • DataFrame: una tabla bidimensional con filas y columnas etiquetadas;
  • Series: una sola columna de datos con un índice.

Usaremos pd.read_csv() para leer un archivo de texto separado por comas. El argumento parse_dates=["time"] le indica a pandas que interprete la columna time como fechas.

In [5]:
oni = pd.read_csv(filename, parse_dates=["time"])
oni.head()
Out[5]:
time oni_degC
0 1981-09-01 -0.25
1 1981-10-01 -0.16
2 1981-11-01 -0.13
3 1981-12-01 -0.15
4 1982-01-01 -0.08

head() es un método del objeto DataFrame: una función asociada al propio objeto. La notación objeto.metodo() aparecerá constantemente en Python científico.

Inspeccionemos nuestro nuevo objeto.

In [6]:
type(oni)
Out[6]:
pandas.DataFrame
In [7]:
oni.columns
Out[7]:
Index(['time', 'oni_degC'], dtype='str')
In [8]:
oni.shape
Out[8]:
(540, 2)
In [9]:
oni.dtypes
Out[9]:
time        datetime64[us]
oni_degC           float64
dtype: object
In [10]:
oni.describe()
Out[10]:
time oni_degC
count 540 540.000000
mean 2004-02-15 08:00:00 0.046185
min 1981-09-01 00:00:00 -1.850000
25% 1992-11-23 12:00:00 -0.552500
50% 2004-02-15 12:00:00 -0.075000
75% 2015-05-08 18:00:00 0.572500
max 2026-08-01 00:00:00 2.750000
std NaN 0.871231

shape contiene el número de filas y columnas. dtypes muestra el tipo de dato de cada columna y describe() calcula un resumen estadístico de las columnas numéricas.

Pon en práctica lo que aprendiste¶

  1. ¿Cuántos meses contiene el archivo?
  2. ¿Cuál es el valor máximo y mínimo de oni_degC?
  3. ¿Qué fecha corresponde al valor máximo?

Intenta resolver las dos primeras preguntas usando la información que ya produjo describe(). Para la tercera puedes experimentar con idxmax().

Una posible solución
len(oni)
oni["oni_degC"].min()
oni["oni_degC"].max()

i = oni["oni_degC"].idxmax()
oni.loc[i]

3. Seleccionar información¶

Preguntas a responder¶

  1. ¿Cómo seleccionamos una columna?
  2. ¿Cómo seleccionamos una fila?
  3. ¿Cómo seleccionamos sólo los datos que cumplen una condición?

Una columna se selecciona usando su nombre entre corchetes:

In [11]:
oni["oni_degC"]
Out[11]:
0     -0.25
1     -0.16
2     -0.13
3     -0.15
4     -0.08
       ... 
535    0.11
536    0.46
537    0.95
538    1.39
539    1.80
Name: oni_degC, Length: 540, dtype: float64

El resultado ya no es un DataFrame con varias columnas sino una Series:

In [12]:
type(oni["oni_degC"])
Out[12]:
pandas.Series

Los métodos de pandas nos permiten operar directamente sobre la serie:

In [13]:
oni["oni_degC"].mean()
Out[13]:
np.float64(0.046185185185185176)

También podemos seleccionar filas. iloc selecciona por posición (la primera fila tiene posición 0):

In [14]:
oni.iloc[0]
Out[14]:
time        1981-09-01 00:00:00
oni_degC                  -0.25
Name: 0, dtype: object

loc puede seleccionar datos usando etiquetas o una condición booleana. Por ejemplo, la expresión siguiente produce True donde ONI es mayor que 0.5 y False donde no:

In [15]:
oni["oni_degC"] > 0.5
Out[15]:
0      False
1      False
2      False
3      False
4      False
       ...  
535    False
536    False
537     True
538     True
539     True
Name: oni_degC, Length: 540, dtype: bool
In [16]:
type(oni["oni_degC"] > 0.5)
Out[16]:
pandas.Series

Podemos usar esa serie de valores True/False como una máscara para quedarnos únicamente con las filas que cumplen la condición:

In [17]:
oni.loc[oni["oni_degC"] > 0.5]
Out[17]:
time oni_degC
9 1982-06-01 0.66
10 1982-07-01 0.72
11 1982-08-01 0.79
12 1982-09-01 1.07
13 1982-10-01 1.58
... ... ...
511 2024-04-01 1.26
512 2024-05-01 0.82
537 2026-06-01 0.95
538 2026-07-01 1.39
539 2026-08-01 1.80

145 rows × 2 columns

Los operadores de comparación que encontraremos con frecuencia son:

Operador Significado
> mayor que
< menor que
>= mayor o igual
<= menor o igual
== igual
!= distinto

Observa que = asigna un valor, mientras que == compara dos valores.

4. Condicionales: tomar decisiones¶

Pregunta a responder¶

¿Cómo hacemos que un programa realice acciones diferentes dependiendo de los datos?

Los condicionales if, elif y else permiten ejecutar código solamente cuando se cumple una condición.

Para fines didácticos vamos a usar una clasificación simplificada de cada valor ONI:

  • ONI >= 0.5: "El Niño"
  • ONI <= -0.5: "La Niña"
  • valores intermedios: "Neutral"

Importante: esto clasifica valores individuales de la serie para practicar programación. No sustituye los criterios operacionales usados para declarar un episodio El Niño o La Niña.

In [18]:
x = 1.2

if x >= 0.5:
    print("El Niño")
elif x <= -0.5:
    print("La Niña")
else:
    print("Neutral")
El Niño

Python utiliza la indentación para indicar qué instrucciones pertenecen a cada bloque. El orden también importa: Python prueba las condiciones de arriba hacia abajo y ejecuta el primer bloque que sea verdadero.

Podríamos cambiar x y volver a ejecutar la celda muchas veces, pero copiar y pegar el mismo código no es una buena estrategia. Para reutilizar código necesitamos una función.

5. Funciones: empaquetar código para reutilizarlo¶

Preguntas a responder¶

  1. ¿Cómo definimos una función?
  2. ¿Cuál es la diferencia entre definirla y llamarla?
  3. ¿Cómo regresa un resultado?

Objetivos¶

  • Definir una función con un parámetro.
  • Usar return para devolver un resultado.
  • Entender por qué una función facilita el mantenimiento y reciclaje del código.

Las funciones nos permiten dar un nombre a una tarea. En lugar de copiar varias veces las mismas líneas, escribimos la lógica una sola vez y la llamamos cada vez que la necesitamos. Esta idea será central cuando transformemos nuestro análisis en un script reproducible.

In [19]:
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"

La definición comienza con def, seguida del nombre de la función y sus parámetros entre paréntesis.

def nombre(parametro):
    # cuerpo de la función
    return resultado

En nuestra función:

  • enso_phase es el nombre;
  • oni_value es el parámetro que recibe la función;
  • el texto entre comillas triples es el docstring, una descripción breve de lo que hace;
  • return devuelve el resultado.

Definir una función no la ejecuta. Para ejecutarla debemos llamarla y darle un argumento:

In [20]:
enso_phase(1.2)
Out[20]:
'El Niño'
In [21]:
enso_phase(-0.8)
Out[21]:
'La Niña'

Puedes consultar el docstring con:

In [22]:
?enso_phase
Signature: enso_phase(oni_value)
Docstring: Clasifica de manera simplificada un valor individual de ONI.
File:      /var/folders/r4/hg2wldcj1hjfp_tt15dp3kc80000gn/T/ipykernel_44062/3735144605.py
Type:      function

Pon en práctica lo que aprendiste¶

¿Qué regresará enso_phase(0.2)? ¿Y enso_phase(0.5)?

Después modifica temporalmente uno de los límites de la función y comprueba cómo cambia el resultado. Devuélvelo a su valor original antes de continuar.

6. Repetir acciones con ciclos o loops¶

Pregunta a responder¶

¿Cómo aplicamos la misma operación a varios valores?

Un ciclo for repite un conjunto de instrucciones para cada elemento de una colección. Por ejemplo:

In [23]:
values = [1.2, -0.8, 0.1]

for value in values:
    print(value, enso_phase(value))
1.2 El Niño
-0.8 La Niña
0.1 Neutral

En cada iteración, value toma el siguiente elemento de la lista values. Igual que en un if o en una función, el cuerpo del ciclo está indentado.

La idea general es:

for elemento in coleccion:
    haz_algo(elemento)

Los ciclos son fundamentales, pero muchas bibliotecas científicas ofrecen operaciones que ya recorren los datos internamente. Cuando existe una operación clara de NumPy, pandas o xarray, suele ser preferible usarla a escribir manualmente un ciclo.

Aplicar nuestra función a una columna completa¶

Series.apply() llama una función para cada valor de una serie. Así podemos reutilizar enso_phase() sobre toda la columna ONI:

In [24]:
oni["phase"] = oni["oni_degC"].apply(enso_phase)

oni.head()
Out[24]:
time oni_degC phase
0 1981-09-01 -0.25 Neutral
1 1981-10-01 -0.16 Neutral
2 1981-11-01 -0.13 Neutral
3 1981-12-01 -0.15 Neutral
4 1982-01-01 -0.08 Neutral
In [25]:
oni["phase"].value_counts()
Out[25]:
phase
Neutral    247
La Niña    147
El Niño    146
Name: count, dtype: int64

La función contiene la lógica; pandas se encarga de repetirla sobre la serie.

7. Segundo CSV: precipitación¶

Ahora leeremos un segundo archivo con precipitación mensual acumulada en una presa de Ensenada. La estructura es similar, por lo que podemos reutilizar exactamente la misma función pd.read_csv().

In [26]:
rain = pd.read_csv(
    "../data/precipitacion_ensenada_1981_2026.csv",
    parse_dates=["time"]
)

rain.head()
Out[26]:
time precip_mm
0 1981-09-01 2.0
1 1981-10-01 5.0
2 1981-11-01 68.5
3 1981-12-01 20.0
4 1982-01-01 54.8

info() resume la estructura del DataFrame, incluyendo cuántos valores no nulos hay en cada columna:

In [27]:
rain.info()
<class 'pandas.DataFrame'>
RangeIndex: 540 entries, 0 to 539
Data columns (total 2 columns):
 #   Column     Non-Null Count  Dtype         
---  ------     --------------  -----         
 0   time       540 non-null    datetime64[us]
 1   precip_mm  511 non-null    float64       
dtypes: datetime64[us](1), float64(1)
memory usage: 8.6 KB

En datos observacionales es normal encontrar datos faltantes. Pandas los representa usualmente como NaN (Not a Number). No debemos confundir un dato faltante con precipitación igual a cero.

In [28]:
rain.isna().sum()
Out[28]:
time          0
precip_mm    29
dtype: int64

Pon en práctica lo que aprendiste¶

Encuentra los meses para los que no tenemos precipitación.

Una posible solución
rain.loc[rain["precip_mm"].isna()]
In [ ]:
 

8. Una primera visualización¶

pandas puede graficar directamente un DataFrame o una Series. Sus métodos de gráfica utilizan matplotlib internamente.

In [29]:
oni.plot(x="time", y="oni_degC", figsize=(10, 3))
plt.ylabel("ONI (°C)")
plt.xlabel("Tiempo")
plt.title("Oceanic Niño Index")
plt.show()
No description has been provided for this image
In [30]:
rain.plot(x="time", y="precip_mm", figsize=(10, 3))
plt.ylabel("Precipitación mensual (mm)")
plt.xlabel("Tiempo")
plt.title("Precipitación en Ensenada")
plt.show()
No description has been provided for this image

Mira las dos series antes de hacer cálculos.

  • ONI cambia de manera relativamente suave porque incorpora un promedio de tres meses.
  • La precipitación mensual es mucho más intermitente.
  • La precipitación presenta una estacionalidad marcada.
  • Hay meses sin observación.

Pregunta para discutir: ¿tiene sentido comparar directamente estas dos series punto por punto? ¿Qué otras decisiones científicas tendríamos que tomar?

Por ahora no intentaremos contestar completamente esa pregunta. Primero necesitamos incorporar el tercer conjunto de datos del proyecto: la temperatura superficial del mar.

9. En resumen¶

En este notebook:

  • importamos bibliotecas con import;
  • usamos variables para dar nombres a objetos;
  • leímos archivos CSV como DataFrame con pd.read_csv();
  • seleccionamos columnas y filas con [], iloc y loc;
  • usamos comparaciones para construir máscaras booleanas;
  • usamos if, elif y else para tomar decisiones;
  • escribimos la función enso_phase() para reutilizar código;
  • usamos un ciclo for para repetir una operación;
  • aplicamos una función a una Series;
  • detectamos valores faltantes con isna();
  • hicimos gráficas básicas con pandas/matplotlib.

Para seguir¶

En el siguiente notebook cambiaremos de tipo de datos. En lugar de una tabla con una serie de tiempo tendremos un campo de SST con dimensiones:

tiempo × latitud × longitud

Para trabajar con estos datos multidimensionales usaremos xarray.