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:
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, iframe1 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()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()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/m2kmDefinamos 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()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 variableAhora 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: float64Las 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)1Esto no afectó los valores del DataFrame; compruébelo:
quake.depth0 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: float64En 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: float64Grafique 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()
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()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()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 steps1.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()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")
figLa 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()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")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()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.dtypesstation 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: objectdata.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: strLas 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.NaTdata.head()# 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)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()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 KCPBAhora seleccione dos estaciones de su elección usando |.
# answer belowAhora 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 hereP Use funciones nativas de pandas para calcular cuántos sitios únicos (nombres de estación) hay en la red.
# students answer hereP 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 hereP ¿Qué nombre de estación tiene el mayor número de canales? pista: puede usar value_counts().
# students answer hereP ¿Cuál es la diferencia máxima de elevación entre las estaciones, usando funciones lambda?
# students answer hereAquí, 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 timesAhora 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 hereSeleccione las estaciones que se desplegaron primero y se recuperaron al final usando agg y funciones lambda
# answer here5 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.csv135 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.pq11.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