bong9513 commited on
Commit
af01ff5
·
verified ·
1 Parent(s): 56762a8

Delete Analysis_code/7.ensemble

Browse files
Analysis_code/7.ensemble/analysis_of_shap.ipynb DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:d4427789841c51710ee09faff31ec283a2cc099a31ed44010a65cfba7f578a98
3
- size 2440086
 
 
 
 
Analysis_code/7.ensemble/ensemble.ipynb DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:b948db39e385865eccedfded6410f2d4719ad87d5a95d857b62ba34f1995065b
3
- size 20952
 
 
 
 
Analysis_code/7.ensemble/shap_utils.py DELETED
@@ -1,1260 +0,0 @@
1
- from joblib import load
2
- import pandas as pd
3
- import numpy as np
4
- import matplotlib.pyplot as plt
5
- import os
6
- import sys
7
- from contextlib import contextmanager
8
- from io import StringIO
9
- from typing import Optional, Tuple, List, Dict, Any
10
- from dataclasses import dataclass
11
- from abc import ABC, abstractmethod
12
- from sklearn.metrics import confusion_matrix
13
- from sklearn.preprocessing import LabelEncoder
14
- from scipy.stats import wasserstein_distance
15
- import shap
16
- import torch
17
- import warnings
18
-
19
- # 모든 경고 무시
20
- warnings.filterwarnings('ignore')
21
- warnings.filterwarnings('ignore', category=UserWarning, message='.*unrecognized nn.Module.*')
22
- warnings.filterwarnings('ignore', category=UserWarning, message='.*LayerNorm.*')
23
- warnings.filterwarnings('ignore', category=UserWarning, message='.*unrecognized.*')
24
- warnings.filterwarnings('ignore', module='shap.*')
25
-
26
- # 모델 클래스 import를 위한 경로 설정
27
- try:
28
- current_file_dir = os.path.dirname(os.path.abspath(__file__))
29
- models_path = os.path.abspath(os.path.join(current_file_dir, '../models'))
30
- except NameError:
31
- cwd = os.getcwd()
32
- if 'Analysis_code' in cwd:
33
- analysis_code_dir = cwd[:cwd.index('Analysis_code') + len('Analysis_code')]
34
- models_path = os.path.join(analysis_code_dir, 'models')
35
- else:
36
- models_path = '/workspace/visibility_prediction/Analysis_code/models'
37
-
38
- if models_path not in sys.path:
39
- sys.path.insert(0, models_path)
40
-
41
- from ft_transformer import FTTransformer
42
- from resnet_like import ResNetLike
43
- from deepgbm import DeepGBM
44
-
45
-
46
- # ==========================================
47
- # 1. Config 클래스
48
- # ==========================================
49
-
50
- class SHAPConfig:
51
- """SHAP 분석 설정을 관리하는 클래스."""
52
-
53
- # 상수 정의
54
- FOLD_YEARS = [[2018, 2019], [2018, 2019], [2019, 2020]]
55
- TEST_YEAR = 2021
56
- DROP_COLUMNS = ['multi_class', 'year']
57
- FEATURE_COLUMNS = [
58
- 'temp_C', 'precip_mm', 'wind_speed', 'wind_dir', 'hm',
59
- 'vap_pressure', 'dewpoint_C', 'loc_pressure', 'sea_pressure',
60
- 'solarRad', 'snow_cm', 'cloudcover', 'lm_cloudcover', 'low_cloudbase',
61
- 'groundtemp', 'O3', 'NO2', 'PM10', 'PM25', 'year',
62
- 'month', 'hour', 'ground_temp - temp_C', 'hour_sin', 'hour_cos',
63
- 'month_sin', 'month_cos', 'multi_class'
64
- ]
65
-
66
- # 기본 설정값
67
- DEFAULT_N_FOLDS = 3
68
- DEFAULT_N_BACKGROUND_SAMPLES = 200
69
- DEFAULT_N_TEST_SAMPLES = 500
70
- DEFAULT_FIGSIZE = (20, 8)
71
- DEFAULT_TOP_N = 3
72
- DEFAULT_TARGET_CLASS = 0
73
- DEFAULT_DEVICE = 'cpu'
74
- DEFAULT_PLOT_MARGIN = 0.05
75
-
76
- def __init__(
77
- self,
78
- n_folds: int = DEFAULT_N_FOLDS,
79
- n_background_samples: int = DEFAULT_N_BACKGROUND_SAMPLES,
80
- n_test_samples: int = DEFAULT_N_TEST_SAMPLES,
81
- figsize: Tuple[int, int] = DEFAULT_FIGSIZE,
82
- top_n: int = DEFAULT_TOP_N,
83
- target_class: int = DEFAULT_TARGET_CLASS,
84
- device: str = DEFAULT_DEVICE
85
- ):
86
- self.n_folds = n_folds
87
- self.n_background_samples = n_background_samples
88
- self.n_test_samples = n_test_samples
89
- self.figsize = figsize
90
- self.top_n = top_n
91
- self.target_class = target_class
92
- self.device = device
93
- self._validate()
94
-
95
- def _validate(self):
96
- """설정값 검증."""
97
- if self.n_folds <= 0:
98
- raise ValueError("n_folds must be positive")
99
- if self.n_background_samples <= 0:
100
- raise ValueError("n_background_samples must be positive")
101
- if self.n_test_samples <= 0:
102
- raise ValueError("n_test_samples must be positive")
103
- if self.top_n <= 0:
104
- raise ValueError("top_n must be positive")
105
-
106
-
107
- # ==========================================
108
- # 2. 결과 데이터 클래스
109
- # ==========================================
110
-
111
- @dataclass
112
- class ImportanceData:
113
- """Feature importance 데이터를 담는 클래스."""
114
- df: pd.DataFrame
115
- top_n_features: List[str]
116
- top_n_share: float
117
-
118
-
119
- @dataclass
120
- class SHAPResult:
121
- """SHAP 분석 결과를 담는 클래스."""
122
- shap_train: Optional[np.ndarray]
123
- X_train: Optional[pd.DataFrame]
124
- shap_test: Optional[np.ndarray]
125
- X_test: Optional[pd.DataFrame]
126
- feature_names: List[str]
127
- importance_train: Optional[ImportanceData]
128
- importance_test: Optional[ImportanceData]
129
- importance_df_combined: Optional[pd.DataFrame]
130
- metadata: Dict[str, Any]
131
-
132
- def to_dict(self) -> Dict[str, Any]:
133
- """기존 dict 형태로 변환 (하위 호환성)."""
134
- return {
135
- 'shap_train': self.shap_train,
136
- 'X_train': self.X_train,
137
- 'shap_test': self.shap_test,
138
- 'X_test': self.X_test,
139
- 'feature_names': self.feature_names,
140
- 'top_n_features_train': self.importance_train.top_n_features if self.importance_train else None,
141
- 'top_n_share_train': self.importance_train.top_n_share if self.importance_train else None,
142
- 'importance_df_train': self.importance_train.df if self.importance_train else None,
143
- 'top_n_features_test': self.importance_test.top_n_features if self.importance_test else None,
144
- 'top_n_share_test': self.importance_test.top_n_share if self.importance_test else None,
145
- 'importance_df_test': self.importance_test.df if self.importance_test else None,
146
- 'importance_df_combined': self.importance_df_combined,
147
- **self.metadata
148
- }
149
-
150
-
151
- # ==========================================
152
- # 3. 헬퍼 클래스들
153
- # ==========================================
154
-
155
- class PreprocessingUtils:
156
- """전처리 유틸리티 클래스."""
157
-
158
- @staticmethod
159
- def add_derived_features(df: pd.DataFrame) -> pd.DataFrame:
160
- """파생 변수 추가."""
161
- df = df.copy()
162
- df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24)
163
- df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24)
164
- df['month_sin'] = np.sin(2 * np.pi * df['month'] / 12)
165
- df['month_cos'] = np.cos(2 * np.pi * df['month'] / 12)
166
- df['ground_temp - temp_C'] = df['groundtemp'] - df['temp_C']
167
- return df
168
-
169
- @staticmethod
170
- def preprocessing(df: pd.DataFrame, config: SHAPConfig) -> pd.DataFrame:
171
- """데이터 전처리."""
172
- df = df[df.columns].copy()
173
- df['year'] = df['year'].astype('int')
174
- df['month'] = df['month'].astype('int')
175
- df['hour'] = df['hour'].astype('int')
176
- df = PreprocessingUtils.add_derived_features(df).copy()
177
- df['multi_class'] = df['multi_class'].astype('int')
178
- df.loc[df['wind_dir'] == '정온', 'wind_dir'] = "0"
179
- df['wind_dir'] = df['wind_dir'].astype('int')
180
- df = df[config.FEATURE_COLUMNS].copy()
181
- return df
182
-
183
-
184
- class ShapValueExtractor:
185
- """SHAP 값 추출 및 변환 유틸리티 클래스."""
186
-
187
- @staticmethod
188
- def _to_numpy(value: Any) -> np.ndarray:
189
- """다양한 타입을 numpy array로 변환."""
190
- if isinstance(value, torch.Tensor):
191
- return value.cpu().numpy()
192
- elif isinstance(value, list):
193
- return np.array(value)
194
- elif isinstance(value, np.ndarray):
195
- return value
196
- else:
197
- return np.array(value)
198
-
199
- @staticmethod
200
- def _combine_num_cat_shap(shap_num: Any, shap_cat: Any) -> np.ndarray:
201
- """Numerical과 categorical SHAP 값을 결합."""
202
- shap_num = ShapValueExtractor._to_numpy(shap_num)
203
- shap_cat = ShapValueExtractor._to_numpy(shap_cat)
204
- return np.concatenate([shap_num, shap_cat], axis=-1)
205
-
206
- @staticmethod
207
- def _extract_from_list_of_tuples(shap_list: List[Tuple]) -> np.ndarray:
208
- """튜플 리스트에서 SHAP 값을 추출."""
209
- combined = []
210
- for shap_item in shap_list:
211
- if isinstance(shap_item, tuple):
212
- combined.append(ShapValueExtractor._combine_num_cat_shap(shap_item[0], shap_item[1]))
213
- else:
214
- combined.append(ShapValueExtractor._to_numpy(shap_item))
215
- return np.array(combined)
216
-
217
- @staticmethod
218
- def _extract_from_list(shap_list: List[Any]) -> np.ndarray:
219
- """리스트에서 SHAP 값을 추출."""
220
- if len(shap_list) == 0:
221
- return np.array([])
222
-
223
- if isinstance(shap_list[0], tuple):
224
- return ShapValueExtractor._extract_from_list_of_tuples(shap_list)
225
-
226
- if len(shap_list) == 2:
227
- return ShapValueExtractor._combine_num_cat_shap(shap_list[0], shap_list[1])
228
-
229
- try:
230
- return np.array([ShapValueExtractor._to_numpy(item) for item in shap_list])
231
- except (ValueError, TypeError):
232
- converted = [ShapValueExtractor._to_numpy(item) for item in shap_list]
233
- if len(converted) == 2:
234
- return np.concatenate(converted, axis=-1)
235
- else:
236
- return np.array(converted)
237
-
238
- @staticmethod
239
- def extract(shap_values: Any, target_class: int = 0) -> np.ndarray:
240
- """SHAP 값을 추출하여 numpy array로 변환."""
241
- # 튜플인 경우
242
- if isinstance(shap_values, tuple):
243
- return ShapValueExtractor._combine_num_cat_shap(shap_values[0], shap_values[1])
244
-
245
- # 리스트인 경우
246
- if isinstance(shap_values, list):
247
- target_shap = shap_values[target_class]
248
-
249
- if isinstance(target_shap, tuple):
250
- return ShapValueExtractor._combine_num_cat_shap(target_shap[0], target_shap[1])
251
-
252
- if isinstance(target_shap, list):
253
- return ShapValueExtractor._extract_from_list(target_shap)
254
-
255
- return ShapValueExtractor._to_numpy(target_shap)
256
-
257
- # numpy array인 경우
258
- if isinstance(shap_values, np.ndarray):
259
- if len(shap_values.shape) == 3:
260
- return shap_values[:, target_class, :]
261
- return shap_values
262
-
263
- # 기타 타입
264
- return ShapValueExtractor._to_numpy(shap_values)
265
-
266
-
267
- @contextmanager
268
- def suppress_shap_output():
269
- """SHAP DeepExplainer의 출력을 억제하는 컨텍스트 매니저."""
270
- old_stdout = sys.stdout
271
- sys.stdout = StringIO()
272
- try:
273
- with warnings.catch_warnings():
274
- warnings.simplefilter('ignore')
275
- yield
276
- finally:
277
- sys.stdout = old_stdout
278
-
279
-
280
- class ModelWrapper(torch.nn.Module):
281
- """PyTorch 모델을 SHAP DeepExplainer에 맞게 래핑하는 클래스."""
282
-
283
- def __init__(self, base_model, numerical_cols):
284
- super().__init__()
285
- self.base_model = base_model
286
- self.num_features = len(numerical_cols)
287
-
288
- def forward(self, *inputs):
289
- """SHAP이 리스트로 전달하는 경우 처리"""
290
- if len(inputs) == 1 and isinstance(inputs[0], (list, tuple)):
291
- x_num, x_cat = inputs[0]
292
- elif len(inputs) == 2:
293
- x_num, x_cat = inputs
294
- else:
295
- x = inputs[0]
296
- x_num = x[:, :self.num_features]
297
- x_cat = x[:, self.num_features:].long()
298
- return self.base_model(x_num, x_cat)
299
-
300
- if x_cat.dtype != torch.long:
301
- x_cat = x_cat.long()
302
-
303
- return self.base_model(x_num, x_cat)
304
-
305
-
306
- # ==========================================
307
- # 4. DataLoader 클래스
308
- # ==========================================
309
-
310
- class DataLoader:
311
- """데이터 로딩 및 전처리를 담당하는 클래스."""
312
-
313
- def __init__(self, config: SHAPConfig):
314
- self.config = config
315
-
316
- def load_raw_data(
317
- self,
318
- region: str,
319
- data_sample: str,
320
- fold_idx: int
321
- ) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
322
- """원본 데이터 로드 (전처리만 적용)."""
323
- if data_sample == 'pure':
324
- dat_path = f"../../data/data_for_modeling/{region}_train.csv"
325
- train_data = PreprocessingUtils.preprocessing(
326
- pd.read_csv(dat_path, index_col=0), self.config
327
- )
328
- train_data = train_data.loc[
329
- ~train_data['year'].isin([self.config.TEST_YEAR - (fold_idx + 1)]), :
330
- ]
331
- else:
332
- train_data = pd.read_csv(
333
- f"../../data/data_oversampled/{data_sample}/{data_sample}_{fold_idx+1}_{region}.csv"
334
- )
335
- train_data = PreprocessingUtils.preprocessing(train_data, self.config)
336
-
337
- val_data = pd.read_csv(f"../../data/data_for_modeling/{region}_train.csv")
338
- val_data = PreprocessingUtils.preprocessing(val_data, self.config)
339
- val_data = val_data.loc[
340
- val_data['year'].isin([self.config.TEST_YEAR - (fold_idx + 1)]), :
341
- ]
342
-
343
- test_data = pd.read_csv(f"../../data/data_for_modeling/{region}_test.csv")
344
- test_data = PreprocessingUtils.preprocessing(test_data, self.config)
345
- test_data = test_data.loc[test_data['year'].isin([self.config.TEST_YEAR]), :]
346
-
347
- # 컬럼 정렬
348
- common_columns = train_data.columns.to_list()
349
- train_data = train_data[common_columns]
350
- val_data = val_data[common_columns]
351
- test_data = test_data[common_columns]
352
-
353
- return train_data, val_data, test_data
354
-
355
- def prepare_for_tree_model(
356
- self,
357
- region: str,
358
- data_sample: str,
359
- fold_idx: int
360
- ) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
361
- """Tree 모델용 데이터 준비."""
362
- train_data = pd.read_csv(
363
- f"../../data/data_oversampled/{data_sample}/{data_sample}_{fold_idx+1}_{region}.csv"
364
- )
365
- val_data = pd.read_csv(f"../../data/data_for_modeling/{region}_train.csv")
366
- test_data = pd.read_csv(f"../../data/data_for_modeling/{region}_test.csv")
367
-
368
- train_data = PreprocessingUtils.preprocessing(train_data, self.config).copy()
369
- val_data = PreprocessingUtils.preprocessing(val_data, self.config).copy()
370
- test_data = PreprocessingUtils.preprocessing(test_data, self.config).copy()
371
-
372
- train_data = train_data.loc[
373
- train_data['year'].isin(self.config.FOLD_YEARS[fold_idx]), :
374
- ]
375
- val_data = val_data.loc[
376
- ~val_data['year'].isin(self.config.FOLD_YEARS[fold_idx]), :
377
- ]
378
- test_data = test_data.loc[test_data['year'].isin([self.config.TEST_YEAR]), :]
379
-
380
- return train_data, val_data, test_data
381
-
382
- def prepare_for_dl_model(
383
- self,
384
- region: str,
385
- data_sample: str,
386
- fold_idx: int,
387
- scaler: Any
388
- ) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.Index, pd.Index,
389
- torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
390
- """Deep Learning 모델용 데이터 준비 (scaler 적용, 텐서 변환)."""
391
- train_data, val_data, test_data = self.load_raw_data(region, data_sample, fold_idx)
392
-
393
- X_train = train_data.drop(columns=self.config.DROP_COLUMNS)
394
- X_val = val_data.drop(columns=self.config.DROP_COLUMNS)
395
- X_test = test_data.drop(columns=self.config.DROP_COLUMNS)
396
-
397
- # 범주형 & 연속형 변수 분리
398
- categorical_cols = X_train.select_dtypes(include=['object', 'category', 'int64']).columns
399
- numerical_cols = X_train.select_dtypes(include=['float64']).columns
400
-
401
- # Label Encoding
402
- label_encoders = {}
403
- for col in categorical_cols:
404
- le = LabelEncoder()
405
- le.fit(X_train[col])
406
- label_encoders[col] = le
407
-
408
- for col in categorical_cols:
409
- X_train[col] = label_encoders[col].transform(X_train[col])
410
- X_val[col] = label_encoders[col].transform(X_val[col])
411
- X_test[col] = label_encoders[col].transform(X_test[col])
412
-
413
- # Scaler 변환
414
- X_train[numerical_cols] = scaler.transform(X_train[numerical_cols])
415
- X_val[numerical_cols] = scaler.transform(X_val[numerical_cols])
416
- X_test[numerical_cols] = scaler.transform(X_test[numerical_cols])
417
-
418
- # 텐서 변환
419
- X_train_num = torch.tensor(X_train[numerical_cols].values, dtype=torch.float32)
420
- X_train_cat = torch.tensor(X_train[categorical_cols].values, dtype=torch.long)
421
- X_val_num = torch.tensor(X_val[numerical_cols].values, dtype=torch.float32)
422
- X_val_cat = torch.tensor(X_val[categorical_cols].values, dtype=torch.long)
423
- X_test_num = torch.tensor(X_test[numerical_cols].values, dtype=torch.float32)
424
- X_test_cat = torch.tensor(X_test[categorical_cols].values, dtype=torch.long)
425
-
426
- return (X_train, X_val, X_test, categorical_cols, numerical_cols,
427
- X_train_num, X_train_cat, X_val_num, X_val_cat, X_test_num, X_test_cat)
428
-
429
-
430
- # ==========================================
431
- # 5. SHAPAnalyzer 베이스 클래스
432
- # ==========================================
433
-
434
- class SHAPAnalyzer(ABC):
435
- """SHAP 분석을 위한 추상 베이스 클래스."""
436
-
437
- def __init__(self, config: SHAPConfig, data_loader: DataLoader):
438
- self.config = config
439
- self.data_loader = data_loader
440
-
441
- @abstractmethod
442
- def create_explainer(self, model: Any, background_data: Any) -> Any:
443
- """SHAP Explainer 생성."""
444
- pass
445
-
446
- @abstractmethod
447
- def calculate_shap_values(self, explainer: Any, sample_data: Any) -> Any:
448
- """SHAP 값 계산."""
449
- pass
450
-
451
- def _calculate_importance(
452
- self,
453
- shap_values: np.ndarray,
454
- feature_names: List[str],
455
- period_name: str = ''
456
- ) -> ImportanceData:
457
- """누적 설명력(Cumulative Share) 계산."""
458
- global_importances = np.abs(shap_values).mean(axis=0)
459
-
460
- importance_df = pd.DataFrame({
461
- 'feature': feature_names,
462
- 'importance': global_importances
463
- }).sort_values('importance', ascending=False)
464
-
465
- total_importance = importance_df['importance'].sum()
466
- top_n_df = importance_df.head(self.config.top_n)
467
- top_n_importance = top_n_df['importance'].sum()
468
- top_n_share = (top_n_importance / total_importance) * 100
469
- top_n_features = top_n_df['feature'].tolist()
470
-
471
- if period_name:
472
- print(f"{period_name} - Top {self.config.top_n} Dominant Features: {top_n_features}")
473
- print(f"{period_name} - Cumulative Importance Share (Top {self.config.top_n}): {top_n_share:.2f}%")
474
- else:
475
- print(f"Top {self.config.top_n} Dominant Features: {top_n_features}")
476
- print(f"Cumulative Importance Share (Top {self.config.top_n}): {top_n_share:.2f}%")
477
- print(f"{'-'*40}")
478
-
479
- return ImportanceData(
480
- df=importance_df,
481
- top_n_features=top_n_features,
482
- top_n_share=top_n_share
483
- )
484
-
485
- def _create_combined_importance_df(
486
- self,
487
- importance_train: Optional[ImportanceData],
488
- importance_test: Optional[ImportanceData],
489
- feature_names: List[str]
490
- ) -> Optional[pd.DataFrame]:
491
- """Train과 Test 중요도 DataFrame 통합."""
492
- if importance_train is None and importance_test is None:
493
- return None
494
-
495
- if importance_train is not None:
496
- train_df = importance_train.df[['feature', 'importance']].copy()
497
- train_df.columns = ['feature', 'importance_train']
498
- total_importance_train = train_df['importance_train'].sum()
499
- train_df['share_train'] = (train_df['importance_train'] / total_importance_train) * 100
500
- else:
501
- train_df = pd.DataFrame({'feature': feature_names})
502
- train_df['importance_train'] = np.nan
503
- train_df['share_train'] = np.nan
504
-
505
- if importance_test is not None:
506
- test_df = importance_test.df[['feature', 'importance']].copy()
507
- test_df.columns = ['feature', 'importance_test']
508
- total_importance_test = test_df['importance_test'].sum()
509
- test_df['share_test'] = (test_df['importance_test'] / total_importance_test) * 100
510
- else:
511
- test_df = pd.DataFrame({'feature': feature_names})
512
- test_df['importance_test'] = np.nan
513
- test_df['share_test'] = np.nan
514
-
515
- importance_df_combined = pd.merge(train_df, test_df, on='feature', how='outer')
516
-
517
- if importance_train is not None:
518
- importance_df_combined = importance_df_combined.sort_values(
519
- 'importance_train', ascending=False
520
- ).reset_index(drop=True)
521
- elif importance_test is not None:
522
- importance_df_combined = importance_df_combined.sort_values(
523
- 'importance_test', ascending=False
524
- ).reset_index(drop=True)
525
- else:
526
- importance_df_combined = importance_df_combined.sort_values('feature').reset_index(drop=True)
527
-
528
- return importance_df_combined
529
-
530
-
531
- # ==========================================
532
- # 6. TreeSHAPAnalyzer 구현
533
- # ==========================================
534
-
535
- class TreeSHAPAnalyzer(SHAPAnalyzer):
536
- """Tree 기반 모델(XGBoost, LightGBM) SHAP 분석 클래스."""
537
-
538
- def create_explainer(self, model: Any, background_data: Any = None) -> shap.TreeExplainer:
539
- """TreeExplainer 생성."""
540
- with warnings.catch_warnings():
541
- warnings.filterwarnings('ignore', category=UserWarning, message='.*deprecated binary model format.*')
542
- return shap.TreeExplainer(model.get_booster())
543
-
544
- def calculate_shap_values(self, explainer: shap.TreeExplainer, sample_data: pd.DataFrame) -> np.ndarray:
545
- """Tree SHAP 값 계산."""
546
- shap_values = explainer.shap_values(sample_data)
547
- if isinstance(shap_values, list):
548
- return shap_values[self.config.target_class]
549
- return shap_values
550
-
551
- def analyze(
552
- self,
553
- model_name: str,
554
- region: str,
555
- data_sample: str,
556
- show_plot: bool = True,
557
- show_train_plot: bool = True,
558
- show_test_plot: bool = True,
559
- suppress_warnings: bool = True
560
- ) -> SHAPResult:
561
- """Tree 모델 SHAP 분석 수행."""
562
- # 모델 로드
563
- model_path = f"../save_model/{model_name}_optima/{model_name}_{data_sample}_{region}.pkl"
564
- models = load(model_path)
565
-
566
- if len(models) != self.config.n_folds:
567
- raise ValueError(f"로드된 모델 개수({len(models)})가 n_folds({self.config.n_folds})와 일치하지 않습니다.")
568
-
569
- # 각 Fold에 대해 SHAP 값 계산
570
- oof_shap_values = []
571
- oof_X_data = []
572
- test_shap_values = []
573
-
574
- for fold_idx in range(self.config.n_folds):
575
- _, val_data, test_data = self.data_loader.prepare_for_tree_model(
576
- region, data_sample, fold_idx
577
- )
578
-
579
- X_val = val_data.drop(columns=self.config.DROP_COLUMNS)
580
- if fold_idx == 0:
581
- X_test = test_data.drop(columns=self.config.DROP_COLUMNS)
582
-
583
- explainer = self.create_explainer(models[fold_idx])
584
- shap_val = self.calculate_shap_values(explainer, X_val)
585
- oof_shap_values.append(shap_val)
586
- oof_X_data.append(X_val)
587
-
588
- shap_test = self.calculate_shap_values(explainer, X_test)
589
- test_shap_values.append(shap_test)
590
-
591
- # 데이터 통합
592
- all_shap_train = np.concatenate(oof_shap_values, axis=0)
593
- all_X_train = pd.concat(oof_X_data, axis=0)
594
- all_shap_test = np.mean(test_shap_values, axis=0)
595
- feature_names = list(all_X_train.columns)
596
-
597
- # 중요도 계산
598
- importance_train = self._calculate_importance(
599
- all_shap_train, feature_names, period_name='Train Period'
600
- )
601
- importance_test = self._calculate_importance(
602
- all_shap_test, feature_names, period_name='Test Period'
603
- )
604
- print()
605
-
606
- importance_df_combined = self._create_combined_importance_df(
607
- importance_train, importance_test, feature_names
608
- )
609
-
610
- # 시각화
611
- if show_plot:
612
- visualizer = SHAPVisualizer(self.config)
613
- visualizer.plot_summary(
614
- all_shap_train, all_X_train, all_shap_test, X_test,
615
- feature_names, importance_train, importance_test,
616
- show_train_plot=show_train_plot, show_test_plot=show_test_plot,
617
- model_name=model_name, data_sample=data_sample, region=region
618
- )
619
-
620
- return SHAPResult(
621
- shap_train=all_shap_train,
622
- X_train=all_X_train,
623
- shap_test=all_shap_test,
624
- X_test=X_test,
625
- feature_names=feature_names,
626
- importance_train=importance_train,
627
- importance_test=importance_test,
628
- importance_df_combined=importance_df_combined,
629
- metadata={'model_name': model_name, 'region': region, 'data_sample': data_sample}
630
- )
631
-
632
-
633
- # ==========================================
634
- # 7. DeepSHAPAnalyzer 구현
635
- # ==========================================
636
-
637
- class DeepSHAPAnalyzer(SHAPAnalyzer):
638
- """Deep Learning 모델 SHAP 분석 클래스."""
639
-
640
- def __init__(self, config: SHAPConfig, data_loader: DataLoader, device: str = 'cpu'):
641
- super().__init__(config, data_loader)
642
- self.device = device
643
-
644
- def create_explainer(
645
- self,
646
- model: torch.nn.Module,
647
- background_data: Tuple[torch.Tensor, torch.Tensor, pd.Index],
648
- model_name: str
649
- ) -> shap.DeepExplainer:
650
- """DeepExplainer 생성."""
651
- X_bg_num, X_bg_cat, numerical_cols = background_data
652
- wrapped_model = ModelWrapper(model, numerical_cols)
653
- wrapped_model.to(self.device)
654
- wrapped_model.eval()
655
-
656
- with suppress_shap_output():
657
- if model_name in ('ft_transformer', 'deepgbm'):
658
- X_bg_cat_float = X_bg_cat.float()
659
- explainer = shap.DeepExplainer(wrapped_model, [X_bg_num, X_bg_cat_float])
660
- elif model_name == 'resnet_like':
661
- X_bg_combined = torch.cat([X_bg_num, X_bg_cat.float()], dim=1)
662
- explainer = shap.DeepExplainer(wrapped_model, X_bg_combined)
663
- else:
664
- raise ValueError(f"Unknown model_name: {model_name}")
665
-
666
- return explainer
667
-
668
- def calculate_shap_values(
669
- self,
670
- explainer: shap.DeepExplainer,
671
- sample_data: Tuple[torch.Tensor, torch.Tensor],
672
- model_name: str
673
- ) -> Any:
674
- """Deep SHAP 값 계산."""
675
- X_sample_num, X_sample_cat = sample_data
676
-
677
- with suppress_shap_output():
678
- if model_name in ('ft_transformer', 'deepgbm'):
679
- X_sample_cat_float = X_sample_cat.float()
680
- shap_values = explainer.shap_values([X_sample_num, X_sample_cat_float])
681
- elif model_name == 'resnet_like':
682
- X_sample_combined = torch.cat([X_sample_num, X_sample_cat.float()], dim=1)
683
- shap_values = explainer.shap_values(X_sample_combined)
684
- else:
685
- raise ValueError(f"Unknown model_name: {model_name}")
686
-
687
- return shap_values
688
-
689
- def analyze(
690
- self,
691
- model_name: str,
692
- region: str,
693
- data_sample: str,
694
- show_plot: bool = True,
695
- show_train_plot: bool = True,
696
- show_test_plot: bool = True
697
- ) -> Optional[SHAPResult]:
698
- """Deep Learning 모델 SHAP 분석 수행."""
699
- print(f"[{model_name.upper()}] {region} - {data_sample} SHAP Analysis Start...")
700
-
701
- # 모델 및 scaler 로드
702
- try:
703
- model_path = f"../save_model/{model_name}_optima/{model_name}_{data_sample}_{region}.pkl"
704
- models = load(model_path)
705
-
706
- if data_sample == 'pure':
707
- scaler_filename = f'{model_name}_pure_{region}_scaler.pkl'
708
- else:
709
- scaler_filename = f'{model_name}_{data_sample}_{region}_scaler.pkl'
710
- scaler_path = f'../save_model/{model_name}_optima/scaler/{scaler_filename}'
711
- scalers = load(scaler_path)
712
- except Exception as e:
713
- print(f"모델 또는 scaler 로드 실패: {e}")
714
- return None
715
-
716
- if len(models) != self.config.n_folds or len(scalers) != self.config.n_folds:
717
- raise ValueError(
718
- f"모델 개수({len(models)}) 또는 scaler 개수({len(scalers)})가 "
719
- f"n_folds({self.config.n_folds})와 일치하지 않습니다."
720
- )
721
-
722
- # 각 fold의 SHAP 값 저장
723
- all_shap_train = []
724
- all_shap_test = []
725
- all_train_data = []
726
- all_test_data = []
727
- feature_names = None
728
-
729
- for fold_idx in range(self.config.n_folds):
730
- print(f" Processing Fold {fold_idx + 1}...")
731
-
732
- try:
733
- (X_train, X_val, X_test, categorical_cols, numerical_cols,
734
- X_train_num, X_train_cat, X_val_num, X_val_cat, X_test_num, X_test_cat) = \
735
- self.data_loader.prepare_for_dl_model(region, data_sample, fold_idx, scalers[fold_idx])
736
-
737
- if feature_names is None:
738
- feature_names = list(numerical_cols) + list(categorical_cols)
739
-
740
- model = models[fold_idx]
741
- model.to(self.device)
742
- model.eval()
743
-
744
- # 배경 데이터 샘플링
745
- n_bg = min(self.config.n_background_samples, len(X_val_num))
746
- bg_indices = np.random.choice(len(X_val_num), n_bg, replace=False)
747
- X_bg_num = X_val_num[bg_indices].to(self.device)
748
- X_bg_cat = X_val_cat[bg_indices].to(self.device)
749
-
750
- # Validation 데이터 샘플링
751
- n_train = min(self.config.n_test_samples, len(X_val_num))
752
- available_indices = np.setdiff1d(np.arange(len(X_val_num)), bg_indices)
753
- if len(available_indices) < n_train:
754
- train_indices = available_indices
755
- else:
756
- train_indices = np.random.choice(available_indices, n_train, replace=False)
757
- X_train_sample_num = X_val_num[train_indices].to(self.device)
758
- X_train_sample_cat = X_val_cat[train_indices].to(self.device)
759
-
760
- # 테스트 데이터 샘플링
761
- n_test = min(self.config.n_test_samples, len(X_test_num))
762
- test_indices = np.random.choice(len(X_test_num), n_test, replace=False)
763
- X_test_sample_num = X_test_num[test_indices].to(self.device)
764
- X_test_sample_cat = X_test_cat[test_indices].to(self.device)
765
-
766
- # SHAP Explainer 생성 및 계산
767
- try:
768
- explainer = self.create_explainer(
769
- model, (X_bg_num, X_bg_cat, numerical_cols), model_name
770
- )
771
-
772
- shap_values_train = self.calculate_shap_values(
773
- explainer, (X_train_sample_num, X_train_sample_cat), model_name
774
- )
775
- shap_val_train = ShapValueExtractor.extract(shap_values_train, self.config.target_class)
776
- all_shap_train.append(shap_val_train)
777
- X_train_original = X_val.iloc[train_indices].copy()
778
- all_train_data.append(X_train_original)
779
-
780
- shap_values_test = self.calculate_shap_values(
781
- explainer, (X_test_sample_num, X_test_sample_cat), model_name
782
- )
783
- shap_val_test = ShapValueExtractor.extract(shap_values_test, self.config.target_class)
784
- all_shap_test.append(shap_val_test)
785
- X_test_original = X_test.iloc[test_indices].copy()
786
- all_test_data.append(X_test_original)
787
-
788
- except Exception as e:
789
- print(f" Warning: Fold {fold_idx + 1} SHAP 계산 실패: {e}")
790
- continue
791
- except Exception as e:
792
- print(f" Warning: Fold {fold_idx + 1} 처리 실패: {e}")
793
- continue
794
-
795
- if len(all_shap_train) == 0 and len(all_shap_test) == 0:
796
- print(" 모든 fold에서 SHAP 계산 실패")
797
- return None
798
-
799
- # 데이터 통합
800
- all_shap_train_combined = np.concatenate(all_shap_train, axis=0) if len(all_shap_train) > 0 else None
801
- all_train_data_combined = pd.concat(all_train_data, axis=0).reset_index(drop=True) if len(all_train_data) > 0 else None
802
-
803
- if len(all_shap_test) > 0:
804
- all_shap_test_combined = np.mean(all_shap_test, axis=0)
805
- all_test_data_combined = all_test_data[0] if len(all_test_data) > 0 else None
806
- else:
807
- all_shap_test_combined = None
808
- all_test_data_combined = None
809
-
810
- # 중요도 계산
811
- if all_shap_train_combined is not None:
812
- importance_train = self._calculate_importance(
813
- all_shap_train_combined, feature_names, period_name='Train Period'
814
- )
815
- else:
816
- importance_train = None
817
-
818
- if all_shap_test_combined is not None:
819
- importance_test = self._calculate_importance(
820
- all_shap_test_combined, feature_names, period_name='Test Period'
821
- )
822
- print()
823
- else:
824
- importance_test = None
825
-
826
- importance_df_combined = self._create_combined_importance_df(
827
- importance_train, importance_test, feature_names
828
- )
829
-
830
- # 시각화
831
- if show_plot:
832
- visualizer = SHAPVisualizer(self.config)
833
- visualizer.plot_summary(
834
- all_shap_train_combined, all_train_data_combined,
835
- all_shap_test_combined, all_test_data_combined,
836
- feature_names, importance_train, importance_test,
837
- show_train_plot=show_train_plot, show_test_plot=show_test_plot,
838
- model_name=model_name, data_sample=data_sample, region=region
839
- )
840
-
841
- print(f" [{model_name.upper()}] SHAP Analysis Complete!\n")
842
-
843
- return SHAPResult(
844
- shap_train=all_shap_train_combined,
845
- X_train=all_train_data_combined[feature_names] if all_train_data_combined is not None else None,
846
- shap_test=all_shap_test_combined,
847
- X_test=all_test_data_combined[feature_names] if all_test_data_combined is not None else None,
848
- feature_names=feature_names,
849
- importance_train=importance_train,
850
- importance_test=importance_test,
851
- importance_df_combined=importance_df_combined,
852
- metadata={'model_name': model_name, 'region': region, 'data_sample': data_sample}
853
- )
854
-
855
-
856
- # ==========================================
857
- # 8. SHAPVisualizer 클래스
858
- # ==========================================
859
-
860
- class SHAPVisualizer:
861
- """SHAP 시각화를 담당하는 클래스."""
862
-
863
- def __init__(self, config: SHAPConfig):
864
- self.config = config
865
-
866
- def _calculate_xlim(
867
- self,
868
- shap_train: Optional[np.ndarray],
869
- shap_test: Optional[np.ndarray]
870
- ) -> Tuple[Optional[float], Optional[float]]:
871
- """x축 범위 계산."""
872
- if shap_train is not None and shap_test is not None:
873
- x_min = min(shap_train.min(), shap_test.min())
874
- x_max = max(shap_train.max(), shap_test.max())
875
- elif shap_train is not None:
876
- x_min, x_max = shap_train.min(), shap_train.max()
877
- elif shap_test is not None:
878
- x_min, x_max = shap_test.min(), shap_test.max()
879
- else:
880
- return None, None
881
-
882
- margin = (x_max - x_min) * self.config.DEFAULT_PLOT_MARGIN
883
- x_min -= margin
884
- x_max += margin
885
- return x_min, x_max
886
-
887
- def plot_summary(
888
- self,
889
- shap_train: Optional[np.ndarray],
890
- X_train: Optional[pd.DataFrame],
891
- shap_test: Optional[np.ndarray],
892
- X_test: Optional[pd.DataFrame],
893
- feature_names: List[str],
894
- importance_train: Optional[ImportanceData],
895
- importance_test: Optional[ImportanceData],
896
- show_train_plot: bool = True,
897
- show_test_plot: bool = True,
898
- model_name: Optional[str] = None,
899
- data_sample: Optional[str] = None,
900
- region: Optional[str] = None
901
- ) -> None:
902
- """SHAP summary plot 생성."""
903
- num_plots = int(show_train_plot and shap_train is not None) + int(show_test_plot and shap_test is not None)
904
- if num_plots == 0:
905
- return
906
-
907
- x_min, x_max = self._calculate_xlim(shap_train, shap_test)
908
-
909
- # Title prefix 생성
910
- title_parts = []
911
- if model_name is not None:
912
- title_parts.append(model_name.upper())
913
- if data_sample is not None:
914
- title_parts.append(data_sample)
915
- if region is not None:
916
- title_parts.append(region.upper())
917
-
918
- title_prefix = " - ".join(title_parts) + "\n" if title_parts else ""
919
-
920
- if num_plots == 2:
921
- fig, axes = plt.subplots(1, 2, figsize=self.config.figsize)
922
-
923
- if show_train_plot and shap_train is not None:
924
- plt.sca(axes[0])
925
- shap.summary_plot(
926
- shap_train,
927
- X_train[feature_names] if isinstance(X_train, pd.DataFrame) else X_train,
928
- show=False, plot_size=None
929
- )
930
- if x_min is not None and x_max is not None:
931
- plt.xlim(x_min, x_max)
932
- share_text = (
933
- f"\nTop {self.config.top_n} Share: {importance_train.top_n_share:.1f}%"
934
- if importance_train else ""
935
- )
936
- plt.title(f"{title_prefix}Train Period (2018-20): Class {self.config.target_class}{share_text}")
937
-
938
- if show_test_plot and shap_test is not None:
939
- plt.sca(axes[1])
940
- shap.summary_plot(
941
- shap_test,
942
- X_test[feature_names] if isinstance(X_test, pd.DataFrame) else X_test,
943
- show=False, plot_size=None
944
- )
945
- if x_min is not None and x_max is not None:
946
- plt.xlim(x_min, x_max)
947
- share_text = (
948
- f"\nTop {self.config.top_n} Share: {importance_test.top_n_share:.1f}%"
949
- if importance_test else ""
950
- )
951
- plt.title(f"{title_prefix}Test Period (2021): Class {self.config.target_class}{share_text}")
952
- else:
953
- plt.figure(figsize=(self.config.figsize[0]//2, self.config.figsize[1]))
954
- if show_train_plot and shap_train is not None:
955
- data = X_train[feature_names] if isinstance(X_train, pd.DataFrame) else X_train
956
- shap_v = shap_train
957
- share_text = (
958
- f"\nTop {self.config.top_n} Share: {importance_train.top_n_share:.1f}%"
959
- if importance_train else ""
960
- )
961
- title = f"{title_prefix}Train Period (2018-20): Class {self.config.target_class}{share_text}"
962
- elif show_test_plot and shap_test is not None:
963
- data = X_test[feature_names] if isinstance(X_test, pd.DataFrame) else X_test
964
- shap_v = shap_test
965
- share_text = (
966
- f"\nTop {self.config.top_n} Share: {importance_test.top_n_share:.1f}%"
967
- if importance_test else ""
968
- )
969
- title = f"{title_prefix}Test Period (2021): Class {self.config.target_class}{share_text}"
970
- else:
971
- return
972
-
973
- shap.summary_plot(shap_v, data, show=False)
974
- if x_min is not None and x_max is not None:
975
- plt.xlim(x_min, x_max)
976
- plt.title(title)
977
-
978
- plt.tight_layout()
979
- plt.show()
980
-
981
- def plot_elbow(self, importance_df_combined: pd.DataFrame) -> None:
982
- """Elbow plot 생성."""
983
- elbow = importance_df_combined.loc[:, ['feature', 'importance_train', 'share_train']]
984
- elbow = elbow.sort_values('importance_train', ascending=False)
985
- plt.figure(figsize=(8, 5))
986
- plt.plot(range(1, len(elbow) + 1), elbow['share_train'].values, marker='o')
987
- plt.xlabel('Feature Rank')
988
- plt.ylabel('Share Train (%)')
989
- plt.title('Elbow Plot for Feature Importance (share_train)')
990
- plt.xticks(range(1, len(elbow) + 1))
991
- plt.grid(True, linestyle='--', alpha=0.5)
992
- plt.show()
993
-
994
-
995
- # ==========================================
996
- # 9. WassersteinCalculator 클래스
997
- # ==========================================
998
-
999
- class WassersteinCalculator:
1000
- """Wasserstein Distance 계산을 담당하는 클래스."""
1001
-
1002
- def __init__(self, config: SHAPConfig, data_loader: DataLoader):
1003
- self.config = config
1004
- self.data_loader = data_loader
1005
-
1006
- def _compute_wd_for_feature(
1007
- self,
1008
- train_dist: np.ndarray,
1009
- test_dist: np.ndarray
1010
- ) -> Dict[str, float]:
1011
- """단일 feature에 대한 WD 계산."""
1012
- wd_val = wasserstein_distance(train_dist, test_dist)
1013
- return {
1014
- 'WD_Score': wd_val,
1015
- 'Train_Mean': np.mean(train_dist),
1016
- 'Test_Mean': np.mean(test_dist),
1017
- 'Train_Std': np.std(train_dist),
1018
- 'Test_Std': np.std(test_dist),
1019
- 'Train_Count': len(train_dist),
1020
- 'Test_Count': len(test_dist)
1021
- }
1022
-
1023
- def calculate(
1024
- self,
1025
- region: str,
1026
- data_sample: str,
1027
- top_features: List[str],
1028
- model_type: str = 'tree',
1029
- fold_idx: int = 0
1030
- ) -> pd.DataFrame:
1031
- """Wasserstein Distance 계산."""
1032
- # 데이터 로드
1033
- if model_type == 'tree':
1034
- train_df, val_df, test_df = self.data_loader.prepare_for_tree_model(
1035
- region, data_sample, fold_idx
1036
- )
1037
- elif model_type == 'dl':
1038
- train_df, val_df, test_df = self.data_loader.load_raw_data(
1039
- region, data_sample, fold_idx
1040
- )
1041
- else:
1042
- raise ValueError(f"Unknown model_type: {model_type}. Must be 'tree' or 'dl'")
1043
-
1044
- train_period = pd.concat([train_df, val_df], axis=0)
1045
- test_period = test_df
1046
-
1047
- results = []
1048
-
1049
- print(f"[{region.upper()}] Wasserstein Distance Analysis")
1050
- print(f"Model Type: {model_type.upper()}, Data Sample: {data_sample}")
1051
- print(f"{'-'*60}")
1052
-
1053
- for feature in top_features:
1054
- if feature in train_period.columns:
1055
- dist_train = train_period[feature].dropna().values
1056
- dist_test = test_period[feature].dropna().values
1057
-
1058
- if len(dist_train) == 0 or len(dist_test) == 0:
1059
- print(f"Warning: {feature} 변수에 유효한 데이터가 없습니다.")
1060
- continue
1061
-
1062
- wd_metrics = self._compute_wd_for_feature(dist_train, dist_test)
1063
- results.append({
1064
- 'Region': region,
1065
- 'Feature': feature,
1066
- **wd_metrics
1067
- })
1068
- print(f"Feature: {feature:20} | WD: {wd_metrics['WD_Score']:.4f} | "
1069
- f"Train Mean: {wd_metrics['Train_Mean']:.4f} | Test Mean: {wd_metrics['Test_Mean']:.4f}")
1070
- else:
1071
- print(f"Warning: {feature} 변수를 찾을 수 없습니다.")
1072
-
1073
- print()
1074
- return pd.DataFrame(results)
1075
-
1076
- def calculate_from_shap_result(
1077
- self,
1078
- shap_result: Dict[str, Any],
1079
- top_n: int = 3,
1080
- use_train_features: bool = True,
1081
- model_type: str = 'tree',
1082
- fold_idx: int = 0
1083
- ) -> pd.DataFrame:
1084
- """SHAP 분석 결과에서 상위 변수를 자동 추출하여 WD 계산."""
1085
- # 상위 변수 추출
1086
- if use_train_features:
1087
- if 'top_n_features_train' in shap_result and shap_result['top_n_features_train'] is not None:
1088
- top_features = shap_result['top_n_features_train'][:top_n]
1089
- elif 'importance_df_train' in shap_result and shap_result['importance_df_train'] is not None:
1090
- top_features = shap_result['importance_df_train'].head(top_n)['feature'].tolist()
1091
- else:
1092
- raise ValueError("SHAP 결과에서 Train 상위 변수를 찾을 수 없습니다.")
1093
- else:
1094
- if 'top_n_features_test' in shap_result and shap_result['top_n_features_test'] is not None:
1095
- top_features = shap_result['top_n_features_test'][:top_n]
1096
- elif 'importance_df_test' in shap_result and shap_result['importance_df_test'] is not None:
1097
- top_features = shap_result['importance_df_test'].head(top_n)['feature'].tolist()
1098
- else:
1099
- raise ValueError("SHAP 결과에서 Test 상위 변수를 찾을 수 없습니다.")
1100
-
1101
- region = shap_result.get('region', 'unknown')
1102
- data_sample = shap_result.get('data_sample', 'unknown')
1103
-
1104
- return self.calculate(region, data_sample, top_features, model_type, fold_idx)
1105
-
1106
-
1107
- # ==========================================
1108
- # 10. 하위 호환성 레이어
1109
- # ==========================================
1110
-
1111
- def analyze_shap_values_across_folds(
1112
- model_name: str,
1113
- region: str,
1114
- data_sample: str,
1115
- target_class: int = 0,
1116
- n_folds: int = 3,
1117
- figsize: Tuple[int, int] = (20, 10),
1118
- show_plot: bool = True,
1119
- show_train_plot: bool = True,
1120
- show_test_plot: bool = True,
1121
- suppress_warnings: bool = True,
1122
- top_n: int = 3
1123
- ) -> Dict[str, Any]:
1124
- """Tree 모델 SHAP 분석 (하위 호환성 래퍼 함수).
1125
-
1126
- Deprecated: Use TreeSHAPAnalyzer instead.
1127
- """
1128
- config = SHAPConfig(
1129
- n_folds=n_folds,
1130
- figsize=figsize,
1131
- top_n=top_n,
1132
- target_class=target_class
1133
- )
1134
- data_loader = DataLoader(config)
1135
- analyzer = TreeSHAPAnalyzer(config, data_loader)
1136
- result = analyzer.analyze(
1137
- model_name, region, data_sample,
1138
- show_plot=show_plot,
1139
- show_train_plot=show_train_plot,
1140
- show_test_plot=show_test_plot
1141
- )
1142
- return result.to_dict()
1143
-
1144
-
1145
- def analyze_dl_model_shap(
1146
- model_name: str,
1147
- region: str,
1148
- data_sample: str,
1149
- target_class: int = 0,
1150
- n_folds: int = 3,
1151
- n_background_samples: int = 200,
1152
- n_test_samples: int = 500,
1153
- device: str = 'cpu',
1154
- figsize: Tuple[int, int] = (20, 8),
1155
- show_plot: bool = True,
1156
- show_train_plot: bool = True,
1157
- show_test_plot: bool = True,
1158
- top_n: int = 3
1159
- ) -> Optional[Dict[str, Any]]:
1160
- """Deep Learning 모델 SHAP 분석 (하위 호환성 래퍼 함수).
1161
-
1162
- Deprecated: Use DeepSHAPAnalyzer instead.
1163
- """
1164
- config = SHAPConfig(
1165
- n_folds=n_folds,
1166
- n_background_samples=n_background_samples,
1167
- n_test_samples=n_test_samples,
1168
- figsize=figsize,
1169
- top_n=top_n,
1170
- target_class=target_class,
1171
- device=device
1172
- )
1173
- data_loader = DataLoader(config)
1174
- analyzer = DeepSHAPAnalyzer(config, data_loader, device=device)
1175
- result = analyzer.analyze(
1176
- model_name, region, data_sample,
1177
- show_plot=show_plot,
1178
- show_train_plot=show_train_plot,
1179
- show_test_plot=show_test_plot
1180
- )
1181
- return result.to_dict() if result else None
1182
-
1183
-
1184
- def calculate_wasserstein_distance(
1185
- region: str,
1186
- data_sample: str,
1187
- top_features: List[str],
1188
- model_type: str = 'tree',
1189
- fold_idx: int = 0
1190
- ) -> pd.DataFrame:
1191
- """Wasserstein Distance 계산 (하위 호환성 래퍼 함수).
1192
-
1193
- Deprecated: Use WassersteinCalculator instead.
1194
- """
1195
- config = SHAPConfig()
1196
- data_loader = DataLoader(config)
1197
- calculator = WassersteinCalculator(config, data_loader)
1198
- return calculator.calculate(region, data_sample, top_features, model_type, fold_idx)
1199
-
1200
-
1201
- def calculate_wd_from_shap_result(
1202
- shap_result: Dict[str, Any],
1203
- top_n: int = 3,
1204
- use_train_features: bool = True,
1205
- model_type: str = 'tree',
1206
- fold_idx: int = 0
1207
- ) -> pd.DataFrame:
1208
- """SHAP 결과에서 WD 계산 (하위 호환성 래퍼 함수).
1209
-
1210
- Deprecated: Use WassersteinCalculator instead.
1211
- """
1212
- config = SHAPConfig()
1213
- data_loader = DataLoader(config)
1214
- calculator = WassersteinCalculator(config, data_loader)
1215
- return calculator.calculate_from_shap_result(
1216
- shap_result, top_n, use_train_features, model_type, fold_idx
1217
- )
1218
-
1219
-
1220
- def elbow_plot(result: Dict[str, Any]) -> None:
1221
- """Elbow plot 생성 (하위 호환성 래퍼 함수)."""
1222
- config = SHAPConfig()
1223
- visualizer = SHAPVisualizer(config)
1224
- if 'importance_df_combined' in result:
1225
- visualizer.plot_elbow(result['importance_df_combined'])
1226
-
1227
-
1228
- # ==========================================
1229
- # 유틸리티 함수 (기존 호환성 유지)
1230
- # ==========================================
1231
-
1232
- def calculate_csi(y_true: np.ndarray, y_pred: np.ndarray) -> float:
1233
- """CSI(Critical Success Index) 점수를 계산합니다."""
1234
- cm = confusion_matrix(y_true, y_pred)
1235
- H = cm[0, 0] + cm[1, 1]
1236
- F = cm[1, 0] + cm[2, 0] + cm[0, 1] + cm[2, 1]
1237
- M = cm[0, 2] + cm[1, 2]
1238
- csi = H / (H + F + M + 1e-10)
1239
- return csi
1240
-
1241
-
1242
- def csi_metric(y_true: np.ndarray, pred_prob: np.ndarray) -> Tuple[str, float, bool]:
1243
- """LightGBM용 CSI 메트릭 함수."""
1244
- y_pred_binary = np.argmax(pred_prob, axis=1)
1245
- score = calculate_csi(y_true, y_pred_binary)
1246
- return 'CSI', score, True
1247
-
1248
-
1249
- def eval_metric_csi(y_true: np.ndarray, pred_prob: np.ndarray) -> float:
1250
- """XGBoost용 CSI 메트릭 함��."""
1251
- pred = np.argmax(pred_prob, axis=1)
1252
- csi = calculate_csi(y_true, pred)
1253
- return -1 * csi
1254
-
1255
-
1256
- # 기존 상수들 (하위 호환성)
1257
- FOLD_YEARS = SHAPConfig.FOLD_YEARS
1258
- TEST_YEAR = SHAPConfig.TEST_YEAR
1259
- DROP_COLUMNS = SHAPConfig.DROP_COLUMNS
1260
- FEATURE_COLUMNS = SHAPConfig.FEATURE_COLUMNS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Analysis_code/7.ensemble/utils.py DELETED
@@ -1,512 +0,0 @@
1
- # 표준 라이브러리
2
- import os
3
- import sys
4
- import warnings
5
- from typing import List, Tuple, Any
6
-
7
- # 서드파티 라이브러리
8
- import numpy as np
9
- import pandas as pd
10
- import torch
11
- import torch.nn.functional as F
12
- from joblib import load
13
- from sklearn.metrics import confusion_matrix
14
- from sklearn.preprocessing import LabelEncoder
15
-
16
- # 경고 무시
17
- warnings.filterwarnings('ignore')
18
-
19
- # ==========================================
20
- # 상수 정의
21
- # ==========================================
22
- TEST_YEAR = 2021
23
- DROP_COLUMNS = ['multi_class', 'year']
24
- FEATURE_COLUMNS = [
25
- 'temp_C', 'precip_mm', 'wind_speed', 'wind_dir', 'hm',
26
- 'vap_pressure', 'dewpoint_C', 'loc_pressure', 'sea_pressure',
27
- 'solarRad', 'snow_cm', 'cloudcover', 'lm_cloudcover', 'low_cloudbase',
28
- 'groundtemp', 'O3', 'NO2', 'PM10', 'PM25', 'year',
29
- 'month', 'hour', 'ground_temp - temp_C', 'hour_sin', 'hour_cos',
30
- 'month_sin', 'month_cos', 'multi_class'
31
- ]
32
-
33
- # 모델 타입 상수
34
- TREE_MODELS = ['xgb', 'lgb']
35
- DL_MODELS = ['ft_transformer', 'resnet_like', 'deepgbm']
36
-
37
- # ==========================================
38
- # 경로 설정
39
- # ==========================================
40
- def _setup_model_path() -> str:
41
- """모델 클래스 import를 위한 경로를 설정하고 반환합니다.
42
-
43
- Returns:
44
- models 디렉토리의 절대 경로
45
- """
46
- try:
47
- current_file_dir = os.path.dirname(os.path.abspath(__file__))
48
- models_path = os.path.abspath(os.path.join(current_file_dir, '../models'))
49
- except NameError:
50
- # 노트북 환경에서 __file__이 없는 경우
51
- cwd = os.getcwd()
52
- if 'Analysis_code' in cwd:
53
- analysis_code_dir = cwd[:cwd.index('Analysis_code') + len('Analysis_code')]
54
- models_path = os.path.join(analysis_code_dir, 'models')
55
- else:
56
- models_path = '/workspace/visibility_prediction/Analysis_code/models'
57
-
58
- if models_path not in sys.path:
59
- sys.path.insert(0, models_path)
60
-
61
- return models_path
62
-
63
- # 모델 경로 설정 및 import
64
- _setup_model_path()
65
- from ft_transformer import FTTransformer
66
- from resnet_like import ResNetLike
67
- from deepgbm import DeepGBM
68
-
69
- # ==========================================
70
- # 메트릭 함수 (pickle 호환성)
71
- # ==========================================
72
- def calculate_csi(y_true: np.ndarray, y_pred: np.ndarray) -> float:
73
- """CSI(Critical Success Index) 점수를 계산합니다.
74
-
75
- Args:
76
- y_true: 실제 레이블
77
- y_pred: 예측 레이블
78
-
79
- Returns:
80
- CSI 점수 (0~1 사이의 값)
81
- """
82
- cm = confusion_matrix(y_true, y_pred)
83
-
84
- # 혼동 행렬에서 H(Hit), F(False alarm), M(Miss) 추출
85
- H = cm[0, 0] + cm[1, 1]
86
- F = cm[1, 0] + cm[2, 0] + cm[0, 1] + cm[2, 1]
87
- M = cm[0, 2] + cm[1, 2]
88
-
89
- # CSI 계산
90
- csi = H / (H + F + M + 1e-10)
91
- return csi
92
-
93
- def eval_metric_csi(y_true: np.ndarray, pred_prob: np.ndarray) -> float:
94
- """XGBoost용 CSI 메트릭 함수.
95
-
96
- Args:
97
- y_true: 실제 레이블
98
- pred_prob: 예측 확률
99
-
100
- Returns:
101
- CSI 점수의 음수값
102
- """
103
- pred = np.argmax(pred_prob, axis=1)
104
- csi = calculate_csi(y_true, pred)
105
- return -1 * csi
106
-
107
- def csi_metric(y_true: np.ndarray, pred_prob: np.ndarray) -> Tuple[str, float, bool]:
108
- """LightGBM용 CSI 메트릭 함수.
109
-
110
- Args:
111
- y_true: 실제 레이블
112
- pred_prob: 예측 확률 (shape: [n_samples, n_classes])
113
-
114
- Returns:
115
- ('CSI', score, higher_better) 튜플
116
- """
117
- y_pred_binary = np.argmax(pred_prob, axis=1)
118
- score = calculate_csi(y_true, y_pred_binary)
119
- return 'CSI', score, True
120
-
121
- # ==========================================
122
- # Pickle 호환성 함수 등록
123
- # ==========================================
124
- def _register_pickle_functions(model_name: str) -> None:
125
- """모델 로드를 위해 __main__ 모듈에 필요한 함수를 등록합니다.
126
-
127
- 모델이 저장될 때 __main__ 모듈에서 정의된 함수를 참조했을 수 있으므로,
128
- pickle이 모델을 로드할 때 함수를 찾을 수 있도록 등록합니다.
129
-
130
- Args:
131
- model_name: 모델 이름 ('xgb' 또는 'lgb')
132
- """
133
- if '__main__' not in sys.modules:
134
- return
135
-
136
- main_module = sys.modules['__main__']
137
- main_module.calculate_csi = calculate_csi
138
-
139
- if model_name == 'xgb':
140
- main_module.eval_metric_csi = eval_metric_csi
141
- elif model_name == 'lgb':
142
- main_module.csi_metric = csi_metric
143
-
144
- # ==========================================
145
- # 데이터 전처리 함수
146
- # ==========================================
147
- def add_derived_features(df: pd.DataFrame) -> pd.DataFrame:
148
- """제거했던 파생 변수들을 복구합니다.
149
-
150
- Args:
151
- df: 원본 데이터프레임
152
-
153
- Returns:
154
- 파생 변수가 추가된 데이터프레임
155
- """
156
- df = df.copy()
157
- df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24)
158
- df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24)
159
- df['month_sin'] = np.sin(2 * np.pi * df['month'] / 12)
160
- df['month_cos'] = np.cos(2 * np.pi * df['month'] / 12)
161
- df['ground_temp - temp_C'] = df['groundtemp'] - df['temp_C']
162
- return df
163
-
164
- def preprocessing(df: pd.DataFrame) -> pd.DataFrame:
165
- """데이터 전처리 함수.
166
-
167
- Args:
168
- df: 원본 데이터프레임
169
-
170
- Returns:
171
- 전처리된 데이터프레임
172
- """
173
- df = df[df.columns].copy()
174
- df['year'] = df['year'].astype('int')
175
- df['month'] = df['month'].astype('int')
176
- df['hour'] = df['hour'].astype('int')
177
- df = add_derived_features(df).copy()
178
- df['multi_class'] = df['multi_class'].astype('int')
179
- df.loc[df['wind_dir'] == '정온', 'wind_dir'] = "0"
180
- df['wind_dir'] = df['wind_dir'].astype('int')
181
- df = df[FEATURE_COLUMNS].copy()
182
- return df
183
-
184
- # ==========================================
185
- # 모델 로드 함수
186
- # ==========================================
187
- def load_model(model_name: str, region: str, data_sample: str) -> List[Any]:
188
- """모델을 로드하는 함수.
189
-
190
- Args:
191
- model_name: 모델 이름 ('xgb', 'lgb', 'ft_transformer', 'resnet_like', 'deepgbm')
192
- region: 지역명 (예: 'seoul', 'busan')
193
- data_sample: 데이터 샘플 타입 (예: 'pure', 'smote', 'ctgan10000')
194
-
195
- Returns:
196
- 로드된 모델 리스트 (각 fold별 모델)
197
-
198
- Raises:
199
- FileNotFoundError: 모델 파일을 찾을 수 없는 경우
200
- ValueError: 모델 이름이 유효하지 않은 경우
201
- """
202
- if model_name not in TREE_MODELS + DL_MODELS:
203
- raise ValueError(
204
- f"Unknown model_name: {model_name}. "
205
- f"Must be one of {TREE_MODELS + DL_MODELS}"
206
- )
207
-
208
- # Tree-based 모델의 경우 pickle 호환성 함수 등록
209
- if model_name in TREE_MODELS:
210
- _register_pickle_functions(model_name)
211
-
212
- model_path = f"../save_model/{model_name}_optima/{model_name}_{data_sample}_{region}.pkl"
213
-
214
- if not os.path.exists(model_path):
215
- raise FileNotFoundError(
216
- f"모델 파일을 찾을 수 없습니다: {model_path}"
217
- )
218
-
219
- try:
220
- models = load(model_path)
221
- except Exception as e:
222
- raise FileNotFoundError(f"모델 로드 실패 ({model_path}): {e}")
223
-
224
- return models
225
-
226
- def load_scaler(model_name: str, region: str, data_sample: str) -> List[Any]:
227
- """각 fold의 scaler를 로드하는 함수.
228
-
229
- Args:
230
- model_name: 모델 이름
231
- region: 지역명
232
- data_sample: 데이터 샘플 타입
233
-
234
- Returns:
235
- 로드된 scaler 리스트 (각 fold별 scaler)
236
-
237
- Raises:
238
- FileNotFoundError: Scaler 파일을 찾을 수 없는 경우
239
- """
240
- if data_sample == 'pure':
241
- scaler_filename = f'{model_name}_pure_{region}_scaler.pkl'
242
- else:
243
- scaler_filename = f'{model_name}_{data_sample}_{region}_scaler.pkl'
244
-
245
- scaler_path = f'../save_model/{model_name}_optima/scaler/{scaler_filename}'
246
-
247
- if not os.path.exists(scaler_path):
248
- raise FileNotFoundError(
249
- f"Scaler 파일을 찾을 수 없습니다: {scaler_path}"
250
- )
251
-
252
- try:
253
- scalers = load(scaler_path)
254
- except Exception as e:
255
- raise FileNotFoundError(f"Scaler 로드 실패 ({scaler_path}): {e}")
256
-
257
- return scalers
258
-
259
- # ==========================================
260
- # Tree-based 모델용 헬퍼 함수
261
- # ==========================================
262
- def _prepare_test_data_tree(region: str) -> Tuple[pd.DataFrame, np.ndarray]:
263
- """Tree-based 모델용 test 데이터를 준비합니다.
264
-
265
- Args:
266
- region: 지역명
267
-
268
- Returns:
269
- tuple: (X_test, y_test)
270
- - X_test: 전처리된 test 데이터 (설명변수만)
271
- - y_test: test 데이터의 target 값 (multi_class)
272
- """
273
- test_data = pd.read_csv(f"../../data/data_for_modeling/{region}_test.csv")
274
- test_data = preprocessing(test_data)
275
- test_data = test_data.loc[test_data['year'].isin([TEST_YEAR]), :]
276
- X_test = test_data.drop(columns=DROP_COLUMNS)
277
- y_test = test_data['multi_class'].values
278
- return X_test, y_test
279
-
280
- def _predict_tree_model(model: Any, X_test: pd.DataFrame) -> np.ndarray:
281
- """Tree-based 모델로 예측을 수행합니다.
282
-
283
- Args:
284
- model: 학습된 모델 (XGBoost 또는 LightGBM)
285
- X_test: Test 데이터
286
-
287
- Returns:
288
- 예측 확률 (n_samples, n_classes)
289
- """
290
- return model.predict_proba(X_test)
291
-
292
- # ==========================================
293
- # Deep Learning 모델용 헬퍼 함수
294
- # ==========================================
295
- def _prepare_test_data_dl(
296
- region: str,
297
- data_sample: str,
298
- fold_idx: int,
299
- scaler: Any
300
- ) -> Tuple[torch.Tensor, torch.Tensor, pd.Index, pd.Index, np.ndarray]:
301
- """Deep learning 모델용 test 데이터를 준비합니다.
302
-
303
- Args:
304
- region: 지역명
305
- data_sample: 데이터 샘플 타입
306
- fold_idx: Fold 인덱스 (0, 1, 2)
307
- scaler: 해당 fold의 scaler (QuantileTransformer)
308
-
309
- Returns:
310
- tuple: (X_test_num, X_test_cat, categorical_cols, numerical_cols, y_test)
311
- - X_test_num: Numerical features tensor
312
- - X_test_cat: Categorical features tensor
313
- - categorical_cols: 범주형 변수 컬럼명
314
- - numerical_cols: 연속형 변수 컬럼명
315
- - y_test: test 데이터의 target 값 (multi_class)
316
- """
317
- # Train 데이터 로드 (Label Encoder 학습용)
318
- if data_sample == 'pure':
319
- train_path = f"../../data/data_for_modeling/{region}_train.csv"
320
- train_data = preprocessing(pd.read_csv(train_path, index_col=0))
321
- train_data = train_data.loc[
322
- ~train_data['year'].isin([TEST_YEAR - (fold_idx + 1)]), :
323
- ]
324
- else:
325
- train_data = pd.read_csv(
326
- f"../../data/data_oversampled/{data_sample}/"
327
- f"{data_sample}_{fold_idx+1}_{region}.csv"
328
- )
329
- train_data = preprocessing(train_data)
330
-
331
- # Test 데이터 로드
332
- test_data = pd.read_csv(f"../../data/data_for_modeling/{region}_test.csv")
333
- test_data = preprocessing(test_data)
334
- test_data = test_data.loc[test_data['year'].isin([TEST_YEAR]), :]
335
-
336
- # 컬럼 정렬 (일관성 유지)
337
- common_columns = train_data.columns.to_list()
338
- train_data = train_data[common_columns]
339
- test_data = test_data[common_columns]
340
-
341
- # 설명변수 분리
342
- X_train = train_data.drop(columns=DROP_COLUMNS)
343
- X_test = test_data.drop(columns=DROP_COLUMNS)
344
-
345
- # 범주형 & 연속형 변수 분리
346
- categorical_cols = X_train.select_dtypes(
347
- include=['object', 'category', 'int64']
348
- ).columns
349
- numerical_cols = X_train.select_dtypes(include=['float64']).columns
350
-
351
- # 범주형 변수 Label Encoding (train 데이터 기준으로 학습)
352
- label_encoders = {}
353
- for col in categorical_cols:
354
- le = LabelEncoder()
355
- le.fit(X_train[col])
356
- label_encoders[col] = le
357
-
358
- # 변환 적용
359
- for col in categorical_cols:
360
- X_test[col] = label_encoders[col].transform(X_test[col])
361
-
362
- # 연속형 변수 Scaler 변환
363
- X_test[numerical_cols] = scaler.transform(X_test[numerical_cols])
364
-
365
- # 텐서 변환
366
- X_test_num = torch.tensor(
367
- X_test[numerical_cols].values, dtype=torch.float32
368
- )
369
- X_test_cat = torch.tensor(
370
- X_test[categorical_cols].values, dtype=torch.long
371
- )
372
-
373
- # Target 값 추출
374
- y_test = test_data['multi_class'].values
375
-
376
- return X_test_num, X_test_cat, categorical_cols, numerical_cols, y_test
377
-
378
- def _predict_dl_model(
379
- model: torch.nn.Module,
380
- X_test_num: torch.Tensor,
381
- X_test_cat: torch.Tensor,
382
- model_name: str,
383
- device: str,
384
- batch_size: int = 1024
385
- ) -> np.ndarray:
386
- """Deep learning 모델로 예측을 수행합니다.
387
-
388
- Args:
389
- model: PyTorch 모델
390
- X_test_num: Numerical features tensor
391
- X_test_cat: Categorical features tensor
392
- model_name: 모델 이름 ('ft_transformer', 'resnet_like', 'deepgbm')
393
- device: 연산 장치 ('cpu' 또는 'cuda')
394
- batch_size: 배치 크기 (메모리 효율을 위해, 기본값: 1024)
395
-
396
- Returns:
397
- 예측 확률 (n_samples, n_classes)
398
-
399
- Raises:
400
- ValueError: 알 수 없는 모델 이름인 경우
401
- """
402
- model.eval()
403
- model.to(device)
404
-
405
- n_samples = X_test_num.shape[0]
406
- all_probs = []
407
-
408
- with torch.no_grad():
409
- for i in range(0, n_samples, batch_size):
410
- end_idx = min(i + batch_size, n_samples)
411
- batch_num = X_test_num[i:end_idx].to(device)
412
- batch_cat = X_test_cat[i:end_idx].to(device)
413
-
414
- # 모델별 입력 형식 처리
415
- # 모든 딥러닝 모델이 (x_num, x_cat) 두 개의 텐서를 받음
416
- # resnet_like는 내부에서 torch.cat으로 결합함
417
- if model_name in DL_MODELS:
418
- output = model(batch_num, batch_cat)
419
- else:
420
- raise ValueError(
421
- f"Unknown model_name: {model_name}. "
422
- f"Must be one of {DL_MODELS}"
423
- )
424
-
425
- # Softmax 적용하여 확률 변환
426
- probs = F.softmax(output, dim=1)
427
- all_probs.append(probs.cpu().numpy())
428
-
429
- return np.vstack(all_probs)
430
-
431
- # ==========================================
432
- # 메인 함수
433
- # ==========================================
434
- def predict_test_proba(
435
- model_name: str,
436
- region: str,
437
- data_sample: str,
438
- device: str = 'cpu',
439
- n_folds: int = 3
440
- ) -> Tuple[np.ndarray, np.ndarray]:
441
- """Test 데이터에 대한 예측 확률과 target 값을 반환하는 함수.
442
-
443
- Args:
444
- model_name: 모델 이름 ('xgb', 'lgb', 'ft_transformer', 'resnet_like', 'deepgbm')
445
- region: 지역명 (예: 'seoul', 'busan')
446
- data_sample: 데이터 샘플 타입 (예: 'pure', 'smote', 'ctgan10000')
447
- device: 연산 장치 ('cpu' 또는 'cuda', 기본값: 'cpu')
448
- n_folds: Fold 개수 (기본값: 3)
449
-
450
- Returns:
451
- tuple: (probs, y_test)
452
- - probs: shape (n_folds, n_samples, n_classes) - 각 fold별 예측 확률
453
- - y_test: shape (n_samples,) - test 데이터의 target 값 (multi_class)
454
-
455
- Raises:
456
- ValueError: 모델 이름이 유효하지 않거나 fold 개수가 일치하지 않는 경우
457
- FileNotFoundError: 모델 또는 scaler 파일을 찾을 수 없는 경우
458
- """
459
- # 모델 타입 확인
460
- if model_name not in TREE_MODELS + DL_MODELS:
461
- raise ValueError(
462
- f"Unknown model_name: {model_name}. "
463
- f"Must be one of {TREE_MODELS + DL_MODELS}"
464
- )
465
-
466
- # 모델 로드
467
- models = load_model(model_name, region, data_sample)
468
-
469
- if len(models) != n_folds:
470
- raise ValueError(
471
- f"로드된 모델 개수({len(models)})가 "
472
- f"n_folds({n_folds})와 일치하지 않습니다."
473
- )
474
-
475
- # Tree-based 모델 처리
476
- if model_name in TREE_MODELS:
477
- X_test, y_test = _prepare_test_data_tree(region)
478
- all_probs = []
479
-
480
- for fold_idx in range(n_folds):
481
- prob = _predict_tree_model(models[fold_idx], X_test)
482
- all_probs.append(prob)
483
-
484
- return np.array(all_probs), y_test
485
-
486
- # Deep learning 모델 처리
487
- else:
488
- # Scaler 로드
489
- scalers = load_scaler(model_name, region, data_sample)
490
-
491
- if len(scalers) != n_folds:
492
- raise ValueError(
493
- f"로드된 scaler 개수({len(scalers)})가 "
494
- f"n_folds({n_folds})와 일치하지 않습니다."
495
- )
496
-
497
- all_probs = []
498
- y_test = None
499
-
500
- for fold_idx in range(n_folds):
501
- # Test 데이터 준비
502
- X_test_num, X_test_cat, _, _, y_test = _prepare_test_data_dl(
503
- region, data_sample, fold_idx, scalers[fold_idx]
504
- )
505
-
506
- # 예측
507
- prob = _predict_dl_model(
508
- models[fold_idx], X_test_num, X_test_cat, model_name, device
509
- )
510
- all_probs.append(prob)
511
-
512
- return np.array(all_probs), y_test