Maison  >  Article  >  développement back-end  >  Collecte et traitement des données climatiques INMET-BDMEP

Collecte et traitement des données climatiques INMET-BDMEP

Barbara Streisand
Barbara Streisandoriginal
2024-09-30 06:11:021021parcourir

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.

1. Packages Python requis

Pour atteindre les objectifs précités, il vous suffira d'avoir installé trois packages :

  • httpx pour faire des requêtes HTTP
  • Pandas pour lire et traiter les données
  • tqdm pour afficher une barre de progression conviviale dans le terminal pendant que le programme télécharge ou lit des fichiers

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

2. Collecte de fichiers

2.1 Modèle d'URL de fichier

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.

2.2 Stratégie de collecte

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()

2.3 Collecte de fichiers

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)

3. Lecture et traitement des données

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

3.1 Structure du fichier

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.

3.2 Leitura dos dados com pandas

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.

3.2.1 Metadados

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.

3.2.2 Dados históricos

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").

3.3 Tratamento dos dados

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! (•̀ᴗ•́)و ̑̑

3.4 Encapsulando em funções

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

4. Gráfico de exemplo

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)


Coletando e Tratando os Dados Climáticos do INMET-BDMEP


Temperatura horária e média diária em São Paulo em 2023

5. Conclusão

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!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn