Necesita un método simple, atractivo y escalable para presentar datos geoespaciales, a menudo recurren a un mapa de calor. Esta visualización 2D divide un mapa en celdas de cuadrícula de igual tamaño y usa color para representar la magnitud de los valores de datos agregados dentro de las celdas.
La superposición de mapas de calor en mapas geográficos permite una visualización rápida de fenómenos espaciales. Los patrones como grupos, puntos calientes, valores atípicos o gradientes se vuelven inmediatamente obvios. Este formato puede ser útil para los tomadores de decisiones y el público, que podrían no estar bien versados en resultados estadísticos sin procesar.
Los mapas de calor pueden estar compuestos de células cuadradas (llamadas basado en la cuadrícula o matriz mapas de calor) o valores “continuos” contorneados suavemente (llamados espacial o densidad del núcleo mapas de calor). Los siguientes mapas muestran la densidad del tornado a partir de ubicaciones que utilizan ambos enfoques.
Si entrecerras los ojos mientras mira el mapa superior, debería ver tendencias similares a las del mapa inferior.
Prefiero mapas de calor a base de cuadrícula porque los límites afilados y distintos hacen que sea más fácil comparar los valores de las células adyacentes, y los valores atípicos no se “suavizan”. También tengo un punto débil para su apariencia pixelada, ya que mis primeros videojuegos fueron Pong y Wolfenstein 3D.
Además, los mapas de calor de densidad del núcleo pueden ser computacionalmente costosos y sensibles a los parámetros de entrada. Su apariencia depende en gran medida de la función de núcleo elegida y su ancho de banda o radio. Una mala elección de parametrización puede ser más suave o bajo suave los datos, oscureciendo los patrones.
En esto Ciencia de datos de éxito rápido Proyecto, usaremos Python para hacer mapas de calor estáticos a base de cuadrícula para la actividad de tornados en los Estados Unidos continentales.
El conjunto de datos
Para este tutorial, utilizaremos datos de tornado de la Administración Nacional Oceánica y Atmosférica maravilloso dominio público base de datos. Al extenderse a 1950, cubre ubicaciones de inicio y finalización de tornados, magnitudes, lesiones, muertes, costos financieros y más.
Se puede acceder a los datos a través de NOAA’s Centro de predicción de tormentas. El conjunto de datos de formato CSV, resaltado amarillo en la siguiente figura, cubre el período 1950 a 2023. No se moleste en descargarlo. Por conveniencia, he proporcionado un enlace en el código para acceder a él mediante programación.
Este archivo CSV contiene 29 columnas para casi 69,000 tornados. Puede encontrar una clave para los nombres de la columna aquí. Trabajaremos principalmente con los lugares iniciales del tornado (Slat, Slon), el año (año) y las magnitudes de la tormenta (MAG).
Instalación de bibliotecas
Querrás instalar Numpy, Mate, pandasy Geopandas. Los enlaces anteriores proporcionan instrucciones de instalación. También usaremos la biblioteca de bienapely, que forma parte de la instalación de Geopandas.
Como referencia, este proyecto utilizó las siguientes versiones:
Python 3.10.18
Numpy 2.2.5
matplotlib 3.10.0
Pandas 2.2.3
Geopandas 1.0.1
bien formado 2.0.6
El código simplificado
No se necesita mucho código para superponer un mapa de calor en un mapa geográfico. El siguiente código ilustra el proceso básico, aunque gran parte de él tiene propósitos auxiliares, como limitar los datos de EE. UU. A los 48 estados inferiores y mejorar el aspecto de la barra de color.
En la siguiente sección, refactorizaremos y ampliaremos este código para realizar un filtrado adicional, emplear más constantes de configuración para obtener actualizaciones fáciles y usar funciones auxiliares para mayor claridad.
# --- Plotting ---
print("Plotting results...")
fig, ax = plt.subplots(figsize=FIGURE_SIZE)
fig.subplots_adjust(top=0.85) # Make room for titles.
# Plot state boundaries and heatmap:
clipped_states.plot(ax=ax, color='none',
edgecolor='white', linewidth=1)
vmax = np.max(heatmap)
img = ax.imshow(heatmap.T,
extent=[x_edges[0], x_edges[-1],
y_edges[0], y_edges[-1]],
origin='lower',
cmap=cmap, norm=norm,
alpha=1.0, vmin=0, vmax=vmax)
# Annotations:
plt.text(TITLE_X, TITLE_Y, 'Tornado Density Heatmap',
fontsize=22, fontweight='bold', ha='left')
plt.text(x=SUBTITLE_X, y=SUBTITLE_Y, s=(
f'{START_YEAR}–{END_YEAR} EF Magnitude 3–5 '
f'{GRID_SIZE_MILES}×{GRID_SIZE_MILES} mile cells'),
fontsize=15, ha='left')
plt.text(x=SOURCE_X, y=SOURCE_Y,
s='Data Source: NOAA Storm Prediction Center',
color='white', fontsize=11,
fontstyle='italic', ha='left')
# Clean up the axes:
ax.set_xlabel('')
ax.set_ylabel('')
ax.axis('off')
# Post the Colorbar:
ticks = np.linspace(0, vmax, 6, dtype=int)
cbar = plt.colorbar(img, ax=ax, shrink=0.6, ticks=ticks)
cbar.set_label('\nTornado Count per Grid Cell', fontsize=15)
cbar.ax.set_yticklabels(list(map(str, ticks)))
print(f"Saving plot as '{SAVE_FILENAME}'...")
plt.savefig(SAVE_FILENAME, bbox_inches='tight', dpi=SAVE_DPI)
print("Plot saved.\n")
plt.show()
Aquí está el resultado:
El código expandido
El siguiente código de Python fue escrito en Jupyterlab y se describe por Cell.
Importación de bibliotecas / asignación de constantes
Después de importar las bibliotecas necesarias, definimos una serie de constantes de configuración que nos permiten ajustar fácilmente los criterios de filtro, los límites de mapa, las dimensiones de la gráfica y más. Para este análisis, nos centramos en los tornados dentro de los Estados Unidos contiguos, filtrando estados y territorios fuera de esta área y seleccionando solo eventos significativos (los calificados de EF3 a EF5 en el Escala Fujita mejorada) del conjunto de datos completo que abarca 1950 hasta 2023. Los resultados se agregan a celdas de cuadrícula de 50 × 50 millas.
import matplotlib.pyplot as plt
import pandas as pd
import geopandas as gpd
import numpy as np
from shapely.geometry import box
# --- Configuration Constants ---
# Data URLs:
TORNADO_DATA_URL = 'https://bit.ly/40xJCMK'
STATES_DATA_URL = ("https://www2.census.gov/geo/tiger/TIGER2020/STATE/"
"tl_2020_us_state.zip")
# Geographic Filtering:
EXCLUDED_STATES_ABBR = ['AK', 'HI', 'PR', 'VI']
TORNADO_MAGNITUDE_FILTER = [3, 4, 5]
# Year Filtering (inclusive):
START_YEAR = 1950
END_YEAR = 2023
# Coordinate Reference Systems (CRS):
CRS_LAT_LON = "EPSG:4326" # WGS 84 geographic CRS (lat/lon)
CRS_ALBERS_EQUAL_AREA = "EPSG:5070" # NAD83/Conus Albers (projected CRS in m)
# Box for Contiguous US (CONUS) in Albers Equal Area (EPSG:5070 meters):
CONUS_BOUNDS_MIN_X = -2500000
CONUS_BOUNDS_MIN_Y = 100000
CONUS_BOUNDS_MAX_X = 2500000
CONUS_BOUNDS_MAX_Y = 3200000
# Grid Parameters for Heatmap (50x50 mile cells):
GRID_SIZE_MILES = 50
HEATMAP_GRID_SIZE = 80500 # ~50 miles in meters.
# Plotting Configuration:
FIGURE_SIZE = (15, 12)
SAVE_DPI = 600
SAVE_FILENAME = 'tornado_heatmap.png'
# Annotation positions (in EPSG:5070 meters, relative to plot extent):
TITLE_X = CONUS_BOUNDS_MIN_X
TITLE_Y = CONUS_BOUNDS_MAX_Y + 250000 # Offset above max Y
SUBTITLE_X = CONUS_BOUNDS_MIN_X
SUBTITLE_Y = CONUS_BOUNDS_MAX_Y + 100000 # Offset above max Y
SOURCE_X = CONUS_BOUNDS_MIN_X + 50000 # Slightly indented from min X
SOURCE_Y = CONUS_BOUNDS_MIN_Y + 20000 # Slightly above min Y
Definición de funciones auxiliares
La siguiente celda define dos funciones auxiliares para cargar y filtrar el conjunto de datos y para cargar y filtrar los límites de estado. Tenga en cuenta que llamamos a las constantes de configuración anteriores durante el proceso.
# --- Helper Functions ---
def load_and_filter_tornado_data():
"""Load data, apply filters, and create a GeoDataFrame."""
print("Loading and filtering tornado data...")
df_raw = pd.read_csv(TORNADO_DATA_URL)
# Combine filtering steps into one chained operation:
mask = (~df_raw['st'].isin(EXCLUDED_STATES_ABBR) &
df_raw['mag'].isin(TORNADO_MAGNITUDE_FILTER) &
(df_raw['yr'] >= START_YEAR) & (df_raw['yr'] <= END_YEAR))
df = df_raw[mask].copy()
# Create and project a GeoDataFrame:
geometry = gpd.points_from_xy(df['slon'], df['slat'],
crs=CRS_LAT_LON)
temp_gdf = gpd.GeoDataFrame(df, geometry=geometry).to_crs(
CRS_ALBERS_EQUAL_AREA)
return temp_gdf
def load_and_filter_states():
"""Load US state boundaries and filter for CONUS."""
print("Loading state boundary data...")
states_temp_gdf = gpd.read_file(STATES_DATA_URL)
states_temp_gdf = (states_temp_gdf[~states_temp_gdf['STUSPS']
.isin(EXCLUDED_STATES_ABBR)].copy())
states_temp_gdf = states_temp_gdf.to_crs(CRS_ALBERS_EQUAL_AREA)
return states_temp_gdf
Tenga en cuenta que el Tilde (~) frente a la máscara y states_temp_gdf Las expresiones invierten los resultados, por lo que seleccionamos filas donde está la abreviatura del estado no en la lista excluida.
Ejecutar funciones auxiliar y generar el mapa de calor
La siguiente celda llama a las funciones de Helper para cargar y filtrar el conjunto de datos y luego recortar los datos a los límites del mapa, generar el mapa de calor (con el Numpy histogram2d() método), y defina un color de color continuo para el mapa de calor. Tenga en cuenta que nuevamente llamamos a las constantes de configuración anteriores durante el proceso.
# --- Data Loading and Preprocessing ---
gdf = load_and_filter_tornado_data()
states_gdf = load_and_filter_states()
# Create bounding box and clip state boundaries & tornado points:
conus_bounds_box = box(CONUS_BOUNDS_MIN_X, CONUS_BOUNDS_MIN_Y,
CONUS_BOUNDS_MAX_X, CONUS_BOUNDS_MAX_Y)
clipped_states = gpd.clip(states_gdf, conus_bounds_box)
gdf = gdf[gdf.geometry.within(conus_bounds_box)].copy()
# --- Heatmap Generation ---
print("Generating heatmap bins...")
x_bins = np.arange(CONUS_BOUNDS_MIN_X, CONUS_BOUNDS_MAX_X +
HEATMAP_GRID_SIZE, HEATMAP_GRID_SIZE)
y_bins = np.arange(CONUS_BOUNDS_MIN_Y, CONUS_BOUNDS_MAX_Y +
HEATMAP_GRID_SIZE, HEATMAP_GRID_SIZE)
print("Computing 2D histogram...")
heatmap, x_edges, y_edges = np.histogram2d(gdf.geometry.x,
gdf.geometry.y,
bins=[x_bins, y_bins])
# Define continuous colormap (e.g., 'hot', 'viridis', 'plasma', etc.):
print("Defining continuous colormap...")
cmap = plt.cm.hot
norm = None
print("Finished execution.")
Porque este proceso puede tomar unos segundos, el print() La función nos mantiene al día con el progreso del programa:
Loading and filtering tornado data...
Loading state boundary data...
Generating heatmap bins...
Computing 2D histogram...
Defining continuous colormap...
Finished execution.
Trazar los resultados
La celda final genera y guarda la trama. Matlotlib imshow() El método es responsable de trazar el mapa de calor. Para más imshow()ver esto artículo.
# --- Plotting ---
print("Plotting results...")
fig, ax = plt.subplots(figsize=FIGURE_SIZE)
fig.subplots_adjust(top=0.85) # Make room for titles.
# Plot state boundaries and heatmap:
clipped_states.plot(ax=ax, color='none',
edgecolor='white', linewidth=1)
vmax = np.max(heatmap)
img = ax.imshow(heatmap.T,
extent=[x_edges[0], x_edges[-1],
y_edges[0], y_edges[-1]],
origin='lower',
cmap=cmap, norm=norm,
alpha=1.0, vmin=0, vmax=vmax)
# Annotations:
plt.text(TITLE_X, TITLE_Y, 'Tornado Density Heatmap',
fontsize=22, fontweight='bold', ha='left')
plt.text(x=SUBTITLE_X, y=SUBTITLE_Y, s=(
f'{START_YEAR}–{END_YEAR} EF Magnitude 3–5 '
f'{GRID_SIZE_MILES}×{GRID_SIZE_MILES} mile cells'),
fontsize=15, ha='left')
plt.text(x=SOURCE_X, y=SOURCE_Y,
s='Data Source: NOAA Storm Prediction Center',
color='white', fontsize=11,
fontstyle='italic', ha='left')
# Clean up the axes:
ax.set_xlabel('')
ax.set_ylabel('')
ax.axis('off')
# Post the Colorbar:
ticks = np.linspace(0, vmax, 6, dtype=int)
cbar = plt.colorbar(img, ax=ax, shrink=0.6, ticks=ticks)
cbar.set_label('\nTornado Count per Grid Cell', fontsize=15)
cbar.ax.set_yticklabels(list(map(str, ticks)))
print(f"Saving plot as '{SAVE_FILENAME}'...")
plt.savefig(SAVE_FILENAME, bbox_inches='tight', dpi=SAVE_DPI)
print("Plot saved.\n")
plt.show()
Esto produce el siguiente mapa:
Este es un mapa hermoso y más informativo que una alternativa KDE suave.
Agregar ubicaciones finales de tornado
Nuestros mapas de calor de densidad de tornado se basan en el a partir de Ubicaciones de tornados. Pero los tornados se mueven después de tocar hacia abajo. La pista de tornado promedio tiene aproximadamente 3 millas de largo, pero cuando miras tormentas más fuertes, los números aumentan. Los tornados EF -3 promedian 18 millas, y los tornados EF -4 promedian 27 millas. Sin embargo, los tornados de vía larga son raros que comprenden Menos del 2% de todos los tornados.
Debido a que la longitud promedio de la pista del tornado es menor que la dimensión de 50 millas de nuestras células de la cuadrícula, podemos esperar que cubran solo una o dos células en general. Por lo tanto, incluida la ubicación final del tornado debería ayudarnos a mejorar nuestra medición de densidad.
Para hacer esto, tendremos que combinar las ubicaciones de LAT-Lon iniciales y finales filtrar puntos finales que comparten el misma celda como sus puntos de partida correspondientes. De lo contrario, “doble” al contar. El código para esto es un poco largo, así que lo he almacenado en esto Esencia.
Aquí hay una comparación del mapa “Comenzar solo” con las ubicaciones combinadas de inicio y finalización:
Los vientos predominantes conducen a la mayoría de los tornados hacia el este y el noreste. Puedes ver el impacto en estados como Missouri, Mississippi, Alabama y Tennessee. Estas áreas tienen células más brillantes en el mapa inferior debido a los tornados que comienzan en una celda y terminan en la celda del este adyacente. Tenga en cuenta también que el número máximo de tornados en una celda dada ha aumentado de 29 (en el mapa superior) a 33 (en el mapa inferior).
Resumen
Utilizamos Python, Pandas, Geopandas y Matplotlib para proyectar y superponer mapas de calor en mapas geográficos. Los mapas de calor geoespaciales son una forma muy efectiva de visualizar las tendencias regionales, patrones, puntos calientes y valores atípicos en los datos estadísticos.
Si disfruta de este tipo de proyectos, asegúrese de consultar mi libro, Real World Python: una guía de hackers para resolver problemas con el códigodisponible en librerías y en línea.