Forex-AI-Analyst / src /models /ensemble.py
leovean's picture
Upload folder using huggingface_hub
d82ffcc verified
Raw History Blame Contribute Delete
5.49 kB
# src/models/ensemble.py
import numpy as np
import pandas as pd
import lightgbm as lgb
import xgboost as xgb
from catboost import CatBoostClassifier
from sklearn.linear_model import LogisticRegression
import joblib
import os
class SuperLearnerEnsemble:
"""
Ensemble of XGBoost, LightGBM, CatBoost with a logistic regression meta-learner.
Now stores feature names to prevent shape mismatches.
"""
def __init__(self):
self.xgb_model = None
self.lgb_model = None
self.cat_model = None
self.meta_learner = LogisticRegression(max_iter=1000)
self.is_fitted = False
self.feature_names = None # NEW: store feature names
def fit(self, X_train, y_train, X_val=None, y_val=None, feature_names=None):
"""
Train base models and meta-learner.
Args:
X_train: Training features (numpy array or DataFrame)
y_train: Training targets
X_val: Validation features for meta-learner training
y_val: Validation targets for meta-learner training
feature_names: List of feature names (optional, extracted if X is DataFrame)
"""
# Extract feature names if available
if feature_names is not None:
self.feature_names = feature_names
elif hasattr(X_train, 'columns'):
self.feature_names = list(X_train.columns)
else:
self.feature_names = [f"feature_{i}" for i in range(X_train.shape[1])]
# Convert to numpy if DataFrame
if hasattr(X_train, 'values'):
X_train = X_train.values
if X_val is not None and hasattr(X_val, 'values'):
X_val = X_val.values
# Initialize base models
self.xgb_model = xgb.XGBClassifier(
n_estimators=200, max_depth=6, learning_rate=0.05,
objective='binary:logistic', random_state=42,
use_label_encoder=False, eval_metric='logloss'
)
self.lgb_model = lgb.LGBMClassifier(
n_estimators=200, max_depth=6, learning_rate=0.05,
random_state=42, verbose=-1
)
self.cat_model = CatBoostClassifier(
n_estimators=200, depth=6, learning_rate=0.05,
random_state=42, verbose=0
)
# Train base models
self.xgb_model.fit(X_train, y_train)
self.lgb_model.fit(X_train, y_train)
self.cat_model.fit(X_train, y_train)
# Meta-learner training
if X_val is not None and y_val is not None:
meta_X = np.column_stack([
self.xgb_model.predict_proba(X_val)[:, 1],
self.lgb_model.predict_proba(X_val)[:, 1],
self.cat_model.predict_proba(X_val)[:, 1]
])
self.meta_learner.fit(meta_X, y_val)
else:
split = int(len(X_train) * 0.8)
X_train_base, X_meta = X_train[:split], X_train[split:]
y_train_base, y_meta = y_train[:split], y_train[split:]
self.xgb_model.fit(X_train_base, y_train_base)
self.lgb_model.fit(X_train_base, y_train_base)
self.cat_model.fit(X_train_base, y_train_base)
meta_X = np.column_stack([
self.xgb_model.predict_proba(X_meta)[:, 1],
self.lgb_model.predict_proba(X_meta)[:, 1],
self.cat_model.predict_proba(X_meta)[:, 1]
])
self.meta_learner.fit(meta_X, y_meta)
self.is_fitted = True
return self
def predict_proba(self, X):
if not self.is_fitted:
raise ValueError("Model not fitted.")
# Convert to numpy if DataFrame
if hasattr(X, 'values'):
X = X.values
xgb_proba = self.xgb_model.predict_proba(X)[:, 1]
lgb_proba = self.lgb_model.predict_proba(X)[:, 1]
cat_proba = self.cat_model.predict_proba(X)[:, 1]
meta_X = np.column_stack([xgb_proba, lgb_proba, cat_proba])
return self.meta_learner.predict_proba(meta_X)[:, 1]
def predict(self, X):
proba = self.predict_proba(X)
return (proba >= 0.5).astype(int)
def save(self, path_prefix: str):
os.makedirs(os.path.dirname(path_prefix), exist_ok=True)
joblib.dump(self.xgb_model, f"{path_prefix}_xgb.joblib")
joblib.dump(self.lgb_model, f"{path_prefix}_lgb.joblib")
joblib.dump(self.cat_model, f"{path_prefix}_cat.joblib")
joblib.dump(self.meta_learner, f"{path_prefix}_meta.joblib")
# Save feature names
joblib.dump(self.feature_names, f"{path_prefix}_features.joblib")
print(f"✅ Ensemble saved with prefix '{path_prefix}'")
def load(self, path_prefix: str):
self.xgb_model = joblib.load(f"{path_prefix}_xgb.joblib")
self.lgb_model = joblib.load(f"{path_prefix}_lgb.joblib")
self.cat_model = joblib.load(f"{path_prefix}_cat.joblib")
self.meta_learner = joblib.load(f"{path_prefix}_meta.joblib")
# Load feature names if available
try:
self.feature_names = joblib.load(f"{path_prefix}_features.joblib")
except:
self.feature_names = None
print("⚠️ No feature names file found.")
self.is_fitted = True
print(f"✅ Ensemble loaded from '{path_prefix}_*.joblib'")
return self