ホームページ >バックエンド開発 >Python チュートリアル >INMET-BDMEP 気候データの収集と処理
気候データはいくつかの分野で重要な役割を果たしており、農業、都市計画、天然資源管理などの分野に影響を与える研究や予測に役立ちます。
国立気象研究所 (INMET) は、ウェブサイトで気象データベース (BDMEP) を毎月提供しています。このデータベースには、ブラジル全土に分布する何百もの測定局によって収集された一連の過去の気候情報が含まれています。 BDMEP では、降雨量、気温、湿度、風速に関する詳細なデータが見つかります。
このデータは 1 時間ごとに更新されるため、非常に大量であり、詳細な分析と情報に基づいた意思決定のための豊富な基礎を提供します。
この投稿では、INMET-BDMEP から気候データを収集して処理する方法を説明します。 INMET Web サイトで入手可能な生データ ファイルを収集し、分析を容易にするためにこのデータを処理します。
前述の目的を達成するには、次の 3 つのパッケージをインストールするだけで済みます。
必要なパッケージをインストールするには、ターミナルで次のコマンドを実行します。
pip install httpx pandas tqdm
たとえば、詩で仮想環境 (venv) を使用している場合は、次のコマンドを使用します。
poetry add httpx pandas tqdm
BDMEP データ ファイルのアドレスは、非常に単純なパターンに従います。パターンは次のとおりです。
https://portal.inmet.gov.br/uploads/dadoshistoricos/{year}.zip
変更される部分はファイル名だけであり、これは単にデータの参照年です。毎月、最新 (現在) 年のファイルが更新されたデータに置き換えられます。
これにより、利用可能なすべての年からデータ ファイルを自動的に収集するコードを簡単に作成できます。
実際、利用可能な歴史シリーズは 2000 年に始まります。
INMET-BDMEP からデータ ファイルを収集するには、httpx ライブラリを使用して HTTP リクエストを作成し、tqdm ライブラリを使用してターミナルにわかりやすい進行状況バーを表示します。
まず、必要なパッケージをインポートしましょう:
import datetime as dt from pathlib import Path import httpx from tqdm import tqdm
INMET-BDMEP データ ファイルの URL パターンはすでに特定されています。次に、年を引数として受け取り、その年のファイルの URL を返す関数を作成しましょう。
def build_url(year): return f"https://portal.inmet.gov.br/uploads/dadoshistoricos/{year}.zip"
URL ファイルが更新されたかどうかを確認するには、HTTP リクエストによって返されたヘッダーに存在する情報を使用できます。適切に構成されたサーバーでは、HEAD メソッドを使用してこのヘッダーのみをリクエストできます。この場合、サーバーは適切に構成されているため、この方法を使用できます。
HEAD リクエストに対する応答は次の形式になります:
Mon, 01 Sep 2024 00:01:00 GMT
この日付/時刻を解析するために、Python で次の関数を作成しました。この関数は 文字列 を受け取り、日時オブジェクトを返します。
def parse_last_modified(last_modified: str) -> dt.datetime: return dt.datetime.strptime( last_modified, "%a, %d %b %Y %H:%M:%S %Z" )
したがって、文字列補間 (f-strings) を使用して、最終変更の日付/時刻をダウンロードするファイルの名前に含めることができます。
def build_local_filename(year: int, last_modified: dt.datetime) -> str: return f"inmet-bdmep_{year}_{last_modified:%Y%m%d}.zip"
この方法により、最新のデータを含むファイルがローカル ファイル システムに既に存在するかどうかを簡単に確認できます。ファイルがすでに存在する場合は、プログラムを終了できます。それ以外の場合は、ファイルの収集を続行し、サーバーにリクエストを行う必要があります。
以下の download_year 関数は、特定の年のファイルをダウンロードします。ファイルが宛先ディレクトリにすでに存在する場合、関数は何もせずに単に戻ります。
ファイルのダウンロード中に tqdm を使用してターミナルにわかりやすい進行状況バーを表示する方法に注目してください。
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()
必要な関数がすべて揃ったので、INMET-BDMEP データ ファイルを収集できます。
for ループを使用すると、利用可能なすべての年のファイルをダウンロードできます。次のコードはまさにそれを行います。 2000 年から今年まで。
destdirpath = Path("data") for year in range(2000, dt.datetime.now().year + 1): download_year(year, destdirpath)
INMET-BDMEP 生データ ファイルをダウンロードすると、データを読み取り、処理できるようになります。
必要なパッケージをインポートしましょう:
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
INMET が提供する ZIP ファイル内には、気象観測所ごとに 1 つずつ、複数の CSV ファイルがあります。
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ê.
このテキストで示した関数を含む Python パッケージを作成しました。パッケージは私の Git リポジトリから入手できます。必要に応じて、パッケージをダウンロードして、独自のコードで関数を使用できます。
Git リポジトリ: https://github.com/dankkom/inmet-bdmep-data
(~ ̄▽ ̄)~
以上がINMET-BDMEP 気候データの収集と処理の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。