Os dados climáticos desempenham um papel crucial em diversos setores, auxiliando em estudos e previsões que impactam áreas como agricultura, planejamento urbano, e gestão de recursos naturais.
O Instituto Nacional de Meteorologia (INMET) oferece, mensalmente, o Banco de Dados Meteorológicos (BDMEP) em seu site. Este banco de dados contém uma série histórica de informações climáticas coletadas por centenas de estações de medição distribuídas por todo o Brasil. No BDMEP, você encontrará dados detalhados sobre pluviometria, temperatura, umidade do ar e velocidade do vento.
Com atualizações horárias, esses dados são bastante volumosos, proporcionando uma base rica para análises detalhadas e tomadas de decisão informadas.
Neste post vou mostrar como coletar e tratar os dados climáticos do INMET-BDMEP. Vamos coletar os arquivos de dados brutos disponíveis no site do INMET e depois vamos tratar esses dados para facilitar a análise.
Para alcançar os objetivos mencionados será preciso ter instalado apenas três pacotes:
Para instalar os pacotes necessários execute o seguinte comando no terminal:
pip install httpx pandas tqdm
Se estiver usando um ambiente virtual (venv) com poetry, por exemplo, use o seguinte comando:
poetry add httpx pandas tqdm
O endereço dos arquivos de dados do BDMEP seguem um padrão bem simples. O padrão é o seguinte:
https://portal.inmet.gov.br/uploads/dadoshistoricos/{year}.zip
A única parte que muda é o nome do arquivo que é, simplesmente, o ano de referência dos dados. Mensalmente, o arquivo para o ano mais recente (corrente) é substituído com dados atualizados.
Assim fica fácil criar um código para coletar automaticamente os arquivos de dados de todos os anos disponíveis.
Aliás, a série histórica disponível começa no ano 2000.
Para a coleta dos arquivos de dados do INMET-BDMEP vamos usar a biblioteca httpx para fazer as requisições HTTP e a biblioteca tqdm para mostrar uma barra de progresso amigável no terminal.
Primeiro, vamos importar os pacotes necessários:
import datetime as dt from pathlib import Path import httpx from tqdm import tqdm
Já identificamos o padrão da URL dos arquivos de dados do INMET-BDMEP. Agora vamos criar uma função que aceita um ano como argumento e devolve a URL do arquivo referente a esse ano.
def build_url(year): return f"https://portal.inmet.gov.br/uploads/dadoshistoricos/{year}.zip"
Para checar se o arquivo da URL foi atualizado podemos utilizar a informação presente no cabeçalho retornado por uma requisição HTTP. Em servidores bem configurados, podemos pedir apenas esse cabeçalho com o método HEAD. Nesse o servidor foi bem configurado e podemos usar desse método.
A resposta à requisição HEAD terá o seguinte formato:
Mon, 01 Sep 2024 00:01:00 GMT
Para parsear essa data/hora fiz a seguinte função em Python, que aceita uma string e devolve um objeto datetime:
def parse_last_modified(last_modified: str) -> dt.datetime: return dt.datetime.strptime( last_modified, "%a, %d %b %Y %H:%M:%S %Z" )
Assim, podemos usar a data/hora da última modificação para incluir no nome do arquivo que vamos baixar, usando interpolação de strings (f-strings):
def build_local_filename(year: int, last_modified: dt.datetime) -> str: return f"inmet-bdmep_{year}_{last_modified:%Y%m%d}.zip"
Dessa forma, é possível verificar facilmente se o arquivo com os dados mais recentes já existe no nosso sistema de arquivos local. Se o arquivo já existir, o programa pode ser encerrado; caso contrário, devemos prosseguir com a coleta do arquivo, fazendo a requisição ao servidor.
A função download_year abaixo faz o download do arquivo referente a um ano específico. Se o arquivo já existe no diretório de destino, a função simplesmente retorna sem fazer nada.
Note como usamos o tqdm para mostrar uma barra de progresso amigável no terminal enquanto o arquivo é baixado.
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()
Agora que temos todas as funções necessárias, podemos fazer a coleta dos arquivos de dados do INMET-BDMEP.
Usando um loop for podemos baixar os arquivos de todos os anos disponíveis. O código a seguir faz exatamente isso. Começando do ano 2000 até o ano corrente.
destdirpath = Path("data") for year in range(2000, dt.datetime.now().year 1): download_year(year, destdirpath)
Com os arquivos de dados brutos do INMET-BDMEP baixados, agora podemos fazer a leitura e o tratamento dos dados.
Vamos importar os pacotes necessários:
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
Dentro do arquivo ZIP disponibilizado pelo INMET encontramos diversos arquivos CSV, um para cada estação meteorológica.
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:464. 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 filter(uf == "SP") # Temperatura horária em São Paulo dados_sp_h 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 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)
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ê.
Criei um pacote Python com as funções que mostrei neste texto. O pacote está disponível no meu repositório Git. Se quiser, pode baixar o pacote e usar as funções no seu próprio código.
Repositório Git: https://github.com/dankkom/inmet-bdmep-data
(~ ̄▽ ̄)~
부인 성명: 제공된 모든 리소스는 부분적으로 인터넷에서 가져온 것입니다. 귀하의 저작권이나 기타 권리 및 이익이 침해된 경우 자세한 이유를 설명하고 저작권 또는 권리 및 이익에 대한 증거를 제공한 후 이메일([email protected])로 보내주십시오. 최대한 빨리 처리해 드리겠습니다.
Copyright© 2022 湘ICP备2022001581号-3