기후 데이터는 농업, 도시 계획, 천연자원 관리 등의 분야에 영향을 미치는 연구 및 예측을 지원하는 등 여러 부문에서 중요한 역할을 합니다.
국립기상연구소(INMET)에서는 매달 홈페이지를 통해 기상데이터베이스(BDMEP)를 제공하고 있습니다. 이 데이터베이스에는 브라질 전역에 분산된 수백 개의 측정소에서 수집된 일련의 기후 정보가 포함되어 있습니다. BDMEP에서는 강우량, 기온, 대기습도, 풍속에 대한 자세한 데이터를 확인할 수 있습니다.
매시간 업데이트되는 이 데이터는 상당히 방대하여 상세한 분석과 정보에 입각한 의사 결정을 위한 풍부한 기반을 제공합니다.
이번 게시물에서는 INMET-BDMEP에서 기후 데이터를 수집하고 처리하는 방법을 보여 드리겠습니다. INMET 웹사이트에서 제공되는 원시 데이터 파일을 수집한 후 분석을 용이하게 하기 위해 이 데이터를 처리합니다.
앞서 언급한 목표를 달성하려면 세 가지 패키지만 설치하면 됩니다.
필요한 패키지를 설치하려면 터미널에서 다음 명령을 실행하세요.
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-문자열)을 사용하여 마지막 수정 날짜/시간을 다운로드할 파일 이름에 포함할 수 있습니다.
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 파일에는 기상 관측소마다 하나씩 여러 개의 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中文网其他相关文章!