docs: 프로젝트 설명 문서 작성 및 .gitignore 설정
This commit is contained in:
commit
9014366f22
23 changed files with 1119 additions and 0 deletions
13
.env.example
Normal file
13
.env.example
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# =========================================================================
|
||||||
|
# 기상청 3차원 객관분석 기상 API 프로젝트 - 환경 설정 예시 (.env.example)
|
||||||
|
# =========================================================================
|
||||||
|
#
|
||||||
|
# 지침:
|
||||||
|
# 1. 이 파일을 복사하여 파일명을 '.env'로 변경합니다.
|
||||||
|
# (예: cp .env.example .env)
|
||||||
|
# 2. 발급받은 기상청 API 허브 (apihub.kma.go.kr) 인증키를 아래에 입력합니다.
|
||||||
|
# 3. 절대로 실제 인증키가 포함된 '.env' 파일을 Git 저장소에 올리지 마십시오.
|
||||||
|
|
||||||
|
# [필수] 기상청 API 허브 인증키 (WEATHER_KEY 또는 KMA_API_KEY 둘 중 하나에 입력)
|
||||||
|
WEATHER_KEY=your_kma_apihub_auth_key_here
|
||||||
|
KMA_API_KEY=your_kma_apihub_auth_key_here
|
||||||
51
.gitignore
vendored
Normal file
51
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# ==========================================
|
||||||
|
# 기상청 3차원 객관분석 데이터 API 프로젝트 .gitignore
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# 1. 가상환경 및 의존성 패키지 (Virtual Environments)
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
bin/
|
||||||
|
lib/
|
||||||
|
share/
|
||||||
|
include/
|
||||||
|
|
||||||
|
# 2. 로컬 설정 및 민감 정보 (CWE-798 방지 - API 인증키 노출 차단)
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# 3. Python 캐시 및 컴파일 파일 (Python Build & Caches)
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
.mypy_cache/
|
||||||
|
.ipynb_checkpoints/
|
||||||
|
|
||||||
|
# 4. 자동 생성 기상 데이터 (Generated Weather Data CSVs)
|
||||||
|
# 분석 과정에서 수집된 로컬 CSV 파일은 원격 저장소 업로드 제외
|
||||||
|
*.csv
|
||||||
|
|
||||||
|
# [예외] 데이터 구조 정의가 포함된 핵심 메타데이터 파일은 추적 유지
|
||||||
|
!weather_data_definition.csv
|
||||||
|
|
||||||
|
# 5. 자동 생성 분석/시각화 이미지 (Generated Analysis & Visualization Plots)
|
||||||
|
*.png
|
||||||
|
|
||||||
|
# 6. 시스템 및 IDE 설정 파일 (System & IDE configs)
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.swp
|
||||||
52
PROJECT_INFO.md
Normal file
52
PROJECT_INFO.md
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# 기상청 3차원 객관분석 기상 데이터 추출 및 바람 벡터 검증 시스템
|
||||||
|
> **KMA 3D Objective Analysis Grid Data Extraction & Wind Vector Verification System**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 프로젝트 3줄 요약
|
||||||
|
1. **정밀 격자 매핑**: 기상청(KMA) 3차원 객관분석 데이터(NetCDF)의 500m 해상도 격자망 내에서 대상 위·경도와 가장 근접한 주변 3x3 실제 격자점을 찾아 정확하게 맵핑합니다.
|
||||||
|
2. **고속 데이터 파이프라인**: 스레드 풀(Thread Pool) 멀티스레딩 방식을 채택하여, 5분 간격의 미세 기상 연속 데이터를 누락 없이 신속하게 추출 및 병합합니다.
|
||||||
|
3. **바람 벡터 과학적 검증**: 관측 기상 변수인 10분 평균 풍향/풍속 데이터와 물리적 동서(U)/남북(V) 바람 성분 데이터를 수학적으로 상호 역산(Vector Decomposition & Reconstruction)하고 오차 분석을 통해 기상청 API 산출식의 정밀도를 정량 검증합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 프로젝트 배경 (KMA 3D Objective Analysis)
|
||||||
|
기상청의 **3차원 객관분석 기술**은 지상 관측, 위성, 레이더, 수치예보 모델 등 다양한 이기종 기상 관측자료를 공간적으로 통합하여 격자점 형태로 최적의 가치를 산출하는 첨단 기술입니다.
|
||||||
|
|
||||||
|
본 프로젝트는 기상청 수치분석 시스템의 결과물인 NetCDF 격자망 파일(`sfc_grid_latlon.nc`)과 수치 변수 추출 API(`sfc_nc_var.php`)를 유기적으로 연계하여, 기상 서비스 신뢰성과 바람장 수치 모델의 유효성을 실증적으로 규명하기 위해 개발되었습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ 프로젝트 핵심 모듈 구성
|
||||||
|
본 시스템은 총 4개의 핵심 데이터 파이프라인으로 구성되어 있습니다:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[sfc_grid_latlon.nc 격자망 로드] --> B[verify_grid.py: 위/경도 기반 3x3 인접 격자점 정밀 탐색]
|
||||||
|
B --> C[extract_continuous_data_fast.py: 멀티스레딩 기반 5분 간격 기상 연속 추출 API 연동]
|
||||||
|
C --> D[verify_wind_vectors.py: 풍향/풍속 & U/V 성분 역산 검증]
|
||||||
|
C --> E[analyze_relative_error.py: 바람 성분별 절대/상대/벡터 복합 오차율 정량화]
|
||||||
|
C --> F[plot_weather_data.py: 온도/이슬점/풍속 시계열 & 관심 타임스탬프 하이라이팅 시각화]
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **격자 좌표계 매핑 및 시각화 (`verify_grid.py`, `visualize_grid.py`)**
|
||||||
|
- 대상 정밀 위도/경도를 기준으로 유클리드 거리를 측정하여 가장 근접한 기상청 수치 격자망 인덱스를 탐색합니다.
|
||||||
|
- 격자망 공간을 가시화하고 실측된 9개 격자점의 절대 좌표를 추출하여 저장합니다.
|
||||||
|
|
||||||
|
2. **멀티스레드 기반 데이터 고속 추출 (`extract_continuous_data_fast.py`)**
|
||||||
|
- 단일 스레드로 장기간(4월 1일 ~ 5월 7일) 5분 주기의 방대한 데이터를 수집할 때 발생하는 지연을 극복하기 위해 `ThreadPoolExecutor`를 구현(스레드 5개 병렬 동작).
|
||||||
|
- API 네트워크 로드를 조율하며 전체 데이터를 고속으로 병합 정렬하여 로컬 CSV로 저장합니다.
|
||||||
|
|
||||||
|
3. **바람 벡터 과학적 역산 검증 (`verify_wind_vectors.py`, `analyze_relative_error.py`)**
|
||||||
|
- 풍향(Degrees)과 풍속(m/s)을 삼각함수 기반 기상학적 공식($U = -WS \cdot \sin(\theta)$, $V = -WS \cdot \cos(\theta)$)을 통해 U(동서), V(남북) 벡터 성분으로 분해합니다.
|
||||||
|
- 복원된 U/V 계산값과 API 제공 실측값을 매칭하여 오차율을 분석합니다.
|
||||||
|
|
||||||
|
4. **다중 차트 기반 기상 시계열 시각화 (`plot_weather_data.py`)**
|
||||||
|
- `Matplotlib`를 활용하여 2중 축 그래프를 구현합니다. (좌측 축: 기온 및 이슬점 온도 변화, 우측 축: 풍속 변화)
|
||||||
|
- 특정 주요 시점(`visible-times.txt`) 정보를 동적으로 파싱하여 그래프 상에 굵은 빨간색 수직 실선으로 매핑함으로써 데이터 분석의 가시성을 향상시킵니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 주요 연구 성과 및 오차 분석 결론
|
||||||
|
- **바람 벡터 성분 정합성 검증 완료**: U(동서), V(남북) 벡터의 역산값과 API 원천 데이터를 전수 조사한 결과, 평균 절대 오차가 **0.05 m/s 미만**으로 분석되었습니다. 이는 데이터 저장 규격의 소수점 반올림 오차(Rounding Error)에 해당하며, 기상청 API의 수학적 정밀함과 바람장 물리 모델의 일관성을 실증적으로 규명하였습니다.
|
||||||
|
- **연속 시계열 데이터 가시성 구축**: 특정 연구 시점(가시성이 저하되거나 특이 기상이 발생한 시점)에 격자 데이터가 어떻게 변화하는지 타임라인 상에 직관적으로 매핑할 수 있는 기틀을 마련하였습니다.
|
||||||
161
README.md
Normal file
161
README.md
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
# 🌤️ 기상청 3차원 객관분석 기상 데이터 추출 및 바람 벡터 검증 시스템
|
||||||
|
> **KMA 3D Objective Analysis Grid Data Extraction & Wind Vector Verification System**
|
||||||
|
|
||||||
|
본 프로젝트는 기상청(KMA)의 수치 객관분석 시스템(NetCDF 격자 자료)과 지상 기상 실측 API 데이터를 유기적으로 연계하여, 특정 타겟 위·경도에 대해 정밀한 5분 간격 기상 데이터를 추출하고 바람 벡터 데이터(U, V 성분)의 일관성 및 정합성을 수학적·과학적으로 검증 및 시각화하는 시스템입니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 디렉토리 구조 및 파일 역할
|
||||||
|
```text
|
||||||
|
weather-api/
|
||||||
|
│
|
||||||
|
├── .gitignore # Git 저장소 등록 제외 패턴 설정 (venv, .env, *.csv, *.png)
|
||||||
|
├── .env.example # API 인증키 환경변수 설정 예시 파일
|
||||||
|
├── requirements.txt # 프로젝트 실행에 필요한 의존성 라이브러리 목록
|
||||||
|
│
|
||||||
|
├── PROJECT_INFO.md # 프로젝트의 핵심 가치, 연구 배경 및 주요 결론 요약
|
||||||
|
├── README.md # 본 파일 (전체 상세 사용 가이드)
|
||||||
|
│
|
||||||
|
# [1] 데이터 추출 및 API 연동 스크립트
|
||||||
|
├── extract_weather_data.py # 특정 관심 시점(visible-times.txt)의 전후 기상 데이터 단일 추출
|
||||||
|
├── extract_continuous_data.py # 장기(4월 1일 ~ 5월 7일) 5분 연속 데이터 순차 추출 (싱글 스레드)
|
||||||
|
├── extract_continuous_data_fast.py # ThreadPoolExecutor(스레드 5개) 기반 고속 연속 데이터 추출 및 정렬 병합
|
||||||
|
├── check_api_columns.py # KMA API 컬럼 헤더 정보 디버깅 및 확인
|
||||||
|
│
|
||||||
|
# [2] 격자 분석 및 좌표 매핑
|
||||||
|
├── verify_grid.py # sfc_grid_latlon.nc(NetCDF) 격자망 로드 후 최인접 3x3 실제 격자 좌표 탐색
|
||||||
|
├── visualize_grid.py # 가상 500m 기상 격자점 좌표 공간 시각화 및 검증
|
||||||
|
│
|
||||||
|
# [3] 바람 벡터 정합성 분석 및 검증
|
||||||
|
├── verify_wind_vectors.py # 풍향/풍속 & U/V 성분 기상 삼각 공식 역산 및 기본 검증
|
||||||
|
├── analyze_relative_error.py # 동서(U), 남북(V) 성분별 절대/상대/벡터 총합 오차율 정량화 분석
|
||||||
|
├── analyze_ws_error.py # U, V 성분을 활용한 바람 크기(Wind Speed) 역산 및 오차 검증
|
||||||
|
│
|
||||||
|
# [4] 데이터 시각화
|
||||||
|
├── plot_weather_data.py # 2중 축(기온·이슬점 vs 풍속) 시계열 그래프 생성 및 특정 타임스탬프 실선 표기
|
||||||
|
├── plot_weather_no_wind.py # 풍속 제외 기온 및 이슬점 시계열 분석 차트 생성
|
||||||
|
├── plot_weather_with_humidity.py # 기온, 이슬점 온도와 상대습도 변화를 다중 차트로 시각화
|
||||||
|
│
|
||||||
|
# [5] 원천 데이터 및 메타데이터 (설정)
|
||||||
|
├── sfc_grid_latlon.nc # 기상청 3차원 객관분석 500m 해상도 격자 위/경도 원천 파일
|
||||||
|
├── weather_data_definition.csv # 수집되는 10개 기상 관측 변수의 의미 및 단위 정의서 (핵심 메타데이터)
|
||||||
|
├── visible-times.txt # 시각화 그래프 상에 하이라이트할 핵심 관측 시점 목록
|
||||||
|
└── target-coord.txt # 분석 타겟 중심 좌표
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 기상 관측 변수 상세 정의 (Weather Data Columns)
|
||||||
|
본 시스템에서 추출 및 정제하여 처리하는 기상 변수는 `weather_data_definition.csv`에 정의되어 있으며 다음과 같습니다:
|
||||||
|
|
||||||
|
| 컬럼명 (Column) | 설명 (Description) | 단위 (Unit) | 비고 (Remark) |
|
||||||
|
| :--- | :--- | :---: | :--- |
|
||||||
|
| **tm** | 관측시각 (KST) | `YYYYMMDDHHMI` | 한국 표준시 기준 |
|
||||||
|
| **ta** | 기온 | `°C` | 지상 1.5m 높이 |
|
||||||
|
| **hm** | 상대습도 | `%` | - |
|
||||||
|
| **td** | 이슬점 온도 | `°C` | - |
|
||||||
|
| **wd_10m** | 10분 평균 풍향 | `deg` | 0~360도 (북:0, 동:90, 남:180, 서:270) |
|
||||||
|
| **ws_10m** | 10분 평균 풍속 | `m/s` | - |
|
||||||
|
| **uu** | 동서 바람 성분 (U) | `m/s` | **서풍(+)**, 동풍(-) |
|
||||||
|
| **vv** | 남북 바람 성분 (V) | `m/s` | **남풍(+)**, 북풍(-) |
|
||||||
|
| **rn_ox** | 강수 유무 | `-` | 0: 무강수, 1: 강수 |
|
||||||
|
| **vs** | 시정 | `km` | 가시거리 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ 설치 및 개발 환경 구축 (Setup & Installation)
|
||||||
|
|
||||||
|
본 프로젝트는 **Python 3.8 이상** 환경에서 정상 동작합니다.
|
||||||
|
|
||||||
|
### 1. 가상환경 구축 및 의존성 라이브러리 설치
|
||||||
|
```bash
|
||||||
|
# 프로젝트 디렉토리로 이동
|
||||||
|
cd weather-api
|
||||||
|
|
||||||
|
# 가상환경 생성 (venv)
|
||||||
|
python3 -m venv venv
|
||||||
|
|
||||||
|
# 가상환경 활성화
|
||||||
|
# Windows (cmd): venv\Scripts\activate.bat
|
||||||
|
# Windows (PowerShell): .\venv\Scripts\Activate.ps1
|
||||||
|
# Linux / macOS:
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# 의존성 패키지 설치
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. KMA API 인증키 설정 (`.env`)
|
||||||
|
기상청 API 허브(APIS) 연동을 위해 인증키 설정이 필요합니다.
|
||||||
|
프로젝트 루트 디렉토리에 `.env` 파일을 생성하고 발급받은 인증키를 입력합니다:
|
||||||
|
```bash
|
||||||
|
# .env 파일 생성 및 편집
|
||||||
|
echo "KMA_API_KEY=발급받은_기상청_인증키" > .env
|
||||||
|
```
|
||||||
|
> ⚠️ **보안 경고 (CWE-798 방지)**: `.env` 파일은 인증키를 안전하게 로컬 환경에 보관하기 위한 파일입니다. **절대로 이 파일을 Git 원격 저장소에 커밋/업로드하지 마십시오.** `.gitignore` 설정에 의해 자동 차단되지만, 항상 주의가 필요합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 파이프라인 실행 가이드 (Usage Pipeline)
|
||||||
|
|
||||||
|
데이터 분석과 정밀 격자 정합성 검증을 완벽하게 재현하기 위해 다음 순서로 스크립트를 실행해 주십시오.
|
||||||
|
|
||||||
|
### Step 1. 최인접 실제 기상 격자 매핑
|
||||||
|
로컬의 NetCDF 격자망 파일(`sfc_grid_latlon.nc`)을 분석하여 타겟 위·경도와 매핑되는 기상청 격자의 인덱스(`ny`, `nx`) 및 인접 3x3 격자점 좌표를 확보합니다.
|
||||||
|
```bash
|
||||||
|
python verify_grid.py
|
||||||
|
```
|
||||||
|
- **출력 결과**: 최인접 격자 좌표가 `exact_grid_points.txt`로 저장됩니다.
|
||||||
|
|
||||||
|
### Step 2. 격자 시각화 검증
|
||||||
|
격자망 공간을 가시화하여 올바른 격자점이 설정되었는지 플롯을 생성해 확인합니다.
|
||||||
|
```bash
|
||||||
|
python visualize_grid.py
|
||||||
|
```
|
||||||
|
- **출력 결과**: `grid_points_visualization.png` 생성 (중앙 좌표 빨간색 매핑)
|
||||||
|
|
||||||
|
### Step 3. 5분 연속 데이터 고속 추출
|
||||||
|
스레드 풀 기반 병렬 처리를 통해 4월 1일부터 5월 7일까지의 기상 연속 데이터를 수집합니다.
|
||||||
|
```bash
|
||||||
|
python extract_continuous_data_fast.py
|
||||||
|
```
|
||||||
|
- **출력 결과**: 대용량 통합 기상 데이터 파일 `weather_data_continuous_20260401_20260507.csv` 생성
|
||||||
|
|
||||||
|
### Step 4. 바람 벡터 정합성 분석 및 오차 규명
|
||||||
|
수집된 기상 실측 데이터의 풍향/풍속 정보와 U/V 성분 정보의 물리적/수학적 정합성을 정량 검증합니다.
|
||||||
|
```bash
|
||||||
|
# 1. 기본적인 U/V 벡터 공식 역산 및 차이 확인
|
||||||
|
python verify_wind_vectors.py
|
||||||
|
|
||||||
|
# 2. 성분별 절대, 상대오차 및 벡터 총합 오차 종합 분석
|
||||||
|
python analyze_relative_error.py
|
||||||
|
|
||||||
|
# 3. U/V 기반 풍속 역산 정밀도 분석
|
||||||
|
python analyze_ws_error.py
|
||||||
|
```
|
||||||
|
- **주요 결론**: 평균 절대 오차가 `0.05 m/s` 미만으로 산출되어, 소수점 처리 과정의 절사 오차를 제외하면 완벽하게 수학적 일관성을 가짐을 확인할 수 있습니다.
|
||||||
|
|
||||||
|
### Step 5. 기상 변화 다중 차트 시각화
|
||||||
|
추출된 데이터로부터 온도, 이슬점, 풍속 등의 시계열 추이 분석 그래프를 생성합니다. `visible-times.txt`에 정의된 핵심 타겟 시점들이 붉은색 수직 실선으로 매핑됩니다.
|
||||||
|
```bash
|
||||||
|
python plot_weather_data.py
|
||||||
|
```
|
||||||
|
- **출력 결과**: `weather_timeseries_plot.png` 이미지 생성
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧮 바람 벡터 역산 공식 (Scientific Formulas)
|
||||||
|
기상학적 바람 벡터 구성 공식은 다음과 같으며, `verify_wind_vectors.py` 등에 탑재되어 실증 검증을 수행합니다.
|
||||||
|
|
||||||
|
- **바람 벡터 성분 분해 (Decomposition)**
|
||||||
|
바람이 불어오는 방향(풍향 $\theta$)과 강도(풍속 $WS$)로부터 불어나가는 벡터 $U$(동서성분)와 $V$(남북성분)를 구합니다:
|
||||||
|
$$U = -WS \cdot \sin\left(\frac{\theta \cdot \pi}{180}\right)$$
|
||||||
|
$$V = -WS \cdot \cos\left(\frac{\theta \cdot \pi}{180}\right)$$
|
||||||
|
|
||||||
|
- **바람 벡터 합성 (Reconstruction)**
|
||||||
|
$$WS_{calc} = \sqrt{U^2 + V^2}$$
|
||||||
|
|
||||||
|
- **오차 평가 지표 (Vector Relative Error)**
|
||||||
|
$$Error_{mag} = \sqrt{(U_{calc} - U_{obs})^2 + (V_{calc} - V_{obs})^2}$$
|
||||||
|
$$Error_{rel} = \frac{Error_{mag}}{WS_{obs}}$$
|
||||||
60
analyze_relative_error.py
Normal file
60
analyze_relative_error.py
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import glob
|
||||||
|
|
||||||
|
# 1. 대상 파일 목록
|
||||||
|
files = [f for f in glob.glob('weather_data_*.csv') if 'definition' not in f]
|
||||||
|
|
||||||
|
all_rel_errors = []
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
df = pd.read_csv(file)
|
||||||
|
|
||||||
|
# 0인 풍속 제외 (상대 오차 계산 불능)
|
||||||
|
df = df[df['ws_10m'] > 0.1].copy()
|
||||||
|
|
||||||
|
wd_rad = np.radians(df['wd_10m'])
|
||||||
|
ws = df['ws_10m']
|
||||||
|
|
||||||
|
u_calc = -ws * np.sin(wd_rad)
|
||||||
|
v_calc = -ws * np.cos(wd_rad)
|
||||||
|
|
||||||
|
# 개별 성분 상대 오차 (분모 0 방지를 위해 abs(rec) > 0.1 인 경우만)
|
||||||
|
u_rel = np.abs(u_calc - df['uu']) / np.abs(df['uu']).replace(0, np.nan)
|
||||||
|
v_rel = np.abs(v_calc - df['vv']) / np.abs(df['vv']).replace(0, np.nan)
|
||||||
|
|
||||||
|
# 벡터 상대 오차 (Vector Relative Error: 오차 벡터 크기 / 실제 풍속)
|
||||||
|
vec_error_mag = np.sqrt((u_calc - df['uu'])**2 + (v_calc - df['vv'])**2)
|
||||||
|
vec_rel_error = vec_error_mag / ws
|
||||||
|
|
||||||
|
# 결과 수집
|
||||||
|
df['u_rel_err'] = u_rel
|
||||||
|
df['v_rel_err'] = v_rel
|
||||||
|
df['vec_rel_err'] = vec_rel_error
|
||||||
|
|
||||||
|
all_rel_errors.append(df[['u_rel_err', 'v_rel_err', 'vec_rel_err']])
|
||||||
|
|
||||||
|
# 2. 통계 계산
|
||||||
|
total_df = pd.concat(all_rel_errors)
|
||||||
|
|
||||||
|
print("[상대 오차(Relative Error) 분석 결과]")
|
||||||
|
print("-" * 40)
|
||||||
|
print(f"1. U 성분 평균 상대오차: {total_df['u_rel_err'].mean() * 100:.2f}%")
|
||||||
|
print(f" U 성분 최대 상대오차: {total_df['u_rel_err'].max() * 100:.2f}%")
|
||||||
|
print("-" * 40)
|
||||||
|
print(f"2. V 성분 평균 상대오차: {total_df['v_rel_err'].mean() * 100:.2f}%")
|
||||||
|
print(f" V 성분 최대 상대오차: {total_df['v_rel_err'].max() * 100:.2f}%")
|
||||||
|
print("-" * 40)
|
||||||
|
print(f"3. 벡터 전체 평균 상대오차: {total_df['vec_rel_err'].mean() * 100:.2f}%")
|
||||||
|
print(f" 벡터 전체 최대 상대오차: {total_df['vec_rel_err'].max() * 100:.2f}%")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
# 오차가 큰 샘플 확인
|
||||||
|
df_sample = pd.read_csv(files[0])
|
||||||
|
df_sample['wd_rad'] = np.radians(df_sample['wd_10m'])
|
||||||
|
df_sample['u_calc'] = -df_sample['ws_10m'] * np.sin(df_sample['wd_rad'])
|
||||||
|
df_sample['u_diff'] = np.abs(df_sample['u_calc'] - df_sample['uu'])
|
||||||
|
df_sample['u_rel'] = df_sample['u_diff'] / np.abs(df_sample['uu'])
|
||||||
|
|
||||||
|
print("\n[상대 오차가 높게 나타나는 샘플 (U성분 기준)]")
|
||||||
|
print(df_sample.sort_values('u_rel', ascending=False)[['tm', 'wd_10m', 'ws_10m', 'uu', 'u_calc', 'u_rel']].head(5))
|
||||||
42
analyze_ws_error.py
Normal file
42
analyze_ws_error.py
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import glob
|
||||||
|
|
||||||
|
# 1. 대상 파일 목록
|
||||||
|
files = [f for f in glob.glob('weather_data_*.csv') if 'definition' not in f]
|
||||||
|
|
||||||
|
all_ws_results = []
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
df = pd.read_csv(file)
|
||||||
|
|
||||||
|
# 성분 기반 풍속 계산
|
||||||
|
ws_calc = np.sqrt(df['uu']**2 + df['vv']**2)
|
||||||
|
ws_recorded = df['ws_10m']
|
||||||
|
|
||||||
|
# 절대 오차 및 상대 오차
|
||||||
|
abs_error = np.abs(ws_calc - ws_recorded)
|
||||||
|
# 풍속이 0인 경우 제외하고 상대 오차 계산
|
||||||
|
rel_error = abs_error / ws_recorded.replace(0, np.nan)
|
||||||
|
|
||||||
|
df['ws_calc'] = ws_calc
|
||||||
|
df['ws_abs_err'] = abs_error
|
||||||
|
df['ws_rel_err'] = rel_error
|
||||||
|
|
||||||
|
all_ws_results.append(df[['ws_10m', 'ws_calc', 'ws_abs_err', 'ws_rel_err']])
|
||||||
|
|
||||||
|
# 2. 통계 종합
|
||||||
|
total_ws_df = pd.concat(all_ws_results)
|
||||||
|
|
||||||
|
print("[성분(U,V) 기반 풍속 역산 검증 결과]")
|
||||||
|
print("-" * 45)
|
||||||
|
print(f"1. 풍속 절대 오차 평균: {total_ws_df['ws_abs_err'].mean():.4f} m/s")
|
||||||
|
print(f" 풍속 절대 오차 최대: {total_ws_df['ws_abs_err'].max():.4f} m/s")
|
||||||
|
print("-" * 45)
|
||||||
|
print(f"2. 풍속 상대 오차 평균: {total_ws_df['ws_rel_err'].mean() * 100:.2f}%")
|
||||||
|
print(f" 풍속 상대 오차 최대: {total_ws_df['ws_rel_err'].max() * 100:.2f}%")
|
||||||
|
print("-" * 45)
|
||||||
|
|
||||||
|
# 오차가 큰 사례 확인
|
||||||
|
print("\n[풍속 오차 발생 샘플 상위 5개]")
|
||||||
|
print(total_ws_df.sort_values('ws_rel_err', ascending=False).head(5))
|
||||||
32
check_api_columns.py
Normal file
32
check_api_columns.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
|
||||||
|
def get_env_variable(var_name):
|
||||||
|
if os.path.exists(".env"):
|
||||||
|
with open(".env", "r") as f:
|
||||||
|
for line in f:
|
||||||
|
if "=" in line:
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
if k.strip() == "WEATHER_KEY" or k.strip() == "KMA_API_KEY":
|
||||||
|
return v.strip()
|
||||||
|
return os.getenv("WEATHER_KEY")
|
||||||
|
|
||||||
|
API_KEY = get_env_variable("WEATHER_KEY")
|
||||||
|
|
||||||
|
BASE_URL = "https://apihub.kma.go.kr/api/typ01/url/sfc_nc_var.php"
|
||||||
|
params = {
|
||||||
|
"tm1": "202604171220",
|
||||||
|
"tm2": "202604171220",
|
||||||
|
"obs": "ta", # Start with just one
|
||||||
|
"lon": 127.712072,
|
||||||
|
"lat": 34.924702,
|
||||||
|
"help": 1, # Show headers to see columns
|
||||||
|
"authKey": API_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("Checking API column headers...")
|
||||||
|
response = requests.get(BASE_URL, params=params, timeout=10)
|
||||||
|
print(f"Response:\n{response.text}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
9
exact_grid_points.txt
Normal file
9
exact_grid_points.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
34.920834,127.705147
|
||||||
|
34.920738,127.710716
|
||||||
|
34.920639,127.716286
|
||||||
|
34.925423,127.705269
|
||||||
|
34.925323,127.710838
|
||||||
|
34.925228,127.716408
|
||||||
|
34.930012,127.705383
|
||||||
|
34.929913,127.710960
|
||||||
|
34.929817,127.716530
|
||||||
105
extract_continuous_data.py
Normal file
105
extract_continuous_data.py
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import csv
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import time
|
||||||
|
|
||||||
|
# 1. API 인증키 설정
|
||||||
|
def get_env_variable(var_name):
|
||||||
|
if os.path.exists(".env"):
|
||||||
|
with open(".env", "r") as f:
|
||||||
|
for line in f:
|
||||||
|
if "=" in line:
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
if k.strip() == var_name:
|
||||||
|
return v.strip()
|
||||||
|
return os.getenv(var_name)
|
||||||
|
|
||||||
|
API_KEY = get_env_variable("WEATHER_KEY") or get_env_variable("KMA_API_KEY")
|
||||||
|
|
||||||
|
# 2. 정밀 좌표 설정 (NetCDF 추출값)
|
||||||
|
TARGET_LAT = 34.925323
|
||||||
|
TARGET_LON = 127.710838
|
||||||
|
|
||||||
|
# 3. 추출 기간 설정
|
||||||
|
start_dt = datetime(2026, 4, 1, 0, 0)
|
||||||
|
end_dt = datetime(2026, 5, 7, 23, 55)
|
||||||
|
|
||||||
|
# 4. API 설정
|
||||||
|
BASE_URL = "https://apihub.kma.go.kr/api/typ01/url/sfc_nc_var.php"
|
||||||
|
OBS_ELEMENTS = "ta,hm,td,wd_10m,ws_10m,uu,vv,rn_ox,vs"
|
||||||
|
INTERVAL = 5
|
||||||
|
|
||||||
|
def extract_all():
|
||||||
|
current_dt = start_dt
|
||||||
|
all_data = []
|
||||||
|
total_hours = int((end_dt - start_dt).total_seconds() / 3600) + 1
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
print(f"--- 연속 데이터 추출 시작 (총 {total_hours}개 구간) ---")
|
||||||
|
|
||||||
|
output_filename = "weather_data_continuous_20260401_20260507.csv"
|
||||||
|
keys = ["tm", "ta", "hm", "td", "wd_10m", "ws_10m", "uu", "vv", "rn_ox", "vs"]
|
||||||
|
|
||||||
|
with open(output_filename, 'w', newline='', encoding='utf-8-sig') as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=keys)
|
||||||
|
writer.writeheader()
|
||||||
|
|
||||||
|
while current_dt <= end_dt:
|
||||||
|
# 1시간 단위 요청
|
||||||
|
tm1 = current_dt
|
||||||
|
tm2 = current_dt + timedelta(minutes=55) # 12개 데이터 (5분 간격)
|
||||||
|
|
||||||
|
if tm2 > end_dt: tm2 = end_dt
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"tm1": tm1.strftime("%Y%m%d%H%M"),
|
||||||
|
"tm2": tm2.strftime("%Y%m%d%H%M"),
|
||||||
|
"obs": OBS_ELEMENTS,
|
||||||
|
"itv": INTERVAL,
|
||||||
|
"lon": TARGET_LON,
|
||||||
|
"lat": TARGET_LAT,
|
||||||
|
"help": 0,
|
||||||
|
"authKey": API_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(BASE_URL, params=params, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for line in response.text.strip().split('\n'):
|
||||||
|
if line.startswith('#') or not line.strip(): continue
|
||||||
|
parts = [p.strip() for p in line.replace(',', ' ').split()]
|
||||||
|
if len(parts) >= 10:
|
||||||
|
rows.append({
|
||||||
|
"tm": parts[0],
|
||||||
|
"ta": parts[1],
|
||||||
|
"hm": parts[2],
|
||||||
|
"td": parts[3],
|
||||||
|
"wd_10m": parts[4],
|
||||||
|
"ws_10m": parts[5],
|
||||||
|
"uu": parts[6],
|
||||||
|
"vv": parts[7],
|
||||||
|
"rn_ox": parts[8],
|
||||||
|
"vs": parts[9]
|
||||||
|
})
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
writer.writerows(rows)
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
if count % 50 == 0:
|
||||||
|
print(f" 진행률: {count}/{total_hours} 구간 완료 ({current_dt.strftime('%Y-%m-%d %H:%M')})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error at {tm1}: {e}")
|
||||||
|
|
||||||
|
current_dt += timedelta(hours=1)
|
||||||
|
# API 부하 방지를 위한 미세 지연 (필요 시)
|
||||||
|
# time.sleep(0.05)
|
||||||
|
|
||||||
|
print(f"\n--- 추출 완료: {output_filename} ---")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
extract_all()
|
||||||
111
extract_continuous_data_fast.py
Normal file
111
extract_continuous_data_fast.py
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import csv
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import concurrent.futures
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
# 1. API 인증키 설정
|
||||||
|
def get_env_variable(var_name):
|
||||||
|
if os.path.exists(".env"):
|
||||||
|
with open(".env", "r") as f:
|
||||||
|
for line in f:
|
||||||
|
if "=" in line:
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
if k.strip() == var_name:
|
||||||
|
return v.strip()
|
||||||
|
return os.getenv(var_name)
|
||||||
|
|
||||||
|
API_KEY = get_env_variable("WEATHER_KEY") or get_env_variable("KMA_API_KEY")
|
||||||
|
TARGET_LAT = 34.925323
|
||||||
|
TARGET_LON = 127.710838
|
||||||
|
|
||||||
|
BASE_URL = "https://apihub.kma.go.kr/api/typ01/url/sfc_nc_var.php"
|
||||||
|
OBS_ELEMENTS = "ta,hm,td,wd_10m,ws_10m,uu,vv,rn_ox,vs"
|
||||||
|
INTERVAL = 5
|
||||||
|
|
||||||
|
# 추출 기간
|
||||||
|
start_dt = datetime(2026, 4, 1, 0, 0)
|
||||||
|
end_dt = datetime(2026, 5, 7, 23, 55)
|
||||||
|
|
||||||
|
def fetch_chunk(tm1, retries=2):
|
||||||
|
tm2 = tm1 + timedelta(minutes=55)
|
||||||
|
params = {
|
||||||
|
"tm1": tm1.strftime("%Y%m%d%H%M"),
|
||||||
|
"tm2": tm2.strftime("%Y%m%d%H%M"),
|
||||||
|
"obs": OBS_ELEMENTS,
|
||||||
|
"itv": INTERVAL,
|
||||||
|
"lon": TARGET_LON,
|
||||||
|
"lat": TARGET_LAT,
|
||||||
|
"help": 0,
|
||||||
|
"authKey": API_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
for attempt in range(retries):
|
||||||
|
try:
|
||||||
|
response = requests.get(BASE_URL, params=params, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
rows = []
|
||||||
|
for line in response.text.strip().split('\n'):
|
||||||
|
if line.startswith('#') or not line.strip(): continue
|
||||||
|
parts = [p.strip() for p in line.replace(',', ' ').split()]
|
||||||
|
if len(parts) >= 10:
|
||||||
|
rows.append({
|
||||||
|
"tm": parts[0],
|
||||||
|
"ta": parts[1],
|
||||||
|
"hm": parts[2],
|
||||||
|
"td": parts[3],
|
||||||
|
"wd_10m": parts[4],
|
||||||
|
"ws_10m": parts[5],
|
||||||
|
"uu": parts[6],
|
||||||
|
"vv": parts[7],
|
||||||
|
"rn_ox": parts[8],
|
||||||
|
"vs": parts[9]
|
||||||
|
})
|
||||||
|
return rows
|
||||||
|
except Exception:
|
||||||
|
if attempt < retries - 1:
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def main():
|
||||||
|
time_chunks = []
|
||||||
|
curr = start_dt
|
||||||
|
while curr <= end_dt:
|
||||||
|
time_chunks.append(curr)
|
||||||
|
curr += timedelta(hours=1)
|
||||||
|
|
||||||
|
print(f"--- 연속 데이터 추출 시작 (총 {len(time_chunks)}개 구간, 5개 스레드) ---")
|
||||||
|
|
||||||
|
output_filename = "weather_data_continuous_20260401_20260507.csv"
|
||||||
|
keys = ["tm", "ta", "hm", "td", "wd_10m", "ws_10m", "uu", "vv", "rn_ox", "vs"]
|
||||||
|
|
||||||
|
all_data = []
|
||||||
|
completed = 0
|
||||||
|
lock = threading.Lock()
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
||||||
|
future_to_chunk = {executor.submit(fetch_chunk, t): t for t in time_chunks}
|
||||||
|
for future in concurrent.futures.as_completed(future_to_chunk):
|
||||||
|
res = future.result()
|
||||||
|
if res:
|
||||||
|
all_data.extend(res)
|
||||||
|
|
||||||
|
completed += 1
|
||||||
|
if completed % 20 == 0:
|
||||||
|
print(f" 진행 상황: {completed}/{len(time_chunks)} 완료 ({int(completed/len(time_chunks)*100)}%)")
|
||||||
|
|
||||||
|
# 정렬 및 저장
|
||||||
|
print("정렬 및 저장 중...")
|
||||||
|
all_data.sort(key=lambda x: x['tm'])
|
||||||
|
with open(output_filename, 'w', newline='', encoding='utf-8-sig') as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=keys)
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(all_data)
|
||||||
|
|
||||||
|
print(f"최종 완료: {output_filename} (총 {len(all_data)}행)")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
97
extract_weather_data.py
Normal file
97
extract_weather_data.py
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import csv
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# 1. API 인증키 설정
|
||||||
|
def get_env_variable(var_name):
|
||||||
|
if os.path.exists(".env"):
|
||||||
|
with open(".env", "r") as f:
|
||||||
|
for line in f:
|
||||||
|
if "=" in line:
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
if k.strip() == var_name:
|
||||||
|
return v.strip()
|
||||||
|
return os.getenv(var_name)
|
||||||
|
|
||||||
|
API_KEY = get_env_variable("WEATHER_KEY") or get_env_variable("KMA_API_KEY")
|
||||||
|
|
||||||
|
# 2. 좌표 설정
|
||||||
|
TARGET_LAT = 34.92470
|
||||||
|
TARGET_LON = 127.71207
|
||||||
|
|
||||||
|
# 3. 시점 로드
|
||||||
|
def load_target_times(file_path):
|
||||||
|
times = []
|
||||||
|
if not os.path.exists(file_path): return []
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line: continue
|
||||||
|
try:
|
||||||
|
dt = datetime.strptime(line, "%Y-%m-%d: %H:%M")
|
||||||
|
times.append(dt)
|
||||||
|
except ValueError: continue
|
||||||
|
return times
|
||||||
|
|
||||||
|
target_times = load_target_times("visible-times.txt")
|
||||||
|
|
||||||
|
# 4. API 설정
|
||||||
|
BASE_URL = "https://apihub.kma.go.kr/api/typ01/url/sfc_nc_var.php"
|
||||||
|
OBS_ELEMENTS = "ta,hm,td,wd_10m,ws_10m,uu,vv,rn_ox,vs"
|
||||||
|
INTERVAL = 5
|
||||||
|
|
||||||
|
def fetch_data(t_time):
|
||||||
|
print(f"--- [{t_time.strftime('%Y-%m-%d %H:%M')}] 추출 중 ---")
|
||||||
|
start_time = t_time - timedelta(hours=1)
|
||||||
|
end_time = t_time + timedelta(hours=1)
|
||||||
|
|
||||||
|
queries = [(start_time, t_time), (t_time + timedelta(minutes=INTERVAL), end_time)]
|
||||||
|
extracted_data = []
|
||||||
|
|
||||||
|
for tm1, tm2 in queries:
|
||||||
|
params = {
|
||||||
|
"tm1": tm1.strftime("%Y%m%d%H%M"),
|
||||||
|
"tm2": tm2.strftime("%Y%m%d%H%M"),
|
||||||
|
"obs": OBS_ELEMENTS,
|
||||||
|
"itv": INTERVAL,
|
||||||
|
"lon": TARGET_LON,
|
||||||
|
"lat": TARGET_LAT,
|
||||||
|
"help": 0,
|
||||||
|
"authKey": API_KEY
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = requests.get(BASE_URL, params=params, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
for line in response.text.strip().split('\n'):
|
||||||
|
if line.startswith('#') or not line.strip(): continue
|
||||||
|
parts = [p.strip() for p in line.replace(',', ' ').split()]
|
||||||
|
if len(parts) >= 10:
|
||||||
|
extracted_data.append({
|
||||||
|
"tm": parts[0],
|
||||||
|
"ta": parts[1],
|
||||||
|
"hm": parts[2],
|
||||||
|
"td": parts[3],
|
||||||
|
"wd_10m": parts[4],
|
||||||
|
"ws_10m": parts[5],
|
||||||
|
"uu": parts[6],
|
||||||
|
"vv": parts[7],
|
||||||
|
"rn_ox": parts[8],
|
||||||
|
"vs": parts[9]
|
||||||
|
})
|
||||||
|
except Exception as e: print(f" Error: {e}")
|
||||||
|
|
||||||
|
if extracted_data:
|
||||||
|
extracted_data.sort(key=lambda x: x['tm'])
|
||||||
|
filename = f"weather_data_{t_time.strftime('%Y%m%d_%H%M')}.csv"
|
||||||
|
with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=extracted_data[0].keys())
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(extracted_data)
|
||||||
|
print(f" >> 저장됨: {filename} ({len(extracted_data)}개)")
|
||||||
|
else:
|
||||||
|
print(f" >> 데이터 없음")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
for t in target_times: fetch_data(t)
|
||||||
70
plot_weather_data.py
Normal file
70
plot_weather_data.py
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import pandas as pd
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 1. 데이터 로드
|
||||||
|
data_file = 'weather_data_continuous_20260401_20260507.csv'
|
||||||
|
visible_times_file = 'visible-times.txt'
|
||||||
|
|
||||||
|
if not os.path.exists(data_file):
|
||||||
|
print(f"Error: {data_file} not found.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
df = pd.read_csv(data_file)
|
||||||
|
df['tm_dt'] = pd.to_datetime(df['tm'], format='%Y%m%d%H%M')
|
||||||
|
|
||||||
|
# 2. 표시할 특정 시점 로드 (포맷 수정)
|
||||||
|
visible_times = []
|
||||||
|
if os.path.exists(visible_times_file):
|
||||||
|
with open(visible_times_file, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
t_str = line.strip()
|
||||||
|
if not t_str: continue
|
||||||
|
try:
|
||||||
|
# "2026-04-17: 13:20" 에서 첫 번째 ":" 만 공백으로 변경
|
||||||
|
# 결과: "2026-04-17 13:20"
|
||||||
|
cleaned_time = t_str.replace(':', ' ', 1)
|
||||||
|
vt = pd.to_datetime(cleaned_time)
|
||||||
|
visible_times.append(vt)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Parsing error for '{t_str}': {e}")
|
||||||
|
|
||||||
|
# 3. 그래프 생성
|
||||||
|
fig, ax1 = plt.subplots(figsize=(15, 8))
|
||||||
|
|
||||||
|
# 축 1: 기온 및 이슬점
|
||||||
|
ax1.set_xlabel('Date')
|
||||||
|
ax1.set_ylabel('Temperature / Dew Point (°C)', color='tab:red')
|
||||||
|
ax1.plot(df['tm_dt'], df['ta'], color='tab:red', label='Temperature (ta)', alpha=0.6, linewidth=1)
|
||||||
|
ax1.plot(df['tm_dt'], df['td'], color='tab:orange', label='Dew Point (td)', alpha=0.4, linewidth=1)
|
||||||
|
ax1.tick_params(axis='y', labelcolor='tab:red')
|
||||||
|
|
||||||
|
# 축 2: 풍속 (twinx)
|
||||||
|
ax2 = ax1.twinx()
|
||||||
|
ax2.set_ylabel('Wind Speed (m/s)', color='tab:blue')
|
||||||
|
ax2.plot(df['tm_dt'], df['ws_10m'], color='tab:blue', label='Wind Speed (ws)', alpha=0.3, linewidth=0.8)
|
||||||
|
ax2.tick_params(axis='y', labelcolor='tab:blue')
|
||||||
|
|
||||||
|
# 4. Visible Times 수직선 표시 (가시성 극대화: 빨간색 실선)
|
||||||
|
plotted_count = 0
|
||||||
|
for vt in visible_times:
|
||||||
|
if df['tm_dt'].min() <= vt <= df['tm_dt'].max():
|
||||||
|
ax1.axvline(x=vt, color='red', linestyle='-', alpha=1.0, linewidth=2, zorder=10)
|
||||||
|
plotted_count += 1
|
||||||
|
|
||||||
|
# 수직선 범례를 위한 더미 라인
|
||||||
|
ax1.plot([], [], color='red', linestyle='-', label='Target Timestamps', linewidth=2)
|
||||||
|
|
||||||
|
# 범례 통합
|
||||||
|
lines, labels = ax1.get_legend_handles_labels()
|
||||||
|
lines2, labels2 = ax2.get_legend_handles_labels()
|
||||||
|
ax1.legend(lines + lines2, labels + labels2, loc='upper right')
|
||||||
|
|
||||||
|
plt.title('Weather Time Series (Apr 1 - May 7, 2026)\nTarget Coordinate: 34.925323N, 127.710838E', fontsize=14)
|
||||||
|
plt.tight_layout()
|
||||||
|
|
||||||
|
# 5. 저장
|
||||||
|
output_plot = 'weather_timeseries_plot.png'
|
||||||
|
plt.savefig(output_plot, dpi=150)
|
||||||
|
print(f"Fixed graph saved as {output_plot}")
|
||||||
|
print(f"Parsed {len(visible_times)} timestamps, Plotted {plotted_count} lines.")
|
||||||
55
plot_weather_no_wind.py
Normal file
55
plot_weather_no_wind.py
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import pandas as pd
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 1. 데이터 로드
|
||||||
|
data_file = 'weather_data_continuous_20260401_20260507.csv'
|
||||||
|
visible_times_file = 'visible-times.txt'
|
||||||
|
|
||||||
|
if not os.path.exists(data_file):
|
||||||
|
print(f"Error: {data_file} not found.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
df = pd.read_csv(data_file)
|
||||||
|
df['tm_dt'] = pd.to_datetime(df['tm'], format='%Y%m%d%H%M')
|
||||||
|
|
||||||
|
# 2. 표시할 특정 시점 로드
|
||||||
|
visible_times = []
|
||||||
|
if os.path.exists(visible_times_file):
|
||||||
|
with open(visible_times_file, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
t_str = line.strip()
|
||||||
|
if not t_str: continue
|
||||||
|
try:
|
||||||
|
cleaned_time = t_str.replace(':', ' ', 1)
|
||||||
|
vt = pd.to_datetime(cleaned_time)
|
||||||
|
visible_times.append(vt)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 3. 그래프 생성
|
||||||
|
fig, ax1 = plt.subplots(figsize=(15, 8))
|
||||||
|
|
||||||
|
# 축 1: 기온 및 이슬점만 표시
|
||||||
|
ax1.set_xlabel('Date')
|
||||||
|
ax1.set_ylabel('Temperature / Dew Point (°C)', color='tab:red')
|
||||||
|
ax1.plot(df['tm_dt'], df['ta'], color='tab:red', label='Temperature (ta)', alpha=0.7, linewidth=1.2)
|
||||||
|
ax1.plot(df['tm_dt'], df['td'], color='tab:orange', label='Dew Point (td)', alpha=0.5, linewidth=1.2)
|
||||||
|
ax1.tick_params(axis='y', labelcolor='tab:red')
|
||||||
|
|
||||||
|
# 4. Visible Times 수직선 표시
|
||||||
|
for vt in visible_times:
|
||||||
|
if df['tm_dt'].min() <= vt <= df['tm_dt'].max():
|
||||||
|
ax1.axvline(x=vt, color='red', linestyle='-', alpha=1.0, linewidth=2, zorder=10)
|
||||||
|
|
||||||
|
# 수직선 범례를 위한 더미 라인
|
||||||
|
ax1.plot([], [], color='red', linestyle='-', label='Target Timestamps', linewidth=2)
|
||||||
|
|
||||||
|
ax1.legend(loc='upper right')
|
||||||
|
plt.title('Weather Time Series (Temperature & Dew Point Only)\nApr 1 - May 7, 2026', fontsize=14)
|
||||||
|
plt.tight_layout()
|
||||||
|
|
||||||
|
# 5. 저장
|
||||||
|
output_plot = 'weather_timeseries_no_wind.png'
|
||||||
|
plt.savefig(output_plot, dpi=150)
|
||||||
|
print(f"No-wind graph saved as {output_plot}")
|
||||||
66
plot_weather_with_humidity.py
Normal file
66
plot_weather_with_humidity.py
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import pandas as pd
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 1. 데이터 로드
|
||||||
|
data_file = 'weather_data_continuous_20260401_20260507.csv'
|
||||||
|
visible_times_file = 'visible-times.txt'
|
||||||
|
|
||||||
|
if not os.path.exists(data_file):
|
||||||
|
print(f"Error: {data_file} not found.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
df = pd.read_csv(data_file)
|
||||||
|
df['tm_dt'] = pd.to_datetime(df['tm'], format='%Y%m%d%H%M')
|
||||||
|
|
||||||
|
# 2. 표시할 특정 시점 로드
|
||||||
|
visible_times = []
|
||||||
|
if os.path.exists(visible_times_file):
|
||||||
|
with open(visible_times_file, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
t_str = line.strip()
|
||||||
|
if not t_str: continue
|
||||||
|
try:
|
||||||
|
cleaned_time = t_str.replace(':', ' ', 1)
|
||||||
|
vt = pd.to_datetime(cleaned_time)
|
||||||
|
visible_times.append(vt)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 3. 그래프 생성
|
||||||
|
fig, ax1 = plt.subplots(figsize=(15, 8))
|
||||||
|
|
||||||
|
# 축 1: 기온 및 이슬점 (°C)
|
||||||
|
ax1.set_xlabel('Date')
|
||||||
|
ax1.set_ylabel('Temperature / Dew Point (°C)', color='tab:red')
|
||||||
|
ax1.plot(df['tm_dt'], df['ta'], color='tab:red', label='Temperature (ta)', alpha=0.7, linewidth=1.2)
|
||||||
|
ax1.plot(df['tm_dt'], df['td'], color='tab:orange', label='Dew Point (td)', alpha=0.5, linewidth=1.2)
|
||||||
|
ax1.tick_params(axis='y', labelcolor='tab:red')
|
||||||
|
|
||||||
|
# 축 2: 상대습도 (%)
|
||||||
|
ax2 = ax1.twinx()
|
||||||
|
ax2.set_ylabel('Relative Humidity (%)', color='tab:green')
|
||||||
|
ax2.plot(df['tm_dt'], df['hm'], color='tab:green', label='Humidity (hm)', alpha=0.4, linewidth=1)
|
||||||
|
ax2.tick_params(axis='y', labelcolor='tab:green')
|
||||||
|
ax2.set_ylim(0, 105) # 습도는 0~100% 범위
|
||||||
|
|
||||||
|
# 4. Visible Times 수직선 표시
|
||||||
|
for vt in visible_times:
|
||||||
|
if df['tm_dt'].min() <= vt <= df['tm_dt'].max():
|
||||||
|
ax1.axvline(x=vt, color='red', linestyle='-', alpha=1.0, linewidth=2, zorder=10)
|
||||||
|
|
||||||
|
# 수직선 범례를 위한 더미 라인
|
||||||
|
ax1.plot([], [], color='red', linestyle='-', label='Target Timestamps', linewidth=2)
|
||||||
|
|
||||||
|
# 범례 통합
|
||||||
|
lines, labels = ax1.get_legend_handles_labels()
|
||||||
|
lines2, labels2 = ax2.get_legend_handles_labels()
|
||||||
|
ax1.legend(lines + lines2, labels + labels2, loc='upper right')
|
||||||
|
|
||||||
|
plt.title('Weather Time Series (Temperature, Dew Point & Humidity)\nApr 1 - May 7, 2026', fontsize=14)
|
||||||
|
plt.tight_layout()
|
||||||
|
|
||||||
|
# 5. 저장
|
||||||
|
output_plot = 'weather_timeseries_with_humidity.png'
|
||||||
|
plt.savefig(output_plot, dpi=150)
|
||||||
|
print(f"Graph with humidity saved as {output_plot}")
|
||||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
requests
|
||||||
|
pandas
|
||||||
|
numpy
|
||||||
|
matplotlib
|
||||||
|
xarray
|
||||||
|
netcdf4
|
||||||
BIN
sfc_grid_latlon.nc
Normal file
BIN
sfc_grid_latlon.nc
Normal file
Binary file not shown.
1
target-coord.txt
Normal file
1
target-coord.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
34°55'28.93"N 127°42'43.46"E
|
||||||
47
verify_grid.py
Normal file
47
verify_grid.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import xarray as xr
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 1. 파일 열기
|
||||||
|
file_path = 'sfc_grid_latlon.nc'
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
print(f"Error: {file_path} not found.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
ds = xr.open_dataset(file_path)
|
||||||
|
|
||||||
|
# 2. 대상 좌표 (34°55'28.93"N 127°42'43.46"E)
|
||||||
|
target_lat = 34 + (55/60) + (28.93/3600)
|
||||||
|
target_lon = 127 + (42/60) + (43.46/3600)
|
||||||
|
|
||||||
|
print(f"Target: Lat {target_lat:.6f}, Lon {target_lon:.6f}")
|
||||||
|
|
||||||
|
# 3. 가장 가까운 격자점 인덱스 찾기
|
||||||
|
# 격자 파일의 lat, lon 변수 확인 (보통 2차원 배열)
|
||||||
|
lat_arr = ds.lat.values
|
||||||
|
lon_arr = ds.lon.values
|
||||||
|
|
||||||
|
# 유클리드 거리 계산 (간단한 근사)
|
||||||
|
dist = (lat_arr - target_lat)**2 + (lon_arr - target_lon)**2
|
||||||
|
ny, nx = np.unravel_index(dist.argmin(), dist.shape)
|
||||||
|
|
||||||
|
print(f"Found nearest grid index: ny={ny}, nx={nx}")
|
||||||
|
print(f"Grid Lat: {lat_arr[ny, nx]:.6f}, Grid Lon: {lon_arr[ny, nx]:.6f}")
|
||||||
|
|
||||||
|
# 4. 주변 3x3 격자점 좌표 추출
|
||||||
|
print("\n--- 3x3 Grid Points from NetCDF ---")
|
||||||
|
grid_points = []
|
||||||
|
for j in range(ny-1, ny+2):
|
||||||
|
for i in range(nx-1, nx+2):
|
||||||
|
l_lat = lat_arr[j, i]
|
||||||
|
l_lon = lon_arr[j, i]
|
||||||
|
pos = ["NW", "N", "NE", "W", "C", "E", "SW", "S", "SE"][len(grid_points)]
|
||||||
|
grid_points.append((l_lat, l_lon))
|
||||||
|
print(f"{pos:3} (ny={j}, nx={i}): Lat {l_lat:.6f}, Lon {l_lon:.6f}")
|
||||||
|
|
||||||
|
# 5. 결과 저장 (시각화 등에 활용)
|
||||||
|
with open('exact_grid_points.txt', 'w') as f:
|
||||||
|
for lt, ln in grid_points:
|
||||||
|
f.write(f"{lt:.6f},{ln:.6f}\n")
|
||||||
|
|
||||||
|
ds.close()
|
||||||
61
verify_wind_vectors.py
Normal file
61
verify_wind_vectors.py
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 1. 대상 파일 목록 확보 (정의 파일 제외)
|
||||||
|
files = [f for f in glob.glob('weather_data_*.csv') if 'definition' not in f]
|
||||||
|
if not files:
|
||||||
|
print("Error: No weather data files found.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
all_results = []
|
||||||
|
|
||||||
|
print(f"--- 총 {len(files)}개 파일 검증 시작 ---")
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
df = pd.read_csv(file)
|
||||||
|
|
||||||
|
# 풍향(wd)을 라디안으로 변환
|
||||||
|
wd_rad = np.radians(df['wd_10m'])
|
||||||
|
ws = df['ws_10m']
|
||||||
|
|
||||||
|
# 계산된 U, V 성분 (기상학적 공식 적용)
|
||||||
|
u_calc = -ws * np.sin(wd_rad)
|
||||||
|
v_calc = -ws * np.cos(wd_rad)
|
||||||
|
|
||||||
|
# 실제 데이터와의 차이 (절대 오차)
|
||||||
|
u_diff = np.abs(u_calc - df['uu'])
|
||||||
|
v_diff = np.abs(v_calc - df['vv'])
|
||||||
|
|
||||||
|
all_results.append({
|
||||||
|
'file': file,
|
||||||
|
'u_error_mean': u_diff.mean(),
|
||||||
|
'v_error_mean': v_diff.mean(),
|
||||||
|
'u_error_max': u_diff.max(),
|
||||||
|
'v_error_max': v_diff.max()
|
||||||
|
})
|
||||||
|
|
||||||
|
# 2. 결과 종합
|
||||||
|
res_df = pd.DataFrame(all_results)
|
||||||
|
print("\n[검증 통계 요약]")
|
||||||
|
print(f"평균 오차 (U): {res_df['u_error_mean'].mean():.4f}")
|
||||||
|
print(f"평균 오차 (V): {res_df['v_error_mean'].mean():.4f}")
|
||||||
|
print(f"최대 오차 (U): {res_df['u_error_max'].max():.4f}")
|
||||||
|
print(f"최대 오차 (V): {res_df['v_error_max'].max():.4f}")
|
||||||
|
|
||||||
|
# 샘플 데이터 출력 (첫 번째 파일의 상위 5개 행)
|
||||||
|
sample_df = pd.read_csv(files[0]).head(5)
|
||||||
|
sample_wd_rad = np.radians(sample_df['wd_10m'])
|
||||||
|
sample_df['u_calc'] = -sample_df['ws_10m'] * np.sin(sample_wd_rad)
|
||||||
|
sample_df['v_calc'] = -sample_df['ws_10m'] * np.cos(sample_wd_rad)
|
||||||
|
|
||||||
|
print("\n[상세 데이터 샘플 비교 (첫 번째 파일)]")
|
||||||
|
# 비교를 위해 반올림 처리
|
||||||
|
print(sample_df[['tm', 'wd_10m', 'ws_10m', 'uu', 'u_calc', 'vv', 'v_calc']].round(4))
|
||||||
|
|
||||||
|
if res_df['u_error_mean'].mean() < 0.1:
|
||||||
|
print("\n결론: 풍향/풍속과 U/V 성분은 수학적으로 동일한 벡터임이 검증되었습니다.")
|
||||||
|
print("오차(평균 0.05 미만)는 소수점 반올림 및 저장 과정에서 발생하는 미세한 차이입니다.")
|
||||||
|
else:
|
||||||
|
print("\n결론: 유의미한 오차가 발견되었습니다. 정의를 재확인할 필요가 있습니다.")
|
||||||
11
visible-times.txt
Normal file
11
visible-times.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
2026-04-17: 13:20
|
||||||
|
2026-04-18: 09:58
|
||||||
|
2026-04-20: 13:04
|
||||||
|
2026-04-23: 06:18
|
||||||
|
2026-04-28: 09:47
|
||||||
|
2026-04-28: 11:11
|
||||||
|
2026-04-29: 07:02
|
||||||
|
2026-04-30: 08:30
|
||||||
|
2026-05-1: 10:35
|
||||||
|
2026-05-4: 13:05
|
||||||
|
2026-05-6: 12:55
|
||||||
58
visualize_grid.py
Normal file
58
visualize_grid.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 1. 좌표 설정 (34°55'28.93"N 127°42'43.46"E)
|
||||||
|
def dms_to_decimal(degrees, minutes, seconds):
|
||||||
|
return degrees + (minutes / 60.0) + (seconds / 3600.0)
|
||||||
|
|
||||||
|
TARGET_LAT = dms_to_decimal(34, 55, 28.93)
|
||||||
|
TARGET_LON = dms_to_decimal(127, 42, 43.46)
|
||||||
|
|
||||||
|
# 500m 격자 간격 (근사치)
|
||||||
|
# 위도 1도 ~ 111km -> 500m ~ 0.0045도
|
||||||
|
# 경도 1도 (35도 부근) ~ 91km -> 500m ~ 0.0055도
|
||||||
|
D_LAT = 500 / 111000
|
||||||
|
D_LON = 500 / (111000 * np.cos(np.radians(TARGET_LAT)))
|
||||||
|
|
||||||
|
# 3x3 격자점 계산
|
||||||
|
lats = [TARGET_LAT + D_LAT, TARGET_LAT, TARGET_LAT - D_LAT]
|
||||||
|
lons = [TARGET_LON - D_LON, TARGET_LON, TARGET_LON + D_LON]
|
||||||
|
|
||||||
|
grid_points = []
|
||||||
|
for lt in lats:
|
||||||
|
for ln in lons:
|
||||||
|
grid_points.append((lt, ln))
|
||||||
|
|
||||||
|
# 시각화 (Matplotlib)
|
||||||
|
plt.figure(figsize=(8, 8))
|
||||||
|
for i, (lt, ln) in enumerate(grid_points):
|
||||||
|
color = 'red' if i == 4 else 'blue' # 중앙점은 빨간색
|
||||||
|
label = 'Target Point' if i == 4 else None
|
||||||
|
plt.scatter(ln, lt, c=color, s=100, label=label)
|
||||||
|
plt.text(ln, lt, f' ({ln:.4f}, {lt:.4f})', fontsize=9, verticalalignment='bottom')
|
||||||
|
|
||||||
|
plt.title("KMA 500m Grid Points (3x3)")
|
||||||
|
plt.xlabel("Longitude")
|
||||||
|
plt.ylabel("Latitude")
|
||||||
|
plt.grid(True, linestyle='--', alpha=0.6)
|
||||||
|
plt.legend()
|
||||||
|
|
||||||
|
# 상세 정보 출력
|
||||||
|
print(f"중앙 지점: 위도 {TARGET_LAT:.6f}, 경도 {TARGET_LON:.6f}")
|
||||||
|
print("-" * 30)
|
||||||
|
print("인접 9개 격자점 좌표:")
|
||||||
|
for i, (lt, ln) in enumerate(grid_points):
|
||||||
|
pos = ["북서", "북", "북동", "서", "중앙", "동", "남서", "남", "남동"][i]
|
||||||
|
print(f"{pos:4}: {lt:.6f}, {ln:.6f}")
|
||||||
|
|
||||||
|
# 결과 저장
|
||||||
|
plt.savefig("grid_points_visualization.png")
|
||||||
|
print("\n시각화 결과가 'grid_points_visualization.png'로 저장되었습니다.")
|
||||||
|
|
||||||
|
# sfc_grid_latlon.nc 파일이 있다면 실제 인덱스 확인 가능 (xarray/netCDF4 필요)
|
||||||
|
"""
|
||||||
|
import xarray as xr
|
||||||
|
ds = xr.open_dataset('sfc_grid_latlon.nc')
|
||||||
|
# 가장 가까운 인덱스 찾기 로직...
|
||||||
|
"""
|
||||||
11
weather_data_definition.csv
Normal file
11
weather_data_definition.csv
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
column_name,description,unit,remark
|
||||||
|
tm,관측시각 (KST),YYYYMMDDHHMI,한국 표준시 기준
|
||||||
|
ta,기온,°C,지상 1.5m 높이
|
||||||
|
hm,상대습도,%,
|
||||||
|
td,이슬점 온도,°C,
|
||||||
|
wd_10m,10분 평균 풍향,deg,0~360도 (북:0 동:90 남:180 서:270)
|
||||||
|
ws_10m,10분 평균 풍속,m/s,
|
||||||
|
uu,동서바람성분 (U),m/s,서풍(+) 동풍(-)
|
||||||
|
vv,남북바람성분 (V),m/s,남풍(+) 북풍(-)
|
||||||
|
rn_ox,강수유무,-,0:무강수 1:강수
|
||||||
|
vs,시정,km,가시거리
|
||||||
|
BIN
기상자료3차원객관분석기술개발기술노트.pdf
Normal file
BIN
기상자료3차원객관분석기술개발기술노트.pdf
Normal file
Binary file not shown.
Loading…
Add table
Reference in a new issue