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.
@@ -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
+