ICEPyUS 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- icepyus/Ploteo/ETCCDI_precip_plot.py +1019 -0
- icepyus/Ploteo/__init__.py +1 -0
- icepyus/Procesamiento/ETCCDI_precip_procesamiento.py +1442 -0
- icepyus/Procesamiento/__init__.py +1 -0
- icepyus/__init__.py +2 -0
- icepyus-1.0.1.dist-info/METADATA +148 -0
- icepyus-1.0.1.dist-info/RECORD +10 -0
- icepyus-1.0.1.dist-info/WHEEL +5 -0
- icepyus-1.0.1.dist-info/licenses/LICENSE +21 -0
- icepyus-1.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1442 @@
|
|
|
1
|
+
from cdo import Cdo
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import logging
|
|
6
|
+
import tempfile
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# Logger
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
logging.basicConfig(
|
|
16
|
+
level=logging.INFO,
|
|
17
|
+
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
18
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
19
|
+
)
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# CDO perezoso (lazy)
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Instanciar Cdo() ejecuta internamente el binario 'cdo' del sistema
|
|
26
|
+
# (via subprocess) para detectar sus operadores disponibles. Si se hace a
|
|
27
|
+
# nivel de módulo, con solo hacer `import icepyus` se rompe todo el paquete
|
|
28
|
+
# en cualquier máquina donde el binario 'cdo' no esté instalado, incluso si
|
|
29
|
+
# el usuario solo quiere usar funciones que no lo necesitan.
|
|
30
|
+
#
|
|
31
|
+
# _LazyCdo pospone esa inicialización hasta el primer uso real de un
|
|
32
|
+
# operador (ej. cdo.etccdi_r10mm(...)), y si el binario no está disponible,
|
|
33
|
+
# lanza un error claro en vez del ImportError críptico original.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _LazyCdo:
|
|
37
|
+
"""Proxy que crea la instancia real de Cdo() solo en su primer uso."""
|
|
38
|
+
|
|
39
|
+
_instance = None
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def _get_instance(cls):
|
|
43
|
+
if cls._instance is None:
|
|
44
|
+
try:
|
|
45
|
+
cls._instance = Cdo()
|
|
46
|
+
except FileNotFoundError as exc:
|
|
47
|
+
raise RuntimeError(
|
|
48
|
+
"No se encontró el binario 'cdo' (Climate Data Operators) "
|
|
49
|
+
"en el sistema. El paquete de Python 'cdo' es solo un "
|
|
50
|
+
"envoltorio: además necesitas instalar el programa CDO. "
|
|
51
|
+
"Por ejemplo:\n"
|
|
52
|
+
" - Ubuntu/Debian: sudo apt install cdo\n"
|
|
53
|
+
" - conda: conda install -c conda-forge cdo\n"
|
|
54
|
+
" - macOS (Homebrew): brew install cdo"
|
|
55
|
+
) from exc
|
|
56
|
+
return cls._instance
|
|
57
|
+
|
|
58
|
+
def __getattr__(self, name):
|
|
59
|
+
# Se llama solo si el atributo no existe en esta clase, es decir,
|
|
60
|
+
# cuando el código pide un operador real de cdo (etccdi_r10mm, etc.)
|
|
61
|
+
return getattr(self._get_instance(), name)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
cdo = _LazyCdo()
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# Utilidades
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _validate_file(filepath: str) -> Path:
|
|
72
|
+
"""Verifica que el archivo de entrada exista."""
|
|
73
|
+
path = Path(filepath)
|
|
74
|
+
if not path.exists():
|
|
75
|
+
raise FileNotFoundError(f"Archivo no encontrado: {filepath}")
|
|
76
|
+
return path
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _remove_files(*paths: str) -> None:
|
|
80
|
+
"""Elimina archivos temporales ignorando errores si no existen."""
|
|
81
|
+
for p in paths:
|
|
82
|
+
try:
|
|
83
|
+
os.remove(p)
|
|
84
|
+
logger.debug("Temporal eliminado: %s", p)
|
|
85
|
+
except FileNotFoundError:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ETCCDI_precip_malla:
|
|
90
|
+
"""
|
|
91
|
+
Módulo para calcular índices climáticos ETCCDI en datos NetCDF (malla).
|
|
92
|
+
PR = Precipitación diaria en mm.
|
|
93
|
+
|
|
94
|
+
Índices de precipitación soportados:
|
|
95
|
+
|
|
96
|
+
*** ID ** Nombre Indicador ***
|
|
97
|
+
|
|
98
|
+
*** R10mm ** Número de días con precipitaciones intensas (PR ≥ 10 mm) ***
|
|
99
|
+
Ejemplo: ETCCDI_malla.r10mm_malla(infile: str, output_file: str)
|
|
100
|
+
|
|
101
|
+
*** R20mm ** Número de días con precipitaciones muy intensas (PR es ≥ 20 mm) ***
|
|
102
|
+
Ejemplo: ETCCDI_malla.r20mm(infile: str, output_file: str)
|
|
103
|
+
|
|
104
|
+
*** Rx1day ** Precipitación máxima en un día ***
|
|
105
|
+
Ejemplo: ETCCDI_malla.rx1day(infile: str, output_file: str)
|
|
106
|
+
|
|
107
|
+
*** Rx5day ** Precipitación máxima en 5 días ***
|
|
108
|
+
Ejemplo: ETCCDI_malla.rx5day(infile: str, output_file: str)
|
|
109
|
+
|
|
110
|
+
*** CDD ** Días secos (PR < 1 mm) consecutivos ***
|
|
111
|
+
Ejemplo: ETCCDI_malla.cdd(infile: str, output_file: str)
|
|
112
|
+
|
|
113
|
+
*** CWD ** Días húmedos (PR ≥ 1 mm) consecutivos ***
|
|
114
|
+
Ejemplo: ETCCDI_malla.cwd(infile: str, output_file: str)
|
|
115
|
+
|
|
116
|
+
*** SDII ** Índice de intensidad diaria simple ***
|
|
117
|
+
Ejemplo: ETCCDI_malla.sdii(infile: str, output_file: str)
|
|
118
|
+
|
|
119
|
+
*** PRCPTOT **** Precipitación total anual en días lluviosos (PR ≥ 1 mm) ***
|
|
120
|
+
Ejemplo: ETCCDI_malla.prcptot(infile: str, output_file: str)
|
|
121
|
+
|
|
122
|
+
*** R95p ** Días muy húmedos (días > Percentil 95th) ***
|
|
123
|
+
Periodo base actual: 1991 - 2020
|
|
124
|
+
Periodos anteriores: 1961 - 1990
|
|
125
|
+
1981 - 2010
|
|
126
|
+
Ejemplo: ETCCDI_malla.r95p(input_file, output_file, base_start, base_end)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
*** Cada función tiene su descripción ***
|
|
130
|
+
|
|
131
|
+
Índices climáticos según la definición de http://etccdi.pacificclimate.org/list_27_indices.shtml
|
|
132
|
+
|
|
133
|
+
Autor(es): Omar Ramos Pérez
|
|
134
|
+
Facultad Interdisciplinaria de Ciencias Exactas y Naturales - Departamento de Física - Universidad de Sonora
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def r10mm(archivo_entrada: str, archivo_salida: str):
|
|
139
|
+
""" Esta función calcula el Índice ETCCDI R10 para un archivo netcdf
|
|
140
|
+
|
|
141
|
+
Índice climático: R10 [Número de días con precipitaciones intensas]
|
|
142
|
+
Definición: Recuento anual de días en los que PR es ≥ 10 mm
|
|
143
|
+
Unidades: días
|
|
144
|
+
|
|
145
|
+
archivo_entrada: Ruta del archivo netcdf
|
|
146
|
+
archivo_salida: Ruta donde se guardará el archivo NetCDF con el índice correspondiente.
|
|
147
|
+
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
cdo.etccdi_r10mm(input=archivo_entrada, output=archivo_salida)
|
|
151
|
+
print("*****************************************")
|
|
152
|
+
print(f"✅ Índice R10mm ejecutado exitosamente. Archivo guardado: {archivo_salida}")
|
|
153
|
+
print("*****************************************")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def r20mm(archivo_entrada: str, archivo_salida: str):
|
|
157
|
+
""" Esta función calcula el índice ETCCDI R20 para un archivo netcdf
|
|
158
|
+
|
|
159
|
+
Índice climático: R20 [Número de días con precipitaciones muy intensas]
|
|
160
|
+
Definición: Recuento anual de días en los que PR es ≥ 20 mm
|
|
161
|
+
Unidades: días
|
|
162
|
+
|
|
163
|
+
archivo_entrada: Ruta del archivo netcdf
|
|
164
|
+
archivo_salida: Ruta donde se guardará el archivo NetCDF con el índice correspondiente.
|
|
165
|
+
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
cdo.etccdi_r20mm(input=archivo_entrada, output=archivo_salida)
|
|
169
|
+
print("*****************************************")
|
|
170
|
+
print(f"✅ Índice R20mm ejecutado exitosamente. Archivo guardado: {archivo_salida}")
|
|
171
|
+
print("*****************************************")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def rx1day(archivo_entrada: str, archivo_salida: str):
|
|
175
|
+
""" Esta función calcula el índice ETCCDI RX1day para un archivo netcdf
|
|
176
|
+
|
|
177
|
+
Índice climático: RX1day [Precipitación máxima en un día]
|
|
178
|
+
Definición: Precipitación máxima mensual en un día.
|
|
179
|
+
Unidades: mm
|
|
180
|
+
|
|
181
|
+
archivo_entrada: Ruta del archivo netcdf
|
|
182
|
+
archivo_salida: Ruta donde se guardará el archivo NetCDF con el índice correspondiente.
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
cdo.etccdi_rx1day(input=archivo_entrada, output=archivo_salida)
|
|
186
|
+
print("*****************************************")
|
|
187
|
+
print(f"✅ Índice RX1day ejecutado exitosamente. Archivo guardado: {archivo_salida}")
|
|
188
|
+
print("*****************************************")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def rx5day(archivo_entrada: str, archivo_salida: str):
|
|
192
|
+
""" Esta función calcula el índice ETCCDI RX5day para un archivo netcdf
|
|
193
|
+
|
|
194
|
+
Índice climático: RX5day [Precipitación máxima en 5 días].
|
|
195
|
+
Definición: Precipitación máxima mensual en 5 días
|
|
196
|
+
Unidades: mm
|
|
197
|
+
|
|
198
|
+
archivo_entrada: Ruta del archivo netcdf
|
|
199
|
+
archivo_salida: Ruta donde se guardará el archivo NetCDF con el índice correspondiente.
|
|
200
|
+
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
cdo.etccdi_rx5day(input=archivo_entrada, output=archivo_salida)
|
|
204
|
+
print("*****************************************")
|
|
205
|
+
print(f"✅ Índice RX5day ejecutado exitosamente. Archivo guardado: {archivo_salida}")
|
|
206
|
+
print("*****************************************")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def cdd(archivo_entrada: str, archivo_salida: str):
|
|
210
|
+
""" Esta función calcula el índice ETCCDI CDD para un archivo netcdf
|
|
211
|
+
|
|
212
|
+
Índice climático: CDD [Días secos consecutivos]
|
|
213
|
+
Definición: Número máximo de días consecutivos en los que PR < 1 mm
|
|
214
|
+
Unidades: días
|
|
215
|
+
|
|
216
|
+
archivo_entrada: Ruta del archivo netcdf
|
|
217
|
+
archivo_salida: Ruta donde se guardará el archivo NetCDF con el índice correspondiente.
|
|
218
|
+
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
cdo.etccdi_cdd(input=archivo_entrada, output=archivo_salida)
|
|
222
|
+
print("*****************************************")
|
|
223
|
+
print(f"✅ Índice CDD ejecutado exitosamente. Archivo guardado: {archivo_salida}")
|
|
224
|
+
print("*****************************************")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def cwd(archivo_entrada: str, archivo_salida: str):
|
|
228
|
+
""" Esta función calcula el índice ETCCDI CWD para un archivo netcdf
|
|
229
|
+
|
|
230
|
+
Índice climático: CWD [Días húmedos consecutivos]
|
|
231
|
+
Definición: Número máximo de días consecutivos en los que PR ≥ a 1 mm
|
|
232
|
+
Unidades: días
|
|
233
|
+
|
|
234
|
+
archivo_entrada: Ruta del archivo netcdf
|
|
235
|
+
archivo_salida: Ruta donde se guardará el archivo NetCDF con el índice correspondiente.
|
|
236
|
+
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
cdo.etccdi_cwd(input=archivo_entrada, output=archivo_salida)
|
|
240
|
+
print("*****************************************")
|
|
241
|
+
print(f"✅ Índice CWD ejecutado exitosamente. Archivo guardado: {archivo_salida}")
|
|
242
|
+
print("*****************************************")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def sdii(archivo_entrada: str, archivo_salida: str):
|
|
246
|
+
""" Esta función calcula el índice ETCCDI SDII para un archivo netcdf
|
|
247
|
+
|
|
248
|
+
Índice climático: SDII [Índice de intensidad diaria simple].
|
|
249
|
+
Definición: Se calcula dividiendo la precipitación total anual
|
|
250
|
+
entre el número de días con lluvia (PR ≥ 1 mm) en el año.
|
|
251
|
+
Unidades: mm/día
|
|
252
|
+
|
|
253
|
+
archivo_entrada: Ruta del archivo netcdf
|
|
254
|
+
archivo_salida: Ruta donde se guardará el archivo NetCDF con el índice correspondiente.
|
|
255
|
+
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
cdo.etccdi_sdii(input=archivo_entrada, output=archivo_salida)
|
|
259
|
+
print("*****************************************")
|
|
260
|
+
print(f"✅ Índice SDII ejecutado exitosamente. Archivo guardado: {archivo_salida}")
|
|
261
|
+
print("*****************************************")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def prcptot(archivo_entrada: str, archivo_salida: str):
|
|
265
|
+
""" Esta función calcula el Indice ETCCDI PRCPTOT para un archivo netcdf
|
|
266
|
+
|
|
267
|
+
Índice climático: PRCPTOT [Precipitación total anual en días lluviosos]
|
|
268
|
+
Definición: Precipitación total anual en días lluviosos (PR ≥ 1 mm).
|
|
269
|
+
Unidades: mm
|
|
270
|
+
|
|
271
|
+
archivo_entrada: Ruta del archivo netcdf
|
|
272
|
+
archivo_salida: Ruta donde se guardará el archivo NetCDF con el índice correspondiente.
|
|
273
|
+
|
|
274
|
+
"""
|
|
275
|
+
# Paso 1: máscara de días húmedos (pr >= 1 mm/día) → archivo temporal
|
|
276
|
+
tmp_mask = "/tmp/mask_wet_days.nc"
|
|
277
|
+
cdo.gtc(1, input=archivo_entrada, output=tmp_mask)
|
|
278
|
+
|
|
279
|
+
# Paso 2: multiplicar precipitación original por la máscara
|
|
280
|
+
tmp_mul = "/tmp/pr_wet_days.nc"
|
|
281
|
+
cdo.mul(input=f"{archivo_entrada} {tmp_mask}", output=tmp_mul)
|
|
282
|
+
|
|
283
|
+
# Paso 3: suma anual → PRCPTOT
|
|
284
|
+
cdo.yearsum(input=tmp_mul, output=archivo_salida)
|
|
285
|
+
|
|
286
|
+
print("*****************************************")
|
|
287
|
+
print(f"✅ Índice PRCPTOT calculado exitosamente. Archivo guardado: {archivo_salida}")
|
|
288
|
+
print("*****************************************")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def r95p( archivo_entrada: str,
|
|
294
|
+
archivo_salida: str,
|
|
295
|
+
base_inicio: int ,
|
|
296
|
+
base_fin: int ,
|
|
297
|
+
tmp_dir: str | None = None,
|
|
298
|
+
) -> str:
|
|
299
|
+
"""
|
|
300
|
+
Calcula el índice ETCCDI R95p para un archivo netcdf
|
|
301
|
+
|
|
302
|
+
Índice climático: R95p [Días muy húmedos]
|
|
303
|
+
Definición: Suma anual de precipitación en los días en que la precipitación supera
|
|
304
|
+
el percentil 95 del período de referencia (solo días húmedos ≥ 1 mm).
|
|
305
|
+
Unidades: mm
|
|
306
|
+
|
|
307
|
+
R95p = Σ RRij donde RRij > RR95p y RRij ≥ 1 mm
|
|
308
|
+
|
|
309
|
+
Parámetros
|
|
310
|
+
----------
|
|
311
|
+
archivo_entrada : str Archivo NetCDF de precipitación diaria (mm/día).
|
|
312
|
+
archivo_salida : str Archivo NetCDF de salida con R95p anual.
|
|
313
|
+
base_start : int Año inicio del período de referencia (p. ej., 1981).
|
|
314
|
+
base_end : int Año fin del período de referencia (p. ej., 2010).
|
|
315
|
+
tmp_dir : str | None Directorio para temporales (None → carpeta del sistema).
|
|
316
|
+
|
|
317
|
+
"""
|
|
318
|
+
_validate_file(archivo_entrada)
|
|
319
|
+
tmp = tmp_dir or tempfile.gettempdir()
|
|
320
|
+
|
|
321
|
+
f_base = os.path.join(tmp, "r95p_base.nc")
|
|
322
|
+
f_pct95 = os.path.join(tmp, "r95p_pct95.nc")
|
|
323
|
+
f_wet = os.path.join(tmp, "r95p_wet.nc")
|
|
324
|
+
f_mask = os.path.join(tmp, "r95p_mask.nc")
|
|
325
|
+
f_masked = os.path.join(tmp, "r95p_masked.nc")
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
logger.info("R95p | Paso 1: Extrayendo período base %d–%d", base_inicio, base_fin)
|
|
329
|
+
cdo.selyear(
|
|
330
|
+
f"{base_inicio}/{base_fin}",
|
|
331
|
+
input=archivo_entrada,
|
|
332
|
+
output=f_base,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
logger.info("R95p | Paso 2: Calculando percentil 95 estacional")
|
|
336
|
+
cdo.ydrunpctl(
|
|
337
|
+
"95,5",
|
|
338
|
+
input=f"{f_base} -ydrunmin,5 {f_base} -ydrunmax,5 {f_base}",
|
|
339
|
+
output=f_pct95,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
logger.info("R95p | Paso 3: Máscara días húmedos (≥ 1 mm)")
|
|
343
|
+
cdo.gtc(
|
|
344
|
+
"1.0",
|
|
345
|
+
input=archivo_entrada,
|
|
346
|
+
output=f_wet,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
logger.info("R95p | Paso 4: Máscara días > percentil 95")
|
|
350
|
+
# cdo sub resta el percentil (365 pasos) a cada día de la serie completa
|
|
351
|
+
# usando la lógica de día-del-año; luego gtc,0 devuelve 1 donde pr > pct95
|
|
352
|
+
f_sub95 = f_base.replace("r95p_base", "r95p_sub95")
|
|
353
|
+
cdo.ydaysub(
|
|
354
|
+
input=f"{archivo_entrada} {f_pct95}",
|
|
355
|
+
output=f_sub95,
|
|
356
|
+
)
|
|
357
|
+
cdo.gtc(
|
|
358
|
+
"0",
|
|
359
|
+
input=f_sub95,
|
|
360
|
+
output=f_mask,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
logger.info("R95p | Paso 5: Precipitación filtrada (húmedo AND > pct95)")
|
|
364
|
+
cdo.mul(
|
|
365
|
+
input=f"{archivo_entrada} -mul {f_wet} {f_mask}",
|
|
366
|
+
output=f_masked,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
logger.info("R95p | Paso 6: Suma anual → R95p")
|
|
370
|
+
cdo.yearsum(
|
|
371
|
+
input=f_masked,
|
|
372
|
+
output=archivo_salida,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# logger.info("✅ R95p calculado → %s", archivo_salida)
|
|
376
|
+
print("*****************************************")
|
|
377
|
+
print(f"✅ Índice R95P calculado exitosamente. Archivo guardado: {archivo_salida}")
|
|
378
|
+
print("*****************************************")
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
finally:
|
|
382
|
+
_remove_files(f_base, f_pct95, f_wet, f_mask, f_masked,
|
|
383
|
+
f_base.replace('r95p_base', 'r95p_sub95'))
|
|
384
|
+
|
|
385
|
+
return archivo_salida
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def r99p(
|
|
389
|
+
archivo_entrada: str,
|
|
390
|
+
archivo_salida: str,
|
|
391
|
+
base_inicio: int ,
|
|
392
|
+
base_fin: int ,
|
|
393
|
+
tmp_dir: str | None = None,
|
|
394
|
+
) -> str:
|
|
395
|
+
"""
|
|
396
|
+
Calcula el índice ETCCDI R99p para un archivo netcdf
|
|
397
|
+
|
|
398
|
+
Índice Climático: R99p [Días extremadamente húmedos]
|
|
399
|
+
Definición: Suma anual de precipitación en los días en que la precipitación supera
|
|
400
|
+
el percentil 99 del período de referencia (solo días húmedos ≥ 1 mm).
|
|
401
|
+
Unidades: mm
|
|
402
|
+
|
|
403
|
+
R99p = Σ RRij donde RRij > RR99p y RRij ≥ 1 mm
|
|
404
|
+
|
|
405
|
+
Parámetros
|
|
406
|
+
----------
|
|
407
|
+
input_file : str Archivo NetCDF de precipitación diaria (mm/día).
|
|
408
|
+
output_file : str Archivo NetCDF de salida con R99p anual.
|
|
409
|
+
base_start : int Año inicio del período de referencia (default 1981).
|
|
410
|
+
base_end : int Año fin del período de referencia (default 2010).
|
|
411
|
+
tmp_dir : str | None Directorio para temporales (None → carpeta del sistema).
|
|
412
|
+
|
|
413
|
+
"""
|
|
414
|
+
_validate_file(archivo_entrada)
|
|
415
|
+
tmp = tmp_dir or tempfile.gettempdir()
|
|
416
|
+
|
|
417
|
+
f_base = os.path.join(tmp, "r99p_base.nc")
|
|
418
|
+
f_pct99 = os.path.join(tmp, "r99p_pct99.nc")
|
|
419
|
+
f_wet = os.path.join(tmp, "r99p_wet.nc")
|
|
420
|
+
f_mask = os.path.join(tmp, "r99p_mask.nc")
|
|
421
|
+
f_masked = os.path.join(tmp, "r99p_masked.nc")
|
|
422
|
+
|
|
423
|
+
try:
|
|
424
|
+
logger.info("R99p | Paso 1: Extrayendo período base %d–%d", base_inicio, base_fin)
|
|
425
|
+
cdo.selyear(
|
|
426
|
+
f"{base_inicio}/{base_fin}",
|
|
427
|
+
input=archivo_entrada,
|
|
428
|
+
output=f_base,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
logger.info("R99p | Paso 2: Calculando percentil 99 estacional")
|
|
432
|
+
cdo.ydrunpctl(
|
|
433
|
+
"99,5",
|
|
434
|
+
input=f"{f_base} -ydrunmin,5 {f_base} -ydrunmax,5 {f_base}",
|
|
435
|
+
output=f_pct99,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
logger.info("R99p | Paso 3: Máscara días húmedos (≥ 1 mm)")
|
|
439
|
+
cdo.gtc(
|
|
440
|
+
"1.0",
|
|
441
|
+
input=archivo_entrada,
|
|
442
|
+
output=f_wet,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
logger.info("R99p | Paso 4: Máscara días > percentil 99")
|
|
446
|
+
f_sub99 = f_base.replace("r99p_base", "r99p_sub99")
|
|
447
|
+
cdo.ydaysub(
|
|
448
|
+
input=f"{archivo_entrada} {f_pct99}",
|
|
449
|
+
output=f_sub99,
|
|
450
|
+
)
|
|
451
|
+
cdo.gtc(
|
|
452
|
+
"0",
|
|
453
|
+
input=f_sub99,
|
|
454
|
+
output=f_mask,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
logger.info("R99p | Paso 5: Precipitación filtrada (húmedo AND > pct99)")
|
|
458
|
+
cdo.mul(
|
|
459
|
+
input=f"{archivo_entrada} -mul {f_wet} {f_mask}",
|
|
460
|
+
output=f_masked,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
logger.info("R99p | Paso 6: Suma anual → R99p")
|
|
464
|
+
cdo.yearsum(
|
|
465
|
+
input=f_masked,
|
|
466
|
+
output=archivo_salida,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
#logger.info("✅ R99p calculado → %s", output_file)
|
|
470
|
+
|
|
471
|
+
print("*****************************************")
|
|
472
|
+
print(f"✅ Índice R99P calculado exitosamente. Archivo guardado: {archivo_salida}")
|
|
473
|
+
print("*****************************************")
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
finally:
|
|
477
|
+
_remove_files(f_base, f_pct99, f_wet, f_mask, f_masked,
|
|
478
|
+
f_base.replace('r99p_base', 'r99p_sub99'))
|
|
479
|
+
|
|
480
|
+
return archivo_salida
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
#/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
485
|
+
from calendar import isleap
|
|
486
|
+
|
|
487
|
+
class ETCCDI_precip_in_situ:
|
|
488
|
+
"""
|
|
489
|
+
Módulo para calcular índices climáticos ETCCDI usando datos --in situ-- (estación meteorológica) en formato .txt
|
|
490
|
+
|
|
491
|
+
Índices soportados:
|
|
492
|
+
Precipitación : Función del módulo:
|
|
493
|
+
|
|
494
|
+
*** ID ** Nombre Indicador ***
|
|
495
|
+
|
|
496
|
+
*** R10mm ** Número de días con precipitaciones intensas (PR ≥ 10 mm) ***
|
|
497
|
+
Ejemplo: ETCCDI_in_situ.r10mm(archivo_entrada: str, archivo_salida: str)
|
|
498
|
+
|
|
499
|
+
*** R20mm ** Número de días con precipitaciones muy intensas (PR es ≥ 20 mm) ***
|
|
500
|
+
Ejemplo: ETCCDI_in_situ.r20mm(archivo_entrada: str, archivo_salida: str)
|
|
501
|
+
|
|
502
|
+
*** Rx1day ** Precipitación máxima en un día ***
|
|
503
|
+
Ejemplo: ETCCDI_in_situ.rx1day(archivo_entrada: str, archivo_salida: str)
|
|
504
|
+
|
|
505
|
+
*** Rx5day ** Precipitación máxima en 5 días ***
|
|
506
|
+
Ejemplo: ETCCDI_in_situ.rx5day(archivo_entrada: str, archivo_salida: str)
|
|
507
|
+
|
|
508
|
+
*** CDD ** Días secos (PR < 1 mm) consecutivos ***
|
|
509
|
+
Ejemplo: ETCCDI_in_situ.cdd(archivo_entrada: str, archivo_salida: str)
|
|
510
|
+
|
|
511
|
+
*** CWD ** Días húmedos (PR ≥ 1 mm) consecutivos ***
|
|
512
|
+
Ejemplo: ETCCDI_in_situ.cwd(archivo_entrada: str, archivo_salida: str)
|
|
513
|
+
|
|
514
|
+
*** SDII ** Índice de intensidad diaria simple ***
|
|
515
|
+
Ejemplo: ETCCDI_in_situ.sdii(archivo_entrada: str, archivo_salida: str)
|
|
516
|
+
|
|
517
|
+
*** PRCPTOT **** Precipitación total anual en días lluviosos (PR ≥ 1 mm) ***
|
|
518
|
+
Ejemplo: ETCCDI_in_situ. (infile: str, output_file: str)
|
|
519
|
+
|
|
520
|
+
*** R95p ** Días muy húmedos (días > Percentil 95th) ***
|
|
521
|
+
Periodo base actual: 1991 - 2020
|
|
522
|
+
Periodos anteriores: 1961 - 1990
|
|
523
|
+
1981 - 2010
|
|
524
|
+
|
|
525
|
+
Ejemplo: ETCCDI_in_situ.r95p(archivo_entrada, archivo_salida, base_inicio, base_fin)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
*** R99p ** Días muy húmedos (días > Percentil 99th) ***
|
|
529
|
+
Periodo base actual: 1991 - 2020
|
|
530
|
+
Periodos anteriores: 1961 - 1990
|
|
531
|
+
1981 - 2010
|
|
532
|
+
Ejemplo: ETCCDI_in_situ.r99p(archivo_entrada, archivo_salida, base_inicio, base_fin)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
*** Cada función tiene su descripción ***
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
Índices climáticos según la definición de http://etccdi.pacificclimate.org/list_27_indices.shtml
|
|
539
|
+
|
|
540
|
+
Autor(es): Omar Ramos Pérez
|
|
541
|
+
Facultad Interdisciplinaria de Ciencias Exactas y Naturales - Departamento de Física - Universidad de Sonora
|
|
542
|
+
|
|
543
|
+
Basado en el https://github.com/ioannidispanagiotis/Climate-Indices-ETCCDI/blob/main/cdd_y_climex_1.0.py
|
|
544
|
+
|
|
545
|
+
"""
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def r10mm(archivo_entrada:str, archivo_salida:str):
|
|
549
|
+
"""
|
|
550
|
+
Índice climático: R10mm [Número de días con precipitaciones intensas].
|
|
551
|
+
Definición: Recuento anual de días en los que PR >= 10 mm.
|
|
552
|
+
Unidades: días
|
|
553
|
+
|
|
554
|
+
archivo_entrada: Ruta del archivo .txt de la estación
|
|
555
|
+
|
|
556
|
+
Formato del archivo de texto de la estación
|
|
557
|
+
Columnas: Año, Mes, Día, PR, TX, TN
|
|
558
|
+
|
|
559
|
+
1901 1 1 -99.9 -3.1 -6.8
|
|
560
|
+
1901 1 2 -99.9 -1.3 -3.6
|
|
561
|
+
1901 1 3 -99.9 -0.5 -7.9
|
|
562
|
+
1901 1 4 -99.9 -1.0 -9.1
|
|
563
|
+
1901 1 5 -99.9 -1.8 -8.4
|
|
564
|
+
|
|
565
|
+
mes y día deben ser números enteros.
|
|
566
|
+
Las unidades de PR (Precipitación) son milímetros.
|
|
567
|
+
Las unidades de TX (Temperatura Máxima)/TN (Temperatura Mínima) son grados Celsius.
|
|
568
|
+
|
|
569
|
+
archivo_salida: Ruta donde se guarda el archivo Excel (.xlsx)
|
|
570
|
+
|
|
571
|
+
"""
|
|
572
|
+
|
|
573
|
+
df = pd.read_csv(archivo_entrada, sep='\s+') # Data import
|
|
574
|
+
|
|
575
|
+
r10_lst = []
|
|
576
|
+
for y in df['Year'].unique():
|
|
577
|
+
year_data = df[df['Year'] == y]
|
|
578
|
+
r10 = (year_data['PR'] >= 10).sum()
|
|
579
|
+
r10_lst.append(r10)
|
|
580
|
+
|
|
581
|
+
R10_out = np.array(r10_lst)
|
|
582
|
+
|
|
583
|
+
dates_r10 = pd.date_range(start=str(df['Year'].min()), end=str(df['Year'].max() + 1), freq="YE")
|
|
584
|
+
df_out = pd.DataFrame({'Year': dates_r10.year, 'Indice_R10mm': R10_out})
|
|
585
|
+
|
|
586
|
+
# 3. Save the DataFrame to an Excel file (.xlsx)
|
|
587
|
+
# Set index=False to avoid writing the DataFrame index as an extra column in Excel
|
|
588
|
+
df_out.to_excel(archivo_salida, index=False)
|
|
589
|
+
|
|
590
|
+
print("***************************************")
|
|
591
|
+
print(f"✅ Índice R10mm ejecutado exitosamente. Archivo guardado: {archivo_salida}")
|
|
592
|
+
print("***************************************")
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def r20mm(archivo_entrada:str, archivo_salida:str):
|
|
597
|
+
"""
|
|
598
|
+
Índice climático: R20 [Número de días con precipitaciones muy intensas]
|
|
599
|
+
Definición: Recuento anual de días en los que PR >= 20 mm.
|
|
600
|
+
Unidades: Días
|
|
601
|
+
|
|
602
|
+
archivo_entrada: Ruta del archivo .txt de la estación.
|
|
603
|
+
|
|
604
|
+
Formato del archivo de texto de la estación
|
|
605
|
+
Columnas: Año, Mes, Día, PR, TX, TN
|
|
606
|
+
|
|
607
|
+
1901 1 1 -99.9 -3.1 -6.8
|
|
608
|
+
1901 1 2 -99.9 -1.3 -3.6
|
|
609
|
+
1901 1 3 -99.9 -0.5 -7.9
|
|
610
|
+
1901 1 4 -99.9 -1.0 -9.1
|
|
611
|
+
1901 1 5 -99.9 -1.8 -8.4
|
|
612
|
+
|
|
613
|
+
mes y día deben ser números enteros.
|
|
614
|
+
Las unidades de PR (Precipitación) son milímetros.
|
|
615
|
+
Las unidades de TX (Temperatura Máxima)/TN (Temperatura Mínima) son grados Celsius.
|
|
616
|
+
|
|
617
|
+
archivo_salida: Ruta donde se guarda el archivo Excel (.xlsx)
|
|
618
|
+
|
|
619
|
+
"""
|
|
620
|
+
|
|
621
|
+
df = pd.read_csv(archivo_entrada, sep='\s+') # Data import
|
|
622
|
+
|
|
623
|
+
r20_lst = []
|
|
624
|
+
for y in df['Year'].unique():
|
|
625
|
+
df_year = df[df['Year'] == y] # ✅ Filtra por columna Year
|
|
626
|
+
if not df_year.empty:
|
|
627
|
+
# Counting days of precipitation greater than or equal to 20 mm
|
|
628
|
+
r20 = (df_year['PR'] >= 20).sum()
|
|
629
|
+
r20_lst.append(r20)
|
|
630
|
+
else:
|
|
631
|
+
r20 = float("NaN")
|
|
632
|
+
r20_lst.append(r20)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
r20_out = np.array(r20_lst)
|
|
636
|
+
|
|
637
|
+
dates_r20 = pd.date_range(start=str(df['Year'].min()), end=str(df['Year'].max() + 1), freq="YE")
|
|
638
|
+
df_out_r20 = pd.DataFrame({'Year': dates_r20.year, 'Indice_R20mm': r20_out} )
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
# 3. Save the DataFrame to an Excel file (.xlsx)
|
|
642
|
+
# Set index=False to avoid writing the DataFrame index as an extra column in Excel
|
|
643
|
+
df_out_r20.to_excel(archivo_salida, index=False)
|
|
644
|
+
|
|
645
|
+
print("**************************")
|
|
646
|
+
print(f"✅ Índice R20mm ejecutado exitosamente. Archivo guardado: {archivo_salida}")
|
|
647
|
+
print("**************************")
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def rx1day(archivo_entrada:str, archivo_salida:str):
|
|
652
|
+
"""
|
|
653
|
+
Índice climático: RX1day [Precipitación máxima en un día]
|
|
654
|
+
Definición: Precipitación máxima mensual en un día.
|
|
655
|
+
Unidades: mm
|
|
656
|
+
|
|
657
|
+
archivo_entrada: Ruta del archivo .txt de la estación.
|
|
658
|
+
|
|
659
|
+
Formato del archivo de texto de la estación
|
|
660
|
+
Columnas: Año, Mes, Día, PR, TX, TN
|
|
661
|
+
|
|
662
|
+
1901 1 1 -99.9 -3.1 -6.8
|
|
663
|
+
1901 1 2 -99.9 -1.3 -3.6
|
|
664
|
+
1901 1 3 -99.9 -0.5 -7.9
|
|
665
|
+
1901 1 4 -99.9 -1.0 -9.1
|
|
666
|
+
1901 1 5 -99.9 -1.8 -8.4
|
|
667
|
+
|
|
668
|
+
Mes y Día deben ser números enteros.
|
|
669
|
+
Las unidades de PR (Precipitación) son milímetros.
|
|
670
|
+
Las unidades de TX (Temperatura Máxima)/TN (Temperatura Mínima) son grados Celsius.
|
|
671
|
+
|
|
672
|
+
archivo_salida: Ruta donde se guarda el archivo Excel (.xlsx)
|
|
673
|
+
|
|
674
|
+
"""
|
|
675
|
+
|
|
676
|
+
df = pd.read_csv(archivo_entrada, sep='\s+') # Data import
|
|
677
|
+
rx1day_lst = [] # List to save index values per year
|
|
678
|
+
|
|
679
|
+
for y in df['Year'].unique():
|
|
680
|
+
df_year = df[df['Year'] == y] # ✅ Filtra por columna Year
|
|
681
|
+
if not df_year.empty:
|
|
682
|
+
for m in range(1, 13):
|
|
683
|
+
df_month = df_year[df_year['Month'] == m] # ✅ Filtra por columna Month
|
|
684
|
+
if not df_month.empty:
|
|
685
|
+
rx1day = df_month['PR'].max() # ✅ Máximo de precipitación
|
|
686
|
+
rx1day_lst.append(rx1day)
|
|
687
|
+
else:
|
|
688
|
+
rx1day_lst.append(float("NaN"))
|
|
689
|
+
else:
|
|
690
|
+
rx1day_lst.append(float("NaN"))
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
rx1day_out = np.array(rx1day_lst)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
dates_rx1day = pd.date_range(start=str(df['Year'].min()), end=str(df['Year'].max() + 1), freq="ME")
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
df_out_rx1day = pd.DataFrame({'Year': dates_rx1day.year, 'Month': dates_rx1day.month, 'Indice_rx1daymm': rx1day_out})
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
# 3. Save the DataFrame to an Excel file (.xlsx)
|
|
703
|
+
# Set index=False to avoid writing the DataFrame index as an extra column in Excel
|
|
704
|
+
df_out_rx1day.to_excel(archivo_salida, index=False)
|
|
705
|
+
|
|
706
|
+
print("**************************")
|
|
707
|
+
print(f"✅ Índice RX1day ejecutado exitosamente. Archivo guardado: {archivo_salida}")
|
|
708
|
+
print("**************************")
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def rx5day(archivo_entrada:str, archivo_salida:str):
|
|
714
|
+
"""
|
|
715
|
+
Índice climático: RX5day [Precipitación máxima en 5 días]
|
|
716
|
+
Definición: Precipitación máxima mensual en 5 días.
|
|
717
|
+
Unidades: mm
|
|
718
|
+
|
|
719
|
+
archivo_entrada: Ruta del archivo .txt de la estación
|
|
720
|
+
|
|
721
|
+
Formato del archivo de texto de la estación
|
|
722
|
+
Columnas: Año, Mes, Día, PR, TX, TN
|
|
723
|
+
|
|
724
|
+
1901 1 1 -99.9 -3.1 -6.8
|
|
725
|
+
1901 1 2 -99.9 -1.3 -3.6
|
|
726
|
+
1901 1 3 -99.9 -0.5 -7.9
|
|
727
|
+
1901 1 4 -99.9 -1.0 -9.1
|
|
728
|
+
1901 1 5 -99.9 -1.8 -8.4
|
|
729
|
+
|
|
730
|
+
Mes y Día deben ser números enteros.
|
|
731
|
+
Las unidades de PR (Precipitación) son milímetros.
|
|
732
|
+
Las unidades de TX (Temperatura Máxima)/TN (Temperatura Mínima) son grados Celsius.
|
|
733
|
+
|
|
734
|
+
archivo_salida: Ruta donde se guarda el archivo Excel (.xlsx)
|
|
735
|
+
|
|
736
|
+
"""
|
|
737
|
+
|
|
738
|
+
df = pd.read_csv(archivo_entrada, sep='\s+') # Data import
|
|
739
|
+
|
|
740
|
+
rx5day_lst = [] # List to save index values per year
|
|
741
|
+
|
|
742
|
+
for y in df['Year'].unique():
|
|
743
|
+
df_year = df[df['Year'] == y]
|
|
744
|
+
if not df_year.empty:
|
|
745
|
+
for m in range(1, 13):
|
|
746
|
+
df_month = df_year[df_year['Month'] == m].reset_index(drop=True) # reset para índices 0,1,2,...
|
|
747
|
+
if not df_month.empty:
|
|
748
|
+
consecutive_5_day_rr = []
|
|
749
|
+
q = 0
|
|
750
|
+
# Iterar mientras haya al menos 5 días desde la posición q
|
|
751
|
+
while q <= len(df_month) - 5:
|
|
752
|
+
rx5day_m = df_month['PR'].iloc[q:q+5].sum() # suma de 5 días consecutivos
|
|
753
|
+
consecutive_5_day_rr.append(rx5day_m)
|
|
754
|
+
q += 1
|
|
755
|
+
if consecutive_5_day_rr:
|
|
756
|
+
rx5day_lst.append(round(max(consecutive_5_day_rr), 2))
|
|
757
|
+
else:
|
|
758
|
+
rx5day_lst.append(float("NaN"))
|
|
759
|
+
else:
|
|
760
|
+
rx5day_lst.append(float("NaN")) # ← también estaba mal (usaba rx1day_lst)
|
|
761
|
+
else:
|
|
762
|
+
rx5day_lst.append(float("NaN")) # ← ídem
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
rx5day_out = np.array(rx5day_lst)
|
|
766
|
+
|
|
767
|
+
dates_rx5day = pd.date_range(start=str(df['Year'].min()), end=str(df['Year'].max() + 1), freq="ME")
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
df_out_rx5day = pd.DataFrame({'Year': dates_rx5day.year, 'Month': dates_rx5day.month, 'Indice_rx5daymm': rx5day_out})
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
# 3. Save the DataFrame to an Excel file (.xlsx)
|
|
774
|
+
# Set index=False to avoid writing the DataFrame index as an extra column in Excel
|
|
775
|
+
df_out_rx5day.to_excel(archivo_salida, index=False)
|
|
776
|
+
|
|
777
|
+
print("**************************")
|
|
778
|
+
print(f"✅ Índice RX5day ejecutado exitosamente. Archivo guardado: {archivo_salida}")
|
|
779
|
+
print("**************************")
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def cdd(archivo_entrada:str, archivo_salida:str):
|
|
784
|
+
"""
|
|
785
|
+
Índice climático: CDD [Días secos (PR < 1 mm) consecutivos]
|
|
786
|
+
Definición: Número máximo de días consecutivos con PR < 1 mm al año.
|
|
787
|
+
Unidades: Días
|
|
788
|
+
|
|
789
|
+
archivo_entrada: Ruta del archivo .txt de la estación
|
|
790
|
+
|
|
791
|
+
Formato del archivo de texto de la estación
|
|
792
|
+
Columnas: Año, Mes, Día, PR, TX, TN
|
|
793
|
+
|
|
794
|
+
1901 1 1 -99.9 -3.1 -6.8
|
|
795
|
+
1901 1 2 -99.9 -1.3 -3.6
|
|
796
|
+
1901 1 3 -99.9 -0.5 -7.9
|
|
797
|
+
1901 1 4 -99.9 -1.0 -9.1
|
|
798
|
+
1901 1 5 -99.9 -1.8 -8.4
|
|
799
|
+
|
|
800
|
+
mes y día deben ser números enteros.
|
|
801
|
+
Las unidades de PR (Precipitación) son milímetros.
|
|
802
|
+
Las unidades de TX (Temperatura Máxima)/TN (Temperatura Mínima) son grados Celsius.
|
|
803
|
+
|
|
804
|
+
archivo_salida: Ruta donde se guarda el archivo Excel (.xlsx)
|
|
805
|
+
|
|
806
|
+
"""
|
|
807
|
+
|
|
808
|
+
df = pd.read_csv(archivo_entrada, sep='\s+') # Data import
|
|
809
|
+
|
|
810
|
+
cdd_lst = [] # List to save index values per year
|
|
811
|
+
|
|
812
|
+
for y in df['Year'].unique():
|
|
813
|
+
df_year = df[df['Year'] == y]
|
|
814
|
+
if not df_year.empty:
|
|
815
|
+
# Yearly count of consecutive days with daily precipitation < 1 mm
|
|
816
|
+
cdd_df = df_year[df_year['PR'] < 1].dropna().reset_index(drop=True)
|
|
817
|
+
count = 0
|
|
818
|
+
count_lst = []
|
|
819
|
+
for q in range(1, len(cdd_df)):
|
|
820
|
+
if (cdd_df['Day'].iloc[q] - cdd_df['Day'].iloc[q-1]) != 1: # ← compara columna Day
|
|
821
|
+
count = 0
|
|
822
|
+
else:
|
|
823
|
+
count = count + 1
|
|
824
|
+
count_lst.append(count)
|
|
825
|
+
if len(count_lst) == 0: # Added in order to avoid error in finding the max() of an empty list
|
|
826
|
+
pass
|
|
827
|
+
else:
|
|
828
|
+
cdd = max(count_lst) + 1
|
|
829
|
+
cdd_lst.append(cdd)
|
|
830
|
+
else:
|
|
831
|
+
cdd = float("NaN")
|
|
832
|
+
cdd_lst.append(cdd)
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
cdd_out = np.array(cdd_lst)
|
|
837
|
+
|
|
838
|
+
dates_cdd = pd.date_range(start=str(df['Year'].min()), end=str(df['Year'].max() + 1), freq="YE")
|
|
839
|
+
df_out_cdd = pd.DataFrame({'Year': dates_cdd.year, 'Indice_cdd': cdd_out} )
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
## 3. Save the DataFrame to an Excel file (.xlsx)
|
|
844
|
+
# Set index=False to avoid writing the DataFrame index as an extra column in Excel
|
|
845
|
+
df_out_cdd.to_excel(archivo_salida, index=False)
|
|
846
|
+
|
|
847
|
+
print("**************************")
|
|
848
|
+
print(f"✅ Índice CDD ejecutado exitosamente. Archivo guardado: {archivo_salida}")
|
|
849
|
+
print("**************************")
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def cwd(archivo_entrada:str, archivo_salida:str):
|
|
854
|
+
"""
|
|
855
|
+
Índice climático: CWD [Días consecutivos con precipitación]
|
|
856
|
+
Definición: Número máximo de días consecutivos con PR >= 1 mm al año.
|
|
857
|
+
Unidades: días
|
|
858
|
+
|
|
859
|
+
archivo_entrada: Ruta del archivo .txt de la estación
|
|
860
|
+
|
|
861
|
+
Formato del archivo de texto de la estación
|
|
862
|
+
Columnas: Año, Mes, Día, PR, TX, TN
|
|
863
|
+
|
|
864
|
+
1901 1 1 -99.9 -3.1 -6.8
|
|
865
|
+
1901 1 2 -99.9 -1.3 -3.6
|
|
866
|
+
1901 1 3 -99.9 -0.5 -7.9
|
|
867
|
+
1901 1 4 -99.9 -1.0 -9.1
|
|
868
|
+
1901 1 5 -99.9 -1.8 -8.4
|
|
869
|
+
|
|
870
|
+
mes y día deben ser números enteros.
|
|
871
|
+
Las unidades de PR (Precipitación) son milímetros.
|
|
872
|
+
Las unidades de TX (Temperatura Máxima)/TN (Temperatura Mínima) son grados Celsius.
|
|
873
|
+
|
|
874
|
+
archivo_salida: Ruta donde se guarda el archivo Excel (.xlsx)
|
|
875
|
+
|
|
876
|
+
"""
|
|
877
|
+
|
|
878
|
+
cwd_lst = [] # List to save index values per year
|
|
879
|
+
|
|
880
|
+
df = pd.read_csv(archivo_entrada, sep='\s+') # import
|
|
881
|
+
|
|
882
|
+
for y in df['Year'].unique():
|
|
883
|
+
df_year = df[df['Year'] == y]
|
|
884
|
+
if not df_year.empty:
|
|
885
|
+
# Yearly count of consecutive days with daily precipitation > 1 mm
|
|
886
|
+
cwd_df = df_year[df_year['PR'] > 1].dropna().reset_index(drop=True)
|
|
887
|
+
count = 0
|
|
888
|
+
count_lst = []
|
|
889
|
+
for q in range(1, len(cwd_df)):
|
|
890
|
+
if (cwd_df['Day'].iloc[q] - cwd_df['Day'].iloc[q-1]) != 1: # ← compara columna Day
|
|
891
|
+
count = 0
|
|
892
|
+
else:
|
|
893
|
+
count = count + 1
|
|
894
|
+
count_lst.append(count)
|
|
895
|
+
if len(count_lst) == 0: # Added in order to avoid error in finding the max() of an empty list
|
|
896
|
+
pass
|
|
897
|
+
else:
|
|
898
|
+
cdd = max(count_lst) + 1
|
|
899
|
+
cwd_lst.append(cdd)
|
|
900
|
+
else:
|
|
901
|
+
cwd = float("NaN")
|
|
902
|
+
cwd_lst.append(cwd)
|
|
903
|
+
|
|
904
|
+
cwd_out = np.array(cwd_lst)
|
|
905
|
+
|
|
906
|
+
dates_cwd = pd.date_range(start=str(df['Year'].min()), end=str(df['Year'].max() + 1), freq="YE")
|
|
907
|
+
df_out_cwd = pd.DataFrame({'Year': dates_cwd.year, 'Indice_cwd': cwd_out} )
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
## 3. Save the DataFrame to an Excel file (.xlsx)
|
|
911
|
+
# Set index=False to avoid writing the DataFrame index as an extra column in Excel
|
|
912
|
+
df_out_cwd.to_excel(archivo_salida, index=False)
|
|
913
|
+
|
|
914
|
+
print("**************************")
|
|
915
|
+
print(f"✅ Índice CWD ejecutado exitosamente. Archivo guardado: {archivo_salida}")
|
|
916
|
+
print("**************************")
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
def sdii(archivo_entrada:str, archivo_salida:str):
|
|
921
|
+
"""
|
|
922
|
+
Índice climático: SDII [Índice de intensidad diaria simple]
|
|
923
|
+
Definición: Precipitación total anual dividida por el
|
|
924
|
+
número de días lluviosos del año.
|
|
925
|
+
Se considera un día lluvioso cuando PR >= 1 mm.
|
|
926
|
+
Unidades: mm/día
|
|
927
|
+
|
|
928
|
+
archivo_entrada: Ruta del archivo .txt de la estación
|
|
929
|
+
|
|
930
|
+
Formato del archivo de texto de la estación
|
|
931
|
+
Columnas: Año, Mes, Día, PR, TX, TN
|
|
932
|
+
|
|
933
|
+
1901 1 1 -99.9 -3.1 -6.8
|
|
934
|
+
1901 1 2 -99.9 -1.3 -3.6
|
|
935
|
+
1901 1 3 -99.9 -0.5 -7.9
|
|
936
|
+
1901 1 4 -99.9 -1.0 -9.1
|
|
937
|
+
1901 1 5 -99.9 -1.8 -8.4
|
|
938
|
+
|
|
939
|
+
Mes y Día deben ser números enteros.
|
|
940
|
+
Las unidades de PR (Precipitación) son milímetros.
|
|
941
|
+
Las unidades de TX (Temperatura Máxima)/TN (Temperatura Mínima) son grados Celsius.
|
|
942
|
+
|
|
943
|
+
archivo_salida: Ruta donde se guarda el archivo Excel (.xlsx)
|
|
944
|
+
|
|
945
|
+
"""
|
|
946
|
+
|
|
947
|
+
sdii_lst = [] # List to save index values per year
|
|
948
|
+
df = pd.read_csv(archivo_entrada, sep='\s+') # import
|
|
949
|
+
|
|
950
|
+
for y in df['Year'].unique():
|
|
951
|
+
df_year = df[df['Year'] == y]
|
|
952
|
+
if not df_year.empty:
|
|
953
|
+
wet_days_df = df_year[df_year['PR'] >= 1].dropna() # Days with daily precipitation >= 1 mm
|
|
954
|
+
annual_precip = wet_days_df['PR'].sum() # Calculates the sum of precipitation in wet days
|
|
955
|
+
wet_days = len(df_year[df_year['PR'] >= 1].dropna())
|
|
956
|
+
sdii = annual_precip / wet_days
|
|
957
|
+
sdii_lst.append(round(sdii, 2))
|
|
958
|
+
else:
|
|
959
|
+
sdii = float("NaN")
|
|
960
|
+
sdii_lst.append(sdii)
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
sdii_out = np.array(sdii_lst)
|
|
965
|
+
|
|
966
|
+
dates_sdii = pd.date_range(start=str(df['Year'].min()), end=str(df['Year'].max() + 1), freq="YE")
|
|
967
|
+
df_out_sdii = pd.DataFrame({'Year': dates_sdii.year, 'Indice_sdii': sdii_out} )
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
## 3. Save the DataFrame to an Excel file (.xlsx)
|
|
971
|
+
# Set index=False to avoid writing the DataFrame index as an extra column in Excel
|
|
972
|
+
df_out_sdii.to_excel(archivo_salida, index=False)
|
|
973
|
+
|
|
974
|
+
print("**************************")
|
|
975
|
+
print(f"✅ Índice SDII ejecutado exitosamente. Archivo guardado: {archivo_salida}")
|
|
976
|
+
print("**************************")
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def prcptot(archivo_entrada:str, archivo_salida:str):
|
|
980
|
+
"""
|
|
981
|
+
Índice climático: PRCPTOT [Precipitación total anual en días con lluvia (PR >= 1 mm)]
|
|
982
|
+
Definición: Precipitación total anual en días con lluvia.
|
|
983
|
+
Se considera que hay un día con lluvia cuando PR >= 1 mm.
|
|
984
|
+
Unidades: mm
|
|
985
|
+
|
|
986
|
+
archivo_entrada: Ruta del archivo .txt de la estación
|
|
987
|
+
|
|
988
|
+
Formato del archivo de texto de la estación
|
|
989
|
+
Columnas: Año, Mes, Día, PR, TX, TN
|
|
990
|
+
|
|
991
|
+
1901 1 1 -99.9 -3.1 -6.8
|
|
992
|
+
1901 1 2 -99.9 -1.3 -3.6
|
|
993
|
+
1901 1 3 -99.9 -0.5 -7.9
|
|
994
|
+
1901 1 4 -99.9 -1.0 -9.1
|
|
995
|
+
1901 1 5 -99.9 -1.8 -8.4
|
|
996
|
+
|
|
997
|
+
Mes y Día deben ser números enteros.
|
|
998
|
+
Las unidades de PR (Precipitación) son milímetros.
|
|
999
|
+
Las unidades de TX (Temperatura Máxima)/TN (Temperatura Mínima) son grados Celsius.
|
|
1000
|
+
|
|
1001
|
+
archivo_salida: Ruta donde se guarda el archivo Excel (.xlsx)
|
|
1002
|
+
|
|
1003
|
+
"""
|
|
1004
|
+
|
|
1005
|
+
prcptot_lst = [] # List to save index values per year
|
|
1006
|
+
df = pd.read_csv(archivo_entrada, sep='\s+') # import
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
for y in df['Year'].unique():
|
|
1010
|
+
df_year = df[df['Year'] == y]
|
|
1011
|
+
if not df_year.empty:
|
|
1012
|
+
wet_days_df = df_year[df_year >= 1].dropna() # Days with daily precipitation >= 1 mm
|
|
1013
|
+
prcptot = wet_days_df['PR'].sum() # Calculates the sum of precipitation in wet days
|
|
1014
|
+
prcptot_lst.append(round(prcptot, 2))
|
|
1015
|
+
else:
|
|
1016
|
+
prcptot = float("NaN")
|
|
1017
|
+
prcptot_lst.append(prcptot)
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
prcptot_out = np.array(prcptot_lst)
|
|
1021
|
+
|
|
1022
|
+
dates_prcptot = pd.date_range(start=str(df['Year'].min()), end=str(df['Year'].max() + 1), freq="YE")
|
|
1023
|
+
df_out_prcptot = pd.DataFrame({'Year': dates_prcptot.year, 'Indice_prcptot': prcptot_out} )
|
|
1024
|
+
|
|
1025
|
+
## 3. Save the DataFrame to an Excel file (.xlsx)
|
|
1026
|
+
# Set index=False to avoid writing the DataFrame index as an extra column in Excel
|
|
1027
|
+
df_out_prcptot.to_excel(archivo_salida, index=False)
|
|
1028
|
+
|
|
1029
|
+
print("**************************")
|
|
1030
|
+
print(f"✅ Índice PRCPTOT ejecutado exitosamente. Archivo guardado: {archivo_salida}")
|
|
1031
|
+
print("**************************")
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def r95p(archivo_entrada:str, archivo_salida:str, base_inicio: int, base_fin: int):
|
|
1036
|
+
"""
|
|
1037
|
+
Índice climático: R95p [Precipitación total anual cuando la precipitación es superior al percentil 95]
|
|
1038
|
+
Definición: Sea PRwj la cantidad de precipitación diaria en un día lluvioso
|
|
1039
|
+
w (PR ≥ 1,0 mm) en el período i y sea PRwn95 el percentil 95 de la precipitación en días lluviosos
|
|
1040
|
+
en el período base.
|
|
1041
|
+
Periodo base actual: 1991 - 2020
|
|
1042
|
+
Periodos anteriores: 1961 - 1990
|
|
1043
|
+
1981 - 2010
|
|
1044
|
+
Unidades: mm
|
|
1045
|
+
|
|
1046
|
+
archivo_entrada: Ruta del archivo .txt de la estación
|
|
1047
|
+
|
|
1048
|
+
Formato del archivo de texto de la estación
|
|
1049
|
+
Columnas: Año, Mes, Día, PR, TX, TN
|
|
1050
|
+
|
|
1051
|
+
1901 1 1 -99.9 -3.1 -6.8
|
|
1052
|
+
1901 1 2 -99.9 -1.3 -3.6
|
|
1053
|
+
1901 1 3 -99.9 -0.5 -7.9
|
|
1054
|
+
1901 1 4 -99.9 -1.0 -9.1
|
|
1055
|
+
1901 1 5 -99.9 -1.8 -8.4
|
|
1056
|
+
|
|
1057
|
+
Mes y Día deben ser números enteros.
|
|
1058
|
+
Las unidades de PR (Precipitación) son milímetros.
|
|
1059
|
+
Las unidades de TX (Temperatura Máxima)/TN (Temperatura Mínima) son grados Celsius.
|
|
1060
|
+
|
|
1061
|
+
archivo_salida: Ruta donde se guarda el archivo Excel (.xlsx)
|
|
1062
|
+
|
|
1063
|
+
"""
|
|
1064
|
+
|
|
1065
|
+
VARIABLE = 'PR'
|
|
1066
|
+
PERCENTILE = 95
|
|
1067
|
+
WET_THRESH = 1.0
|
|
1068
|
+
|
|
1069
|
+
def leap_year(y):
|
|
1070
|
+
return "True" if isleap(y) else "False"
|
|
1071
|
+
|
|
1072
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1073
|
+
# 1. Carga y preparación
|
|
1074
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1075
|
+
df = pd.read_csv(archivo_entrada, sep=r'\s+')
|
|
1076
|
+
df['Date'] = pd.to_datetime(df[['Year', 'Month', 'Day']])
|
|
1077
|
+
df = df.set_index('Date').sort_index()
|
|
1078
|
+
|
|
1079
|
+
df_wet = df[df[VARIABLE] >= WET_THRESH][[VARIABLE]].copy()
|
|
1080
|
+
bp = df_wet[(df_wet.index.year >= base_inicio) & (df_wet.index.year <= base_fin)].copy()
|
|
1081
|
+
|
|
1082
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1083
|
+
def window_values_with_initial(source_df, m, d, years_range):
|
|
1084
|
+
"""
|
|
1085
|
+
Replica exactamente el patrón del original:
|
|
1086
|
+
1. Filtra source_df en [ancla-2, ancla+2] (ancla = primer (m,d) en source_df)
|
|
1087
|
+
2. Itera sobre years_range y concatena la ventana de cada año
|
|
1088
|
+
Devuelve array 1-D con todos los valores acumulados.
|
|
1089
|
+
"""
|
|
1090
|
+
mask_md = (source_df.index.month == m) & (source_df.index.day == d)
|
|
1091
|
+
if not mask_md.any():
|
|
1092
|
+
return np.array([])
|
|
1093
|
+
|
|
1094
|
+
anchor = source_df.index[mask_md][0]
|
|
1095
|
+
d1_init = anchor - pd.DateOffset(days=2)
|
|
1096
|
+
d2_init = anchor + pd.DateOffset(days=2)
|
|
1097
|
+
|
|
1098
|
+
# Bloque inicial (ancla, primer año)
|
|
1099
|
+
mask_init = (source_df.index >= d1_init) & (source_df.index <= d2_init)
|
|
1100
|
+
parts = [source_df.loc[mask_init, VARIABLE].to_numpy(dtype=float)]
|
|
1101
|
+
|
|
1102
|
+
# Loop sobre los años del rango
|
|
1103
|
+
for yr in years_range:
|
|
1104
|
+
try:
|
|
1105
|
+
d1 = d1_init.replace(year=yr)
|
|
1106
|
+
d2 = d1 + pd.DateOffset(days=4)
|
|
1107
|
+
mask = (source_df.index >= d1) & (source_df.index <= d2)
|
|
1108
|
+
arr = source_df.loc[mask, VARIABLE].to_numpy(dtype=float)
|
|
1109
|
+
if len(arr) > 0:
|
|
1110
|
+
parts.append(arr)
|
|
1111
|
+
except Exception:
|
|
1112
|
+
pass # e.g. feb-29 en año no bisiesto
|
|
1113
|
+
|
|
1114
|
+
return np.concatenate(parts) if parts else np.array([])
|
|
1115
|
+
|
|
1116
|
+
def build_r95p(source_df, years_range_func):
|
|
1117
|
+
"""
|
|
1118
|
+
Construye la lista r95p igual que el original:
|
|
1119
|
+
itera m=1..12, d=1..32, solo añade entrada cuando d existe como día húmedo.
|
|
1120
|
+
years_range_func(source_df) → el rango de años para el loop.
|
|
1121
|
+
"""
|
|
1122
|
+
r95p_list = []
|
|
1123
|
+
first = source_df.index.year.unique()[0]
|
|
1124
|
+
last = source_df.index.year.unique()[-1]
|
|
1125
|
+
|
|
1126
|
+
for m in range(1, 13):
|
|
1127
|
+
df_month = source_df[source_df.index.month == m]
|
|
1128
|
+
for d in range(1, 32):
|
|
1129
|
+
if d not in df_month.index.day:
|
|
1130
|
+
continue
|
|
1131
|
+
|
|
1132
|
+
if m == 2 and d == 29:
|
|
1133
|
+
# Caso feb-29: loop while t < last_base desde primer bisiesto
|
|
1134
|
+
# → reproducimos con la lista de años bisiestos en [first, last)
|
|
1135
|
+
leap_years = [y for y in range(first, last) if isleap(y)]
|
|
1136
|
+
vals = window_values_with_initial(source_df, m, d, leap_years[1:] if leap_years else [])
|
|
1137
|
+
# El original comienza con t=primer_bisiesto y hace t+=4 mientras t<1990
|
|
1138
|
+
# → lo reproducimos con los bisiestos a partir del segundo
|
|
1139
|
+
elif m == 12 and d in [30, 31]:
|
|
1140
|
+
yr_range = range(first + 1, last + 1)
|
|
1141
|
+
vals = window_values_with_initial(source_df, m, d, yr_range)
|
|
1142
|
+
elif m == 1 and d in [1, 2]:
|
|
1143
|
+
yr_range = range(first, last)
|
|
1144
|
+
vals = window_values_with_initial(source_df, m, d, yr_range)
|
|
1145
|
+
else:
|
|
1146
|
+
yr_range = range(first, last) # ← EXCLUYE last, igual que el original
|
|
1147
|
+
vals = window_values_with_initial(source_df, m, d, yr_range)
|
|
1148
|
+
|
|
1149
|
+
if len(vals) > 0:
|
|
1150
|
+
r95p_list.append(float(np.percentile(vals, PERCENTILE)))
|
|
1151
|
+
|
|
1152
|
+
return r95p_list
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1156
|
+
# 3. Percentil de referencia (para años fuera del período base)
|
|
1157
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1158
|
+
# print("Calculando el percentil de referencia del período base...")
|
|
1159
|
+
r95p = build_r95p(bp, None)
|
|
1160
|
+
# print(f" → {len(r95p)} entradas en r95p")
|
|
1161
|
+
|
|
1162
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1163
|
+
# 4. Helper: calcular R95p de un año dado r95p como lista
|
|
1164
|
+
# Reproduce exactamente el original: itera días húmedos, compara con r95p[doy-1]
|
|
1165
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1166
|
+
def calc_r95p_year(df_year, pct_list, is_leap):
|
|
1167
|
+
pct_use = pct_list if is_leap else (pct_list[:59] + pct_list[60:])
|
|
1168
|
+
count = 0.0
|
|
1169
|
+
for ts in df_year.index:
|
|
1170
|
+
doy = ts.dayofyear
|
|
1171
|
+
val = float(df_year.loc[ts, VARIABLE])
|
|
1172
|
+
idx = doy - 1
|
|
1173
|
+
if idx < len(pct_use) and val > pct_use[idx]:
|
|
1174
|
+
count += val
|
|
1175
|
+
return count
|
|
1176
|
+
|
|
1177
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1178
|
+
# 5. Índice para años fuera del período base
|
|
1179
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1180
|
+
df_out = pd.concat([df_wet[df_wet.index.year < base_inicio],
|
|
1181
|
+
df_wet[df_wet.index.year > base_fin]])
|
|
1182
|
+
out_years, out_values = [], []
|
|
1183
|
+
|
|
1184
|
+
# print(f"Calculando R95p para {len(df_out.index.year.unique())} años fuera del período base...")
|
|
1185
|
+
for yr in sorted(df_out.index.year.unique()):
|
|
1186
|
+
yr_data = df_out[df_out.index.year == yr]
|
|
1187
|
+
out_values.append(calc_r95p_year(yr_data, r95p, leap_year(yr) == "True"))
|
|
1188
|
+
out_years.append(yr)
|
|
1189
|
+
|
|
1190
|
+
df_output_out = pd.DataFrame({"Date": out_years, "R95p (mm)": out_values})
|
|
1191
|
+
|
|
1192
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1193
|
+
# 6. Bootstrap para años del período base
|
|
1194
|
+
# Reproduce exactamente el original: para cada año o, itera sobre los 29
|
|
1195
|
+
# años restantes, construye df_bootstrap = año_p duplicado + boot (29 años),
|
|
1196
|
+
# calcula r95p_bootstrap con build_r95p, evalúa el año o y promedia.
|
|
1197
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1198
|
+
base_year_lst, index_base_list = [], []
|
|
1199
|
+
bp_years = sorted(bp.index.year.unique())
|
|
1200
|
+
|
|
1201
|
+
#print(f"Bootstrap para {len(bp_years)} años del período base...")
|
|
1202
|
+
for idx_o, o in enumerate(bp_years):
|
|
1203
|
+
df_boot = bp[bp.index.year != o]
|
|
1204
|
+
iter_years = sorted(df_boot.index.year.unique()) # 29 años
|
|
1205
|
+
index_lst = []
|
|
1206
|
+
|
|
1207
|
+
for p in iter_years:
|
|
1208
|
+
# df_bootstrap = año p + los 29 restantes (30 años total, con p duplicado)
|
|
1209
|
+
df_bootstrap = pd.concat([df_boot[df_boot.index.year == p], df_boot])
|
|
1210
|
+
df_bootstrap = df_bootstrap.sort_index()
|
|
1211
|
+
|
|
1212
|
+
r95p_bootstrap = build_r95p(df_bootstrap, None)
|
|
1213
|
+
|
|
1214
|
+
df_o = bp[bp.index.year == o]
|
|
1215
|
+
counter = calc_r95p_year(df_o, r95p_bootstrap, leap_year(o) == "True")
|
|
1216
|
+
index_lst.append(counter)
|
|
1217
|
+
|
|
1218
|
+
base_year_lst.append(o)
|
|
1219
|
+
index_base_list.append(float(np.mean(index_lst)))
|
|
1220
|
+
# print(f" [{idx_o+1:2d}/{len(bp_years)}] Año {o} → R95p = {index_base_list[-1]:.2f} mm")
|
|
1221
|
+
|
|
1222
|
+
df_output_base = pd.DataFrame({"Date": base_year_lst, "R95p (mm)": index_base_list})
|
|
1223
|
+
|
|
1224
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1225
|
+
# 7. Unir y exportar
|
|
1226
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1227
|
+
df_output = (pd.concat([df_output_base, df_output_out])
|
|
1228
|
+
.sort_values(by="Date")
|
|
1229
|
+
.reset_index(drop=True))
|
|
1230
|
+
|
|
1231
|
+
df_output.to_excel(archivo_salida, index=False)
|
|
1232
|
+
|
|
1233
|
+
print("**************************")
|
|
1234
|
+
print(f"\n✅ Índice R95p ejecutado exitosamente. Archivo guardado: {archivo_salida}")
|
|
1235
|
+
print("**************************")
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
def r99p(archivo_entrada:str, archivo_salida:str, base_inicio: int, base_fin: int):
|
|
1240
|
+
"""
|
|
1241
|
+
Índice climático: R99p [Precipitación total anual (PRCP) cuando PR > percentil 99]
|
|
1242
|
+
Definición: Sea PRwj la cantidad de precipitación diaria en un día lluvioso
|
|
1243
|
+
w (PR ≥ 1,0 mm) en el período i y sea PRwn99 el percentil 99 de la precipitación en días lluviosos
|
|
1244
|
+
en el período de referencia.
|
|
1245
|
+
Periodo base actual: 1991-2020
|
|
1246
|
+
Periodos anteriores: 1981-2010
|
|
1247
|
+
1961-1990
|
|
1248
|
+
|
|
1249
|
+
Unidades: mm
|
|
1250
|
+
|
|
1251
|
+
archivo_entrada: Ruta del archivo .txt de la estación
|
|
1252
|
+
|
|
1253
|
+
Formato del archivo de texto de la estación
|
|
1254
|
+
Columnas: Año, Mes, Día, PR, TX, TN
|
|
1255
|
+
|
|
1256
|
+
1901 1 1 -99.9 -3.1 -6.8
|
|
1257
|
+
1901 1 2 -99.9 -1.3 -3.6
|
|
1258
|
+
1901 1 3 -99.9 -0.5 -7.9
|
|
1259
|
+
1901 1 4 -99.9 -1.0 -9.1
|
|
1260
|
+
1901 1 5 -99.9 -1.8 -8.4
|
|
1261
|
+
|
|
1262
|
+
Mes y Día deben ser números enteros.
|
|
1263
|
+
Las unidades de PR (Precipitación) son milímetros.
|
|
1264
|
+
Las unidades de TX (Temperatura Máxima)/TN (Temperatura Mínima) son grados Celsius.
|
|
1265
|
+
|
|
1266
|
+
archivo_salida: Ruta donde se guarda el archivo Excel (.xlsx)
|
|
1267
|
+
|
|
1268
|
+
"""
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
VARIABLE = 'PR'
|
|
1272
|
+
PERCENTILE = 99
|
|
1273
|
+
WET_THRESH = 1.0
|
|
1274
|
+
|
|
1275
|
+
def leap_year(y):
|
|
1276
|
+
return "True" if isleap(y) else "False"
|
|
1277
|
+
|
|
1278
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1279
|
+
# 1. Carga y preparación
|
|
1280
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1281
|
+
df = pd.read_csv(archivo_entrada, sep=r'\s+')
|
|
1282
|
+
df['Date'] = pd.to_datetime(df[['Year', 'Month', 'Day']])
|
|
1283
|
+
df = df.set_index('Date').sort_index()
|
|
1284
|
+
|
|
1285
|
+
df_wet = df[df[VARIABLE] >= WET_THRESH][[VARIABLE]].copy()
|
|
1286
|
+
bp = df_wet[(df_wet.index.year >= base_inicio) & (df_wet.index.year <= base_fin)].copy()
|
|
1287
|
+
|
|
1288
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1289
|
+
def window_values_with_initial(source_df, m, d, years_range):
|
|
1290
|
+
"""
|
|
1291
|
+
Replica exactamente el patrón del original:
|
|
1292
|
+
1. Filtra source_df en [ancla-2, ancla+2] (ancla = primer (m,d) en source_df)
|
|
1293
|
+
2. Itera sobre years_range y concatena la ventana de cada año
|
|
1294
|
+
Devuelve array 1-D con todos los valores acumulados.
|
|
1295
|
+
"""
|
|
1296
|
+
mask_md = (source_df.index.month == m) & (source_df.index.day == d)
|
|
1297
|
+
if not mask_md.any():
|
|
1298
|
+
return np.array([])
|
|
1299
|
+
|
|
1300
|
+
anchor = source_df.index[mask_md][0]
|
|
1301
|
+
d1_init = anchor - pd.DateOffset(days=2)
|
|
1302
|
+
d2_init = anchor + pd.DateOffset(days=2)
|
|
1303
|
+
|
|
1304
|
+
# Bloque inicial (ancla, primer año)
|
|
1305
|
+
mask_init = (source_df.index >= d1_init) & (source_df.index <= d2_init)
|
|
1306
|
+
parts = [source_df.loc[mask_init, VARIABLE].to_numpy(dtype=float)]
|
|
1307
|
+
|
|
1308
|
+
# Loop sobre los años del rango
|
|
1309
|
+
for yr in years_range:
|
|
1310
|
+
try:
|
|
1311
|
+
d1 = d1_init.replace(year=yr)
|
|
1312
|
+
d2 = d1 + pd.DateOffset(days=4)
|
|
1313
|
+
mask = (source_df.index >= d1) & (source_df.index <= d2)
|
|
1314
|
+
arr = source_df.loc[mask, VARIABLE].to_numpy(dtype=float)
|
|
1315
|
+
if len(arr) > 0:
|
|
1316
|
+
parts.append(arr)
|
|
1317
|
+
except Exception:
|
|
1318
|
+
pass # e.g. feb-29 en año no bisiesto
|
|
1319
|
+
|
|
1320
|
+
return np.concatenate(parts) if parts else np.array([])
|
|
1321
|
+
|
|
1322
|
+
def build_r99p(source_df, years_range_func):
|
|
1323
|
+
"""
|
|
1324
|
+
Construye la lista r99p igual que el original:
|
|
1325
|
+
itera m=1..12, d=1..32, solo añade entrada cuando d existe como día húmedo.
|
|
1326
|
+
years_range_func(source_df) → el rango de años para el loop.
|
|
1327
|
+
"""
|
|
1328
|
+
r99p_list = []
|
|
1329
|
+
first = source_df.index.year.unique()[0]
|
|
1330
|
+
last = source_df.index.year.unique()[-1]
|
|
1331
|
+
|
|
1332
|
+
for m in range(1, 13):
|
|
1333
|
+
df_month = source_df[source_df.index.month == m]
|
|
1334
|
+
for d in range(1, 32):
|
|
1335
|
+
if d not in df_month.index.day:
|
|
1336
|
+
continue
|
|
1337
|
+
|
|
1338
|
+
if m == 2 and d == 29:
|
|
1339
|
+
# Caso feb-29: loop while t < last_base desde primer bisiesto
|
|
1340
|
+
# → reproducimos con la lista de años bisiestos en [first, last)
|
|
1341
|
+
leap_years = [y for y in range(first, last) if isleap(y)]
|
|
1342
|
+
vals = window_values_with_initial(source_df, m, d, leap_years[1:] if leap_years else [])
|
|
1343
|
+
# El original comienza con t=primer_bisiesto y hace t+=4 mientras t<1990
|
|
1344
|
+
# → lo reproducimos con los bisiestos a partir del segundo
|
|
1345
|
+
elif m == 12 and d in [30, 31]:
|
|
1346
|
+
yr_range = range(first + 1, last + 1)
|
|
1347
|
+
vals = window_values_with_initial(source_df, m, d, yr_range)
|
|
1348
|
+
elif m == 1 and d in [1, 2]:
|
|
1349
|
+
yr_range = range(first, last)
|
|
1350
|
+
vals = window_values_with_initial(source_df, m, d, yr_range)
|
|
1351
|
+
else:
|
|
1352
|
+
yr_range = range(first, last) # ← EXCLUYE last, igual que el original
|
|
1353
|
+
vals = window_values_with_initial(source_df, m, d, yr_range)
|
|
1354
|
+
|
|
1355
|
+
if len(vals) > 0:
|
|
1356
|
+
r99p_list.append(float(np.percentile(vals, PERCENTILE)))
|
|
1357
|
+
|
|
1358
|
+
return r99p_list
|
|
1359
|
+
|
|
1360
|
+
|
|
1361
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1362
|
+
# 3. Percentil de referencia (para años fuera del período base)
|
|
1363
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1364
|
+
# print("Calculando el percentil de referencia del período base...")
|
|
1365
|
+
r99p = build_r99p(bp, None)
|
|
1366
|
+
# print(f" → {len(r99p)} entradas en r99p")
|
|
1367
|
+
|
|
1368
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1369
|
+
# 4. Helper: calcular r99p de un año dado r99p como lista
|
|
1370
|
+
# Reproduce exactamente el original: itera días húmedos, compara con r99p[doy-1]
|
|
1371
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1372
|
+
def calc_r99p_year(df_year, pct_list, is_leap):
|
|
1373
|
+
pct_use = pct_list if is_leap else (pct_list[:59] + pct_list[60:])
|
|
1374
|
+
count = 0.0
|
|
1375
|
+
for ts in df_year.index:
|
|
1376
|
+
doy = ts.dayofyear
|
|
1377
|
+
val = float(df_year.loc[ts, VARIABLE])
|
|
1378
|
+
idx = doy - 1
|
|
1379
|
+
if idx < len(pct_use) and val > pct_use[idx]:
|
|
1380
|
+
count += val
|
|
1381
|
+
return count
|
|
1382
|
+
|
|
1383
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1384
|
+
# 5. Índice para años fuera del período base
|
|
1385
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1386
|
+
df_out = pd.concat([df_wet[df_wet.index.year < base_inicio],
|
|
1387
|
+
df_wet[df_wet.index.year > base_fin]])
|
|
1388
|
+
out_years, out_values = [], []
|
|
1389
|
+
|
|
1390
|
+
# print(f"Calculando r99p para {len(df_out.index.year.unique())} años fuera del período base...")
|
|
1391
|
+
for yr in sorted(df_out.index.year.unique()):
|
|
1392
|
+
yr_data = df_out[df_out.index.year == yr]
|
|
1393
|
+
out_values.append(calc_r99p_year(yr_data, r99p, leap_year(yr) == "True"))
|
|
1394
|
+
out_years.append(yr)
|
|
1395
|
+
|
|
1396
|
+
df_output_out = pd.DataFrame({"Date": out_years, "r99p (mm)": out_values})
|
|
1397
|
+
|
|
1398
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1399
|
+
# 6. Bootstrap para años del período base
|
|
1400
|
+
# Reproduce exactamente el original: para cada año o, itera sobre los 29
|
|
1401
|
+
# años restantes, construye df_bootstrap = año_p duplicado + boot (29 años),
|
|
1402
|
+
# calcula r99p_bootstrap con build_r99p, evalúa el año o y promedia.
|
|
1403
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1404
|
+
base_year_lst, index_base_list = [], []
|
|
1405
|
+
bp_years = sorted(bp.index.year.unique())
|
|
1406
|
+
|
|
1407
|
+
#print(f"Bootstrap para {len(bp_years)} años del período base...")
|
|
1408
|
+
for idx_o, o in enumerate(bp_years):
|
|
1409
|
+
df_boot = bp[bp.index.year != o]
|
|
1410
|
+
iter_years = sorted(df_boot.index.year.unique()) # 29 años
|
|
1411
|
+
index_lst = []
|
|
1412
|
+
|
|
1413
|
+
for p in iter_years:
|
|
1414
|
+
# df_bootstrap = año p + los 29 restantes (30 años total, con p duplicado)
|
|
1415
|
+
df_bootstrap = pd.concat([df_boot[df_boot.index.year == p], df_boot])
|
|
1416
|
+
df_bootstrap = df_bootstrap.sort_index()
|
|
1417
|
+
|
|
1418
|
+
r99p_bootstrap = build_r99p(df_bootstrap, None)
|
|
1419
|
+
|
|
1420
|
+
df_o = bp[bp.index.year == o]
|
|
1421
|
+
counter = calc_r99p_year(df_o, r99p_bootstrap, leap_year(o) == "True")
|
|
1422
|
+
index_lst.append(counter)
|
|
1423
|
+
|
|
1424
|
+
base_year_lst.append(o)
|
|
1425
|
+
index_base_list.append(float(np.mean(index_lst)))
|
|
1426
|
+
# print(f" [{idx_o+1:2d}/{len(bp_years)}] Año {o} → r99p = {index_base_list[-1]:.2f} mm")
|
|
1427
|
+
|
|
1428
|
+
df_output_base = pd.DataFrame({"Date": base_year_lst, "r99p (mm)": index_base_list})
|
|
1429
|
+
|
|
1430
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1431
|
+
# 7. Unir y exportar
|
|
1432
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1433
|
+
df_output = (pd.concat([df_output_base, df_output_out])
|
|
1434
|
+
.sort_values(by="Date")
|
|
1435
|
+
.reset_index(drop=True))
|
|
1436
|
+
|
|
1437
|
+
df_output.to_excel(archivo_salida, index=False)
|
|
1438
|
+
|
|
1439
|
+
print("**************************")
|
|
1440
|
+
print(f"\n ✅ Índice R99p ejecutado exitosamente. Archivo guardado: {archivo_salida}")
|
|
1441
|
+
print("**************************")
|
|
1442
|
+
|