Maison > Article > développement back-end > Collecte et traitement des données climatiques INMET-BDMEP
Les données climatiques jouent un rôle crucial dans plusieurs secteurs, contribuant aux études et aux prévisions qui ont un impact sur des domaines tels que l'agriculture, l'urbanisme et la gestion des ressources naturelles.
L'Institut National de Météorologie (INMET) propose mensuellement la Base de Données Météorologiques (BDMEP) sur son site internet. Cette base de données contient une série historique d'informations climatiques collectées par des centaines de stations de mesure réparties dans tout le Brésil. Dans BDMEP, vous trouverez des données détaillées sur les précipitations, la température, l'humidité de l'air et la vitesse du vent.
Avec des mises à jour horaires, ces données sont assez volumineuses, fournissant une base riche pour une analyse détaillée et une prise de décision éclairée.
Dans cet article, je vais montrer comment collecter et traiter les données climatiques de l'INMET-BDMEP. Nous collecterons les fichiers de données brutes disponibles sur le site INMET puis traiterons ces données pour faciliter l'analyse.
Pour atteindre les objectifs précités, il vous suffira d'avoir installé trois packages :
Pour installer les packages nécessaires, exécutez la commande suivante dans le terminal :
pip install httpx pandas tqdm
Si vous utilisez un environnement virtuel (venv) avec de la poésie par exemple, utilisez la commande suivante :
poetry add httpx pandas tqdm
L'adresse des fichiers de données BDMEP suit un modèle très simple. Le modèle est le suivant :
https://portal.inmet.gov.br/uploads/dadoshistoricos/{year}.zip
La seule partie qui change est le nom du fichier, qui est simplement l'année de référence des données. Mensuellement, le fichier de l'année la plus récente (en cours) est remplacé par des données mises à jour.
Cela facilite la création de code pour collecter automatiquement les fichiers de données de toutes les années disponibles.
En fait, les séries historiques disponibles commencent en l'an 2000.
Pour collecter les fichiers de données d'INMET-BDMEP, nous utiliserons la bibliothèque httpx pour effectuer des requêtes HTTP et la bibliothèque tqdm pour afficher une barre de progression conviviale dans le terminal.
Tout d'abord, importons les packages nécessaires :
import datetime as dt from pathlib import Path import httpx from tqdm import tqdm
Nous avons déjà identifié le modèle d'URL des fichiers de données INMET-BDMEP. Créons maintenant une fonction qui accepte une année comme argument et renvoie l'URL du fichier pour cette année.
def build_url(year): return f"https://portal.inmet.gov.br/uploads/dadoshistoricos/{year}.zip"
Pour vérifier si le fichier URL a été mis à jour, on peut utiliser les informations présentes dans l'en-tête renvoyées par une requête HTTP. Sur des serveurs bien configurés, nous pouvons demander uniquement cet en-tête avec la méthode HEAD. Dans ce cas, le serveur était bien configuré et nous pouvons utiliser cette méthode.
La réponse à la requête HEAD aura le format suivant :
Mon, 01 Sep 2024 00:01:00 GMT
Pour analyser cette date/heure, j'ai créé la fonction suivante en Python, qui accepte une chaîne et renvoie un objet datetime :
def parse_last_modified(last_modified: str) -> dt.datetime: return dt.datetime.strptime( last_modified, "%a, %d %b %Y %H:%M:%S %Z" )
Nous pouvons donc utiliser la date/heure de la dernière modification pour l'inclure dans le nom du fichier que nous allons télécharger, en utilisant l'interpolation de chaîne (f-strings) :
def build_local_filename(year: int, last_modified: dt.datetime) -> str: return f"inmet-bdmep_{year}_{last_modified:%Y%m%d}.zip"
De cette façon, vous pouvez facilement vérifier si le fichier contenant les données les plus récentes existe déjà sur notre système de fichiers local. Si le fichier existe déjà, le programme peut être terminé ; sinon, nous devons procéder à la collecte du fichier en faisant la demande au serveur.
La fonction download_year ci-dessous télécharge le fichier pour une année spécifique. Si le fichier existe déjà dans le répertoire de destination, la fonction revient simplement sans rien faire.
Notez comment nous utilisons tqdm pour afficher une barre de progression conviviale dans le terminal pendant le téléchargement du fichier.
def download_year( year: int, destdirpath: Path, blocksize: int = 2048, ) -> None: if not destdirpath.exists(): destdirpath.mkdir(parents=True) url = build_url(year) headers = httpx.head(url).headers last_modified = parse_last_modified(headers["Last-Modified"]) file_size = int(headers.get("Content-Length", 0)) destfilename = build_local_filename(year, last_modified) destfilepath = destdirpath / destfilename if destfilepath.exists(): return with httpx.stream("GET", url) as r: pb = tqdm( desc=f"{year}", dynamic_ncols=True, leave=True, total=file_size, unit="iB", unit_scale=True, ) with open(destfilepath, "wb") as f: for data in r.iter_bytes(blocksize): f.write(data) pb.update(len(data)) pb.close()
Maintenant que nous disposons de toutes les fonctions nécessaires, nous pouvons collecter les fichiers de données INMET-BDMEP.
En utilisant une boucle for, nous pouvons télécharger les fichiers pour toutes les années disponibles. Le code suivant fait exactement cela. De l'an 2000 à l'année en cours.
destdirpath = Path("data") for year in range(2000, dt.datetime.now().year + 1): download_year(year, destdirpath)
Avec les fichiers de données brutes INMET-BDMEP téléchargés, nous pouvons désormais lire et traiter les données.
Importons les packages nécessaires :
import csv import datetime as dt import io import re import zipfile from pathlib import Path import numpy as np import pandas as pd from tqdm import tqdm
Dans le fichier ZIP fourni par INMET, nous trouvons plusieurs fichiers CSV, un pour chaque station météo.
Porém, nas primeiras linhas desses arquivos CSV encontramos informações sobre a estação, como a região, a unidade federativa, o nome da estação, o código WMO, as coordenadas geográficas (latitude e longitude), a altitude e a data de fundação. Vamos extrair essas informações para usar como metadados.
A leitura dos arquivos será feita em duas partes: primeiro, será feita a leitura dos metadados das estações meteorológicas; depois, será feita a leitura dos dados históricos propriamente ditos.
Para extrair os metadados nas primeiras 8 linhas do arquivo CSV vamos usar o pacote embutido csv do Python.
Para entender a função a seguir é necessário ter um conhecimento um pouco mais avançado de como funciona handlers de arquivos (open), iteradores (next) e expressões regulares (re.match).
def read_metadata(filepath: Path | zipfile.ZipExtFile) -> dict[str, str]: if isinstance(filepath, zipfile.ZipExtFile): f = io.TextIOWrapper(filepath, encoding="latin-1") else: f = open(filepath, "r", encoding="latin-1") reader = csv.reader(f, delimiter=";") _, regiao = next(reader) _, uf = next(reader) _, estacao = next(reader) _, codigo_wmo = next(reader) _, latitude = next(reader) try: latitude = float(latitude.replace(",", ".")) except: latitude = np.nan _, longitude = next(reader) try: longitude = float(longitude.replace(",", ".")) except: longitude = np.nan _, altitude = next(reader) try: altitude = float(altitude.replace(",", ".")) except: altitude = np.nan _, data_fundacao = next(reader) if re.match("[0-9]{4}-[0-9]{2}-[0-9]{2}", data_fundacao): data_fundacao = dt.datetime.strptime( data_fundacao, "%Y-%m-%d", ) elif re.match("[0-9]{2}/[0-9]{2}/[0-9]{2}", data_fundacao): data_fundacao = dt.datetime.strptime( data_fundacao, "%d/%m/%y", ) f.close() return { "regiao": regiao, "uf": uf, "estacao": estacao, "codigo_wmo": codigo_wmo, "latitude": latitude, "longitude": longitude, "altitude": altitude, "data_fundacao": data_fundacao, }
Em resumo, a função read_metadata definida acima lê as primeiras oito linhas do arquivo, processa os dados e retorna um dicionário com as informações extraídas.
Aqui, finalmente, veremos como fazer a leitura do arquivo CSV. Na verdade é bastante simples. Basta usar a função read_csv do Pandas com os argumentos certos.
A seguir está exposto a chamada da função com os argumentos que eu determinei para a correta leitura do arquivo.
pd.read_csv( "arquivo.csv", sep=";", decimal=",", na_values="-9999", encoding="latin-1", skiprows=8, usecols=range(19), )
Primeiro é preciso dizer que o caractere separador das colunas é o ponto-e-vírgula (;), o separador de número decimal é a vírgula (,) e o encoding é latin-1, muito comum no Brasil.
Também é preciso dizer para pular as 8 primeiras linhas do arquivo (skiprows=8), que contém os metadados da estação), e usar apenas as 19 primeiras colunas (usecols=range(19)).
Por fim, vamos considerar o valor -9999 como sendo nulo (na_values="-9999").
Os nomes das colunas dos arquivos CSV do INMET-BDMEP são bem descritivos, mas um pouco longos. E os nomes não são consistentes entre os arquivos e ao longo do tempo. Vamos renomear as colunas para padronizar os nomes e facilitar a manipulação dos dados.
A seguinte função será usada para renomear as colunas usando expressões regulares (RegEx):
def columns_renamer(name: str) -> str: name = name.lower() if re.match(r"data", name): return "data" if re.match(r"hora", name): return "hora" if re.match(r"precipita[çc][ãa]o", name): return "precipitacao" if re.match(r"press[ãa]o atmosf[ée]rica ao n[íi]vel", name): return "pressao_atmosferica" if re.match(r"press[ãa]o atmosf[ée]rica m[áa]x", name): return "pressao_atmosferica_maxima" if re.match(r"press[ãa]o atmosf[ée]rica m[íi]n", name): return "pressao_atmosferica_minima" if re.match(r"radia[çc][ãa]o", name): return "radiacao" if re.match(r"temperatura do ar", name): return "temperatura_ar" if re.match(r"temperatura do ponto de orvalho", name): return "temperatura_orvalho" if re.match(r"temperatura m[áa]x", name): return "temperatura_maxima" if re.match(r"temperatura m[íi]n", name): return "temperatura_minima" if re.match(r"temperatura orvalho m[áa]x", name): return "temperatura_orvalho_maxima" if re.match(r"temperatura orvalho m[íi]n", name): return "temperatura_orvalho_minima" if re.match(r"umidade rel\. m[áa]x", name): return "umidade_relativa_maxima" if re.match(r"umidade rel\. m[íi]n", name): return "umidade_relativa_minima" if re.match(r"umidade relativa do ar", name): return "umidade_relativa" if re.match(r"vento, dire[çc][ãa]o", name): return "vento_direcao" if re.match(r"vento, rajada", name): return "vento_rajada" if re.match(r"vento, velocidade", name): return "vento_velocidade"
Agora que temos os nomes das colunas padronizados, vamos tratar a data/hora. Os arquivos CSV do INMET-BDMEP têm duas colunas separadas para data e hora. Isso é inconveniente, pois é mais prático ter uma única coluna de data/hora. Além disso existem inconsistências nos horários, que às vezes têm minutos e às vezes não.
As três funções a seguir serão usadas para criar uma única coluna de data/hora:
def convert_dates(dates: pd.Series) -> pd.DataFrame: dates = dates.str.replace("/", "-") return dates def convert_hours(hours: pd.Series) -> pd.DataFrame: def fix_hour_string(hour: str) -> str: if re.match(r"^\d{2}\:\d{2}$", hour): return hour else: return hour[:2] + ":00" hours = hours.apply(fix_hour_string) return hours def fix_data_hora(d: pd.DataFrame) -> pd.DataFrame: d = d.assign( data_hora=pd.to_datetime( convert_dates(d["data"]) + " " + convert_hours(d["hora"]), format="%Y-%m-%d %H:%M", ), ) d = d.drop(columns=["data", "hora"]) return d
Existe um problema com os dados do INMET-BDMEP que é a presença de linhas vazias. Vamos remover essas linhas vazias para evitar problemas futuros. O código a seguir faz isso:
# Remove empty rows empty_columns = [ "precipitacao", "pressao_atmosferica", "pressao_atmosferica_maxima", "pressao_atmosferica_minima", "radiacao", "temperatura_ar", "temperatura_orvalho", "temperatura_maxima", "temperatura_minima", "temperatura_orvalho_maxima", "temperatura_orvalho_minima", "umidade_relativa_maxima", "umidade_relativa_minima", "umidade_relativa", "vento_direcao", "vento_rajada", "vento_velocidade", ] empty_rows = data[empty_columns].isnull().all(axis=1) data = data.loc[~empty_rows]
Problema resolvido! (•̀ᴗ•́)و ̑̑
Para finalizar esta seção vamos encapsular o código de leitura e tratamento em funções.
Primeiro uma função para a leitura do arquivo CSV contino no arquivo comprimido.
def read_data(filepath: Path) -> pd.DataFrame: d = pd.read_csv( filepath, sep=";", decimal=",", na_values="-9999", encoding="latin-1", skiprows=8, usecols=range(19), ) d = d.rename(columns=columns_renamer) # Remove empty rows empty_columns = [ "precipitacao", "pressao_atmosferica", "pressao_atmosferica_maxima", "pressao_atmosferica_minima", "radiacao", "temperatura_ar", "temperatura_orvalho", "temperatura_maxima", "temperatura_minima", "temperatura_orvalho_maxima", "temperatura_orvalho_minima", "umidade_relativa_maxima", "umidade_relativa_minima", "umidade_relativa", "vento_direcao", "vento_rajada", "vento_velocidade", ] empty_rows = d[empty_columns].isnull().all(axis=1) d = d.loc[~empty_rows] d = fix_data_hora(d) return d
Tem um problema com a função acima. Ela não lida com arquivos ZIP.
Criamos, então, a função read_zipfile para a leitura de todos os arquivos contidos no arquivo ZIP. Essa função itera sobre todos os arquivos CSV no arquivo zipado, faz a leitura usando a função read_data e os metadados usando a função read_metadata, e depois junta os dados e os metadados em um único DataFrame.
def read_zipfile(filepath: Path) -> pd.DataFrame: data = pd.DataFrame() with zipfile.ZipFile(filepath) as z: files = [zf for zf in z.infolist() if not zf.is_dir()] for zf in tqdm(files): d = read_data(z.open(zf.filename)) meta = read_metadata(z.open(zf.filename)) d = d.assign(**meta) data = pd.concat((data, d), ignore_index=True) return data
No final, basta usar essa última função definida (read_zipfile) para fazer a leitura dos arquivos ZIP baixados do site do INMET. (. ❛ ᴗ ❛.)
df = reader.read_zipfile("inmet-bdmep_2023_20240102.zip") # 100%|████████████████████████████████████████████████████████████████████████████████| 567/567 [01:46<00:00, 5.32it/s] df # precipitacao pressao_atmosferica pressao_atmosferica_maxima ... longitude altitude data_fundacao # 0 0.0 887.7 887.7 ... -47.925833 1160.96 2000-05-07 # 1 0.0 888.1 888.1 ... -47.925833 1160.96 2000-05-07 # 2 0.0 887.8 888.1 ... -47.925833 1160.96 2000-05-07 # 3 0.0 887.8 887.9 ... -47.925833 1160.96 2000-05-07 # 4 0.0 887.6 887.9 ... -47.925833 1160.96 2000-05-07 # ... ... ... ... ... ... ... ... # 342078 0.0 902.6 903.0 ... -51.215833 963.00 2019-02-15 # 342079 0.0 902.2 902.7 ... -51.215833 963.00 2019-02-15 # 342080 0.2 902.3 902.3 ... -51.215833 963.00 2019-02-15 # 342081 0.0 903.3 903.3 ... -51.215833 963.00 2019-02-15 # 342082 0.0 903.8 903.8 ... -51.215833 963.00 2019-02-15 # [342083 rows x 26 columns] df.to_csv("inmet-bdmep_2023.csv", index=False) # Salvando o DataFrame em um arquivo CSV
Para finalizar, nada mais satisfatório do que fazer gráficos com os dados que coletamos e tratamos. ヾ(≧▽≦*)o
Nessa parte uso o R com o pacote tidyverse para fazer um gráfico combinando a temperatura horária e a média diária em São Paulo.
library(tidyverse) dados <- read_csv("inmet-bdmep_2023.csv") print(names(dados)) # [1] "precipitacao" "pressao_atmosferica" # [3] "pressao_atmosferica_maxima" "pressao_atmosferica_minima" # [5] "radiacao" "temperatura_ar" # [7] "temperatura_orvalho" "temperatura_maxima" # [9] "temperatura_minima" "temperatura_orvalho_maxima" # [11] "temperatura_orvalho_minima" "umidade_relativa_maxima" # [13] "umidade_relativa_minima" "umidade_relativa" # [15] "vento_direcao" "vento_rajada" # [17] "vento_velocidade" "data_hora" # [19] "regiao" "uf" # [21] "estacao" "codigo_wmo" # [23] "latitude" "longitude" # [25] "altitude" "data_fundacao" print(unique(dados$regiao)) # [1] "CO" "N" "NE" "SE" "S" print(unique(dados$uf)) # [1] "DF" "GO" "MS" "MT" "AC" "AM" "AP" "AL" "BA" "CE" "MA" "PB" "PE" "PI" "RN" # [16] "SE" "PA" "RO" "RR" "TO" "ES" "MG" "RJ" "SP" "PR" "RS" "SC" dados_sp <- dados |> filter(uf == "SP") # Temperatura horária em São Paulo dados_sp_h <- dados_sp |> group_by(data_hora) |> summarise( temperatura_ar = mean(temperatura_ar, na.rm = TRUE), ) # Temperatura média diária em São Paulo dados_sp_d <- dados_sp |> group_by(data = floor_date(data_hora, "day")) |> summarise( temperatura_ar = mean(temperatura_ar, na.rm = TRUE), ) # Gráfico combinando temperatura horária e média diária em São Paulo dados_sp_h |> ggplot(aes(x = data_hora, y = temperatura_ar)) + geom_line( alpha = 0.5, aes( color = "Temperatura horária" ) ) + geom_line( data = dados_sp_d, aes( x = data, y = temperatura_ar, color = "Temperatura média diária" ), linewidth = 1 ) + labs( x = "Data", y = "Temperatura (°C)", title = "Temperatura horária e média diária em São Paulo", color = "Variável" ) + theme_minimal() + theme(legend.position = "top") ggsave("temperatura_sp.png", width = 16, height = 8, dpi = 300)
Neste texto mostrei como coletar e tratar os dados climáticos do INMET-BDMEP. Os dados coletados são muito úteis para estudos e previsões nas mais variadas áreas. Com os dados tratados, é possível fazer análises e gráficos como o que mostrei no final.
Espero que tenha gostado do texto e que tenha sido útil para você.
J'ai créé un package Python avec les fonctions que j'ai montrées dans ce texte. Le package est disponible dans mon référentiel Git. Si vous le souhaitez, vous pouvez télécharger le package et utiliser les fonctions dans votre propre code.
Dépôt Git : https://github.com/dankkom/inmet-bdmep-data
(~ ̄▽ ̄)~
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!