Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Esta sección busca aportar nuevas habilidades en Python para manejar datos estructurados y tabulares.

Resultados de aprendizaje:

  • Manipulación de data frames (describir, filtrar, ...)
  • Conocer las funciones lambda
  • Introducción a los objetos datetime
  • Graficación de datos desde data frames (histogramas y mapas)
  • Introducción a Plotly
  • Introducción a CSV y Parquet

Este tutorial puede impartirse en una clase de 2 horas. Las secciones están rotuladas como Nivel 1, 2 y 3 en las secciones 1, 2 y 3, y quienes imparten el curso pueden dejar los niveles superiores para el aprendizaje asíncrono y autoguiado.

Trabajaremos con varios conjuntos de datos estructurados: metadatos de sensores y un producto de datos sísmicos (un catálogo de sismos).

Primero, importamos todos los módulos que necesitamos:

🖥️ Diapositivas — Sesión 05 (vie 9 oct)

import io
import os

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pooch
import requests

os.makedirs('data', exist_ok=True)

Este cuaderno usa plotly para hacer gráficas interactivas

import plotly.express as px
import plotly.io as pio
# pio.renderers.default = 'vscode' # writes as standalone html, 
# pio.renderers.default = 'iframe' # writes files as standalone html, 
# pio.renderers.default = 'png' # writes files as standalone png, 
# try notebook, jupyterlab, png, vscode, iframe

1 Fundamentos de Pandas

1.1 Nociones básicas

Los objetos centrales de Pandas son Series y DataFrame. Una Series es un arreglo etiquetado unidimensional. El DataFrame es una tabla bidimensional cuyas columnas son Series.

Podemos crear un DataFrame compuesto de series desde cero usando un diccionario de Python:

data = {
    'temperature' : [36,37,30,50],
    'precipitation':[3,1,0,0]
}
my_pd = pd.DataFrame(data)
print(my_pd)
   temperature  precipitation
0           36              3
1           37              1
2           30              0
3           50              0

Cada elemento (clave, valor) del dataframe corresponde a un valor de data. Para obtener las claves del dataframe, escriba:

my_pd.keys()
Index(['temperature', 'precipitation'], dtype='str')

obtener una Series específica (distinta del arreglo)

print(my_pd.temperature[:])
print(type(my_pd.temperature[:]))
0    36
1    37
2    30
3    50
Name: temperature, dtype: int64
<class 'pandas.Series'>

para obtener el valor de una clave específica (por ejemplo, temperature) en un índice específico (por ejemplo, 2), escriba:

print(my_pd.temperature[2])
print(type(my_pd.temperature[2]))
30
<class 'numpy.int64'>

1.2 Leer un DataFrame desde un archivo CSV

Podemos leer un pandas directamente desde un archivo estándar. Descargaremos un catálogo de sismos con pooch, que guarda el archivo en la carpeta local data/.

ff = pooch.retrieve(
    url="https://raw.githubusercontent.com/UW-MLGEO/MLGeo-dataset/refs/heads/main/data/Global_Quakes_IRIS.csv",
    known_hash=None,
    fname="Global_Quakes_IRIS.csv",
    path="./data",
)
quake = pd.read_csv(ff)

Ahora use la función head para mostrar el contenido del archivo

# enter answer here
quake.head()
Loading...

Muestre la profundidad usando las dos maneras de usar el objeto de pandas

print(quake.depth)
print(quake['depth'])
0        34400.0
1        30100.0
2        16900.0
3       109400.0
4        25700.0
          ...   
1780     10000.0
1781    105000.0
1782    608510.0
1783     10000.0
1784     35440.0
Name: depth, Length: 1785, dtype: float64
0        34400.0
1        30100.0
2        16900.0
3       109400.0
4        25700.0
          ...   
1780     10000.0
1781    105000.0
1782    608510.0
1783     10000.0
1784     35440.0
Name: depth, Length: 1785, dtype: float64

Calcule estadísticas básicas de los datos usando la función describe.

quake.describe()
Loading...

Calcule la media y la mediana de una Series específica, por ejemplo la profundidad.

# answer it here
print(quake.depth.mean())
print(quake.depth.median())
82773.18767507003
24400.0

1.3 Manipulación de Pandas con Python

Funciones clásicas

Ahora practicaremos cómo modificar el contenido del DataFrame usando funciones. Tomaremos el ejemplo en que queremos convertir los valores de profundidad de metros a kilómetros. Primero podemos definir esta operación como una función

# this function converts a value in meters to a value in kilometers
m2km = 1000 # this is defined as a global variable
def meters2kilometers(x):
    return x/m2km

# now test it using the first element of the quake DataFrame
meters2kilometers(quake.depth[0])
np.float64(34.4)
# this function converts a value in meters to a value in kilometers
def meters2kilometers2(x):
    m2km = 1000 # this is defined as a global variable
    return x/m2km

Definamos otra función que usa una variable local en lugar de una global

# Apply the meters2kilometers2 function to the 'depth' column and add it as a new column 'depth_km' to the quake DataFrame
quake['depth_km'] = quake['depth'].apply(meters2kilometers2)

# Display the first few rows to verify the new column
quake.head()
Loading...

Funciones lambda

Ahora hablemos de las funciones lambda.

  • Las funciones lambda son funciones pequeñas y anónimas de Python.
  • Se usan para operaciones rápidas y simples sin necesidad de definir una función completa con def.
  • En las geociencias, donde tratamos con conjuntos de datos grandes (por ejemplo, datos climáticos, mediciones sísmicas), las funciones lambda nos permiten procesar los datos con eficiencia.
  • Las funciones lambda en Pandas permiten transformar, filtrar o procesar estos datos con un mínimo de código.
  • Lea tutoriales adicionales en RealPython.
lambda x: x * 1.2
<function __main__.<lambda>(x)>

Esta función lambda multiplica cualquier entrada x por 1.2, lo que podría ser útil para tareas como convertir unidades (por ejemplo, de km a m).

Abajo hay un ejemplo de cómo usar lambda en un dataframe de pandas. Ejemplo 1: La función lambda x: (x - 32) * 5 / 9 convierte de Fahrenheit a Celsius cada valor de la columna Temperature_F.

import pandas as pd

# Sample DataFrame
data = {'Temperature_F': [32, 50, 77, 100]}
df = pd.DataFrame(data)

# Convert Fahrenheit to Celsius
df['Temperature_C'] = df['Temperature_F'].apply(lambda x: (x - 32) * 5 / 9)

print(df)
   Temperature_F  Temperature_C
0             32       0.000000
1             50      10.000000
2             77      25.000000
3            100      37.777778
# Now the equivalent in lambda is:
lambda_meters2kilometers = lambda x:x/1000
# x is the variable

Ahora aplique la función a toda la serie

# apply it to the entire series
lambda_meters2kilometers(quake.depth)
0 34.40 1 30.10 2 16.90 3 109.40 4 25.70 ... 1780 10.00 1781 105.00 1782 608.51 1783 10.00 1784 35.44 Name: depth, Length: 1785, dtype: float64

Las funciones lambda pueden tomar varias entradas

# you can add several variables into lambda functions
remove_anything = lambda x,y:x-y
remove_anything(3,2)
1

Esto no afectó los valores del DataFrame; compruébelo:

quake.depth
0 34400.0 1 30100.0 2 16900.0 3 109400.0 4 25700.0 ... 1780 10000.0 1781 105000.0 1782 608510.0 1783 10000.0 1784 35440.0 Name: depth, Length: 1785, dtype: float64

En su lugar, podría sobrescribir quake.depth=X. Pruebe los dos enfoques, ¡pero hágalo solo una vez! Puede usar las funciones de Python map (una función para aplicar funciones, genérica de Python) y apply (una función para aplicar funciones, específica de Pandas).

Pruebe map para aplicar una función lambda que reescale la profundidad de metros a kilómetros.

#type answer below
quake.depth=quake.depth.map(lambda x:x/1000)

Para discutir en clase: ¿qué le pasó al campo de profundidad?

¿Qué le pasó al data frame original?

Pruebe apply para aplicar una función lambda que reescale la profundidad de metros a kilómetros. Aquí solo mostramos el resultado sin asignarlo, porque la celda anterior ya convirtió la columna a kilómetros.

# or like this (display only, the column is already in km)
quake.depth.apply(lambda x:x/1000)
0 0.03440 1 0.03010 2 0.01690 3 0.10940 4 0.02570 ... 1780 0.01000 1781 0.10500 1782 0.60851 1783 0.01000 1784 0.03544 Name: depth, Length: 1785, dtype: float64

Grafique un histograma de las distribuciones de profundidad usando la función hist de matplotlib.

# answer here
plt.hist(quake.depth,100)
plt.grid(True)
plt.xlabel('Quake depth (km)')
plt.show()
<Figure size 640x480 with 1 Axes>

Puede usar el paquete de graficación interactiva Plotly. Primero mostraremos un histograma de la profundidad de los eventos usando la función histogram.

fig = px.histogram(quake,   #specify what dataframe to use
             x="depth",  #specify the variable for the histogram 
             nbins=50,       #number of bins for the histogram 
             height=400,     #dimensions of the figure
             width=600);
fig.show()
Loading...

Ejemplo 2: lógica condicional para la magnitud de los sismos

Puede usar funciones lambda para agregar lógica condicional, por ejemplo para clasificar las magnitudes de los sismos en categorías.

Escenario: tiene una columna Magnitude con las magnitudes de eventos sísmicos y quiere clasificar los eventos como “Minor”, “Moderate” o “Severe”.

# Classify earthquake magnitudes
quake['Category'] = quake['magnitude'].apply(lambda x: 'Minor' if x < 4.0 else ('Moderate' if x < 6.5 else 'Severe'))
quake.head()
Loading...

Ahora haremos una nueva gráfica con la ubicación de los sismos. Usaremos la herramienta Plotly.

El tamaño del marcador se escalará con la magnitud del sismo. Para eso, agregamos una serie marker_size al DataFrame

quake['marker_size'] = quake['magnitude'].apply(lambda x: np.trunc(np.exp(x))) # add marker size as exp(mag)
quake['magnitude_bin'] = quake['magnitude'].apply(lambda x: 0.5*np.trunc(2*x)) # bin magnitude in 0.5 steps
# another way to do it
quake['marker_size'] = np.trunc(np.exp(quake['magnitude'])) # add marker size as exp(mag)
quake['magnitude_bin'] = 0.5*np.trunc(2*quake['magnitude']) # bin magnitude in 0.5 steps

1.4 Manipulación intermedia con Pandas

También puede aplicar funciones lambda para trabajar con varias columnas, algo común en los conjuntos de datos geocientíficos, donde puede haber datos espaciales o temporales.

Ejemplo 3: calcular un índice a partir de varias mediciones Escenario: tiene datos de lluvia (Rainfall_mm) y de evaporación (Evaporation_mm), y quiere calcular el balance hídrico neto (Net Water Balance) de cada registro.

data = {'Rainfall_mm': [100, 80, 120], 'Evaporation_mm': [60, 70, 65]}
df = pd.DataFrame(data)

# Calculate Net Water Balance
df['Net_Water_Balance'] = df.apply(lambda row: row['Rainfall_mm'] - row['Evaporation_mm'], axis=1)

print(df)
   Rainfall_mm  Evaporation_mm  Net_Water_Balance
0          100              60                 40
1           80              70                 10
2          120              65                 55

En este ejemplo, lambda row: row['Rainfall_mm'] - row['Evaporation_mm'] calcula el balance hídrico neto restando la evaporación de la lluvia en cada registro.

1.5 Avanzado: manipulación de series de tiempo con funciones lambda

Quienes hacen geociencias trabajan con frecuencia con series de tiempo (por ejemplo, datos climáticos). Las funciones lambda pueden usarse para transformaciones eficientes dentro de las series de tiempo.

Ejemplo 4: aplicar un cálculo con ventana móvil Escenario: suponga que tiene datos diarios de temperatura y quiere calcular un promedio móvil de 3 días.

# Sample daily temperature data
data = {'Date': pd.date_range(start='2023-09-01', periods=10, freq='D'),
        'Temperature_C': [20, 22, 23, 21, 19, 24, 25, 26, 22, 20]}
df = pd.DataFrame(data)

# Calculate 3-day rolling average using lambda
df['7_day_avg'] = df['Temperature_C'].rolling(window=3).apply(lambda x: x.mean())
df.head()
Loading...

Aquí, lambda x: x.mean() calcula la media móvil sobre una ventana de 3 días para los datos de temperatura, algo esencial en el análisis climático para suavizar las fluctuaciones de corto plazo.

1.6 Función de agregación

agg es un método poderoso de Pandas que permite realizar múltiples operaciones sobre DataFrames y Series. Puede aplicar una o más funciones de agregación, como sum, mean, min, max, etc., sobre distintas columnas o grupos de datos.

# Sample DataFrame
data = {
    'A': [1, 2, 3, 4],
    'B': [5, 6, 7, 8]
}
df = pd.DataFrame(data)

# Apply aggregation functions
result = df['A'].agg(['sum', 'mean'])
print(result)
sum     10.0
mean     2.5
Name: A, dtype: float64
Agregar varias columnas con varias funciones

Puede pasarle a agg un diccionario donde las claves son nombres de columnas y los valores son las funciones a aplicar.

# Apply different functions to different columns
result = df.agg({'A': ['sum', 'mean'], 'B': ['min', 'max']})
print(result)
         A    B
sum   10.0  NaN
mean   2.5  NaN
min    NaN  5.0
max    NaN  8.0

En este ejemplo, la columna ‘A’ recibe sum y mean, mientras que la columna ‘B’ recibe min y max.

También puede usar funciones personalizadas

# Custom function to calculate range (max - min)
def data_range(x):
    return x.max() - x.min()

# Apply custom function
result = df.agg({'A': ['mean', data_range], 'B': data_range})
print(result)
              A    B
mean        2.5  NaN
data_range  3.0  3.0

2 Mapas con Plotly

Ahora graficaremos las ubicaciones de los sismos en un mapa usando el paquete Plotly. Hay más tutoriales en Plotly. Los argumentos de la función se explican solos y son típicos de una función de Python. La documentación de scatter_geo de Plotly enumera las variables.

fig = px.scatter_geo(quake,
                     lat='latitude',lon='longitude', 
                     range_color=(6,9),
                     height=600, width=600,
                     size='marker_size', color='magnitude',
                     hover_name="description",
                     hover_data=['description','magnitude','depth']);
fig.update_geos(resolution=110, showcountries=True)
fig.update_geos(resolution=110, showcountries=True,projection_type="orthographic")
fig
Loading...

La función lambda lambda x: 'Minor' if x < 4.0 else ('Moderate' if x < 6.5 else 'Severe') clasifica las magnitudes de los sismos según sus valores.

Los datos estaban ordenados por tiempo. Ahora queremos ordenarlos y mostrarlos por magnitud. Usamos la función sort de pandas para crear un nuevo DataFrame con los valores ordenados.

quakes2plot=quake.sort_values(by='magnitude_bin')
quakes2plot.head()
Loading...

Ahora graficaremos de nuevo con Plotly

fig = px.scatter_geo(quakes2plot,
                     lat='latitude',lon='longitude', 
                     range_color=(6,9),
                     height=600, width=600,
                     size='marker_size', color='magnitude',
                     hover_name="description",
                     hover_data=['description','magnitude','depth']);
fig.update_geos(resolution=110, showcountries=True)
# fig.update_geos(resolution=110, showcountries=True,projection_type="orthographic")
Loading...

3 Crear un Pandas desde un archivo de texto genérico.

El paquete de Python pandas es muy útil para leer archivos csv, pero también muchos archivos de texto que están más o menos formateados como una observación por fila y una columna por cada característica (feature).

Como ejemplo, veremos la lista de estaciones sísmicas de la red sísmica del norte de California, disponible aquí:

url = 'https://ncedc.org/ftp/pub/doc/NC.info/NC.channel.summary.day'
# this gets the file linked in the URL page and convert it to a string
s = requests.get(url).content
# this will convert the string, decode it , and make it a table
data = pd.read_csv(io.StringIO(s.decode('utf-8')), header=None, skiprows=2, sep=r'\s+', usecols=list(range(0, 13)))
# because columns/keys were not assigned, assign them now
data.columns = ['station', 'network', 'channel', 'location', 'rate', 'start_time', 'end_time', 'latitude', 'longitude', 'elevation', 'depth', 'dip', 'azimuth']

Veamos los datos. Ahora están almacenados en un dataframe de pandas.

data.head()
Loading...

Podemos mostrar el primer elemento del DataFrame:

data.iloc[0]
station AAR network NC channel EHZ location -- rate 0.0 start_time 1976/07/20,17:38:00 end_time 1977/12/01,22:37:00 latitude 39.27594 longitude -121.02696 elevation 911.0 depth 0.0 dip -90.0 azimuth 0.0 Name: 0, dtype: object
# display the type of each column
data.dtypes
station str network str channel str location str rate float64 start_time str end_time str latitude float64 longitude float64 elevation float64 depth float64 dip float64 azimuth float64 dtype: object
data.iloc[:, 0]
0 AAR 1 AAR 2 AAR 3 AAR 4 AAR ... 33375 WMP 33376 WMP 33377 WMP 33378 WSL 33379 WWVB Name: station, Length: 33380, dtype: str

Las columnas start_time y end_time están almacenadas como object (cadenas de texto), así que todavía no podemos hacer aritmética de fechas con ellas. Las convertimos con pd.to_datetime y una cadena de formato explícita.

Una advertencia sobre los asistentes de IA: un asistente de IA puede fallar en esta conversión si usted le da una instrucción (prompt) vaga, por ejemplo adivinando una cadena de formato incorrecta o descartando en silencio las fechas fuera de rango. Revise siempre dtypes (y algunos valores reales) después de cualquier código de interpretación escrito por una IA.

# convert start_time to datetime with an explicit format
data['start_time'] = pd.to_datetime(data['start_time'], format='%Y/%m/%d,%H:%M:%S')

Los tiempos de fin necesitan un paso adicional. El NCEDC marca los canales que siguen operando con la fecha de fin ficticia 3000/01/01. Esa no es una fecha de fin real. Pasamos errors='coerce' para que cualquier entrada no interpretable se convierta en NaT (not-a-time) en lugar de lanzar un error; en versiones antiguas de pandas, que almacenaban las marcas de tiempo con resolución de nanosegundos, el año 3000 quedaba fuera de rango y se convertía de esa manera. Pandas 3 interpreta las marcas de tiempo con resolución de microsegundos, así que el año 3000 ahora se interpreta sin problema, y fijamos el valor ficticio en NaT de manera explícita. Esto es deliberado y significativo: después de la conversión, un NaT en end_time identifica los canales que siguen en funcionamiento.

data['end_time'] = pd.to_datetime(data['end_time'], format='%Y/%m/%d,%H:%M:%S', errors='coerce')
# the year-3000 placeholder is not a real end date: mark those channels as still operating
data.loc[data['end_time'] == pd.Timestamp('3000-01-01'), 'end_time'] = pd.NaT
data.head()
Loading...
# check the conversion worked
print(data.dtypes)
print('still-operating channels (NaT end_time):', data['end_time'].isna().sum())
station                  str
network                  str
channel                  str
location                 str
rate                 float64
start_time    datetime64[us]
end_time      datetime64[us]
latitude             float64
longitude            float64
elevation            float64
depth                float64
dip                  float64
azimuth              float64
dtype: object
still-operating channels (NaT end_time): 5619

Use Plotly para mapear las estaciones. Descartamos las filas sin coordenadas y conservamos los tiempos de fin NaT, porque llevan información.

data = data.dropna(subset=['latitude', 'longitude'])
data = data[data.longitude != 0]
fig = px.scatter_geo(data,
                     lat='latitude',lon='longitude', 
                     range_color=(6,9),
                     height=600, width=600,
                     hover_name="station",
                     hover_data=['network','station','channel','rate']);
fig.update_geos(resolution=110, showcountries=True)
Loading...
fig = px.scatter_map(data,
                     lat='latitude', lon='longitude',
                     range_color=(6,9), map_style="carto-positron",
                     height=600, width=500,
                     hover_name="station",
                     hover_data=['network','station','channel','rate']);
fig.update_layout(title="Northern California Seismic Network")
fig.show()
Loading...

4 Ejercicio

Ahora practicaremos la manipulación de pandas. Este ejercicio descarga metadatos de estaciones desde una URL y se espera que el estudiantado practique tareas específicas.

Descargue los datos desde la URL del NCEDC

url = 'https://ncedc.org/ftp/pub/doc/NC.info/NC.channel.summary.day'

Los nombres de las columnas aparecen al inicio del archivo de texto; haga una lista de cadenas con esos nombres, en orden, para renombrar las columnas una vez creado el dataframe

# students answer here
# students answer here
# request the data from the URL and use the IO package
# assign the column names
data.columns = ['station', 'network', 'channel', 'location', 'rate', 'start_time', 'end_time', 'latitude', 'longitude', 'elevation', 'depth', 'dip', 'azimuth']

Encuentre la fila del canal KCPB

# find the row of station KCPB

Ahora seleccione dos estaciones de su elección usando |.

# answer below

Ahora seleccione una estación dada y un código de canal específico; el ejemplo es KCPB con el código de canal HNZ.

# Select two stations, use the typical "AND" 

También puede elegir la función de pandas isin para seleccionar las filas cuyo atributo pertenece a una lista. Por ejemplo, use isin para seleccionar todas las filas cuya clave station está dentro de la lista ['KCPB','KHBB'].

#  students answer here

P Use funciones nativas de pandas para calcular cuántos sitios únicos (nombres de estación) hay en la red.

# students answer here

P Use funciones nativas de pandas para seleccionar el conjunto único de códigos de canal que terminan en Z: esto le dirá cuántos tipos de datos digitalizados maneja la red sísmica.

# students answer here

P ¿Qué nombre de estación tiene el mayor número de canales? pista: puede usar value_counts().

# students answer here

P ¿Cuál es la diferencia máxima de elevación entre las estaciones, usando funciones lambda?

# students answer here

Aquí, pandas no reconoce las columnas start_time y end_time como formato datetime, así que no podemos usar operaciones de fechas con ellas. Primero debemos convertir estas columnas a un formato datetime:

# answer here
# Transform column from string into datetime format
# do the same for end times

Ahora podemos ver cuándo se instaló cada estación sísmica usando groupby y ordenando por el despliegue más temprano (es decir, el mínimo de start_time)

# students answer here

Seleccione las estaciones que se desplegaron primero y se recuperaron al final usando agg y funciones lambda

# answer here

5 CSV frente a Parquet

Parquet es un formato de datos comprimido que almacena y comprime las columnas. Es rápido en E/S y compacto.

Guarde data en un archivo CSV:

%timeit data.to_csv("data/my_metadata.csv")
!ls -lh data/my_metadata.csv
135 ms ± 4.59 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
-rw-r--r--@ 1 marinedenolle  staff   3.2M Aug 12 09:28 data/my_metadata.csv

Intente guardar en Parquet y compare el tiempo y la memoria.

%timeit data.to_parquet("data/my_metadata.pq")
!ls -lh data/my_metadata.pq
11.1 ms ± 253 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
-rw-r--r--@ 1 marinedenolle  staff   338K Aug 12 09:28 data/my_metadata.pq