Open pachi opened 2 years ago
Podemos hacer un workflow con Snakemake.
Creo que los procesos serían:
1 - Script src/select_input.py
#11 :
Seleccionar datos de entrada relevantes en archivo data/ign/MUNICIPIOS.csv
y generar archivo de datos data/output/Municipios.csv
que contenga:
COD_INE
, COD_PROV
, PROVINCIA
, NOMBRE_ACTUAL
, LONGITUD_ETRS89
, LATITUD_ETRS89
, ALTITUD
, ARCHIVO_TMY
2 - Script src/download_TMY.py
#3 :
Para cada municipio, descargar, si no existe ya, el archivo TMY del JRC y guardarlo en data/output/tmy/${COD_INE}_${NOMBRE_ACTUAL}.tmy
3 - Script src/compute_indicators.py
#4 :
Para cada municipio, leer el archivo data/output/tmy/$[COD_INE}_${NOMBRE_ACTUAL}.tmy
y calcular:
GD
, n_N
, SCI
, SCV
, ZCI_TMY
, ZCV_TMY
a partir de datos del TMYZC_CTE_2019
, ZCI_CTE_2019
y ZCV_CTE_2019
a partir de la tabla del CTE DB-HEZCI_DIFF
y ZCV_DIFF
.Combinar con los datos del archivo de municipios y generar data/output/Results.csv
.
4 - Script src/plot_results.py
#5 :
Graficar:
@pachi, si quieres editar que en el paso 2 guardamos la información en data/output/tmy/${COD_INE}_${NOMBRE_ACTUAL}.tmy
?
@pachi, si quieres editar que en el paso 2 guardamos la información en
data/output/tmy/${COD_INE}_${NOMBRE_ACTUAL}.tmy
?
Sí, pero fíjate que además la extensión es .csv, porque en el PVGIS hay una opción de guardar el TMY como JSON y me pareció así más claro el proceso para alguien que lo siga "manualmente".
Vale, lo había cambiado a .tmy
por respetar el que habías puesto al inicio y no sabía si era una convención, pero totalmente de acuerdo en que le pedimos que descargue el formato csv y poner eso como extensión es lo más claro.
@pachi, he visto que has añadido una imagen de binder". Me parece genial porque así estamos dando 3 alternativas de reproducibilidad: conda, snakemake y el propio binder.
Si quieres podríamos añadirlo en el README en el apartado 2. (Cómo usar este flujo de trabajo) a continuación de la opción de con snakemake.
Perfecto. Estoy revisando el README. He visto que has añadido un índice, así que añado también eso y actualizo el índice.
He conseguido también lanzar el snakemake pero no nos vale el wildcard. En el issue correspondiente pongo más información
Justo estaba añadiendo alguna cosa, he hecho un último push al README pero lo dejo ya porque no quiero entrar en conflicto con lo que estuvieras haciendo. Yo voy a dejar corriéndolo en mi local con la opción de conda y después de comer te digo si ha funcionado;)
Guay
Ahora mismo tenemos en el Snakefile
estas reglas que usan un wildcard {dataset}
:
rule download_data:
input:
"data/output/Municipios.csv"
output:
"data/output/tmy/{dataset}.csv"
script:
"src/download_TMY.py"
rule compute_indicators:
input:
"data/output/Municipios.csv"
"data/output/tmy/{dataset}.csv"
output:
"data/output/Results.csv"
script:
"src/compute_indicators.py"
Pero Snakemake no es capaz de deducir los archivos de entrada a partir de la salida de otras reglas. Hay información sobre cómo establecer esas dependencias de forma maś concreta, tomándolas directamente del archivo de entrada, aunque en nuestro caso es un archivo generado y no es tan sencillo (sin duplicar la lógica de definir los nombres de archivo).
No sé si esto podría valer (basado en):
tmy_files = glob_wildcards("data/output/tmy/{tmy}.csv")
rule download_data: input: "data/output/Municipios.csv" output: "data/output/tmy/{tmy}.csv" script: "src/download_TMY.py"
rule compute_indicators: input: "data/output/Municipios.csv" expand("data/output/tmy/{tmy}.csv", tmy=tmy_files) output: "data/output/Results.csv" script: "src/compute_indicators.py"
Lo de conda está funcionando (pasos 1 - 6a) pero todavía está descargando ficheros.
Creo que no valdría, porque en el estado inicial no existen los archivos csv, solo tras dowload data. Una opción que estoy viendo si es posible es que dowload_data y compute_indicators no sean dos reglas sino una secuencia única. Esto lo "malo" que tiene es que es poco eficiente si hay interrupciones en las descargas, pero es más simple.
Ok, la otra es preguntarle directamente a @jmoldon para no tener que cambiar código..
A mi es que todavía no se me han descargado todos los tmys pero si tú has probado los scripts 3º y 4º y te funcionan en local, con la opción de conda
no va a haber problema.
Es verdad que probablemente el flujo no sea el más eficiente, seguro que podríamos haber paralelizado el cómputo de los indicadores conforme se van descargando los tmys pero creo que intentar resolver esto con lo que tenemos sería lo ideal. Quizás nos pueda echar una mano :grimacing:
Otra opción ingeniosa:
rule download_data:
input:
"data/output/Municipios.csv"
output:
"data/output/flag.txt"
shell:
"python3 src/download_TMY.py && touch data/output/flag.txt"
rule compute_indicators:
input:
"data/output/Municipios.csv",
"data/output/flag.txt"
output:
"data/output/Results.csv"
script:
"src/compute_indicators.py"
Se podría incluso generar un archivo de log.txt en el propio script download_TMY.py que sirviese de bandera y que solo se generase si se descargan todos los archivos (usando el finally de un try / catch / finally).
Voy a probar esa idea, que parece lo más sencillo.
Sí, lo de paralelizar el cálculo de indicadores lo pensé, pero es que tarda tan poco tiempo el cálculo que parecía mucho más complicado montar un archivo a partir de trozos que esperar a que se acabase todo el proceso.
Funciona! He subido los cambios. Ahora voy a sacar alguna gráfica y completo el workflow.
Fenomenal!!! :clap: Cuando acabe la parte de conda te actualizo
El flujo con conda también funciona (a falta de generar algún plot). He visto también el jpynb que has subido, qué chulada de plots los de la severidad de las zonas climáticas!!
En la versión fa33bde he añadido el notebook que realiza las gráficas. No tenemos material todavía para hacer un paper, pero algo es algo. He actualizado también las instrucciones para ejecutar el notebook y generar las gráficas desde la línea de comandos, sin abrir el archivo y he añadido la dependencia de jupyter_core y nbformat a environment.yaml
Qué maravilla de hilo, así da gusto! Muy interesantes las discusiones. Os comento algunas cosas sobre wildcards, que son algo complicadas de asimilar, y de hecho a mí aún me cuesta identificarlas en todos los casos. Reabro el hilo.
Las wildcards de snakemake no son magiccards, se pueden encadenar muchas rules con wildcards arbitrarias, pero tarde o temprano se tienen que definir, por ejemplo una de las reglas finales puede ser del tipo expand("dataset}/a.txt",dataset=DATASETS)
, donde DATASETS
es una lista bien definida. Otra opción es usar glob_wildcards, para que el script busque todos los archivos que existan en un lugar, pero con eso perdéis un poco de control de lo que se procesa, aunque también sería posible).
En vuestro caso, una opción puede ser definir en el mismo Snakefile una lista de MUNICIPIOS
, que puede ser usada en el último input que necesite ese listado. El resto de inputs/outputs puede usar la wildcard genérica {municipios}, y dejar solo un expand
al final. Algo de este estilo creo que funcionaría:
import unicodedata
import pandas as pd
conda: "environment.yaml"
def removeAccents(text):
text = str(text)
normalized = unicodedata.normalize("NFKD", text)
normalized = "".join([c for c in normalized if not unicodedata.combining(c)])
return normalized.lower()
MUNICIPIOS = list(pd.read_csv("data/output/Municipios.csv")['NOMBRE_ACTUAL'].apply(removeAccents))
rule plot:
input: "data/output/Results.csv"
output:
"data/output/plots/zci-diff-hist.png",
"data/output/plots/zcv-diff-hist.png",
"data/output/plots/zc-tmy.png",
notebook: "notebooks/graficas.ipynb"
rule select_input:
input: "data/ign/MUNICIPIOS.csv"
output: "data/output/Municipios.csv"
script: "src/select_input.py"
rule download_data:
input: "data/output/Municipios.csv"
output: "data/output/{municipio}.txt"
shell: "python3 src/download_TMY.py {municipio}"
rule compute_indicators:
input: expand("data/output/{municipio}.csv", municipio=MUNICIPIOS)
output: "data/output/Results.csv"
script: "src/compute_indicators.py"
Notas:
removeAccents
os lo pongo de propina, podéis ignorarlo si queréis, lo único que hace es quitar acentos y poner todo en minúscula, que me gusta más para nombres de archivosMUNICIPIOS
es una lista de nombres de municitios, que se usa en el input de compute_indicators
download_TMY.py
, de manera que reciba un nombre de un único municipio, y la función solo descargue los datos de ese municipio en concreto. Esto tiene varias ventajas:
downloads_done.txt
, y además así os aseguráis de que las descargas se han hecho porque cada archivo tiene que existir. También os permite ejecutar el workflow en varias tandas (no necesariamente todo a la vez).Espero que la información sea útil. El código que he puesto no es definitivo, solo he cambiado las partes clave que creo que hacen funcionar el workflow, pero tal vez tangáis que retocar otras partes para adecuarlo a vuestro caso
Jeje, gracias, @Jmoldon, la verdad es que hemos trabajado muy bien.
El otro día estuvimos hablando de este problema de la paralelización de la descarga en la reunión en Zoom porque da pena que algo que, en principio, resulta tan estúpidamente paralelo no esté implementado de esa forma.
Hay, sin embargo, alguna dificultad en este caso porque:
el servidor limita las llamadas a la API a 30/s
Se podría limitar el número de procesos a < 30 y poner un sleep en cada proceso de 1s para asegurar que no excedemos las peticiones permitidas.
la conexión me resultó poco fiable.
Es posible que sea un problema con mi conexión (con la activación de la pantalla de bloqueo) pero también que sea poco fiable el servidor. Pensé en mejorar la configuración de la sesión para aumentar los retrys, usar exponential backoff y eliminar el keepalive, por si está saturado el servidor y mejorar la reconexión
Como la descarga es algo que, en principio, se hace solo una vez y, si se corta la conexión, se puede retomar en cuanquier momento sin que se vuelvan a descargar los archivos disponibles, no parecía una ganancia clara para la complejidad que añadía. Pero es cierto que igual merece la pena problar al menos la parte 1.
Respecto al Snakemake, no estoy seguro de que el código que propones funcione, porque MUNICIPIOS depende de Municipios.csv
pero se evalúa antes de la regla que lo genera. La resolución de archivos es previa a la ejecución de las reglas... así que creo que no funcionaría.
Como las reglas son código ejecutable, seguramente se podría llevar ese código a download_data pero no estoy seguro de la manera más sencilla (aparte del archivo de bandera) para señalizar que esa regla dependa de la que genera el archivo Municipios.csv
.
Podemos probar también a ver si funciona la solución propuesta o a explorar la alternativa.
Una ventaja de tenerlo paralelizado (cada descarga es una ejecución independiente de la rule) es que mientras se van descargando nuevos datos, snakemake está libre para ir haciendo cosas con los municipios que ya están disponibles. En vuestro caso, no importa mucho porque el siguiente paso necesita todos los municipios. Pero si tuvierais que procesar más cada municipio antes de juntarlos, esto sería lo más eficiente. Otra ventaja de que sea snakemake quien gestione cada archivo, es que os asegurais de que al final estén todos descargados. Si el input es downloads_done.txt, podría ser que ese archivo exista pero no asegura que todo se haya descargado.
Por el límite de descargas de la API no es un problema, hay un parámetro --max-jobs-per-second
(https://snakemake.readthedocs.io/en/v5.1.4/executable.html), aunque nunca lo he probado. También se podrían limitar cuántos jobs se lanzan en paralelo con la opción que hay en el enlace que puse, o con threads
y creo que hay alguna opción más en la gestión de resources
que hace snakemake. Yo intentaría evitar la opción de usar sleep
, pero si no hay más remedio, ok. También hay opciones para reintentar una descarga si falla (--restart-times) y algunas otras utilidades.
Es verdad, así no funciona el código, me equivoqué y al definir MUNICIPIOS el archivo a leer debería ser el que hay en data/ing/MUNICIPIOS.csv
no el data/output/Municipios.csv
, claro. Lo importante es que es buena práctica que todo el workflow quede fijado con una lista inicial que defina todo lo que se tiene que procesar explícitamente.
Por el límite de descargas de la API no es un problema, hay un parámetro
--max-jobs-per-second
(https://snakemake.readthedocs.io/en/v5.1.4/executable.html), aunque nunca lo he probado.
Sí, esto parece lo que necesitamos y así evitamos el sleep
.
En estos casos mi duda siempre es, ¿hacemos el proyecto dependiente de Snakemake?
En estos momentos Snakemake es una dependencia opcional, ya que tenemos los flujos de trabajo también disponibles simplemente con un entorno Conda y ejecución manual de los script y, como prácticamente solo tiene dependencias de Python y sus librerías, el entorno Conda es meramente para asegurar un entorno e ejecución que es fácil de describir. Pero si Snakemake asume la paralelización, o bien duplicamos esa funcionalidad en los script, o pasa a ser una dependencia siempre necesaria.
En todo caso, creo que intentaré probar a hacerlo ya que me parece útil para otros casos. También me interesa mucho la posibilidad de que se pueda hacer con Snakemake un balanceo de carga en un cluster (no para este caso).
Definir flujo de trabajo con script de SnakeMake