File size: 25,902 Bytes
f082ba4
 
 
 
a723c76
f082ba4
 
 
 
 
a723c76
 
 
a80d289
f082ba4
 
a723c76
f082ba4
a723c76
 
 
 
 
 
 
 
f082ba4
a723c76
 
 
 
 
 
 
 
 
f082ba4
a723c76
f082ba4
 
300af79
a723c76
300af79
 
e57f46a
300af79
e57f46a
300af79
 
 
 
 
 
a723c76
300af79
a723c76
 
 
 
 
0e84eb9
 
 
 
 
 
e57f46a
4bcfd02
 
 
 
 
300af79
 
 
 
 
 
 
e57f46a
 
 
4bcfd02
e57f46a
 
4bcfd02
e57f46a
a723c76
300af79
a723c76
 
 
 
 
300af79
a723c76
 
b58149e
a80d289
 
b58149e
0e84eb9
 
 
 
a80d289
 
 
0e84eb9
 
 
 
a80d289
 
 
 
 
 
 
0e84eb9
 
 
 
 
a80d289
 
 
 
0e84eb9
 
 
 
 
 
a80d289
 
 
 
 
 
 
0e84eb9
 
 
 
 
 
 
 
a80d289
 
 
0e84eb9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a80d289
4bcfd02
300af79
4bcfd02
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300af79
4bcfd02
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a80d289
 
300af79
4bcfd02
300af79
4bcfd02
300af79
4bcfd02
 
 
 
 
 
 
300af79
 
 
 
a723c76
 
 
 
 
 
 
 
300af79
a723c76
 
 
300af79
a723c76
 
 
 
 
 
 
 
 
300af79
1534e52
300af79
a723c76
 
4bcfd02
300af79
a723c76
 
 
e57f46a
a723c76
 
 
 
 
 
300af79
a723c76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300af79
a723c76
 
 
e57f46a
a723c76
300af79
a723c76
 
 
 
 
 
 
 
 
 
 
 
 
 
300af79
a723c76
f082ba4
4bcfd02
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9c67a10
4bcfd02
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a723c76
4bcfd02
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81395b1
 
4bcfd02
81395b1
 
 
 
 
 
 
 
 
 
4bcfd02
 
 
 
 
 
 
3bfa9f2
 
 
a723c76
4bcfd02
 
3bfa9f2
a723c76
 
 
4bcfd02
a723c76
 
3bfa9f2
 
a723c76
 
9c67a10
a723c76
3bfa9f2
a723c76
3bfa9f2
 
 
 
9c67a10
4bcfd02
 
 
a80d289
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
"""
Data Preprocessing for Hanoi Temperature Forecasting
- Load raw CSV
- Handle missing values, numeric/categorical coercion
...
- Save processed CSV
"""

import pandas as pd
import numpy as np
from sklearn.feature_selection import VarianceThreshold
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.preprocessing import LabelEncoder
from src.new_feature_engineering_daily import *


# 1. Cho toàn bộ data set
def load_data(input_path: str):
    """
    Load dataset từ đường dẫn CSV.
    """
    try:
        df = pd.read_csv(input_path)
        return df.drop_duplicates()
    except FileNotFoundError:
        raise FileNotFoundError(f"File not found: {input_path}")


def transform_dtype(df: pd.DataFrame):
    """
    Chuyển đổi các kiểu dữ liệu phù hợp.
    """
    if "datetime" in df.columns:
        df["datetime"] = pd.to_datetime(df["datetime"], errors="coerce")
        df = df.sort_values("datetime").set_index("datetime")
    for col in ["sunrise", "sunset"]:
        if col in df.columns:
            df[col] = pd.to_datetime(df[col], errors="coerce")
    return df

def drop_redundant_column(df: pd.DataFrame):
    """
    Drop description (trùng thông tin với conditions), 
    icon (trùng thông tin với các thông số khác), 
    severisk sẽ drop thông qua handle missing khi miss quá 5% (feature này chỉ available từ năm 2023)
    stations cho thấy insignificant khi kiểm tra ở data_understanding
    name chỉ có 1 location Hanoi
    
    Parameters:
    - df: DataFrame cần xử lý (entire data)

    Returns:
    - df: DataFrame sau khi đã drop column
    """
    # Drop 'description' nếu có
    if 'description' in df.columns:
        df = df.drop('description', axis=1)
        print("Dropped column: 'description'")
    else:
        print("Column 'description' not found, skip dropping.")
    
    if 'precipprob' in df.columns:
        df = df.drop('precipprob', axis=1)
        print("Dropped column: 'precipprob'")
    else:
        print("Column 'precipprob' not found, skip dropping.")

    if 'icon' in df.columns:
        df = df.drop('icon', axis=1)
        print("Dropped column: 'icon'")
    else:
        print("Column 'icon' not found, skip dropping.")

    if 'stations' in df.columns:
        df = df.drop('stations', axis=1)
        print("Dropped column: 'stations'")
    else:
        print("Column 'station' not found, skip dropping.")

    if 'name' in df.columns:
        df = df.drop('name', axis=1)
        print("Dropped column: 'name'")

    else:
        print("Column 'name' not found, skip dropping.")
        
    return df.drop_duplicates()


def basic_preprocessing(df: pd.DataFrame):
    """
    Chạy các bước preprocessing không phụ thuộc train/test
    """
    df = transform_dtype(df)
    df = drop_redundant_column(df)
    return df

def prepare_data(is_print = True, path = r"data\raw data\Hanoi Daily 10 years.csv"):

   # 1. Load raw Data
   df = load_data(path)
   if is_print:
    print("=" * 80)
    print("Step 1: Load Raw Data")
    print(f"→ Initial data shape: {df.shape}\n")

   # 2. Basic preprocessing for all dataset
   df = basic_preprocessing(df=df)
   if is_print:
    print("=" * 80)
    print("Step 2: Basic Preprocessing")
    print(f"→ Data shape after removing redundant columns: {df.shape}\n")

   # 3. Split train, test theo thời gian (80/20)
   train_size = 0.8
   n = len(df)
   train_df = df.iloc[:int(train_size * n)]
   test_df = df.iloc[int(train_size * n):]

   if is_print:
    print("=" * 80)
    print("Step 3: Split Train/Test Sets (80/20)")
    print(f"→ Train shape: {train_df.shape}")
    print(f"→ Test  shape: {test_df.shape}\n")

   # 4. Create multi-target y ['temp_next_1', ..., 'temp_next_5']
   train_df, target_cols = create_targets(train_df, forecast_horizon=5)
   test_df, _ = create_targets(test_df, forecast_horizon=5)
   
   if is_print:
    print("=" * 80)
    print("Step 4: Create Multi-Target Variables")
    print("→ Created targets:", target_cols)
    print("→ Multi-target creation completed successfully.\n")

   # 5. Split X, y
   X_train = train_df.drop(columns=target_cols)
   y_train = train_df[target_cols]
   X_test = test_df.drop(columns=target_cols)
   y_test = test_df[target_cols]

   if is_print:
    print("=" * 80)
    print("Step 5: Split Features (X) and Targets (y)")
    print(f"→ X_train shape: {X_train.shape}")
    print(f"→ y_train shape: {y_train.shape}")
    print(f"→ X_test  shape: {X_test.shape}")
    print(f"→ y_test  shape: {y_test.shape}")
    print("=" * 80)

   return df, train_df, test_df, X_train, y_train, X_test, y_test

def run_preprocessing(X_train, X_test, y_train, y_test, pipeline_builder):
    print("=" * 80)
    print("Step 6: Preprocessing ")
    print("=" * 80)

    # Build và fit pipeline
    preprocessing_pipeline = pipeline_builder()
    preprocessing_pipeline.fit(X_train)

    # Transform data
    X_train_processed = preprocessing_pipeline.transform(X_train)
    X_test_processed = preprocessing_pipeline.transform(X_test)

    # Print shapes
    print(f"→ X_train shape after preprocessing: {X_train_processed.shape}")
    print(f"→ y_train shape after preprocessing: {y_train.shape}")
    print(f"→ X_test shape after preprocessing: {X_test_processed.shape}")
    print(f"→ y_test shape after preprocessing: {y_test.shape}")

    return X_train_processed, X_test_processed

def drop_redundant_column_hourly(df: pd.DataFrame):
    """
    Drops features deemed redundant or insignificant based on the initial data analysis.

    Columns and reasons for dropping:
    - 'description': Redundant information, often duplicating data found in 'conditions'.
    - 'icon': Redundant visual information, covered by 'conditions' and other parameters.
    - 'stations': Insignificant feature identified during data understanding.
    - 'precipprob': Precipitation probability, information can often be inferred from 'preciptype' and 'conditions'.
    - 'preciptype': Type of precipitation, information can often be inferred from 'conditions'.
    - 'source': Source information is irrelevant for the modeling task.
    - 'severerisk': Will be dropped later (via missing data handling) as it's missing >5% of data (only available since 2023).
    - ('name', 'address', 'resolvedAddress'): All three are redundant as the entire dataset only contains data for a single location ('Hanoi').
    - ('snow', 'snowdepth'): Highly insignificant as snow accumulation is typically zero in this geographical region (Hanoi).
    
    Parameters:
    - df: The input DataFrame to process.

    Returns:
    - df: The DataFrame after dropping the unnecessary columns.
    """
    # List of all columns to check and potentially drop
    columns_to_drop = [
        'description', 
        'icon', 
        'stations', 
        'name', 
        'address', 
        'resolvedAddress', 
        'precipprob', 
        'preciptype', 
        'severerisk', 
        'source', 
        'snow', 
        'snowdepth',
        'longitude',
        'latitude'
    ]

    print("--- Starting column dropping process ---")

    # Iterate through the list of columns
    for column in columns_to_drop:
        if column in df.columns:
            # Drop the column if it exists
            df = df.drop(column, axis=1)
            print(f"Dropped column: '{column}'")
        else:
            # Skip if the column is not found
            pass
            #print(f"Column not found: '{column}', skipping.")

    print("--- Column dropping process finished ---")

    return df.drop_duplicates()

def basic_preprocessing_hourly(df: pd.DataFrame):
    """
    Chạy các bước preprocessing không phụ thuộc train/test
    """
    df = transform_dtype(df)
    df = drop_redundant_column_hourly(df)
    return df


 
# mục đích loại cột missing preciptype
class HandleMissing(BaseEstimator, TransformerMixin):
    """Drop và fill missing values"""
    def __init__(self, drop_threshold=0.05):
        self.drop_threshold = drop_threshold
        self.cols_to_drop_ = []
        self.fill_values_ = {}

    def fit(self, X, y=None):
        # Tính tỷ lệ missing
        missing_ratio = X.isnull().mean()
        self.cols_to_drop_ = missing_ratio[missing_ratio > self.drop_threshold].index.tolist()

        # Xác định giá trị fill
        X_train_kept = X.drop(columns=self.cols_to_drop_, errors='ignore')
        for col in X_train_kept.columns:
            if X_train_kept[col].isnull().any():
                if pd.api.types.is_numeric_dtype(X_train_kept[col]):
                    self.fill_values_[col] = X_train_kept[col].median()
                else:
                    self.fill_values_[col] = X_train_kept[col].mode()[0]
        return self

    def transform(self, X, y = None):
        X = X.drop(columns=self.cols_to_drop_, errors='ignore')

        for col, val in self.fill_values_.items():
            if col in X.columns:
                X[col] = X[col].fillna(val)

        return X


# loại các cột numeric chỉ có 1 value (snow, snowdepth)
class DropLowVariance(BaseEstimator, TransformerMixin):
    """Drop numeric feature variance thấp"""
    def __init__(self, threshold=0.0):
        self.threshold = threshold
        self.kept_cols_ = None

    def fit(self, X, y = None): # input là df chưa tách X,y 
        numeric_df = X.select_dtypes(include=['number'])
        if numeric_df.empty:
            self.kept_cols_ = [c for c in X.columns if c != 'stations']
            return self
        
        selector = VarianceThreshold(threshold=self.threshold)
        selector.fit(numeric_df)

        # Chỉ giữ numeric có variance > threshold
        kept_numeric = numeric_df.columns[selector.get_support()].tolist()
        # Chỉ giữ non-numeric trừ 'stations'
        kept_non_numeric = [c for c in X.select_dtypes(exclude=['number']).columns if c != 'stations']

        self.kept_cols_ = kept_numeric + kept_non_numeric
        return self

    def transform(self, X, y = None):
        return X[self.kept_cols_]


# # loại các cột categorical chỉ có 1 value (preciptype)
class DropCategorical(BaseEstimator, TransformerMixin):
    """Drop categorical feature có nunique == 1 hoặc unique ratio >= 0.9 """
    def __init__(self, unique_ratio_threshold=0.9):
        self.unique_ratio_threshold = unique_ratio_threshold
        self.to_drop_ = []

    def fit(self, X, y=None):
        cat_cols = X.select_dtypes(include=['object']).columns
        n_rows = len(X)
        self.to_drop_ = []
        for col in cat_cols:
            nunique_ratio = X[col].nunique() / n_rows
            if X[col].nunique() == 1 or nunique_ratio > self.unique_ratio_threshold:
                self.to_drop_.append(col)
        return self

    def transform(self, X, y = None):
        return X.drop(columns=self.to_drop_, errors='ignore')
    
    
class SeasonClassifier(BaseEstimator, TransformerMixin):
    """
    Phân loại mùa theo tháng hoặc tự động dựa trên nhiệt độ trung bình.
    n_season: số nhóm season muốn chia
    is_category = sử dụng biến ở dạng numeric hay category
    """
    def __init__(self, temp_col='temp',n_seasons=5, is_category = True):
        self.temp_col = temp_col
        self.n_seasons = n_seasons
        self.is_category = is_category
        self.season_map_ = None

    def fit(self, X, y=None):
        df = X.copy()
        df['month'] = X.index.month

        # Nếu y là Series chứa nhiệt độ
        if isinstance(y, (pd.Series, np.ndarray)):
            df[self.temp_col] = y.values

        # Tính nhiệt độ trung bình theo tháng
        month_mean = df.groupby('month')[self.temp_col].mean().reset_index()

        # Sắp xếp theo nhiệt độ tăng dần
        month_mean = month_mean.sort_values(self.temp_col).reset_index(drop=True)

        # Chia thành 5 mùa theo quantile
        bins = pd.qcut(month_mean[self.temp_col], q=self.n_seasons,
                       labels=False, duplicates='drop')
        month_mean['season_id'] = bins

        # Lưu map {month → season_id}
        self.season_map_ = dict(zip(month_mean['month'], month_mean['season_id']))
        return self

    def transform(self, X):
        df = X.copy()

        if isinstance(df.index, pd.DatetimeIndex):
            df['month'] = df.index.month
        elif 'month' not in df.columns:
            raise ValueError("Cần có index dạng datetime hoặc cột 'month'.")


        if self.season_map_ is None:
            raise ValueError("Bạn cần gọi .fit() trước khi .transform()")
        

        if self.is_category:  # để là category
            df['season'] = df['month'].map(self.season_map_) 
            df['season'] = df['season'].astype('category')
            return df
        
        df['season'] = df['month'].map(self.season_map_) # dạng numeric int

        return df
    
class WindCategoryEncoder(BaseEstimator, TransformerMixin):
    """
    Chuyển đổi wind category thành numeric encoding dựa trên nhiệt độ trung bình và chia quantile.
    is_category: sử dụng biến ở dạng numeric hay category
    """
    def __init__(self, wind_category_col='wind_category', is_category=False, n_quantiles=4):
        self.wind_category_col = wind_category_col
        self.is_category = is_category
        self.n_quantiles = n_quantiles
        self.encoding_map_ = None
        self.categories_ = None

    def fit(self, X, y=None):
        df = X.copy()

        if self.wind_category_col not in df.columns:
            raise ValueError(f"Column '{self.wind_category_col}' not found in data")

        # Lấy categories từ training data
        self.categories_ = df[self.wind_category_col].cat.categories.tolist()
            
        if y is None:
            raise ValueError("Cần cung cấp y (target values) cho việc encoding")
        
        self._fit_quantile_encoding(df, y)
            
        return self

    def _fit_quantile_encoding(self, df, y):
        """Encoding dựa trên nhiệt độ trung bình và chia quantile"""
        # Tính mean target cho mỗi category
        encoding_df = pd.DataFrame({
            'category': df[self.wind_category_col],
            'target': y
        })
        
        # Tính nhiệt độ trung bình cho từng wind category
        category_means = encoding_df.groupby('category')['target'].mean().reset_index()
        
        # Sắp xếp theo nhiệt độ trung bình tăng dần
        category_means = category_means.sort_values('target').reset_index(drop=True)

        # Chia thành n quantile dựa trên nhiệt độ trung bình
        bins = pd.qcut(category_means['target'], q=self.n_quantiles,
                      labels=False, duplicates='drop')
        category_means['quantile_id'] = bins

        # Lưu map {category → quantile_id}
        self.encoding_map_ = dict(zip(category_means['category'], category_means['quantile_id']))

    def transform(self, X):
        df = X.copy()
        if self.is_category:
            # sử dụng wind_category cũ không biến đổi nữa
            return df

        if self.encoding_map_ is None:
            raise ValueError("Bạn cần gọi .fit() trước khi .transform()")
        
        encoded_col = 'numeric_wind_category' 
        # Áp dụng mapping
        df[encoded_col] = df[self.wind_category_col].map(self.encoding_map_)
        
        # Xử lý unknown categories (nếu có categories mới trong test data)
        unknown_mask = df[encoded_col].isna()
        if unknown_mask.any():
            unknown_cats = df.loc[unknown_mask, self.wind_category_col].unique()
            df.loc[unknown_mask, encoded_col] = -1

        
        # Dạng numeric, drop category cũ
        df[encoded_col] = df[encoded_col].astype(float)
        df = df.drop('wind_category', axis=1)
        
        return df
    
class ConditionsEncoder(BaseEstimator, TransformerMixin):
    def __init__(self, conditions_col='conditions', 
                 encoding_method='target',  # 'ordinal', 'target', 'quantile'
                 n_quantiles=3,
                 is_category=False):
        self.conditions_col = conditions_col
        self.encoding_method = encoding_method
        self.n_quantiles = n_quantiles
        self.is_category = is_category
        self.encoding_map_ = None
        self.binary_maps_ = None

    def fit(self, X, y=None):
        df = X.copy()
        
        if y is None:
            raise ValueError("Cần cung cấp y (target values)")
        
        if self.encoding_method == 'ordinal':
            self._fit_ordinal_encoding(df, y)
        elif self.encoding_method == 'target':
            self._fit_target_encoding(df, y)
        elif self.encoding_method == 'quantile':
            self._fit_quantile_encoding(df, y)
        else:
            raise ValueError("encoding_method không hợp lệ")
            
        return self

    def _fit_ordinal_encoding(self, df, y):
        """Ordinal encoding theo nhiệt độ trung bình"""
        encoding_df = pd.DataFrame({
            'conditions': df[self.conditions_col],
            'target': y
        })
        
        conditions_means = encoding_df.groupby('conditions')['target'].mean()
        conditions_means = conditions_means.sort_values()
        
        self.encoding_map_ = {cond: idx for idx, cond in enumerate(conditions_means.index)}

    def _fit_target_encoding(self, df, y):
        """Target encoding với smoothing cho conditions ít data"""
        encoding_df = pd.DataFrame({
            'conditions': df[self.conditions_col],
            'target': y
        })
        
        conditions_means = encoding_df.groupby('conditions')['target'].mean()
        conditions_counts = df[self.conditions_col].value_counts()
        
        global_mean = y.mean()
        
        self.encoding_map_ = {}
        for condition in conditions_means.index:
            cond_mean = conditions_means[condition]
            cond_count = conditions_counts[condition]
            
            # Smoothing cho conditions ít data (Rain chỉ có 8 samples)
            alpha = max(50, 100 - cond_count)  
            smoothed_mean = (cond_count * cond_mean + alpha * global_mean) / (cond_count + alpha)
            
            self.encoding_map_[condition] = smoothed_mean

    def _fit_quantile_encoding(self, df, y):
        """Chia conditions thành quantile dựa trên nhiệt độ"""
        encoding_df = pd.DataFrame({
            'conditions': df[self.conditions_col],
            'target': y
        })
        
        conditions_means = encoding_df.groupby('conditions')['target'].mean().reset_index()
        conditions_means = conditions_means.sort_values('target')
        
        # Chia quantile
        if len(conditions_means) >= self.n_quantiles:
            bins = pd.qcut(conditions_means['target'], q=self.n_quantiles, 
                          labels=False, duplicates='drop')
            conditions_means['quantile_id'] = bins
        else:
            conditions_means['quantile_id'] = range(len(conditions_means))
        
        self.encoding_map_ = dict(zip(conditions_means['conditions'], conditions_means['quantile_id']))


    def transform(self, X):
        df = X.copy()
        if self.is_category:
            # sử dụng conditions cũ không biến đổi nữa
            return df

        if self.encoding_map_ is None:
            raise ValueError("Bạn cần gọi .fit() trước khi .transform()")
        
        encoded_col = 'numeric_conditions' 
        
        # Đảm bảo cột là string trước khi map để tránh lỗi
        df[self.conditions_col] = df[self.conditions_col].astype(str)
        
        # Áp dụng mapping, các giá trị không có trong map sẽ thành NaN
        mapped_values = df[self.conditions_col].map(self.encoding_map_)
        
        # Xử lý unknown categories (unseen values) bằng cách điền giá trị -1
        mapped_values = mapped_values.fillna(-1.0)
        
        # Gán cột đã được xử lý vào dataframe
        df[encoded_col] = mapped_values.astype(float)
        
        # Drop cột category cũ
        df = df.drop(self.conditions_col, axis=1)

        return df

class To_Category(BaseEstimator, TransformerMixin):
    """
    Chuyển các object thành category cho LightGBM.
    - Chỉ fit trên train
    - Chuyển object/string -> pandas 'category' dtype
    - Không mã hóa số (giữ để LGB xử lý nội bộ)
    """
    def __init__(self):
        self.columns = None
        self.categories_ = {}

    def fit(self, X, y=None):
        X = X.copy()
        self.columns = X.select_dtypes(include=['category', 'object']).columns
        for col in self.columns:
            if col in X.columns:
                # lưu lại các category duy nhất (tránh lỗi unseen category)
                self.categories_[col] = X[col].astype('category').cat.categories
        return self

    def transform(self, X, y=None):
        X = X.copy()
        for col in self.columns:
            if col in X.columns:
                X[col] = X[col].astype('category')
                # align categories với lúc train
                if col in self.categories_:
                    X[col] = X[col].cat.set_categories(self.categories_[col])
        return X



class ConditionsEncoderHourly(BaseEstimator, TransformerMixin):
    def __init__(self, conditions_cols=None, 
                 encoding_method='target',
                 n_quantiles=3,
                 is_category=False):
        self.conditions_cols = conditions_cols
        self.encoding_method = encoding_method
        self.n_quantiles = n_quantiles
        self.is_category = is_category
        self.encoding_maps_ = {}
        self.global_mean_ = None

    def fit(self, X, y=None):
        df = X.copy()
        
        if y is None:
            raise ValueError("Cần cung cấp y (target values)")
        
        if self.conditions_cols is None:
            self.conditions_cols = [col for col in df.columns if col.startswith('cond_')]
        
        if not self.conditions_cols:
            print("⚠️  Không tìm thấy cột conditions nào")
            return self
            
        self.global_mean_ = y.mean()
        
        for col in self.conditions_cols:
            if col not in df.columns:
                continue
                
            # Đảm bảo cột là string
            df[col] = df[col].astype(str)
                
            if self.encoding_method == 'target':
                self._fit_target_encoding(df, y, col)
            elif self.encoding_method == 'ordinal':
                self._fit_ordinal_encoding(df, y, col)
            elif self.encoding_method == 'quantile':
                self._fit_quantile_encoding(df, y, col)
            
        #print(f"✅ Đã fit encoding cho {len(self.encoding_maps_)} conditions columns")
        return self

    def _fit_target_encoding(self, df, y, col):
        encoding_df = pd.DataFrame({
            'conditions': df[col],
            'target': y
        })
        
        conditions_means = encoding_df.groupby('conditions')['target'].mean()
        conditions_counts = df[col].value_counts()
        
        self.encoding_maps_[col] = {}
        for condition in conditions_means.index:
            cond_mean = conditions_means[condition]
            cond_count = conditions_counts[condition]
            alpha = max(50, 100 - cond_count)  
            smoothed_mean = (cond_count * cond_mean + alpha * self.global_mean_) / (cond_count + alpha)
            self.encoding_maps_[col][condition] = smoothed_mean

    def _fit_ordinal_encoding(self, df, y, col):
        encoding_df = pd.DataFrame({
            'conditions': df[col],
            'target': y
        })
        
        conditions_means = encoding_df.groupby('conditions')['target'].mean()
        conditions_means = conditions_means.sort_values()
        self.encoding_maps_[col] = {cond: idx for idx, cond in enumerate(conditions_means.index)}

    def _fit_quantile_encoding(self, df, y, col):
        encoding_df = pd.DataFrame({
            'conditions': df[col],
            'target': y
        })
        
        conditions_means = encoding_df.groupby('conditions')['target'].mean().reset_index()
        conditions_means = conditions_means.sort_values('target')
        
        if len(conditions_means) >= self.n_quantiles:
            bins = pd.qcut(conditions_means['target'], q=self.n_quantiles, labels=False, duplicates='drop')
            conditions_means['quantile_id'] = bins
        else:
            conditions_means['quantile_id'] = range(len(conditions_means))
        
        self.encoding_maps_[col] = dict(zip(conditions_means['conditions'], conditions_means['quantile_id']))

    def transform(self, X):
        df = X.copy()
        
        if self.is_category:
            return df

        if not self.encoding_maps_:
            return df

        for col in self.conditions_cols:
            if col not in df.columns or col not in self.encoding_maps_:
                continue
                
            encoded_col = f"numeric_{col}"
            
            # Đảm bảo cột là string trước khi map
            df[col] = df[col].astype(str)
            
            # Map values
            mapped_values = df[col].map(self.encoding_maps_[col])
            
            # Xử lý unknown values
            mapped_values = mapped_values.fillna(-1.0)
            
            # Gán giá trị đã xử lý
            df[encoded_col] = mapped_values.astype(float)
            df = df.drop(col, axis=1)

        return df