commit 9014366f22fc472347d9908ceb2e27465ec32c12 Author: ignis Date: Wed May 27 09:55:48 2026 +0900 docs: 프로젝트 설명 문서 작성 및 .gitignore 설정 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..38d8e5a --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1df458d --- /dev/null +++ b/.gitignore @@ -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 diff --git a/PROJECT_INFO.md b/PROJECT_INFO.md new file mode 100644 index 0000000..d61b9bd --- /dev/null +++ b/PROJECT_INFO.md @@ -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의 수학적 정밀함과 바람장 물리 모델의 일관성을 실증적으로 규명하였습니다. +- **연속 시계열 데이터 가시성 구축**: 특정 연구 시점(가시성이 저하되거나 특이 기상이 발생한 시점)에 격자 데이터가 어떻게 변화하는지 타임라인 상에 직관적으로 매핑할 수 있는 기틀을 마련하였습니다. diff --git a/README.md b/README.md new file mode 100644 index 0000000..12a211c --- /dev/null +++ b/README.md @@ -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}}$$ diff --git a/analyze_relative_error.py b/analyze_relative_error.py new file mode 100644 index 0000000..049154b --- /dev/null +++ b/analyze_relative_error.py @@ -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)) diff --git a/analyze_ws_error.py b/analyze_ws_error.py new file mode 100644 index 0000000..b9556ef --- /dev/null +++ b/analyze_ws_error.py @@ -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)) diff --git a/check_api_columns.py b/check_api_columns.py new file mode 100644 index 0000000..1bdaa7c --- /dev/null +++ b/check_api_columns.py @@ -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}") diff --git a/exact_grid_points.txt b/exact_grid_points.txt new file mode 100644 index 0000000..c3db32d --- /dev/null +++ b/exact_grid_points.txt @@ -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 diff --git a/extract_continuous_data.py b/extract_continuous_data.py new file mode 100644 index 0000000..420ac37 --- /dev/null +++ b/extract_continuous_data.py @@ -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() diff --git a/extract_continuous_data_fast.py b/extract_continuous_data_fast.py new file mode 100644 index 0000000..7a51f68 --- /dev/null +++ b/extract_continuous_data_fast.py @@ -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() diff --git a/extract_weather_data.py b/extract_weather_data.py new file mode 100644 index 0000000..94cf45d --- /dev/null +++ b/extract_weather_data.py @@ -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) diff --git a/plot_weather_data.py b/plot_weather_data.py new file mode 100644 index 0000000..80e4065 --- /dev/null +++ b/plot_weather_data.py @@ -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.") diff --git a/plot_weather_no_wind.py b/plot_weather_no_wind.py new file mode 100644 index 0000000..fc9cc3d --- /dev/null +++ b/plot_weather_no_wind.py @@ -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}") diff --git a/plot_weather_with_humidity.py b/plot_weather_with_humidity.py new file mode 100644 index 0000000..701127b --- /dev/null +++ b/plot_weather_with_humidity.py @@ -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}") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..452e4b4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +requests +pandas +numpy +matplotlib +xarray +netcdf4 diff --git a/sfc_grid_latlon.nc b/sfc_grid_latlon.nc new file mode 100644 index 0000000..b21ace3 Binary files /dev/null and b/sfc_grid_latlon.nc differ diff --git a/target-coord.txt b/target-coord.txt new file mode 100644 index 0000000..5396c52 --- /dev/null +++ b/target-coord.txt @@ -0,0 +1 @@ +34°55'28.93"N 127°42'43.46"E \ No newline at end of file diff --git a/verify_grid.py b/verify_grid.py new file mode 100644 index 0000000..3595210 --- /dev/null +++ b/verify_grid.py @@ -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() diff --git a/verify_wind_vectors.py b/verify_wind_vectors.py new file mode 100644 index 0000000..6448719 --- /dev/null +++ b/verify_wind_vectors.py @@ -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결론: 유의미한 오차가 발견되었습니다. 정의를 재확인할 필요가 있습니다.") diff --git a/visible-times.txt b/visible-times.txt new file mode 100644 index 0000000..feb33e8 --- /dev/null +++ b/visible-times.txt @@ -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 \ No newline at end of file diff --git a/visualize_grid.py b/visualize_grid.py new file mode 100644 index 0000000..3290c20 --- /dev/null +++ b/visualize_grid.py @@ -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') +# 가장 가까운 인덱스 찾기 로직... +""" diff --git a/weather_data_definition.csv b/weather_data_definition.csv new file mode 100644 index 0000000..5f95b55 --- /dev/null +++ b/weather_data_definition.csv @@ -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,가시거리 diff --git a/기상자료3차원객관분석기술개발기술노트.pdf b/기상자료3차원객관분석기술개발기술노트.pdf new file mode 100644 index 0000000..c8acbcd Binary files /dev/null and b/기상자료3차원객관분석기술개발기술노트.pdf differ