En el marco del Festival LATAM de Medios Digitales y Periodismo, por Factual y Distintas Latitudes.
26 y 27 de Octubre del 2023
Desplazamiento climático: La migración que no vemos.
Un trabajo de N+ Focus.
Me llamo Isaac Arroyo, y aquí unos puntos rápidos sobre mi:
Narra las historias de las personas que han tenido que dejar su hogar, su estado o el país por las consecuencias de la emergencia climática.
Imagen tomada del trabajo interactivo “Desplazamiento climático: La migración que no vemos”
Imagen tomada del trabajo interactivo “Desplazamiento climático: La migración que no vemos”
Imagen tomada del trabajo interactivo “Desplazamiento climático: La migración que no vemos”
Imagen tomada del trabajo interactivo “Desplazamiento climático: La migración que no vemos”
En el reportaje interactivo puedes jugar con las visualizaciones y los mapas1
Parte fundamental del trabajo era encontrar regiones que hayan sido afectadas por la crisis climática… y la verdad, todo el mundo esta siendo afectado por la crisis climática, unos más que otros.
Además, platicando con expertos en el tema, recomendaron tener datos que tengan como mínimo 30 años de antigüedad, esto para poder observar y calcular las anomalías1
Google Earth Engine es una plataforma que facilita1 el acceso a datos raster e imágenes satelitales.
Dentro de su catálogo de datos se encuentra TerraClimate, un conjunto de datos de reconstrucciones2 del clima
Agarrar las imágenes y llevar los valores a una tabla tomó muchos intentos, mucha lectura de documentación, y también mucho café, pero al final se logró.
import ee
import geemap
ee.Initialize()
print("Earth Engine inicializado")
print("Empieza a correr el código...")
# = = = = Valores variables = = = = = =
banda_interes = "pdsi"
type_of_geometry = "ent"
type_reducer_time = "year"
temp_zscore = False
# - - - - - - - - - - - - - - - - - - - - -
# banda_interes
dict_functions_scalling_var_int = {
"pdsi": {
"scalling_func" : lambda img: img.multiply(0.01).copyProperties(img, img.propertyNames()),
"var_int_func": lambda img: img.copyProperties(img, img.propertyNames()),
},
"pr": {
"scalling_func": lambda img: img.copyProperties(img, img.propertyNames()),
"var_int_func": lambda img: img.subtract(mean_base_months).divide(mean_base_months).copyProperties(img, img.propertyNames())
},
"tmmx": {
"scalling_func": lambda img: img.multiply(0.1).copyProperties(img, img.propertyNames()),
"var_int_func": lambda img: img.subtract(mean_base_months).copyProperties(img, img.propertyNames()),
"var_int_func_zscore": lambda img: img.subtract(mean_base_months).divide(std_base_months).copyProperties(img, img.propertyNames())
},
"tmmn": {
"scalling_func": lambda img: img.multiply(0.1).copyProperties(img, img.propertyNames()),
"var_int_func": lambda img: img.subtract(mean_base_months).copyProperties(img, img.propertyNames()),
"var_int_func_zscore": lambda img: img.subtract(mean_base_months).divide(std_base_months).copyProperties(img, img.propertyNames())
}
}
# type_of_geometry
if type_of_geometry == "mun":
# Municipios
fc = ee.FeatureCollection("projects/ee-unisaacarroyov/assets/GEOM-MX/MX_MUN_2022")
elif type_of_geometry == "ent":
# Estados
fc = ee.FeatureCollection("projects/ee-unisaacarroyov/assets/GEOM-MX/MX_ENT_2022")
elif type_of_geometry == "nac":
# Nación
fc = ee.FeatureCollection("USDOS/LSIB/2017").filter(ee.Filter.eq("COUNTRY_NA","Mexico"))
# type_reducer_time
dict_reducers = {
"month": lambda number: final_data_img_coll
.filter(ee.Filter.eq("date_year", number))\
.first()\
.reduceRegions(reducer = ee.Reducer.mean(), collection = fc, scale = scale_img_coll)\
.map(lambda feature: ee.Feature(feature).set("date_year", number).setGeometry(None)),
"year": lambda number: final_data_img_coll
.filter(ee.Filter.eq("date_year", number))\
.first()\
.reduce(ee.Reducer.mean())\
.reduceRegions(reducer = ee.Reducer.mean(), collection = fc, scale = scale_img_coll)\
.map(lambda feature: ee.Feature(feature).set("date_year", number).setGeometry(None))
}
# = = = = Valores constantes = = = = =
img_coll = ee.ImageCollection("IDAHO_EPSCOR/TERRACLIMATE")
geom_mex = ee.FeatureCollection("USDOS/LSIB/2017").filter(ee.Filter.eq("COUNTRY_NA","Mexico")).first().geometry()
scale_img_coll = 4638.3
start_date_base = "1960-01-01"
end_date_base = "1989-12-31"
img_coll_start_year = 1960
img_coll_end_year = 2022
n_max_features = 3000
# - - Valor "constante" - - - - -
str_folder = "name_of_folder"
# = = = = Funciones escenciales = = = = = =
def tag_month_year(img):
full_date = ee.Date(ee.Number(img.get("system:time_start")))
date_year = ee.Number(full_date.get("year"))
date_month = ee.Number(full_date.get("month"))
return img.set({"date_month": date_month, "date_year": date_year})
def func_base_mean(element):
return data_image_coll_tag_year_month.filterDate(start_date_base, end_date_base).filter(ee.Filter.eq("date_month", element)).mean().set({"date_month": element})
def func_base_std(element):
return data_image_coll_tag_year_month.filterDate(start_date_base, end_date_base).filter(ee.Filter.eq("date_month", element)).reduce(ee.Reducer.stdDev()).set({"date_month": element})
# = = = = INICIO DE CÓDIGO = = = = = = = =
data_image_coll_tag_year_month = img_coll.select(banda_interes).filter(ee.Filter.bounds(geom_mex)).map(dict_functions_scalling_var_int[banda_interes]["scalling_func"]).map(tag_month_year)
# Media historica
list_img_to_img_coll_base_mean_months = ee.List.sequence(1,12,1).map(func_base_mean)
mean_base_months = ee.ImageCollection.fromImages(list_img_to_img_coll_base_mean_months).toBands().rename(["01","02","03","04","05","06","07","08","09","10","11","12"])
# std historica
list_img_to_img_coll_base_std_months = ee.List.sequence(1,12,1).map(func_base_std)
std_base_months = ee.ImageCollection.fromImages(list_img_to_img_coll_base_std_months).toBands().rename(["01","02","03","04","05","06","07","08","09","10","11","12"])
list_new_collection_by_year = ee.List.sequence(img_coll_start_year, img_coll_end_year).map(lambda element: data_image_coll_tag_year_month.filter(ee.Filter.eq("date_year", element)).toBands().set({"date_year": element}).rename(["01","02","03","04","05","06","07","08","09","10","11","12"]))
if temp_zscore == False:
final_data_img_coll = ee.ImageCollection.fromImages(list_new_collection_by_year).map(dict_functions_scalling_var_int[banda_interes]["var_int_func"])
elif temp_zscore == True:
final_data_img_coll = ee.ImageCollection.fromImages(list_new_collection_by_year).map(dict_functions_scalling_var_int[banda_interes]["var_int_func_zscore"])
list_fc_from_img_coll = ee.List.sequence(img_coll_start_year, img_coll_end_year).map(dict_reducers[type_reducer_time])
list_features_from_img_coll = list_fc_from_img_coll.map(lambda fc: ee.FeatureCollection(fc).toList(n_max_features)).flatten()
fc_final = ee.FeatureCollection(list_features_from_img_coll)
# = = = = Exportar como CSV (a una carpeta de Google Drive) = = = = =
if temp_zscore == False:
str_var_interes = lambda banda: f"anomaly_{banda}_" if banda in ["pr","tmmx","tmmn"] else f"{banda}_"
elif temp_zscore == True:
str_var_interes = lambda banda: f"zscore_{banda}_" if banda in ["tmmx","tmmn"] else f"eliminar-error_"
str_description = "export_" + str_var_interes(banda_interes) + "mean_" + f"{type_of_geometry}_" + f"{type_reducer_time}_" + "terraclimate"
str_fileNamePrefix = "ts_" + str_var_interes(banda_interes) + "mean_" + f"{type_of_geometry}_" + f"{type_reducer_time}_" + "terraclimate"
geemap.ee_export_vector_to_drive(
collection = fc_final,
description= str_description,
fileNamePrefix = str_fileNamePrefix,
fileFormat = "CSV",
folder = str_folder)
import numpy as np
import pandas as pd
import geopandas
import os
import re
import functools
# - - Start Funciones
def etiquetar_categoria_pdsi(valor):
if valor <= -4:
return "Sequía extrema"
elif -4 < valor <= -3:
return "Sequía severa"
elif -3 < valor <= -2:
return "Sequía moderada"
elif -2 < valor <= -1:
return "Sequía media"
elif -1 < valor <= -0.5:
return "Sequía incipiente"
elif -0.5 < valor <= 0.5:
return "Condiciones normales"
elif 0.5 < valor <= 1:
return "Humedad incipiente"
elif 1 < valor <= 2:
return "Poca humedad"
elif 2 < valor <= 3:
return "Humedad moderada"
elif 3 < valor <= 4:
return "Muy húmedo"
elif valor > 4:
return "Extremadamente húmedo"
else:
return np.nan
# - - End Funciones
current_directory = os.getcwd()
path2imports = current_directory + "/datos/ee_terraclimate_imports/"
list_all_csvs_with_zscore = os.listdir(path2imports)
# Ignorar Z-score de temperaturas ya que no formaran de la base de datos
list_all_csvs = [csv_name for csv_name in list_all_csvs_with_zscore if "_zscore_" not in csv_name]
list_all_csvs.sort()
# = = Valores anuales nacional + entidades + municipios = = #
# - - Nacional - - #
list_csvs_nac_year = [csv_name for csv_name in list_all_csvs if "_nac_year_" in csv_name]
list_df_nac_year = list()
for csv_file in list_csvs_nac_year:
df_temporal = pd.read_csv(path2imports + csv_file)
df_temporal["cve_geo"] = "00"
df_temporal["cve_ent"] = "00"
df_temporal["date_year"] = df_temporal["date_year"].astype(int)
df_temporal = df_temporal[["cve_geo", "cve_ent", "date_year", "mean"]]
df_temporal.columns = ["cve_geo", "cve_ent", "date_year", re.search("ts_(.*)_nac_year", csv_file).group(1)]
list_df_nac_year.append(df_temporal)
df_nac_year = functools.reduce(lambda df1,df2 : pd.merge(df1, df2, on = ["cve_geo","cve_ent","date_year"]), list_df_nac_year)
# - - Entidades - - #
list_csvs_ent_year = [csv_name for csv_name in list_all_csvs if "_ent_year_" in csv_name]
list_df_ent_year = list()
for csv_file in list_csvs_ent_year:
df_temporal = pd.read_csv(path2imports + csv_file)
df_temporal["cve_geo"] = df_temporal["CVEGEO"].apply(lambda x: f"0{str(x)}" if x <= 9 else str(x))
df_temporal["cve_ent"] = df_temporal["CVEGEO"].apply(lambda x: f"0{str(x)}" if x <= 9 else str(x))
df_temporal["date_year"] = df_temporal["date_year"].astype(int)
df_temporal = df_temporal[["cve_geo", "cve_ent", "date_year", "mean"]]
df_temporal.columns = ["cve_geo", "cve_ent", "date_year", re.search("ts_(.*)_ent_year", csv_file).group(1)]
list_df_ent_year.append(df_temporal)
df_ent_year = functools.reduce(lambda df1,df2 : pd.merge(df1, df2, on = ["cve_geo","cve_ent","date_year"]), list_df_ent_year)
# - - Municipios - - #
list_csvs_mun_year = [csv_name for csv_name in list_all_csvs if "_mun_year_" in csv_name]
list_df_mun_year = list()
for csv_file in list_csvs_mun_year:
df_temporal = pd.read_csv(path2imports + csv_file)
df_temporal["cve_geo"] = df_temporal["CVEGEO"].apply(lambda x: f"0{str(x)}" if x <= 10_000 else str(x))
df_temporal["cve_ent"] = df_temporal["CVE_ENT"].apply(lambda x: f"0{str(x)}" if x <= 9 else str(x))
df_temporal["date_year"] = df_temporal["date_year"].astype(int)
df_temporal = df_temporal[["cve_geo", "cve_ent", "date_year", "mean"]]
df_temporal.columns = ["cve_geo", "cve_ent", "date_year", re.search("ts_(.*)_mun_year", csv_file).group(1)]
list_df_mun_year.append(df_temporal)
df_mun_year = functools.reduce(lambda df1,df2 : pd.merge(df1, df2, on = ["cve_geo","cve_ent","date_year"]), list_df_mun_year)
# - - Nacional + Entidades + Municipios - - #
df_nac_ent_mun_year = pd.concat([df_nac_year, df_ent_year, df_mun_year]).reset_index(drop=True)
df_nac_ent_mun_year["date_year"] = df_nac_ent_mun_year["date_year"].astype(str) + "-06-30"
# = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o
# = = Valores mensuales nacional + entidades = = #
list_months_num = list(map(lambda x: f"0{x}" if x <= 9 else str(x), range(1,13)))
# - - Nacional - - #
list_csvs_nac_month = [csv_name for csv_name in list_all_csvs if "_nac_month_" in csv_name]
list_df_nac_month = list()
for csv_file in list_csvs_nac_month:
value_name = re.search("ts_(.*)_nac_month_", csv_file).group(1)
df_temporal = pd.read_csv(path2imports + csv_file)
df_temporal = df_temporal.melt(id_vars=["date_year"], value_vars= list_months_num, var_name = "date_month", value_name = value_name)
df_temporal["cve_geo"] = "00"
df_temporal["cve_ent"] = "00"
df_temporal["date_year"] = df_temporal["date_year"].astype(int)
df_temporal = df_temporal[["cve_geo", "cve_ent", "date_year", "date_month", value_name]]
list_df_nac_month.append(df_temporal)
df_nac_month = functools.reduce(lambda df1,df2 : pd.merge(df1, df2, on = ["cve_geo","cve_ent","date_year","date_month"]), list_df_nac_month)
# - - Entidades - - #
list_csvs_ent_month = [csv_name for csv_name in list_all_csvs if "_ent_month_" in csv_name]
list_df_ent_month = list()
for csv_file in list_csvs_ent_month:
value_name = re.search("ts_(.*)_ent_month_", csv_file).group(1)
df_temporal = pd.read_csv(path2imports + csv_file)
df_temporal = df_temporal.melt(id_vars=["CVEGEO","CVE_ENT","date_year"], value_vars= list_months_num, var_name = "date_month", value_name = value_name)
df_temporal["cve_geo"] = df_temporal["CVEGEO"].apply(lambda x: f"0{str(x)}" if x <= 9 else str(x))
df_temporal["cve_ent"] = df_temporal["CVE_ENT"].apply(lambda x: f"0{str(x)}" if x <= 9 else str(x))
df_temporal["date_year"] = df_temporal["date_year"].astype(int)
df_temporal = df_temporal[["cve_geo", "cve_ent", "date_year", "date_month", value_name]]
list_df_ent_month.append(df_temporal)
df_ent_month = functools.reduce(lambda df1,df2 : pd.merge(df1, df2, on = ["cve_geo","cve_ent","date_year","date_month"]), list_df_ent_month)
# - - Nacional + Entidades - - #
df_nac_ent_month = pd.concat([df_nac_month, df_ent_month]).reset_index(drop = True)
df_nac_ent_month["date_year_month"] = df_nac_ent_month.apply(lambda row: f"{row['date_year']}-{row['date_month']}-15", axis = 1)
df_nac_ent_month = df_nac_ent_month.drop(columns = ["date_year", "date_month"])
df_nac_ent_month = df_nac_ent_month[['cve_geo', 'cve_ent', 'date_year_month', 'anomaly_pr_mean', 'anomaly_tmmn_mean', 'anomaly_tmmx_mean','pdsi_mean']]
# = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o
# = = Valores mensuales municipios = = #
# - - Municipios - - #
list_csvs_mun_month = [csv_name for csv_name in list_all_csvs if "_mun_month_" in csv_name]
list_df_mun_month = list()
for csv_file in list_csvs_mun_month:
value_name = re.search("ts_(.*)_mun_month_", csv_file).group(1)
df_temporal = pd.read_csv(path2imports + csv_file)
df_temporal = df_temporal.melt(id_vars=["CVEGEO","CVE_ENT","date_year"], value_vars= list_months_num, var_name = "date_month", value_name = value_name)
df_temporal["cve_geo"] = df_temporal["CVEGEO"].apply(lambda x: f"0{str(x)}" if x <= 10_000 else str(x))
df_temporal["cve_ent"] = df_temporal["CVE_ENT"].apply(lambda x: f"0{str(x)}" if x <= 9 else str(x))
df_temporal["date_year"] = df_temporal["date_year"].astype(int)
df_temporal = df_temporal[["cve_geo", "cve_ent", "date_year", "date_month", value_name]]
list_df_mun_month.append(df_temporal)
df_mun_month = functools.reduce(lambda df1,df2 : pd.merge(df1, df2, on = ["cve_geo","cve_ent","date_year","date_month"]), list_df_mun_month)
df_mun_month["date_year_month"] = df_mun_month.apply(lambda row: f"{row['date_year']}-{row['date_month']}-15", axis = 1)
df_mun_month = df_mun_month.drop(columns = ["date_year","date_month"])
df_mun_month = df_mun_month[['cve_geo', 'cve_ent', 'date_year_month', 'anomaly_pr_mean', 'anomaly_tmmn_mean', 'anomaly_tmmx_mean', 'pdsi_mean']]
# = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o
# = = Agregar nombres de entidades y municipios = = #
cve_names_ent = pd.read_csv(current_directory + "/datos/base_nombres_entidades.csv")[["cve_geo","nombre_estado_2"]]
cve_names_ent["cve_geo"] = cve_names_ent["cve_geo"].fillna(0).apply(lambda x: f"0{int(x)}" if x <= 9 else str(int(x)))
cve_names_mun = pd.read_csv(current_directory + "/datos/base_nombres_municipios.csv")
cve_names_mun["CVEGEO"] = cve_names_mun["CVEGEO"].apply(lambda x: f"0{x}" if x < 10000 else f"{x}")
# - - Valores anuales Nacional + Entidades + Municipios - - #
df_nac_ent_mun_year = pd.merge(left = df_nac_ent_mun_year, right = cve_names_ent, left_on = "cve_ent", right_on = "cve_geo", how = "left")\
.drop(columns = "cve_geo_y")\
.rename(columns = {"cve_geo_x": "cve_geo"})\
.merge(cve_names_mun, left_on = "cve_geo", right_on = "CVEGEO", how = "left")\
.drop(columns = "CVEGEO")\
.rename(columns = {"NOMGEO": "nombre_municipio", "nombre_estado_2": "nombre_estado"})
df_nac_ent_mun_year["nombre_municipio"] = df_nac_ent_mun_year["nombre_municipio"].fillna("Estados_Nacionales")
# - - Valores mensuales Nacional + Entidades - - #
df_nac_ent_month = df_nac_ent_month.merge(right = cve_names_ent, on = "cve_geo", how = "left").rename(columns = {"nombre_estado_2" : "nombre_estado"})
# - - Valores mensuales Municipios - - #
df_mun_month = df_mun_month.merge(right = cve_names_mun, left_on = "cve_geo", right_on = "CVEGEO", how = "left")\
.drop(columns = "CVEGEO").rename(columns = {"NOMGEO":"nombre_municipio"})
# = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o = o
# = = Categoría de PDSI = = #
df_nac_ent_mun_year["mean_pdsi_categoria"] = df_nac_ent_mun_year["pdsi_mean"].apply(etiquetar_categoria_pdsi)
df_nac_ent_month["mean_pdsi_categoria"] = df_nac_ent_month["pdsi_mean"].apply(etiquetar_categoria_pdsi)
df_mun_month["mean_pdsi_categoria"] = df_mun_month["pdsi_mean"].apply(etiquetar_categoria_pdsi)
# = = Guardar los 34 CSVs = = #
list_cve_ent = list(map(lambda x: f"0{str(x)}" if x <= 9 else str(x), range(1,33)))
df_nac_ent_mun_year.to_csv(current_directory + "/datos/ee_terraclimate_db/" + "ts_nac-ent-mun_year_terraclimate.csv", index = False)
df_nac_ent_month.to_csv(current_directory + "/datos/ee_terraclimate_db/" + "ts_nac-ent_month_terraclimate.csv", index = False)
for estado in list_cve_ent:
df_mun_month[df_mun_month["cve_ent"] == estado].to_csv(current_directory + "/datos/ee_terraclimate_db/" + f"ts_{estado}mun_month_terraclimate.csv", index = False)
Analizar las anomalías y el comportamiento del clima de todo el país fue de los primeros pasos para encontrar las regiones donde las personas estaban migrando a otros lugares.
De esa manera se encontró que Oaxaca presentó temperaturas fuera de lo normal, guíandonos a buscar las rachas de sequía de sus municipios1 y encontrar el caso de Santiago Pinotepa Nacional.
Foto del autor. Minititán, Santiago Pinotepa Nacional, Oaxaca.
A pesar de que Google Earth Engine de facilita el acceso a muchos conjuntos de datos, sigue estando la barrera de la accesibilidad, ya que no todo el mundo sabe programar.
Los datos se encuentran en el repositorio de GitHub de N+ Focus, y estan listos para descargar y usar.
Ahí pueden encontrar las anomalías de temperatura, precipitación y la sequía mensual y anual de las entidades y municipios de México. También podrán encontrar el código y la metodología del análisis de los datos.
Visualización hecha directamente con los datos de anomalía de temperatura. No se tuvo que hacer un procesamiento de datos extra para obtener el resultado. La paquetería de Python con la que se hizo fue Altair.
Datos: TerraClimate a través de Google Earth Engine
import altair as alt
import pandas as pd
alt.renderers.set_embed_options(actions=False)
ee_terraclimate_mun_tmmx_url = "https://raw.githubusercontent.com/nmasfocusdatos/desplazamiento-climatico/main/datos/ee_terraclimate_db/ts_mun_year_tmmx_terraclimate.csv"
zero_base_line = alt.Chart(
pd.DataFrame({'y': [0]})
).mark_rule(
color = "#122451",
strokeWidth= 1.1,
strokeDash= [8,8],
opacity=1
).encode(
y = 'y'
)
curves = alt.Chart(ee_terraclimate_mun_tmmx_url).encode(
x = alt.X("year(date_year):T"),
y = alt.Y("anomaly_tmmx_mean:Q"),
detail = alt.Detail("cve_geo:N"),
color = alt.Color("nombre_estado:N").legend(None).scale(scheme="goldorange")
).mark_line(
interpolate = "monotone",
strokeWidth= 0.5,
opacity = 0.5
)
datavis = curves + zero_base_line
datavis_final = datavis.properties(
width = 980,
height = 350,
background = "transparent",
).configure_view(
stroke = None
).configure_axis(
title = None,
labelFont = "Lato, sans-serif",
tickColor = "#222222",
tickWidth = 1.3,
domainColor = "#222222",
labelColor = "#222222",
labelFontSize = 15
).configure_axisY(
ticks = False,
domain = False,
labelExpr = "datum.label + ' °C'",
labelFontWeight = 300,
gridWidth = 0.1,
gridColor = "#222222",
labelPadding = 15,
).configure_axisX(
grid = False,
labelFontWeight = 600,
labelPadding = 7,
tickCount = {"interval": "year", "step": 5}
)
datavis_final
Espero en el futuro se pueda mejorar el análisis con otro tipo de metodología o datos.
Escuela de Datos, SocialTIC, Factual y Distintas Latitudes por este espacio.
Omar Sánchez de Tagle e Íñigo Arredondo
Investigación: Saúl Sánchez Lemus y Alberto Pradilla.
Análisis de datos y gráficos interactivos: Isaac Arroyo.
Diseño y desarrollo web: Omar T. Bobadilla.
Realización: Enrique de la Mora, Williams Castañeda y
Rafael López.
Post producción: Jorge Ulloa.
Guión: Cecilia Guadarrama.
Animaciones: Miryam Blancas y Omar T. Bobadilla.
Producción: Aziyadé Sabines.