implement core ML pipeline and unified Streamlit app for data and modeling tasks.
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +8 -0
- README.md +177 -355
- app.py +0 -0
- fix_streamlit_api.py +48 -0
- pipeline/main_pipeline.py → main_pipeline.py +13 -14
- requirements.txt +0 -0
- {config → src/enhanced_preprocessing/config}/__init__.py +0 -0
- {config → src/enhanced_preprocessing/config}/config.py +0 -0
- {config → src/enhanced_preprocessing/config}/default_config.json +0 -0
- {config → src/enhanced_preprocessing/config}/settings.py +0 -0
- {correlations → src/enhanced_preprocessing/correlations}/__init__.py +0 -0
- {correlations → src/enhanced_preprocessing/correlations}/correlation_analyzer.py +3 -1
- {data_loader → src/enhanced_preprocessing/data_loader}/__init__.py +0 -0
- {data_loader → src/enhanced_preprocessing/data_loader}/data_loader.py +3 -1
- {decomposition → src/enhanced_preprocessing/decomposition}/__init__.py +0 -0
- {decomposition → src/enhanced_preprocessing/decomposition}/decomposer.py +1 -1
- {feature_selection → src/enhanced_preprocessing/feature_selection}/__init__.py +0 -0
- {feature_selection → src/enhanced_preprocessing/feature_selection}/feature_selector.py +1 -1
- {features → src/enhanced_preprocessing/features}/__init__.py +0 -0
- {features → src/enhanced_preprocessing/features}/feature_engineer.py +1 -4
- {missing_values → src/enhanced_preprocessing/missing_values}/__init__.py +0 -0
- {missing_values → src/enhanced_preprocessing/missing_values}/missing_analyzer.py +1 -1
- {outliers → src/enhanced_preprocessing/outliers}/__init__.py +0 -0
- {outliers → src/enhanced_preprocessing/outliers}/outlier_analyzer.py +1 -1
- {pipeline → src/enhanced_preprocessing/pipeline}/__init__.py +0 -0
- run_pipeline.py → src/enhanced_preprocessing/run_pipeline.py +17 -7
- {scaling → src/enhanced_preprocessing/scaling}/__init__.py +0 -0
- {scaling → src/enhanced_preprocessing/scaling}/data_scaler.py +1 -1
- {splitting → src/enhanced_preprocessing/splitting}/__init__.py +0 -0
- {splitting → src/enhanced_preprocessing/splitting}/data_splitter.py +1 -1
- {stationarity → src/enhanced_preprocessing/stationarity}/__init__.py +0 -0
- {stationarity → src/enhanced_preprocessing/stationarity}/stationarity_checker.py +1 -1
- {validation → src/enhanced_preprocessing/validation}/__init__.py +0 -0
- {validation → src/enhanced_preprocessing/validation}/data_validator.py +1 -2
- {visualization → src/enhanced_preprocessing/visualization}/__init__.py +0 -0
- {visualization → src/enhanced_preprocessing/visualization}/visualization_manager.py +1 -3
- src/ml_pipeline/Readme.md +1 -0
- src/ml_pipeline/__init__.py +0 -0
- src/ml_pipeline/combine_files.py +127 -0
- src/ml_pipeline/config/__init__.py +0 -0
- src/ml_pipeline/config/ml_config.py +503 -0
- src/ml_pipeline/core/__init__.py +0 -0
- src/ml_pipeline/core/base_model.py +909 -0
- src/ml_pipeline/core/model_factory.py +124 -0
- src/ml_pipeline/core/resource_monitor.py +150 -0
- src/ml_pipeline/features/__init__.py +0 -0
- src/ml_pipeline/features/engineering.py +331 -0
- src/ml_pipeline/features/selection.py +86 -0
- src/ml_pipeline/models/__init__.py +0 -0
- src/ml_pipeline/models/boosting_models.py +514 -0
.gitignore
CHANGED
|
@@ -7,3 +7,11 @@ __pycache__/
|
|
| 7 |
*.pyd
|
| 8 |
.Python
|
| 9 |
streamlit_results/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
*.pyd
|
| 8 |
.Python
|
| 9 |
streamlit_results/
|
| 10 |
+
src/enhanced_preprocessing_results/
|
| 11 |
+
ml_experiment_results/
|
| 12 |
+
|
| 13 |
+
*.dill
|
| 14 |
+
*.tsv
|
| 15 |
+
catboost_info/
|
| 16 |
+
predictions_*
|
| 17 |
+
models_export_*
|
README.md
CHANGED
|
@@ -1,377 +1,226 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: indigo
|
| 6 |
-
sdk:
|
| 7 |
pinned: true
|
| 8 |
app_file: app.py
|
| 9 |
-
sdk_version: 1.
|
| 10 |
-
license: mit
|
| 11 |
-
short_description: Full ML pipeline dashboard with 28+ algorithms and ensembles
|
| 12 |
---
|
| 13 |
|
| 14 |
-
#
|
| 15 |
|
| 16 |
<div align="center">
|
| 17 |
|
| 18 |
-
**
|
| 19 |
|
| 20 |
-
*Advanced
|
| 21 |
|
| 22 |
-
[](https://huggingface.co/spaces/your-username/
|
| 23 |
[](https://streamlit.io)
|
| 24 |
-
[](https://scikit-learn.org)
|
| 26 |
|
| 27 |
</div>
|
| 28 |
|
| 29 |
-
##
|
| 30 |
|
| 31 |
-
|
| 32 |
|
| 33 |
-
##
|
| 34 |
|
| 35 |
-
###
|
| 36 |
-
- **
|
| 37 |
-
- **
|
| 38 |
-
- **
|
| 39 |
-
- **
|
| 40 |
|
| 41 |
-
###
|
| 42 |
-
- **
|
| 43 |
-
- **
|
| 44 |
-
- **
|
| 45 |
-
- **
|
| 46 |
|
| 47 |
-
###
|
| 48 |
-
- **
|
| 49 |
-
- **
|
| 50 |
-
- **Model
|
| 51 |
-
- **
|
| 52 |
-
|
| 53 |
-
### 📈 **Comprehensive Analytics**
|
| 54 |
-
- **Real-time Metrics**: RMSE, MAE, R², MAPE with interactive charts
|
| 55 |
-
- **Feature Importance**: Visual analysis of model decision factors
|
| 56 |
-
- **Error Analysis**: Residual plots, prediction vs actual visualization
|
| 57 |
-
- **Performance Benchmarks**: Model comparison across multiple metrics
|
| 58 |
|
| 59 |
## 🎮 Quick Start
|
| 60 |
|
| 61 |
-
### 1. **
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
|
| 66 |
-
#
|
| 67 |
-
streamlit run app.py
|
| 68 |
-
```
|
| 69 |
-
|
| 70 |
-
### 2. **Load Your Data**
|
| 71 |
-
- **Upload CSV/Excel/Parquet files** or use **demo data**
|
| 72 |
-
- Automatic data validation and schema detection
|
| 73 |
-
- Support for time series and tabular data
|
| 74 |
-
|
| 75 |
-
### 3. **Configure Your Experiment**
|
| 76 |
```python
|
| 77 |
-
#
|
| 78 |
config = {
|
| 79 |
-
'target_column': '
|
| 80 |
'test_size': 0.2,
|
| 81 |
-
'
|
| 82 |
-
'
|
| 83 |
-
'
|
| 84 |
}
|
| 85 |
```
|
| 86 |
|
| 87 |
-
###
|
| 88 |
-
-
|
| 89 |
-
-
|
| 90 |
-
-
|
| 91 |
-
- Export
|
| 92 |
|
| 93 |
-
##
|
| 94 |
|
| 95 |
-
### 🔧 **Pipeline
|
| 96 |
```
|
| 97 |
-
Data
|
| 98 |
↓
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
### 📦 **Core Components**
|
| 103 |
-
- **Model Factory**: Dynamic creation of 28+ sklearn-compatible models
|
| 104 |
-
- **Validator**: Smart model compatibility checking
|
| 105 |
-
- **Visualizer**: Interactive Plotly-based analytics
|
| 106 |
-
- **Exporter**: Multi-format model serialization
|
| 107 |
-
|
| 108 |
-
### 🎯 **Model Categories**
|
| 109 |
-
```
|
| 110 |
-
📈 Linear Models (13 algorithms)
|
| 111 |
-
🌳 Tree & Ensemble Models (8 algorithms)
|
| 112 |
-
🚀 Gradient Boosting (3 algorithms)
|
| 113 |
-
🤖 Advanced Methods (4 algorithms)
|
| 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 |
-
- Algorithm comparison studies
|
| 146 |
-
- Feature engineering experiments
|
| 147 |
-
- Hyperparameter optimization research
|
| 148 |
-
- New model benchmarking
|
| 149 |
-
|
| 150 |
-
### 🏢 **Business Applications**
|
| 151 |
-
- Sales forecasting
|
| 152 |
-
- Customer churn prediction
|
| 153 |
-
- Financial modeling
|
| 154 |
-
- Demand planning
|
| 155 |
-
- Quality prediction
|
| 156 |
-
|
| 157 |
-
### 🎓 **Education & Training**
|
| 158 |
-
- ML algorithm visualization
|
| 159 |
-
- Hands-on model comparison
|
| 160 |
-
- Feature importance demonstration
|
| 161 |
-
- Ensemble method teaching
|
| 162 |
-
|
| 163 |
-
### ⚡ **Prototyping & Proof-of-Concept**
|
| 164 |
-
- Rapid model development
|
| 165 |
-
- Quick performance benchmarking
|
| 166 |
-
- Business case validation
|
| 167 |
-
- MVP development
|
| 168 |
-
|
| 169 |
-
## 🛠️ Installation & Deployment
|
| 170 |
-
|
| 171 |
-
### **Local Installation**
|
| 172 |
```bash
|
| 173 |
# Clone repository
|
| 174 |
-
git clone https://huggingface.co/spaces/your-username/
|
| 175 |
-
cd
|
| 176 |
|
| 177 |
-
# Install
|
| 178 |
pip install -r requirements.txt
|
| 179 |
|
| 180 |
-
#
|
| 181 |
streamlit run app.py
|
| 182 |
```
|
| 183 |
|
| 184 |
-
###
|
| 185 |
-
```
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
WORKDIR /app
|
| 189 |
-
COPY . .
|
| 190 |
-
|
| 191 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
| 192 |
-
|
| 193 |
-
EXPOSE 8501
|
| 194 |
-
CMD ["streamlit", "run", "app.py", "--server.port=8501"]
|
| 195 |
-
```
|
| 196 |
-
|
| 197 |
-
### **Cloud Deployment**
|
| 198 |
-
- **Hugging Face Spaces**: One-click deployment
|
| 199 |
-
- **Streamlit Cloud**: Native Streamlit hosting
|
| 200 |
-
- **AWS/Azure/GCP**: Container-based deployment
|
| 201 |
-
- **Heroku**: Simple web app deployment
|
| 202 |
-
|
| 203 |
-
## 📈 Performance Metrics
|
| 204 |
-
|
| 205 |
-
### **Training Speed**
|
| 206 |
-
| Model Count | Training Time | Memory Usage |
|
| 207 |
-
|-------------|---------------|--------------|
|
| 208 |
-
| 5 models | ~30 seconds | <500 MB |
|
| 209 |
-
| 15 models | ~2 minutes | <1 GB |
|
| 210 |
-
| 28 models | ~5 minutes | <1.5 GB |
|
| 211 |
-
|
| 212 |
-
### **Model Accuracy**
|
| 213 |
-
| Dataset Size | Best R² Range | Typical RMSE Improvement |
|
| 214 |
-
|--------------|---------------|-------------------------|
|
| 215 |
-
| Small (<1K) | 0.6-0.8 | 15-25% |
|
| 216 |
-
| Medium (1K-10K) | 0.7-0.9 | 20-30% |
|
| 217 |
-
| Large (>10K) | 0.8-0.95 | 25-35% |
|
| 218 |
-
|
| 219 |
-
## 🔧 Configuration Options
|
| 220 |
-
|
| 221 |
-
### **Data Settings**
|
| 222 |
-
```yaml
|
| 223 |
-
data:
|
| 224 |
-
target_column: auto_detect
|
| 225 |
-
test_size: 0.2
|
| 226 |
-
validation_size: 0.1
|
| 227 |
-
random_state: 42
|
| 228 |
-
shuffle: true
|
| 229 |
-
```
|
| 230 |
-
|
| 231 |
-
### **Model Settings**
|
| 232 |
-
```yaml
|
| 233 |
-
models:
|
| 234 |
-
baseline_models: all
|
| 235 |
-
enable_ensembles: true
|
| 236 |
-
ensemble_methods: [voting, stacking, weighted]
|
| 237 |
-
top_n_for_ensemble: 5
|
| 238 |
-
```
|
| 239 |
-
|
| 240 |
-
### **Training Settings**
|
| 241 |
-
```yaml
|
| 242 |
-
training:
|
| 243 |
-
use_advanced_pipeline: true
|
| 244 |
-
enable_cross_validation: true
|
| 245 |
-
cv_folds: 5
|
| 246 |
-
n_jobs: 1
|
| 247 |
-
early_stopping: true
|
| 248 |
-
```
|
| 249 |
|
| 250 |
-
#
|
| 251 |
-
|
| 252 |
-
tuning:
|
| 253 |
-
enable_hyperparameter_tuning: true
|
| 254 |
-
tuning_method: grid
|
| 255 |
-
n_trials: 30
|
| 256 |
-
cv_folds: 3
|
| 257 |
```
|
| 258 |
|
| 259 |
-
##
|
| 260 |
-
|
| 261 |
-
### **Linear Regression Family**
|
| 262 |
-
- Linear Regression
|
| 263 |
-
- Ridge Regression
|
| 264 |
-
- Lasso Regression
|
| 265 |
-
- ElasticNet
|
| 266 |
-
- Bayesian Ridge
|
| 267 |
-
- Huber Regressor
|
| 268 |
-
- SGD Regressor
|
| 269 |
-
- Passive Aggressive Regressor
|
| 270 |
-
- RANSAC Regressor
|
| 271 |
-
- Theil-Sen Regressor
|
| 272 |
-
- Orthogonal Matching Pursuit
|
| 273 |
-
|
| 274 |
-
### **Tree & Ensemble Models**
|
| 275 |
-
- Random Forest
|
| 276 |
-
- Extra Trees
|
| 277 |
-
- Decision Tree
|
| 278 |
-
- Gradient Boosting
|
| 279 |
-
- Hist Gradient Boosting
|
| 280 |
-
- AdaBoost
|
| 281 |
-
- Bagging Regressor
|
| 282 |
-
|
| 283 |
-
### **Gradient Boosting**
|
| 284 |
-
- XGBoost
|
| 285 |
-
- LightGBM
|
| 286 |
-
- CatBoost
|
| 287 |
-
|
| 288 |
-
### **Advanced Methods**
|
| 289 |
-
- Support Vector Regression (SVR)
|
| 290 |
-
- Linear SVR
|
| 291 |
-
- K-Nearest Neighbors
|
| 292 |
-
- Multi-layer Perceptron
|
| 293 |
-
- Gaussian Process
|
| 294 |
-
|
| 295 |
-
## 🌐 API Integration
|
| 296 |
|
| 297 |
```python
|
| 298 |
-
|
| 299 |
-
|
| 300 |
|
| 301 |
-
#
|
| 302 |
-
|
| 303 |
-
pipeline.initialize()
|
| 304 |
|
| 305 |
-
#
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
|
|
|
| 311 |
|
| 312 |
-
#
|
| 313 |
-
|
| 314 |
-
|
| 315 |
|
| 316 |
-
#
|
| 317 |
-
pipeline.
|
|
|
|
| 318 |
```
|
| 319 |
|
| 320 |
-
##
|
| 321 |
-
|
| 322 |
-
### **Model Comparison Dashboard**
|
| 323 |
-
- Interactive bar charts for RMSE, MAE, R²
|
| 324 |
-
- Training time comparison
|
| 325 |
-
- Feature importance heatmaps
|
| 326 |
-
- Prediction error distributions
|
| 327 |
|
| 328 |
-
|
| 329 |
-
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
|
| 334 |
-
##
|
| 335 |
|
| 336 |
-
### **Data
|
| 337 |
-
-
|
| 338 |
-
-
|
| 339 |
-
-
|
| 340 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
|
| 342 |
-
### **
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
|
|
|
| 347 |
|
| 348 |
## 🤝 Contributing
|
| 349 |
|
| 350 |
-
We welcome contributions
|
| 351 |
-
|
| 352 |
-
### **How to Contribute**
|
| 353 |
-
1. **Fork** the repository
|
| 354 |
-
2. **Create** a feature branch
|
| 355 |
-
3. **Implement** your changes
|
| 356 |
-
4. **Test** thoroughly
|
| 357 |
-
5. **Submit** a pull request
|
| 358 |
|
| 359 |
### **Areas for Contribution**
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
|
| 366 |
-
### **Development
|
| 367 |
```bash
|
| 368 |
-
#
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
pip install -r requirements-dev.txt # Development dependencies
|
| 375 |
```
|
| 376 |
|
| 377 |
## 📜 License
|
|
@@ -380,67 +229,40 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
|
|
| 380 |
|
| 381 |
## 🙏 Acknowledgments
|
| 382 |
|
| 383 |
-
### **
|
| 384 |
-
- **Streamlit** for the amazing
|
| 385 |
-
- **
|
| 386 |
-
- **
|
| 387 |
-
- **
|
| 388 |
|
| 389 |
-
### **
|
| 390 |
-
-
|
| 391 |
-
-
|
| 392 |
-
-
|
| 393 |
-
-
|
|
|
|
| 394 |
|
| 395 |
## 📞 Support & Contact
|
| 396 |
|
| 397 |
-
### **Get Help**
|
| 398 |
- 📧 **Email**: cool.araby@gmail.com
|
| 399 |
-
- 💬 **Issues**: [
|
| 400 |
-
-
|
| 401 |
-
|
| 402 |
-
### **Community**
|
| 403 |
-
- ⭐ **Star** the Space if you find it useful
|
| 404 |
-
- 🔄 **Share** with colleagues
|
| 405 |
-
- 💡 **Suggest** new features
|
| 406 |
-
- 🐛 **Report** issues
|
| 407 |
|
| 408 |
-
### **Stay Updated**
|
| 409 |
-
-
|
| 410 |
-
-
|
| 411 |
-
-
|
| 412 |
|
| 413 |
---
|
| 414 |
|
| 415 |
<div align="center">
|
| 416 |
|
| 417 |
-
**Transform Your
|
| 418 |
|
| 419 |
-
*
|
| 420 |
|
| 421 |
[](https://huggingface.co/your-username)
|
| 422 |
-
[](https://huggingface.co/spaces/your-username/timeflow-pro)
|
| 21 |
[](https://streamlit.io)
|
| 22 |
+
[](https://python.org)
|
|
|
|
| 23 |
|
| 24 |
</div>
|
| 25 |
|
| 26 |
+
## 🌟 Overview
|
| 27 |
|
| 28 |
+
TimeFlow Pro is a comprehensive platform for time series data analysis, preprocessing, and feature engineering. Designed for data scientists and analysts, it provides an intuitive interface for transforming raw time series data into ML-ready datasets with advanced preprocessing capabilities.
|
| 29 |
|
| 30 |
+
## 🚀 Key Features
|
| 31 |
|
| 32 |
+
### 📈 **Data Analysis & Visualization**
|
| 33 |
+
- **Interactive Data Exploration**: Real-time preview and statistics
|
| 34 |
+
- **Missing Value Analysis**: Smart detection and handling strategies
|
| 35 |
+
- **Outlier Detection**: Multiple methods including IQR, Z-Score, Isolation Forest
|
| 36 |
+
- **Temporal Analysis**: Seasonality detection, trend analysis, decomposition
|
| 37 |
|
| 38 |
+
### ⚙️ **Advanced Preprocessing Pipeline**
|
| 39 |
+
- **Feature Engineering**: Automatic lag features, rolling statistics, seasonal components
|
| 40 |
+
- **Stationarity Checking**: ADF tests and transformation suggestions
|
| 41 |
+
- **Data Scaling**: Robust, Standard, MinMax, and custom scaling methods
|
| 42 |
+
- **Feature Selection**: Correlation, variance, mutual information, RF importance
|
| 43 |
|
| 44 |
+
### 🏗️ **ML-Ready Outputs**
|
| 45 |
+
- **Train/Validation/Test Splits**: Time-based or random splitting
|
| 46 |
+
- **Multiple Export Formats**: CSV, Parquet, Excel, JSON
|
| 47 |
+
- **Model Integration**: Ready-to-use datasets for scikit-learn, XGBoost, LightGBM
|
| 48 |
+
- **Visual Reports**: Comprehensive pipeline execution reports
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
## 🎮 Quick Start
|
| 51 |
|
| 52 |
+
### 1. **Upload Your Data**
|
| 53 |
+
- Support for CSV, Excel, Parquet formats
|
| 54 |
+
- Automatic date parsing and validation
|
| 55 |
+
- Smart column type detection
|
| 56 |
|
| 57 |
+
### 2. **Configure Pipeline**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
```python
|
| 59 |
+
# Example configuration
|
| 60 |
config = {
|
| 61 |
+
'target_column': 'sales',
|
| 62 |
'test_size': 0.2,
|
| 63 |
+
'max_lags': 5,
|
| 64 |
+
'seasonal_period': 365,
|
| 65 |
+
'scaling_method': 'robust'
|
| 66 |
}
|
| 67 |
```
|
| 68 |
|
| 69 |
+
### 3. **Run Pipeline & Export**
|
| 70 |
+
- Execute full preprocessing pipeline
|
| 71 |
+
- Download processed data
|
| 72 |
+
- Get feature importance reports
|
| 73 |
+
- Export modeling datasets
|
| 74 |
|
| 75 |
+
## 📊 Technical Architecture
|
| 76 |
|
| 77 |
+
### 🔧 **Pipeline Components**
|
| 78 |
```
|
| 79 |
+
Data Loading → Validation → Missing Handling → Outlier Treatment
|
| 80 |
↓
|
| 81 |
+
Feature Engineering → Stationarity Check → Correlation Analysis
|
| 82 |
+
↓
|
| 83 |
+
Data Splitting → Scaling → Feature Selection → Final Validation
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
```
|
| 85 |
|
| 86 |
+
### 🏆 **Core Features**
|
| 87 |
+
- **Multi-stage Validation**: Raw, processed, and final data validation
|
| 88 |
+
- **Memory Optimization**: Efficient handling of large datasets
|
| 89 |
+
- **Error Recovery**: Graceful handling of pipeline failures
|
| 90 |
+
- **Reproducible Results**: Configuration saving and logging
|
| 91 |
+
|
| 92 |
+
## 📚 Use Cases
|
| 93 |
+
|
| 94 |
+
### 🏢 **Business Analytics**
|
| 95 |
+
- Sales forecasting and trend analysis
|
| 96 |
+
- Inventory optimization
|
| 97 |
+
- Customer behavior prediction
|
| 98 |
+
- Financial time series analysis
|
| 99 |
+
|
| 100 |
+
### 🏭 **Industrial Applications**
|
| 101 |
+
- Sensor data preprocessing
|
| 102 |
+
- Predictive maintenance
|
| 103 |
+
- Quality control monitoring
|
| 104 |
+
- Energy consumption forecasting
|
| 105 |
+
|
| 106 |
+
### 🎓 **Academic Research**
|
| 107 |
+
- Time series modeling experiments
|
| 108 |
+
- Feature engineering research
|
| 109 |
+
- Algorithm comparison studies
|
| 110 |
+
- Educational tool for data science
|
| 111 |
|
| 112 |
+
## 🛠️ Installation
|
| 113 |
|
| 114 |
+
### Local Development
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
```bash
|
| 116 |
# Clone repository
|
| 117 |
+
git clone https://huggingface.co/spaces/your-username/timeflow-pro
|
| 118 |
+
cd timeflow-pro
|
| 119 |
|
| 120 |
+
# Install dependencies
|
| 121 |
pip install -r requirements.txt
|
| 122 |
|
| 123 |
+
# Run application
|
| 124 |
streamlit run app.py
|
| 125 |
```
|
| 126 |
|
| 127 |
+
### Docker Deployment
|
| 128 |
+
```bash
|
| 129 |
+
# Build Docker image
|
| 130 |
+
docker build -t timeflow-pro .
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
+
# Run container
|
| 133 |
+
docker run -p 8501:8501 timeflow-pro
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
```
|
| 135 |
|
| 136 |
+
## 🌐 API Usage Example
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
```python
|
| 139 |
+
from timeflow_pro import TimeFlowPipeline
|
| 140 |
+
import pandas as pd
|
| 141 |
|
| 142 |
+
# Load your data
|
| 143 |
+
data = pd.read_csv('your_data.csv')
|
|
|
|
| 144 |
|
| 145 |
+
# Configure pipeline
|
| 146 |
+
config = {
|
| 147 |
+
'target_column': 'target',
|
| 148 |
+
'test_size': 0.2,
|
| 149 |
+
'max_lags': 7,
|
| 150 |
+
'seasonal_period': 30
|
| 151 |
+
}
|
| 152 |
|
| 153 |
+
# Create and run pipeline
|
| 154 |
+
pipeline = TimeFlowPipeline(config)
|
| 155 |
+
processed_data = pipeline.run(data)
|
| 156 |
|
| 157 |
+
# Get modeling data
|
| 158 |
+
modeling_data = pipeline.get_modeling_data()
|
| 159 |
+
X_train, y_train = modeling_data['X_train'], modeling_data['y_train']
|
| 160 |
```
|
| 161 |
|
| 162 |
+
## 📈 Performance Benchmarks
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
+
| Dataset Size | Processing Time | Memory Usage | Features Generated |
|
| 165 |
+
|--------------|----------------|--------------|-------------------|
|
| 166 |
+
| 10K rows | ~5 seconds | <500 MB | 50-100 features |
|
| 167 |
+
| 100K rows | ~30 seconds | <1 GB | 100-200 features |
|
| 168 |
+
| 1M rows | ~5 minutes | <2 GB | 200-500 features |
|
| 169 |
|
| 170 |
+
## 🔧 Configuration Options
|
| 171 |
|
| 172 |
+
### **Data Processing**
|
| 173 |
+
- `missing_threshold`: Threshold for column removal (0.0-0.5)
|
| 174 |
+
- `outlier_method`: IQR, Z-Score, or Isolation Forest
|
| 175 |
+
- `scaling_method`: Robust, Standard, MinMax, or None
|
| 176 |
+
|
| 177 |
+
### **Feature Engineering**
|
| 178 |
+
- `max_lags`: Maximum lag features (1-20)
|
| 179 |
+
- `seasonal_period`: Seasonal window (7, 30, 90, 365)
|
| 180 |
+
- `rolling_windows`: List of rolling windows [7, 30, 90]
|
| 181 |
+
|
| 182 |
+
### **Model Preparation**
|
| 183 |
+
- `feature_selection_method`: Correlation, Variance, RF, Mutual Info
|
| 184 |
+
- `max_features`: Maximum features to select (5-100)
|
| 185 |
+
- `split_method`: Time-based or random splitting
|
| 186 |
+
|
| 187 |
+
## 📋 Requirements
|
| 188 |
+
|
| 189 |
+
### **Core Dependencies**
|
| 190 |
+
```txt
|
| 191 |
+
streamlit>=1.28.0
|
| 192 |
+
pandas>=2.0.0
|
| 193 |
+
numpy>=1.24.0
|
| 194 |
+
plotly>=5.17.0
|
| 195 |
+
scikit-learn>=1.3.0
|
| 196 |
+
```
|
| 197 |
|
| 198 |
+
### **Optional Dependencies**
|
| 199 |
+
```txt
|
| 200 |
+
xgboost>=2.0.0 # For XGBoost feature importance
|
| 201 |
+
lightgbm>=4.0.0 # For LightGBM integration
|
| 202 |
+
statsmodels>=0.14.0 # For advanced time series analysis
|
| 203 |
+
```
|
| 204 |
|
| 205 |
## 🤝 Contributing
|
| 206 |
|
| 207 |
+
We welcome contributions! Here's how you can help:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
### **Areas for Contribution**
|
| 210 |
+
1. **New Feature Engineering Methods**
|
| 211 |
+
2. **Additional Visualization Types**
|
| 212 |
+
3. **Export Format Support**
|
| 213 |
+
4. **Performance Optimizations**
|
| 214 |
+
5. **Documentation Improvements**
|
| 215 |
|
| 216 |
+
### **Development Workflow**
|
| 217 |
```bash
|
| 218 |
+
# 1. Fork the repository
|
| 219 |
+
# 2. Create feature branch
|
| 220 |
+
git checkout -b feature/new-feature
|
| 221 |
+
|
| 222 |
+
# 3. Make changes and test
|
| 223 |
+
# 4. Submit pull request
|
|
|
|
| 224 |
```
|
| 225 |
|
| 226 |
## 📜 License
|
|
|
|
| 229 |
|
| 230 |
## 🙏 Acknowledgments
|
| 231 |
|
| 232 |
+
### **Special Thanks To:**
|
| 233 |
+
- **Streamlit Team** for the amazing framework
|
| 234 |
+
- **Hugging Face** for hosting the Space
|
| 235 |
+
- **Open Source Community** for invaluable libraries
|
| 236 |
+
- **All Contributors** who helped improve TimeFlow Pro
|
| 237 |
|
| 238 |
+
### **Built With:**
|
| 239 |
+
- 🐍 Python
|
| 240 |
+
- 📊 Streamlit
|
| 241 |
+
- 🎨 Plotly
|
| 242 |
+
- 🔧 Scikit-learn
|
| 243 |
+
- 📈 Pandas & NumPy
|
| 244 |
|
| 245 |
## 📞 Support & Contact
|
| 246 |
|
| 247 |
+
### **Get Help:**
|
| 248 |
- 📧 **Email**: cool.araby@gmail.com
|
| 249 |
+
- 💬 **Issues**: [GitHub Issues](https://github.com/your-username/timeflow-pro/issues)
|
| 250 |
+
- 💡 **Discussions**: [Community Forum](https://github.com/your-username/timeflow-pro/discussions)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
|
| 252 |
+
### **Stay Updated:**
|
| 253 |
+
- ⭐ **Star** the repository
|
| 254 |
+
- 👁️ **Watch** for releases
|
| 255 |
+
- 🔔 **Enable notifications**
|
| 256 |
|
| 257 |
---
|
| 258 |
|
| 259 |
<div align="center">
|
| 260 |
|
| 261 |
+
**Transform Your Time Series Data with Ease**
|
| 262 |
|
| 263 |
+
*TimeFlow Pro - Making Data Preparation Simple and Powerful*
|
| 264 |
|
| 265 |
[](https://huggingface.co/your-username)
|
| 266 |
+
[](https://github.com/your-username/timeflow-pro)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
|
| 268 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
fix_streamlit_api.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# fix_streamlit_api.py
|
| 2 |
+
import os
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
def replace_in_file(filepath):
|
| 6 |
+
"""Заменяет use_container_width в файле"""
|
| 7 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 8 |
+
content = f.read()
|
| 9 |
+
|
| 10 |
+
# Заменяем use_container_width=True
|
| 11 |
+
new_content = content.replace('use_container_width=True', "width='stretch'")
|
| 12 |
+
|
| 13 |
+
# Заменяем use_container_width=False (если есть)
|
| 14 |
+
new_content = new_content.replace('use_container_width=False', "width='content'")
|
| 15 |
+
|
| 16 |
+
if content != new_content:
|
| 17 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 18 |
+
f.write(new_content)
|
| 19 |
+
return True
|
| 20 |
+
return False
|
| 21 |
+
|
| 22 |
+
def main():
|
| 23 |
+
"""Исправляет все файлы в проекте"""
|
| 24 |
+
current_dir = Path(__file__).parent
|
| 25 |
+
|
| 26 |
+
# Файлы для исправления
|
| 27 |
+
files_to_fix = [
|
| 28 |
+
current_dir / "app.py",
|
| 29 |
+
current_dir / "streamlit" / "preprocessor_app.py",
|
| 30 |
+
current_dir / "streamlit" / "ml_app.py"
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
fixed_count = 0
|
| 34 |
+
for filepath in files_to_fix:
|
| 35 |
+
if filepath.exists():
|
| 36 |
+
print(f"Fixing {filepath.name}...")
|
| 37 |
+
if replace_in_file(filepath):
|
| 38 |
+
fixed_count += 1
|
| 39 |
+
print(f" ✓ Fixed")
|
| 40 |
+
else:
|
| 41 |
+
print(f" No changes needed")
|
| 42 |
+
else:
|
| 43 |
+
print(f" File not found: {filepath}")
|
| 44 |
+
|
| 45 |
+
print(f"\n✅ Fixed {fixed_count} files")
|
| 46 |
+
|
| 47 |
+
if __name__ == "__main__":
|
| 48 |
+
main()
|
pipeline/main_pipeline.py → main_pipeline.py
RENAMED
|
@@ -7,23 +7,22 @@ import os
|
|
| 7 |
import traceback
|
| 8 |
from typing import Any, Dict, Optional
|
| 9 |
from venv import logger
|
| 10 |
-
from config.config import Config
|
| 11 |
-
from correlations.correlation_analyzer import CorrelationAnalyzer
|
| 12 |
-
from data_loader.data_loader import DataLoader
|
| 13 |
-
from decomposition.decomposer import TimeSeriesDecomposer
|
| 14 |
-
from feature_selection.feature_selector import FeatureSelector
|
| 15 |
-
from features.feature_engineer import FeatureEngineer
|
| 16 |
|
| 17 |
-
from missing_values.missing_analyzer import MissingValueAnalyser
|
| 18 |
-
from outliers.outlier_analyzer import OutlierAnalyser
|
| 19 |
-
from scaling.data_scaler import DataScaler
|
| 20 |
-
from splitting.data_splitter import DataSplitter
|
| 21 |
-
from stationarity.stationarity_checker import StationarityChecker
|
| 22 |
-
from validation.data_validator import DataValidator
|
| 23 |
import pandas as pd
|
| 24 |
import numpy as np
|
| 25 |
-
|
| 26 |
-
from
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
class EnhancedDataPreprocessingPipeline:
|
| 29 |
"""Enhanced main data preprocessing pipeline"""
|
|
|
|
| 7 |
import traceback
|
| 8 |
from typing import Any, Dict, Optional
|
| 9 |
from venv import logger
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
import pandas as pd
|
| 12 |
import numpy as np
|
| 13 |
+
from src.enhanced_preprocessing.config.config import Config
|
| 14 |
+
from src.enhanced_preprocessing.correlations.correlation_analyzer import CorrelationAnalyzer
|
| 15 |
+
from src.enhanced_preprocessing.data_loader.data_loader import DataLoader
|
| 16 |
+
from src.enhanced_preprocessing.decomposition.decomposer import TimeSeriesDecomposer
|
| 17 |
+
from src.enhanced_preprocessing.feature_selection.feature_selector import FeatureSelector
|
| 18 |
+
from src.enhanced_preprocessing.features.feature_engineer import FeatureEngineer
|
| 19 |
+
from src.enhanced_preprocessing.missing_values.missing_analyzer import MissingValueAnalyser
|
| 20 |
+
from src.enhanced_preprocessing.outliers.outlier_analyzer import OutlierAnalyser
|
| 21 |
+
from src.enhanced_preprocessing.scaling.data_scaler import DataScaler
|
| 22 |
+
from src.enhanced_preprocessing.splitting.data_splitter import DataSplitter
|
| 23 |
+
from src.enhanced_preprocessing.stationarity.stationarity_checker import StationarityChecker
|
| 24 |
+
from src.enhanced_preprocessing.validation.data_validator import DataValidator
|
| 25 |
+
from src.enhanced_preprocessing.visualization.visualization_manager import VisualisationManager
|
| 26 |
|
| 27 |
class EnhancedDataPreprocessingPipeline:
|
| 28 |
"""Enhanced main data preprocessing pipeline"""
|
requirements.txt
CHANGED
|
Binary files a/requirements.txt and b/requirements.txt differ
|
|
|
{config → src/enhanced_preprocessing/config}/__init__.py
RENAMED
|
File without changes
|
{config → src/enhanced_preprocessing/config}/config.py
RENAMED
|
File without changes
|
{config → src/enhanced_preprocessing/config}/default_config.json
RENAMED
|
File without changes
|
{config → src/enhanced_preprocessing/config}/settings.py
RENAMED
|
File without changes
|
{correlations → src/enhanced_preprocessing/correlations}/__init__.py
RENAMED
|
File without changes
|
{correlations → src/enhanced_preprocessing/correlations}/correlation_analyzer.py
RENAMED
|
@@ -6,10 +6,12 @@ import traceback
|
|
| 6 |
from typing import Any, Dict, List, Optional
|
| 7 |
from venv import logger
|
| 8 |
|
| 9 |
-
|
| 10 |
import numpy as np
|
| 11 |
import pandas as pd
|
| 12 |
|
|
|
|
|
|
|
| 13 |
class CorrelationAnalyzer:
|
| 14 |
"""Class for comprehensive correlation and multicollinearity analysis"""
|
| 15 |
|
|
|
|
| 6 |
from typing import Any, Dict, List, Optional
|
| 7 |
from venv import logger
|
| 8 |
|
| 9 |
+
|
| 10 |
import numpy as np
|
| 11 |
import pandas as pd
|
| 12 |
|
| 13 |
+
from src.enhanced_preprocessing.config.config import Config
|
| 14 |
+
|
| 15 |
class CorrelationAnalyzer:
|
| 16 |
"""Class for comprehensive correlation and multicollinearity analysis"""
|
| 17 |
|
{data_loader → src/enhanced_preprocessing/data_loader}/__init__.py
RENAMED
|
File without changes
|
{data_loader → src/enhanced_preprocessing/data_loader}/data_loader.py
RENAMED
|
@@ -7,10 +7,12 @@ import json
|
|
| 7 |
import traceback
|
| 8 |
from typing import Dict, List, Optional
|
| 9 |
from venv import logger
|
| 10 |
-
|
| 11 |
import numpy as np
|
| 12 |
import pandas as pd
|
| 13 |
|
|
|
|
|
|
|
| 14 |
class DataLoader:
|
| 15 |
"""Class for loading and initial data processing"""
|
| 16 |
|
|
|
|
| 7 |
import traceback
|
| 8 |
from typing import Dict, List, Optional
|
| 9 |
from venv import logger
|
| 10 |
+
|
| 11 |
import numpy as np
|
| 12 |
import pandas as pd
|
| 13 |
|
| 14 |
+
from src.enhanced_preprocessing.config.config import Config, DataType
|
| 15 |
+
|
| 16 |
class DataLoader:
|
| 17 |
"""Class for loading and initial data processing"""
|
| 18 |
|
{decomposition → src/enhanced_preprocessing/decomposition}/__init__.py
RENAMED
|
File without changes
|
{decomposition → src/enhanced_preprocessing/decomposition}/decomposer.py
RENAMED
|
@@ -5,11 +5,11 @@ import traceback
|
|
| 5 |
from typing import Dict, Optional
|
| 6 |
from venv import logger
|
| 7 |
|
| 8 |
-
from config.config import Config
|
| 9 |
|
| 10 |
import pandas as pd
|
| 11 |
import numpy as np
|
| 12 |
import matplotlib.pyplot as plt
|
|
|
|
| 13 |
import statsmodels.api as sm
|
| 14 |
from scipy import stats
|
| 15 |
from statsmodels.tsa.seasonal import seasonal_decompose, STL
|
|
|
|
| 5 |
from typing import Dict, Optional
|
| 6 |
from venv import logger
|
| 7 |
|
|
|
|
| 8 |
|
| 9 |
import pandas as pd
|
| 10 |
import numpy as np
|
| 11 |
import matplotlib.pyplot as plt
|
| 12 |
+
from src.enhanced_preprocessing.config.config import Config
|
| 13 |
import statsmodels.api as sm
|
| 14 |
from scipy import stats
|
| 15 |
from statsmodels.tsa.seasonal import seasonal_decompose, STL
|
{feature_selection → src/enhanced_preprocessing/feature_selection}/__init__.py
RENAMED
|
File without changes
|
{feature_selection → src/enhanced_preprocessing/feature_selection}/feature_selector.py
RENAMED
|
@@ -3,8 +3,8 @@
|
|
| 3 |
# ============================================
|
| 4 |
from typing import Dict, List, Optional, Tuple
|
| 5 |
from venv import logger
|
| 6 |
-
from config.config import Config
|
| 7 |
|
|
|
|
| 8 |
try:
|
| 9 |
import pandas as pd
|
| 10 |
import numpy as np
|
|
|
|
| 3 |
# ============================================
|
| 4 |
from typing import Dict, List, Optional, Tuple
|
| 5 |
from venv import logger
|
|
|
|
| 6 |
|
| 7 |
+
from src.enhanced_preprocessing.config.config import Config
|
| 8 |
try:
|
| 9 |
import pandas as pd
|
| 10 |
import numpy as np
|
{features → src/enhanced_preprocessing/features}/__init__.py
RENAMED
|
File without changes
|
{features → src/enhanced_preprocessing/features}/feature_engineer.py
RENAMED
|
@@ -3,12 +3,9 @@
|
|
| 3 |
# ============================================
|
| 4 |
from typing import Dict, List, Optional
|
| 5 |
from venv import logger
|
| 6 |
-
|
| 7 |
-
from config.config import Config
|
| 8 |
-
|
| 9 |
import pandas as pd
|
| 10 |
import numpy as np
|
| 11 |
-
|
| 12 |
|
| 13 |
class FeatureEngineer:
|
| 14 |
"""Class for creating new features for time series"""
|
|
|
|
| 3 |
# ============================================
|
| 4 |
from typing import Dict, List, Optional
|
| 5 |
from venv import logger
|
|
|
|
|
|
|
|
|
|
| 6 |
import pandas as pd
|
| 7 |
import numpy as np
|
| 8 |
+
from src.enhanced_preprocessing.config.config import Config
|
| 9 |
|
| 10 |
class FeatureEngineer:
|
| 11 |
"""Class for creating new features for time series"""
|
{missing_values → src/enhanced_preprocessing/missing_values}/__init__.py
RENAMED
|
File without changes
|
{missing_values → src/enhanced_preprocessing/missing_values}/missing_analyzer.py
RENAMED
|
@@ -4,8 +4,8 @@
|
|
| 4 |
from typing import Dict, Tuple
|
| 5 |
from venv import logger
|
| 6 |
|
| 7 |
-
from config.config import Config
|
| 8 |
from scipy.interpolate import interp1d
|
|
|
|
| 9 |
from statsmodels.tsa.seasonal import seasonal_decompose, STL
|
| 10 |
try:
|
| 11 |
import pandas as pd
|
|
|
|
| 4 |
from typing import Dict, Tuple
|
| 5 |
from venv import logger
|
| 6 |
|
|
|
|
| 7 |
from scipy.interpolate import interp1d
|
| 8 |
+
from src.enhanced_preprocessing.config.config import Config
|
| 9 |
from statsmodels.tsa.seasonal import seasonal_decompose, STL
|
| 10 |
try:
|
| 11 |
import pandas as pd
|
{outliers → src/enhanced_preprocessing/outliers}/__init__.py
RENAMED
|
File without changes
|
{outliers → src/enhanced_preprocessing/outliers}/outlier_analyzer.py
RENAMED
|
@@ -4,13 +4,13 @@
|
|
| 4 |
from typing import Dict, List, Tuple
|
| 5 |
from venv import logger
|
| 6 |
|
| 7 |
-
from config.config import Config
|
| 8 |
import pandas as pd
|
| 9 |
import numpy as np
|
| 10 |
import matplotlib.pyplot as plt
|
| 11 |
from sklearn.neighbors import LocalOutlierFactor
|
| 12 |
from sklearn.covariance import EllipticEnvelope
|
| 13 |
from scipy import stats
|
|
|
|
| 14 |
|
| 15 |
class OutlierAnalyser:
|
| 16 |
"""Class for analysing and handling outliers"""
|
|
|
|
| 4 |
from typing import Dict, List, Tuple
|
| 5 |
from venv import logger
|
| 6 |
|
|
|
|
| 7 |
import pandas as pd
|
| 8 |
import numpy as np
|
| 9 |
import matplotlib.pyplot as plt
|
| 10 |
from sklearn.neighbors import LocalOutlierFactor
|
| 11 |
from sklearn.covariance import EllipticEnvelope
|
| 12 |
from scipy import stats
|
| 13 |
+
from src.enhanced_preprocessing.config.config import Config
|
| 14 |
|
| 15 |
class OutlierAnalyser:
|
| 16 |
"""Class for analysing and handling outliers"""
|
{pipeline → src/enhanced_preprocessing/pipeline}/__init__.py
RENAMED
|
File without changes
|
run_pipeline.py → src/enhanced_preprocessing/run_pipeline.py
RENAMED
|
@@ -1,9 +1,12 @@
|
|
| 1 |
# ============================================
|
| 2 |
# RUN
|
| 3 |
# ============================================
|
| 4 |
-
|
| 5 |
-
from pipeline.main_pipeline import EnhancedDataPreprocessingPipeline
|
| 6 |
import pandas as pd
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
if __name__ == "__main__":
|
| 9 |
"""
|
|
@@ -13,7 +16,7 @@ if __name__ == "__main__":
|
|
| 13 |
# Configuration with reasonable parameters
|
| 14 |
config = Config(
|
| 15 |
data_path='temp_data.csv',
|
| 16 |
-
results_dir='enhanced_preprocessing_results',
|
| 17 |
target_column='raskhodvoda',
|
| 18 |
start_year=1970,
|
| 19 |
end_year=1990,
|
|
@@ -56,7 +59,14 @@ if __name__ == "__main__":
|
|
| 56 |
print(f" X_test: {modeling_data['X_test'].shape}")
|
| 57 |
print(f" Features: {len(modeling_data['feature_names'])}")
|
| 58 |
|
| 59 |
-
# Save final data
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# ============================================
|
| 2 |
# RUN
|
| 3 |
# ============================================
|
| 4 |
+
|
|
|
|
| 5 |
import pandas as pd
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
from main_pipeline import EnhancedDataPreprocessingPipeline
|
| 9 |
+
from src.enhanced_preprocessing.config.config import Config
|
| 10 |
|
| 11 |
if __name__ == "__main__":
|
| 12 |
"""
|
|
|
|
| 16 |
# Configuration with reasonable parameters
|
| 17 |
config = Config(
|
| 18 |
data_path='temp_data.csv',
|
| 19 |
+
results_dir='src/enhanced_preprocessing_results',
|
| 20 |
target_column='raskhodvoda',
|
| 21 |
start_year=1970,
|
| 22 |
end_year=1990,
|
|
|
|
| 59 |
print(f" X_test: {modeling_data['X_test'].shape}")
|
| 60 |
print(f" Features: {len(modeling_data['feature_names'])}")
|
| 61 |
|
| 62 |
+
# Save final data using config paths
|
| 63 |
+
# Create subdirectory if it doesn't exist
|
| 64 |
+
output_dir = os.path.join(config.results_dir, 'processed_data')
|
| 65 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 66 |
+
|
| 67 |
+
output_path = os.path.join(output_dir, 'enhanced_final_processed_data.csv')
|
| 68 |
+
processed_data.to_csv(
|
| 69 |
+
output_path,
|
| 70 |
+
index=True if isinstance(processed_data.index, pd.DatetimeIndex) else False
|
| 71 |
+
)
|
| 72 |
+
print(f"\n✓ Final data saved to '{output_path}'")
|
{scaling → src/enhanced_preprocessing/scaling}/__init__.py
RENAMED
|
File without changes
|
{scaling → src/enhanced_preprocessing/scaling}/data_scaler.py
RENAMED
|
@@ -4,10 +4,10 @@
|
|
| 4 |
from typing import Dict, List, Optional, Tuple
|
| 5 |
from venv import logger
|
| 6 |
import pandas as pd
|
| 7 |
-
from config.config import Config
|
| 8 |
import numpy as np
|
| 9 |
import matplotlib.pyplot as plt
|
| 10 |
|
|
|
|
| 11 |
|
| 12 |
class DataScaler:
|
| 13 |
"""Class for data scaling and normalisation"""
|
|
|
|
| 4 |
from typing import Dict, List, Optional, Tuple
|
| 5 |
from venv import logger
|
| 6 |
import pandas as pd
|
|
|
|
| 7 |
import numpy as np
|
| 8 |
import matplotlib.pyplot as plt
|
| 9 |
|
| 10 |
+
from src.enhanced_preprocessing.config.config import Config
|
| 11 |
|
| 12 |
class DataScaler:
|
| 13 |
"""Class for data scaling and normalisation"""
|
{splitting → src/enhanced_preprocessing/splitting}/__init__.py
RENAMED
|
File without changes
|
{splitting → src/enhanced_preprocessing/splitting}/data_splitter.py
RENAMED
|
@@ -5,10 +5,10 @@ from datetime import datetime
|
|
| 5 |
from typing import Dict, Optional, Tuple
|
| 6 |
from venv import logger
|
| 7 |
import pandas as pd
|
| 8 |
-
from config.config import Config
|
| 9 |
import numpy as np
|
| 10 |
import matplotlib.pyplot as plt
|
| 11 |
|
|
|
|
| 12 |
|
| 13 |
class DataSplitter:
|
| 14 |
"""Class for splitting data into train, validation and test sets"""
|
|
|
|
| 5 |
from typing import Dict, Optional, Tuple
|
| 6 |
from venv import logger
|
| 7 |
import pandas as pd
|
|
|
|
| 8 |
import numpy as np
|
| 9 |
import matplotlib.pyplot as plt
|
| 10 |
|
| 11 |
+
from src.enhanced_preprocessing.config.config import Config
|
| 12 |
|
| 13 |
class DataSplitter:
|
| 14 |
"""Class for splitting data into train, validation and test sets"""
|
{stationarity → src/enhanced_preprocessing/stationarity}/__init__.py
RENAMED
|
File without changes
|
{stationarity → src/enhanced_preprocessing/stationarity}/stationarity_checker.py
RENAMED
|
@@ -3,10 +3,10 @@
|
|
| 3 |
# ============================================
|
| 4 |
from typing import Dict, Optional
|
| 5 |
from venv import logger
|
| 6 |
-
from config.config import Config
|
| 7 |
import pandas as pd
|
| 8 |
import numpy as np
|
| 9 |
import matplotlib.pyplot as plt
|
|
|
|
| 10 |
from statsmodels.tsa.stattools import adfuller, kpss, acf, pacf
|
| 11 |
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
|
| 12 |
|
|
|
|
| 3 |
# ============================================
|
| 4 |
from typing import Dict, Optional
|
| 5 |
from venv import logger
|
|
|
|
| 6 |
import pandas as pd
|
| 7 |
import numpy as np
|
| 8 |
import matplotlib.pyplot as plt
|
| 9 |
+
from src.enhanced_preprocessing.config.config import Config
|
| 10 |
from statsmodels.tsa.stattools import adfuller, kpss, acf, pacf
|
| 11 |
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
|
| 12 |
|
{validation → src/enhanced_preprocessing/validation}/__init__.py
RENAMED
|
File without changes
|
{validation → src/enhanced_preprocessing/validation}/data_validator.py
RENAMED
|
@@ -6,10 +6,9 @@ import json
|
|
| 6 |
from pathlib import Path
|
| 7 |
from typing import Dict, List
|
| 8 |
from venv import logger
|
| 9 |
-
|
| 10 |
-
from config.config import Config
|
| 11 |
import pandas as pd
|
| 12 |
import numpy as np
|
|
|
|
| 13 |
|
| 14 |
class DataValidator:
|
| 15 |
"""Class for data quality validation"""
|
|
|
|
| 6 |
from pathlib import Path
|
| 7 |
from typing import Dict, List
|
| 8 |
from venv import logger
|
|
|
|
|
|
|
| 9 |
import pandas as pd
|
| 10 |
import numpy as np
|
| 11 |
+
from src.enhanced_preprocessing.config.config import Config
|
| 12 |
|
| 13 |
class DataValidator:
|
| 14 |
"""Class for data quality validation"""
|
{visualization → src/enhanced_preprocessing/visualization}/__init__.py
RENAMED
|
File without changes
|
{visualization → src/enhanced_preprocessing/visualization}/visualization_manager.py
RENAMED
|
@@ -5,17 +5,15 @@ import os
|
|
| 5 |
from datetime import datetime
|
| 6 |
import json
|
| 7 |
from typing import Dict, List, Optional, Tuple, Union, Any
|
| 8 |
-
|
| 9 |
import pandas as pd
|
| 10 |
import numpy as np
|
|
|
|
| 11 |
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
|
| 12 |
import matplotlib.pyplot as plt
|
| 13 |
import seaborn as sns
|
| 14 |
from scipy.stats import gaussian_kde
|
| 15 |
import matplotlib
|
| 16 |
matplotlib.use('Agg') # Use non-display backend
|
| 17 |
-
|
| 18 |
-
from config.config import Config
|
| 19 |
import logging
|
| 20 |
|
| 21 |
# Logging setup
|
|
|
|
| 5 |
from datetime import datetime
|
| 6 |
import json
|
| 7 |
from typing import Dict, List, Optional, Tuple, Union, Any
|
|
|
|
| 8 |
import pandas as pd
|
| 9 |
import numpy as np
|
| 10 |
+
from src.enhanced_preprocessing.config.config import Config
|
| 11 |
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
|
| 12 |
import matplotlib.pyplot as plt
|
| 13 |
import seaborn as sns
|
| 14 |
from scipy.stats import gaussian_kde
|
| 15 |
import matplotlib
|
| 16 |
matplotlib.use('Agg') # Use non-display backend
|
|
|
|
|
|
|
| 17 |
import logging
|
| 18 |
|
| 19 |
# Logging setup
|
src/ml_pipeline/Readme.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
(.venv) PS D:\Science\Narzullo\LastVersionTimeFlowPro\src> python -m ml_pipeline.run_mlpipeline
|
src/ml_pipeline/__init__.py
ADDED
|
File without changes
|
src/ml_pipeline/combine_files.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
def combine_python_files(source_dir, output_file):
|
| 5 |
+
"""
|
| 6 |
+
Combine all Python files from specified directory into a single file.
|
| 7 |
+
|
| 8 |
+
Args:
|
| 9 |
+
source_dir (str): Path to source directory
|
| 10 |
+
output_file (str): Path to output file
|
| 11 |
+
"""
|
| 12 |
+
source_path = Path(source_dir)
|
| 13 |
+
|
| 14 |
+
# Check if source directory exists
|
| 15 |
+
if not source_path.exists():
|
| 16 |
+
print(f"Error: Directory {source_dir} does not exist")
|
| 17 |
+
return
|
| 18 |
+
|
| 19 |
+
# Get all Python files recursively
|
| 20 |
+
python_files = list(source_path.rglob("*.py"))
|
| 21 |
+
|
| 22 |
+
if not python_files:
|
| 23 |
+
print(f"No Python files found in {source_dir}")
|
| 24 |
+
return
|
| 25 |
+
|
| 26 |
+
print(f"Found {len(python_files)} Python files")
|
| 27 |
+
|
| 28 |
+
with open(output_file, 'w', encoding='utf-8') as out_f:
|
| 29 |
+
# Write header
|
| 30 |
+
out_f.write("#" * 80 + "\n")
|
| 31 |
+
out_f.write("# COMBINED FILE WITH ALL PYTHON SCRIPTS\n")
|
| 32 |
+
out_f.write(f"# Directory: {source_dir}\n")
|
| 33 |
+
out_f.write("#" * 80 + "\n\n")
|
| 34 |
+
|
| 35 |
+
# Process each Python file
|
| 36 |
+
for py_file in sorted(python_files):
|
| 37 |
+
relative_path = py_file.relative_to(source_path)
|
| 38 |
+
|
| 39 |
+
out_f.write("\n" + "=" * 80 + "\n")
|
| 40 |
+
out_f.write(f"# FILE: {relative_path}\n")
|
| 41 |
+
out_f.write("#" * 80 + "\n\n")
|
| 42 |
+
|
| 43 |
+
try:
|
| 44 |
+
with open(py_file, 'r', encoding='utf-8') as in_f:
|
| 45 |
+
content = in_f.read()
|
| 46 |
+
out_f.write(content)
|
| 47 |
+
|
| 48 |
+
if not content.endswith('\n'):
|
| 49 |
+
out_f.write('\n')
|
| 50 |
+
|
| 51 |
+
except Exception as e:
|
| 52 |
+
out_f.write(f"# Error reading file: {e}\n")
|
| 53 |
+
|
| 54 |
+
# Add project structure at the end
|
| 55 |
+
out_f.write("\n" + "=" * 80 + "\n")
|
| 56 |
+
out_f.write("# PROJECT STRUCTURE\n")
|
| 57 |
+
out_f.write("#" * 80 + "\n\n")
|
| 58 |
+
|
| 59 |
+
# Generate tree structure
|
| 60 |
+
generate_project_structure(source_path, out_f)
|
| 61 |
+
|
| 62 |
+
def generate_project_structure(root_dir, output_stream):
|
| 63 |
+
"""
|
| 64 |
+
Generate project tree structure.
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
root_dir (Path): Root directory
|
| 68 |
+
output_stream: Output stream for writing
|
| 69 |
+
"""
|
| 70 |
+
def add_directory(dir_path, prefix=""):
|
| 71 |
+
"""Recursively add directory contents."""
|
| 72 |
+
try:
|
| 73 |
+
# Get all items, sorted (directories first, then files)
|
| 74 |
+
items = sorted(dir_path.iterdir(),
|
| 75 |
+
key=lambda x: (not x.is_dir(), x.name.lower()))
|
| 76 |
+
|
| 77 |
+
for i, item in enumerate(items):
|
| 78 |
+
is_last = (i == len(items) - 1)
|
| 79 |
+
connector = "└── " if is_last else "├── "
|
| 80 |
+
|
| 81 |
+
if item.is_dir():
|
| 82 |
+
output_stream.write(f"{prefix}{connector}{item.name}/\n")
|
| 83 |
+
extension = " " if is_last else "│ "
|
| 84 |
+
add_directory(item, prefix + extension)
|
| 85 |
+
else:
|
| 86 |
+
# Count lines for .py files
|
| 87 |
+
if item.suffix == '.py':
|
| 88 |
+
try:
|
| 89 |
+
with open(item, 'r', encoding='utf-8') as f:
|
| 90 |
+
lines = len(f.readlines())
|
| 91 |
+
line_info = f" ({lines} lines)"
|
| 92 |
+
except:
|
| 93 |
+
line_info = " (read error)"
|
| 94 |
+
else:
|
| 95 |
+
line_info = ""
|
| 96 |
+
|
| 97 |
+
output_stream.write(f"{prefix}{connector}{item.name}{line_info}\n")
|
| 98 |
+
|
| 99 |
+
except PermissionError:
|
| 100 |
+
output_stream.write(f"{prefix}└── [Access denied]\n")
|
| 101 |
+
|
| 102 |
+
output_stream.write(f"{root_dir.name}/\n")
|
| 103 |
+
add_directory(root_dir)
|
| 104 |
+
|
| 105 |
+
def main():
|
| 106 |
+
# Specify paths
|
| 107 |
+
source_directory = "src/ml_pipeline"
|
| 108 |
+
output_filename = "combined_ml_pipeline.py"
|
| 109 |
+
|
| 110 |
+
print("Starting file combination...")
|
| 111 |
+
print(f"Source directory: {source_directory}")
|
| 112 |
+
print(f"Output file: {output_filename}")
|
| 113 |
+
|
| 114 |
+
# Perform combination
|
| 115 |
+
combine_python_files(source_directory, output_filename)
|
| 116 |
+
|
| 117 |
+
print(f"\nFiles successfully combined into {output_filename}")
|
| 118 |
+
print(f"Full path: {os.path.abspath(output_filename)}")
|
| 119 |
+
|
| 120 |
+
# Show statistics
|
| 121 |
+
if os.path.exists(output_filename):
|
| 122 |
+
with open(output_filename, 'r', encoding='utf-8') as f:
|
| 123 |
+
lines = len(f.readlines())
|
| 124 |
+
print(f"Final file contains: {lines} lines")
|
| 125 |
+
|
| 126 |
+
if __name__ == "__main__":
|
| 127 |
+
main()
|
src/ml_pipeline/config/__init__.py
ADDED
|
File without changes
|
src/ml_pipeline/config/ml_config.py
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from dataclasses import dataclass, field, asdict
|
| 6 |
+
from typing import Dict, List, Tuple, Optional, Any, Union, Callable
|
| 7 |
+
import logging
|
| 8 |
+
|
| 9 |
+
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler, QuantileTransformer, PowerTransformer
|
| 10 |
+
from sklearn.impute import SimpleImputer, KNNImputer
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class MLConfig:
|
| 17 |
+
"""
|
| 18 |
+
Configuration for an ML experiment.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
# Paths
|
| 22 |
+
preprocessing_results_dir: str = r'src\enhanced_ml_results'
|
| 23 |
+
ml_results_dir: str = r'src\ml_experiment_results'
|
| 24 |
+
models_dir: str = r'src\saved_models'
|
| 25 |
+
feature_store_dir: str = r'src\feature_store'
|
| 26 |
+
|
| 27 |
+
# Target variable
|
| 28 |
+
target_column: str = 'raskhodvoda'
|
| 29 |
+
|
| 30 |
+
# Data splitting
|
| 31 |
+
test_size: float = 0.2
|
| 32 |
+
validation_size: float = 0.1
|
| 33 |
+
shuffle_split: bool = True
|
| 34 |
+
stratify: bool = False
|
| 35 |
+
time_series_split: bool = True
|
| 36 |
+
|
| 37 |
+
baseline_models: List[str] = field(default_factory=lambda: [
|
| 38 |
+
'linear_regression',
|
| 39 |
+
'ridge',
|
| 40 |
+
'lasso',
|
| 41 |
+
'elasticnet',
|
| 42 |
+
'random_forest',
|
| 43 |
+
'xgboost',
|
| 44 |
+
'lightgbm',
|
| 45 |
+
'catboost',
|
| 46 |
+
'gradient_boosting',
|
| 47 |
+
'hist_gradient_boosting',
|
| 48 |
+
'extra_trees',
|
| 49 |
+
'ada_boost',
|
| 50 |
+
'svr',
|
| 51 |
+
'linear_svr',
|
| 52 |
+
'knn',
|
| 53 |
+
'decision_tree',
|
| 54 |
+
'mlp',
|
| 55 |
+
'bayesian_ridge',
|
| 56 |
+
'huber',
|
| 57 |
+
'kernel_ridge',
|
| 58 |
+
'pls',
|
| 59 |
+
'sgd',
|
| 60 |
+
'passive_aggressive',
|
| 61 |
+
'ransac',
|
| 62 |
+
'theil_sen',
|
| 63 |
+
'orthogonal_matching_pursuit',
|
| 64 |
+
'gaussian_process'
|
| 65 |
+
])
|
| 66 |
+
|
| 67 |
+
# Cross-validation parameters
|
| 68 |
+
cv_folds: int = 5
|
| 69 |
+
cv_method: str = 'timeseries'
|
| 70 |
+
random_state: int = 42
|
| 71 |
+
groups: Optional[str] = None
|
| 72 |
+
|
| 73 |
+
# Evaluation metrics
|
| 74 |
+
scoring_metrics: List[str] = field(default_factory=lambda: [
|
| 75 |
+
'neg_mean_squared_error',
|
| 76 |
+
'neg_mean_absolute_error',
|
| 77 |
+
'r2',
|
| 78 |
+
'neg_root_mean_squared_error',
|
| 79 |
+
'explained_variance',
|
| 80 |
+
'neg_mean_squared_log_error',
|
| 81 |
+
'neg_median_absolute_error'
|
| 82 |
+
])
|
| 83 |
+
|
| 84 |
+
# Hyperparameter tuning
|
| 85 |
+
enable_hyperparameter_tuning: bool = True
|
| 86 |
+
tuning_method: str = 'bayesian'
|
| 87 |
+
n_trials: int = 50
|
| 88 |
+
timeout_per_trial: int = 300
|
| 89 |
+
param_search_space: Dict = field(default_factory=dict)
|
| 90 |
+
|
| 91 |
+
# Ensembles
|
| 92 |
+
enable_ensembles: bool = True
|
| 93 |
+
ensemble_methods: List[str] = field(default_factory=lambda: [
|
| 94 |
+
'voting',
|
| 95 |
+
'stacking',
|
| 96 |
+
'blending',
|
| 97 |
+
'weighted',
|
| 98 |
+
'bagging',
|
| 99 |
+
'boosting'])
|
| 100 |
+
top_n_for_ensemble: int = 5
|
| 101 |
+
|
| 102 |
+
# Feature Engineering
|
| 103 |
+
feature_engineering: bool = True
|
| 104 |
+
create_polynomial_features: bool = False
|
| 105 |
+
polynomial_degree: int = 2
|
| 106 |
+
create_interaction_features: bool = True
|
| 107 |
+
use_feature_selection: bool = True
|
| 108 |
+
feature_selection_method: str = 'importance'
|
| 109 |
+
n_features_to_select: Union[int, float] = 0.8
|
| 110 |
+
max_features_limit: int = 100
|
| 111 |
+
|
| 112 |
+
# Preprocessing
|
| 113 |
+
scale_features: bool = True
|
| 114 |
+
scaler_type: str = 'standard'
|
| 115 |
+
handle_imbalance: bool = False
|
| 116 |
+
treat_outliers: bool = True
|
| 117 |
+
outlier_method: str = 'iqr'
|
| 118 |
+
impute_missing: bool = True
|
| 119 |
+
impute_method: str = 'median'
|
| 120 |
+
|
| 121 |
+
# Additional options
|
| 122 |
+
use_pipeline: bool = True
|
| 123 |
+
use_gpu: bool = False
|
| 124 |
+
n_jobs: int = -1
|
| 125 |
+
memory_limit: str = '4G'
|
| 126 |
+
early_stopping_rounds: int = 50
|
| 127 |
+
use_cache: bool = True
|
| 128 |
+
cache_dir: str = '.cache'
|
| 129 |
+
verbose: bool = True
|
| 130 |
+
debug: bool = False
|
| 131 |
+
|
| 132 |
+
# Visualisation
|
| 133 |
+
save_plots: bool = True
|
| 134 |
+
plot_style: str = 'seaborn'
|
| 135 |
+
dpi: int = 300
|
| 136 |
+
plot_language: str = 'ru'
|
| 137 |
+
|
| 138 |
+
# Saving results
|
| 139 |
+
save_models: bool = True
|
| 140 |
+
save_predictions: bool = True
|
| 141 |
+
save_reports: bool = True
|
| 142 |
+
save_config: bool = True
|
| 143 |
+
save_feature_importance: bool = True
|
| 144 |
+
save_residuals: bool = True
|
| 145 |
+
format: str = 'all'
|
| 146 |
+
|
| 147 |
+
# MLOps
|
| 148 |
+
use_mlflow: bool = False
|
| 149 |
+
mlflow_tracking_uri: str = 'mlruns'
|
| 150 |
+
experiment_name: str = 'ml_experiment'
|
| 151 |
+
log_artifacts: bool = True
|
| 152 |
+
|
| 153 |
+
# Monitoring
|
| 154 |
+
monitor_resources: bool = True
|
| 155 |
+
profile_memory: bool = True
|
| 156 |
+
profile_time: bool = True
|
| 157 |
+
|
| 158 |
+
def __post_init__(self):
|
| 159 |
+
self.create_directories()
|
| 160 |
+
self.setup_param_search_space()
|
| 161 |
+
self.setup_scaler()
|
| 162 |
+
self.setup_imputer()
|
| 163 |
+
self.validate_parameters()
|
| 164 |
+
|
| 165 |
+
def create_directories(self):
|
| 166 |
+
"""
|
| 167 |
+
Create all required directories.
|
| 168 |
+
"""
|
| 169 |
+
dirs = [
|
| 170 |
+
self.ml_results_dir,
|
| 171 |
+
f'{self.ml_results_dir}/models',
|
| 172 |
+
f'{self.ml_results_dir}/predictions',
|
| 173 |
+
f'{self.ml_results_dir}/plots',
|
| 174 |
+
f'{self.ml_results_dir}/plots/residuals',
|
| 175 |
+
f'{self.ml_results_dir}/plots/predictions',
|
| 176 |
+
f'{self.ml_results_dir}/plots/feature_importance',
|
| 177 |
+
f'{self.ml_results_dir}/plots/learning_curves',
|
| 178 |
+
f'{self.ml_results_dir}/plots/hyperparameters',
|
| 179 |
+
f'{self.ml_results_dir}/plots/correlations',
|
| 180 |
+
f'{self.ml_results_dir}/plots/diagnostics',
|
| 181 |
+
f'{self.ml_results_dir}/plots/shap',
|
| 182 |
+
f'{self.ml_results_dir}/reports',
|
| 183 |
+
f'{self.ml_results_dir}/tables',
|
| 184 |
+
f'{self.ml_results_dir}/logs',
|
| 185 |
+
f'{self.ml_results_dir}/cache',
|
| 186 |
+
f'{self.ml_results_dir}/feature_store',
|
| 187 |
+
self.models_dir,
|
| 188 |
+
self.cache_dir,
|
| 189 |
+
self.feature_store_dir
|
| 190 |
+
]
|
| 191 |
+
|
| 192 |
+
for directory in dirs:
|
| 193 |
+
Path(directory).mkdir(parents=True, exist_ok=True)
|
| 194 |
+
|
| 195 |
+
logger.info(f"Directories created in {self.ml_results_dir}")
|
| 196 |
+
|
| 197 |
+
def validate_parameters(self):
|
| 198 |
+
"""
|
| 199 |
+
Validate parameter correctness.
|
| 200 |
+
"""
|
| 201 |
+
if self.test_size + self.validation_size >= 1:
|
| 202 |
+
raise ValueError("The sum of test_size and validation_size must be less than 1")
|
| 203 |
+
|
| 204 |
+
if self.n_jobs == -1:
|
| 205 |
+
self.n_jobs = os.cpu_count()
|
| 206 |
+
else:
|
| 207 |
+
self.n_jobs = min(self.n_jobs, os.cpu_count())
|
| 208 |
+
|
| 209 |
+
if self.cv_method not in ['kfold', 'timeseries', 'stratified', 'group', 'loo', 'shuffle']:
|
| 210 |
+
raise ValueError(f"Unknown CV method: {self.cv_method}")
|
| 211 |
+
|
| 212 |
+
def setup_param_search_space(self):
|
| 213 |
+
"""
|
| 214 |
+
Configure the hyperparameter search space.
|
| 215 |
+
"""
|
| 216 |
+
self.param_search_space = {
|
| 217 |
+
'random_forest': {
|
| 218 |
+
'n_estimators': [100, 200, 300, 500],
|
| 219 |
+
'max_depth': [10, 20, 30, None],
|
| 220 |
+
'min_samples_split': [2, 5, 10],
|
| 221 |
+
'min_samples_leaf': [1, 2, 4],
|
| 222 |
+
'max_features': ['sqrt', 'log2', None],
|
| 223 |
+
'bootstrap': [True, False],
|
| 224 |
+
'max_samples': [0.5, 0.7, 0.9, None]
|
| 225 |
+
},
|
| 226 |
+
'xgboost': {
|
| 227 |
+
'n_estimators': [100, 200, 300, 500],
|
| 228 |
+
'max_depth': [3, 5, 7, 9],
|
| 229 |
+
'learning_rate': [0.001, 0.01, 0.05, 0.1, 0.2],
|
| 230 |
+
'subsample': [0.6, 0.8, 1.0],
|
| 231 |
+
'colsample_bytree': [0.6, 0.8, 1.0],
|
| 232 |
+
'gamma': [0, 0.1, 0.2, 0.5],
|
| 233 |
+
'reg_alpha': [0, 0.1, 0.5, 1.0],
|
| 234 |
+
'reg_lambda': [0.5, 1, 1.5, 2],
|
| 235 |
+
'min_child_weight': [1, 3, 5],
|
| 236 |
+
'grow_policy': ['depthwise', 'lossguide']
|
| 237 |
+
},
|
| 238 |
+
'lightgbm': {
|
| 239 |
+
'n_estimators': [100, 200, 300, 500],
|
| 240 |
+
'num_leaves': [31, 50, 100, 150],
|
| 241 |
+
'learning_rate': [0.001, 0.01, 0.05, 0.1],
|
| 242 |
+
'max_depth': [-1, 5, 10, 15],
|
| 243 |
+
'min_child_samples': [20, 30, 50, 100],
|
| 244 |
+
'subsample': [0.8, 0.9, 1.0],
|
| 245 |
+
'colsample_bytree': [0.8, 0.9, 1.0],
|
| 246 |
+
'reg_alpha': [0, 0.1, 0.5, 1.0],
|
| 247 |
+
'reg_lambda': [0, 0.1, 0.5, 1.0],
|
| 248 |
+
'min_split_gain': [0, 0.1, 0.2],
|
| 249 |
+
'boosting_type': ['gbdt', 'dart', 'goss']
|
| 250 |
+
},
|
| 251 |
+
'catboost': {
|
| 252 |
+
'iterations': [100, 200, 300, 500],
|
| 253 |
+
'depth': [4, 6, 8, 10],
|
| 254 |
+
'learning_rate': [0.001, 0.01, 0.05, 0.1],
|
| 255 |
+
'l2_leaf_reg': [1, 3, 5, 10],
|
| 256 |
+
'border_count': [32, 64, 128],
|
| 257 |
+
'random_strength': [0, 0.1, 0.5, 1.0],
|
| 258 |
+
'bagging_temperature': [0, 0.5, 1.0],
|
| 259 |
+
'leaf_estimation_iterations': [1, 5, 10],
|
| 260 |
+
'grow_policy': ['SymmetricTree', 'Depthwise', 'Lossguide']
|
| 261 |
+
},
|
| 262 |
+
'gradient_boosting': {
|
| 263 |
+
'n_estimators': [100, 200, 300],
|
| 264 |
+
'learning_rate': [0.01, 0.05, 0.1, 0.2],
|
| 265 |
+
'max_depth': [3, 5, 7, 9],
|
| 266 |
+
'min_samples_split': [2, 5, 10],
|
| 267 |
+
'min_samples_leaf': [1, 2, 4],
|
| 268 |
+
'subsample': [0.8, 0.9, 1.0],
|
| 269 |
+
'max_features': ['sqrt', 'log2', None],
|
| 270 |
+
'loss': ['squared_error', 'absolute_error', 'huber', 'quantile']
|
| 271 |
+
},
|
| 272 |
+
'hist_gradient_boosting': {
|
| 273 |
+
'max_iter': [100, 200, 300],
|
| 274 |
+
'learning_rate': [0.01, 0.05, 0.1],
|
| 275 |
+
'max_depth': [None, 5, 10, 15],
|
| 276 |
+
'min_samples_leaf': [20, 30, 50],
|
| 277 |
+
'l2_regularization': [0, 0.1, 0.5, 1.0],
|
| 278 |
+
'max_bins': [128, 200, 255],
|
| 279 |
+
'loss': ['squared_error', 'absolute_error'],
|
| 280 |
+
'early_stopping': [True, False]
|
| 281 |
+
},
|
| 282 |
+
'svr': {
|
| 283 |
+
'C': [0.1, 1, 10, 100, 1000],
|
| 284 |
+
'epsilon': [0.01, 0.1, 0.2, 0.5],
|
| 285 |
+
'kernel': ['linear', 'rbf', 'poly', 'sigmoid'],
|
| 286 |
+
'gamma': ['scale', 'auto'] + [0.001, 0.01, 0.1, 1],
|
| 287 |
+
'degree': [2, 3, 4],
|
| 288 |
+
'coef0': [0.0, 0.1, 0.5, 1.0]
|
| 289 |
+
},
|
| 290 |
+
'nu_svr': {
|
| 291 |
+
'nu': [0.1, 0.3, 0.5, 0.7, 0.9],
|
| 292 |
+
'C': [0.1, 1, 10, 100],
|
| 293 |
+
'kernel': ['linear', 'rbf'],
|
| 294 |
+
'gamma': ['scale', 'auto'] + [0.001, 0.01, 0.1]
|
| 295 |
+
},
|
| 296 |
+
'ridge': {
|
| 297 |
+
'alpha': [0.001, 0.01, 0.1, 1.0, 10.0, 100.0, 1000.0],
|
| 298 |
+
'solver': ['auto', 'svd', 'cholesky', 'lsqr', 'sparse_cg', 'sag', 'saga']
|
| 299 |
+
},
|
| 300 |
+
'lasso': {
|
| 301 |
+
'alpha': [0.0001, 0.001, 0.01, 0.1, 1.0, 10.0],
|
| 302 |
+
'selection': ['cyclic', 'random'],
|
| 303 |
+
'max_iter': [1000, 5000, 10000]
|
| 304 |
+
},
|
| 305 |
+
'elasticnet': {
|
| 306 |
+
'alpha': [0.0001, 0.001, 0.01, 0.1, 1.0],
|
| 307 |
+
'l1_ratio': [0.1, 0.3, 0.5, 0.7, 0.9, 1.0],
|
| 308 |
+
'max_iter': [1000, 5000, 10000]
|
| 309 |
+
},
|
| 310 |
+
'bayesian_ridge': {
|
| 311 |
+
'max_iter': [100, 300, 500, 1000],
|
| 312 |
+
'alpha_1': [1e-6, 1e-5, 1e-4],
|
| 313 |
+
'alpha_2': [1e-6, 1e-5, 1e-4],
|
| 314 |
+
'lambda_1': [1e-6, 1e-5, 1e-4],
|
| 315 |
+
'lambda_2': [1e-6, 1e-5, 1e-4],
|
| 316 |
+
'compute_score': [True, False]
|
| 317 |
+
},
|
| 318 |
+
'huber': {
|
| 319 |
+
'epsilon': [1.1, 1.35, 1.5, 2.0],
|
| 320 |
+
'alpha': [0.0001, 0.001, 0.01, 0.1],
|
| 321 |
+
'max_iter': [100, 500, 1000]
|
| 322 |
+
},
|
| 323 |
+
'kernel_ridge': {
|
| 324 |
+
'alpha': [0.1, 1, 10, 100],
|
| 325 |
+
'kernel': ['linear', 'poly', 'rbf', 'sigmoid'],
|
| 326 |
+
'gamma': [None, 0.1, 1, 10],
|
| 327 |
+
'degree': [2, 3, 4]
|
| 328 |
+
},
|
| 329 |
+
'pls': {
|
| 330 |
+
'n_components': [2, 5, 10, 20, 30],
|
| 331 |
+
'scale': [True, False],
|
| 332 |
+
'max_iter': [500, 1000, 2000]
|
| 333 |
+
},
|
| 334 |
+
'mlp': {
|
| 335 |
+
'hidden_layer_sizes': [(50,), (100,), (50, 50), (100, 50), (100, 100)],
|
| 336 |
+
'activation': ['relu', 'tanh', 'logistic'],
|
| 337 |
+
'alpha': [0.0001, 0.001, 0.01, 0.1],
|
| 338 |
+
'learning_rate': ['constant', 'invscaling', 'adaptive'],
|
| 339 |
+
'learning_rate_init': [0.001, 0.01, 0.1],
|
| 340 |
+
'max_iter': [200, 500, 1000],
|
| 341 |
+
'solver': ['adam', 'sgd', 'lbfgs'],
|
| 342 |
+
'batch_size': ['auto', 32, 64, 128],
|
| 343 |
+
'early_stopping': [True, False]
|
| 344 |
+
},
|
| 345 |
+
'knn': {
|
| 346 |
+
'n_neighbors': [3, 5, 7, 9, 11, 15, 21],
|
| 347 |
+
'weights': ['uniform', 'distance'],
|
| 348 |
+
'metric': ['euclidean', 'manhattan', 'minkowski', 'chebyshev'],
|
| 349 |
+
'p': [1, 2, 3],
|
| 350 |
+
'algorithm': ['auto', 'ball_tree', 'kd_tree', 'brute']
|
| 351 |
+
},
|
| 352 |
+
'radius_neighbors': {
|
| 353 |
+
'radius': [1.0, 2.0, 5.0, 10.0],
|
| 354 |
+
'weights': ['uniform', 'distance'],
|
| 355 |
+
'metric': ['euclidean', 'manhattan'],
|
| 356 |
+
'algorithm': ['auto', 'ball_tree', 'kd_tree', 'brute']
|
| 357 |
+
},
|
| 358 |
+
'decision_tree': {
|
| 359 |
+
'max_depth': [None, 5, 10, 20, 30, 50],
|
| 360 |
+
'min_samples_split': [2, 5, 10, 20, 30],
|
| 361 |
+
'min_samples_leaf': [1, 2, 4, 8, 16],
|
| 362 |
+
'max_features': ['sqrt', 'log2', None],
|
| 363 |
+
'splitter': ['best', 'random'],
|
| 364 |
+
'criterion': ['squared_error', 'friedman_mse', 'absolute_error']
|
| 365 |
+
},
|
| 366 |
+
'extra_trees': {
|
| 367 |
+
'n_estimators': [100, 200, 300, 500],
|
| 368 |
+
'max_depth': [10, 20, 30, None],
|
| 369 |
+
'min_samples_split': [2, 5, 10],
|
| 370 |
+
'min_samples_leaf': [1, 2, 4],
|
| 371 |
+
'max_features': ['sqrt', 'log2', None],
|
| 372 |
+
'bootstrap': [True, False]
|
| 373 |
+
},
|
| 374 |
+
'ada_boost': {
|
| 375 |
+
'n_estimators': [50, 100, 200, 300],
|
| 376 |
+
'learning_rate': [0.001, 0.01, 0.1, 0.5, 1.0],
|
| 377 |
+
'loss': ['linear', 'square', 'exponential'],
|
| 378 |
+
'estimator': [None]
|
| 379 |
+
},
|
| 380 |
+
'bagging': {
|
| 381 |
+
'n_estimators': [10, 50, 100],
|
| 382 |
+
'max_samples': [0.5, 0.7, 1.0],
|
| 383 |
+
'max_features': [0.5, 0.7, 1.0],
|
| 384 |
+
'bootstrap': [True, False],
|
| 385 |
+
'bootstrap_features': [True, False],
|
| 386 |
+
'estimator': [None]
|
| 387 |
+
},
|
| 388 |
+
'sgd': {
|
| 389 |
+
'loss': ['squared_error', 'huber', 'epsilon_insensitive', 'squared_epsilon_insensitive'],
|
| 390 |
+
'penalty': ['l2', 'l1', 'elasticnet', None],
|
| 391 |
+
'alpha': [0.0001, 0.001, 0.01, 0.1],
|
| 392 |
+
'learning_rate': ['constant', 'optimal', 'invscaling', 'adaptive'],
|
| 393 |
+
'eta0': [0.001, 0.01, 0.1],
|
| 394 |
+
'max_iter': [1000, 5000, 10000],
|
| 395 |
+
'early_stopping': [True, False]
|
| 396 |
+
},
|
| 397 |
+
'passive_aggressive': {
|
| 398 |
+
'C': [0.1, 1.0, 10.0],
|
| 399 |
+
'loss': ['epsilon_insensitive', 'squared_epsilon_insensitive'],
|
| 400 |
+
'max_iter': [1000, 5000],
|
| 401 |
+
'early_stopping': [True, False]
|
| 402 |
+
},
|
| 403 |
+
'ransac': {
|
| 404 |
+
'min_samples': [0.1, 0.5, 1.0],
|
| 405 |
+
'residual_threshold': [None, 1.0, 2.0, 5.0],
|
| 406 |
+
'max_trials': [100, 500, 1000],
|
| 407 |
+
'stop_probability': [0.99, 0.999],
|
| 408 |
+
'estimator': [None],
|
| 409 |
+
'loss': ['absolute_error', 'squared_error']
|
| 410 |
+
},
|
| 411 |
+
'theil_sen': {
|
| 412 |
+
'max_subpopulation': [10000, 50000, 100000],
|
| 413 |
+
'n_subsamples': [None, 100, 500, 1000],
|
| 414 |
+
'max_iter': [300, 500, 1000],
|
| 415 |
+
'random_state': [42]
|
| 416 |
+
},
|
| 417 |
+
'orthogonal_matching_pursuit': {
|
| 418 |
+
'n_nonzero_coefs': [None, 5, 10, 20, 50],
|
| 419 |
+
'tol': [None, 1e-3, 1e-2],
|
| 420 |
+
'fit_intercept': [True, False],
|
| 421 |
+
'precompute': ['auto', True, False]
|
| 422 |
+
},
|
| 423 |
+
'gaussian_process': {
|
| 424 |
+
'kernel': [None],
|
| 425 |
+
'alpha': [1e-10, 1e-5, 1e-2],
|
| 426 |
+
'optimizer': ['fmin_l_bfgs_b', None],
|
| 427 |
+
'n_restarts_optimizer': [0, 5, 10],
|
| 428 |
+
'normalize_y': [True, False]
|
| 429 |
+
},
|
| 430 |
+
'isotonic': {
|
| 431 |
+
'increasing': [True, False, 'auto'],
|
| 432 |
+
'out_of_bounds': ['nan', 'clip', 'raise']
|
| 433 |
+
}
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
def setup_scaler(self):
|
| 437 |
+
"""
|
| 438 |
+
Configure the feature scaler.
|
| 439 |
+
"""
|
| 440 |
+
scalers = {
|
| 441 |
+
'standard': StandardScaler,
|
| 442 |
+
'minmax': MinMaxScaler,
|
| 443 |
+
'robust': RobustScaler,
|
| 444 |
+
'quantile': QuantileTransformer,
|
| 445 |
+
'power': PowerTransformer
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
if self.scaler_type in scalers:
|
| 449 |
+
self.scaler_class = scalers[self.scaler_type]
|
| 450 |
+
else:
|
| 451 |
+
self.scaler_class = StandardScaler
|
| 452 |
+
logger.warning(f"Unknown scaler_type: {self.scaler_type}. Using StandardScaler.")
|
| 453 |
+
|
| 454 |
+
def setup_imputer(self):
|
| 455 |
+
"""
|
| 456 |
+
Configure the missing value imputer.
|
| 457 |
+
"""
|
| 458 |
+
imputers = {
|
| 459 |
+
'mean': SimpleImputer(strategy='mean'),
|
| 460 |
+
'median': SimpleImputer(strategy='median'),
|
| 461 |
+
'most_frequent': SimpleImputer(strategy='most_frequent'),
|
| 462 |
+
'constant': SimpleImputer(strategy='constant', fill_value=0),
|
| 463 |
+
'knn': KNNImputer(n_neighbors=5)
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
if self.impute_method in imputers:
|
| 467 |
+
self.imputer = imputers[self.impute_method]
|
| 468 |
+
else:
|
| 469 |
+
self.imputer = SimpleImputer(strategy='median')
|
| 470 |
+
|
| 471 |
+
def to_dict(self):
|
| 472 |
+
"""
|
| 473 |
+
Convert configuration to a dictionary.
|
| 474 |
+
"""
|
| 475 |
+
config_dict = asdict(self)
|
| 476 |
+
config_dict.pop('scaler_class', None)
|
| 477 |
+
config_dict.pop('imputer', None)
|
| 478 |
+
return config_dict
|
| 479 |
+
|
| 480 |
+
def save(self, path: str = None):
|
| 481 |
+
"""
|
| 482 |
+
Save configuration to a JSON file.
|
| 483 |
+
"""
|
| 484 |
+
if path is None:
|
| 485 |
+
path = f'{self.ml_results_dir}/ml_config.json'
|
| 486 |
+
|
| 487 |
+
with open(path, 'w', encoding='utf-8') as f:
|
| 488 |
+
json.dump(self.to_dict(), f, indent=4, ensure_ascii=False)
|
| 489 |
+
|
| 490 |
+
logger.info(f"Configuration saved: {path}")
|
| 491 |
+
|
| 492 |
+
@classmethod
|
| 493 |
+
def load(cls, path: str):
|
| 494 |
+
"""
|
| 495 |
+
Load configuration from a JSON file.
|
| 496 |
+
"""
|
| 497 |
+
with open(path, 'r', encoding='utf-8') as f:
|
| 498 |
+
config_dict = json.load(f)
|
| 499 |
+
|
| 500 |
+
config = cls(**config_dict)
|
| 501 |
+
config.setup_scaler()
|
| 502 |
+
config.setup_imputer()
|
| 503 |
+
return config
|
src/ml_pipeline/core/__init__.py
ADDED
|
File without changes
|
src/ml_pipeline/core/base_model.py
ADDED
|
@@ -0,0 +1,909 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import numpy as np
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import hashlib
|
| 6 |
+
import re
|
| 7 |
+
import pickle
|
| 8 |
+
import joblib
|
| 9 |
+
import json
|
| 10 |
+
import time
|
| 11 |
+
import sys
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
from typing import Dict, List, Optional, Any, Union
|
| 14 |
+
from collections import defaultdict
|
| 15 |
+
import traceback
|
| 16 |
+
import logging
|
| 17 |
+
|
| 18 |
+
import shap
|
| 19 |
+
from sklearn.base import BaseEstimator, RegressorMixin
|
| 20 |
+
from sklearn.model_selection import GroupKFold, KFold, LeaveOneOut, ShuffleSplit, StratifiedKFold, TimeSeriesSplit, cross_validate
|
| 21 |
+
from sklearn.pipeline import Pipeline
|
| 22 |
+
from sklearn.metrics import (
|
| 23 |
+
make_scorer, mean_squared_error, mean_absolute_error, mean_squared_log_error, r2_score,
|
| 24 |
+
explained_variance_score, median_absolute_error, max_error
|
| 25 |
+
)
|
| 26 |
+
from sklearn.inspection import permutation_importance
|
| 27 |
+
from sklearn.preprocessing import PolynomialFeatures
|
| 28 |
+
import scipy.stats as stats
|
| 29 |
+
|
| 30 |
+
from src.ml_pipeline.utils.validators import SHAP_AVAILABLE
|
| 31 |
+
|
| 32 |
+
from ..config.ml_config import MLConfig
|
| 33 |
+
from .resource_monitor import ResourceMonitor
|
| 34 |
+
|
| 35 |
+
logger = logging.getLogger(__name__)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class BaseMLModel:
|
| 39 |
+
"""Base class for ML models with extended functionality."""
|
| 40 |
+
|
| 41 |
+
def __init__(self, config: MLConfig, model_name: str):
|
| 42 |
+
self.config = config
|
| 43 |
+
self.model_name = model_name
|
| 44 |
+
self.model = None
|
| 45 |
+
self.pipeline = None
|
| 46 |
+
self.best_params = {}
|
| 47 |
+
self.cv_results = {}
|
| 48 |
+
self.train_metrics = {}
|
| 49 |
+
self.val_metrics = {}
|
| 50 |
+
self.test_metrics = {}
|
| 51 |
+
self.feature_importances = {}
|
| 52 |
+
self.shap_values = None
|
| 53 |
+
self.X_sample_shap = None
|
| 54 |
+
self.predictions = {}
|
| 55 |
+
self.residuals = {}
|
| 56 |
+
self.training_time = None
|
| 57 |
+
self.inference_time = None
|
| 58 |
+
self.is_fitted = False
|
| 59 |
+
self.model_hash = None
|
| 60 |
+
self.feature_names = None
|
| 61 |
+
self.resource_monitor = ResourceMonitor()
|
| 62 |
+
self.X_train_clean = None
|
| 63 |
+
|
| 64 |
+
def create_pipeline(self, **model_params):
|
| 65 |
+
"""Create pipeline with preprocessing and model."""
|
| 66 |
+
steps = []
|
| 67 |
+
|
| 68 |
+
if self.config.impute_missing:
|
| 69 |
+
steps.append(('imputer', self.config.imputer))
|
| 70 |
+
|
| 71 |
+
if self.config.scale_features:
|
| 72 |
+
steps.append(('scaler', self.config.scaler_class()))
|
| 73 |
+
|
| 74 |
+
if self.config.feature_engineering and self.config.create_polynomial_features:
|
| 75 |
+
steps.append(('poly_features', PolynomialFeatures(
|
| 76 |
+
degree=self.config.polynomial_degree,
|
| 77 |
+
include_bias=False,
|
| 78 |
+
interaction_only=self.config.create_interaction_features
|
| 79 |
+
)))
|
| 80 |
+
|
| 81 |
+
steps.append((self.model_name, self.model))
|
| 82 |
+
|
| 83 |
+
self.pipeline = Pipeline(steps, verbose=self.config.verbose)
|
| 84 |
+
return self.pipeline
|
| 85 |
+
|
| 86 |
+
def clean_feature_names(self, X):
|
| 87 |
+
"""Improved feature name cleaning for compatibility with LightGBM and other models."""
|
| 88 |
+
if isinstance(X, pd.DataFrame):
|
| 89 |
+
X_clean = X.copy()
|
| 90 |
+
new_columns = []
|
| 91 |
+
|
| 92 |
+
for col in X_clean.columns:
|
| 93 |
+
col_str = str(col)
|
| 94 |
+
|
| 95 |
+
# For LightGBM strict requirements: only letters, numbers, underscores
|
| 96 |
+
# Remove all special characters and spaces
|
| 97 |
+
col_clean = re.sub(r'[^a-zA-Z0-9_]', '_', col_str)
|
| 98 |
+
|
| 99 |
+
# Remove multiple underscores
|
| 100 |
+
col_clean = re.sub(r'_{2,}', '_', col_clean)
|
| 101 |
+
|
| 102 |
+
# Remove leading and trailing underscores
|
| 103 |
+
col_clean = col_clean.strip('_')
|
| 104 |
+
|
| 105 |
+
# If string is empty after cleaning
|
| 106 |
+
if not col_clean:
|
| 107 |
+
col_clean = f'feature_{abs(hash(col_str)) % 10000:04d}'
|
| 108 |
+
|
| 109 |
+
# LightGBM requires names to start with a letter
|
| 110 |
+
if col_clean and col_clean[0].isdigit():
|
| 111 |
+
col_clean = f'feature_{col_clean}'
|
| 112 |
+
|
| 113 |
+
# Check name is not empty and not too long
|
| 114 |
+
if not col_clean:
|
| 115 |
+
col_clean = f'feature_{abs(hash(col_str)) % 10000:04d}'
|
| 116 |
+
|
| 117 |
+
# Limit name length (LightGBM may have restrictions)
|
| 118 |
+
if len(col_clean) > 100:
|
| 119 |
+
col_clean = col_clean[:100]
|
| 120 |
+
|
| 121 |
+
# Check for duplicates
|
| 122 |
+
base_name = col_clean
|
| 123 |
+
counter = 1
|
| 124 |
+
original_name = col_clean
|
| 125 |
+
while col_clean in new_columns:
|
| 126 |
+
col_clean = f'{original_name}_{counter}'
|
| 127 |
+
counter += 1
|
| 128 |
+
if counter > 100: # Protection against infinite loop
|
| 129 |
+
col_clean = f'{original_name}_{hash(col_str) % 10000:04d}'
|
| 130 |
+
break
|
| 131 |
+
|
| 132 |
+
new_columns.append(col_clean)
|
| 133 |
+
|
| 134 |
+
X_clean.columns = new_columns
|
| 135 |
+
self.feature_names = list(X_clean.columns)
|
| 136 |
+
|
| 137 |
+
if self.config.debug:
|
| 138 |
+
logger.debug(f"Cleaned feature names. Example: {list(X.columns[:3])} -> {new_columns[:3]}")
|
| 139 |
+
|
| 140 |
+
return X_clean
|
| 141 |
+
|
| 142 |
+
return X
|
| 143 |
+
|
| 144 |
+
def clean_feature_names_for_lgbm(self, X):
|
| 145 |
+
"""Special cleaning for LightGBM."""
|
| 146 |
+
if isinstance(X, pd.DataFrame):
|
| 147 |
+
X_clean = X.copy()
|
| 148 |
+
new_columns = []
|
| 149 |
+
|
| 150 |
+
for i, col in enumerate(X_clean.columns):
|
| 151 |
+
col_str = str(col)
|
| 152 |
+
|
| 153 |
+
# LightGBM is very strict - use only simple names
|
| 154 |
+
col_clean = re.sub(r'[^a-zA-Z0-9_]', '', col_str) # Remove all non-alphanumeric
|
| 155 |
+
|
| 156 |
+
if not col_clean:
|
| 157 |
+
col_clean = f'feature_{i:04d}'
|
| 158 |
+
|
| 159 |
+
# Limit length
|
| 160 |
+
col_clean = col_clean[:50]
|
| 161 |
+
|
| 162 |
+
# Check for duplicates
|
| 163 |
+
base_name = col_clean
|
| 164 |
+
counter = 1
|
| 165 |
+
while col_clean in new_columns:
|
| 166 |
+
col_clean = f'{base_name}_{counter}'
|
| 167 |
+
counter += 1
|
| 168 |
+
|
| 169 |
+
new_columns.append(col_clean)
|
| 170 |
+
|
| 171 |
+
X_clean.columns = new_columns
|
| 172 |
+
return X_clean
|
| 173 |
+
|
| 174 |
+
return X
|
| 175 |
+
|
| 176 |
+
def fit(self, X_train, y_train, X_val=None, y_val=None, **kwargs):
|
| 177 |
+
"""Train model with resource monitoring."""
|
| 178 |
+
self.resource_monitor.start()
|
| 179 |
+
|
| 180 |
+
try:
|
| 181 |
+
start_time = datetime.now()
|
| 182 |
+
|
| 183 |
+
self.X_train_clean = self.clean_feature_names(X_train)
|
| 184 |
+
|
| 185 |
+
if self.config.use_pipeline:
|
| 186 |
+
if self.pipeline is None:
|
| 187 |
+
self.create_pipeline()
|
| 188 |
+
|
| 189 |
+
self.resource_monitor.snapshot('pipeline_fit_start')
|
| 190 |
+
self.pipeline.fit(self.X_train_clean, y_train)
|
| 191 |
+
self.model = self.pipeline.named_steps[self.model_name]
|
| 192 |
+
else:
|
| 193 |
+
self.resource_monitor.snapshot('model_fit_start')
|
| 194 |
+
self.model.fit(self.X_train_clean, y_train)
|
| 195 |
+
|
| 196 |
+
self.training_time = (datetime.now() - start_time).total_seconds()
|
| 197 |
+
self.is_fitted = True
|
| 198 |
+
|
| 199 |
+
self.resource_monitor.snapshot('fit_complete')
|
| 200 |
+
|
| 201 |
+
self.model_hash = hashlib.md5(
|
| 202 |
+
f"{self.model_name}_{datetime.now().timestamp()}".encode()
|
| 203 |
+
).hexdigest()[:8]
|
| 204 |
+
|
| 205 |
+
logger.info(f"✓ Model {self.model_name} trained in {self.training_time:.2f} seconds")
|
| 206 |
+
|
| 207 |
+
self.predictions['train'] = self.predict(X_train)
|
| 208 |
+
self.residuals['train'] = y_train - self.predictions['train']
|
| 209 |
+
|
| 210 |
+
if X_val is not None and y_val is not None:
|
| 211 |
+
self.predictions['val'] = self.predict(X_val)
|
| 212 |
+
self.residuals['val'] = y_val - self.predictions['val']
|
| 213 |
+
|
| 214 |
+
return self
|
| 215 |
+
|
| 216 |
+
except Exception as e:
|
| 217 |
+
logger.error(f"✗ Error training model {self.model_name}: {e}")
|
| 218 |
+
logger.error(traceback.format_exc())
|
| 219 |
+
raise
|
| 220 |
+
|
| 221 |
+
def predict(self, X):
|
| 222 |
+
"""Make predictions with timing measurement."""
|
| 223 |
+
if not self.is_fitted:
|
| 224 |
+
raise ValueError("Model is not trained")
|
| 225 |
+
|
| 226 |
+
start_time = datetime.now()
|
| 227 |
+
|
| 228 |
+
try:
|
| 229 |
+
X_clean = self.clean_feature_names(X)
|
| 230 |
+
|
| 231 |
+
if self.config.use_pipeline and self.pipeline is not None:
|
| 232 |
+
predictions = self.pipeline.predict(X_clean)
|
| 233 |
+
else:
|
| 234 |
+
predictions = self.model.predict(X_clean)
|
| 235 |
+
|
| 236 |
+
if len(X) > 0:
|
| 237 |
+
self.inference_time = (datetime.now() - start_time).total_seconds() / len(X) * 1000
|
| 238 |
+
else:
|
| 239 |
+
self.inference_time = 0
|
| 240 |
+
|
| 241 |
+
return predictions
|
| 242 |
+
except Exception as e:
|
| 243 |
+
logger.error(f"Prediction error {self.model_name}: {e}")
|
| 244 |
+
raise
|
| 245 |
+
|
| 246 |
+
def evaluate(self, X, y, dataset_name: str = 'test'):
|
| 247 |
+
"""Extended model evaluation."""
|
| 248 |
+
if not self.is_fitted:
|
| 249 |
+
raise ValueError("Model is not trained")
|
| 250 |
+
|
| 251 |
+
y_pred = self.predict(X)
|
| 252 |
+
metrics = self._calculate_metrics(y, y_pred, X)
|
| 253 |
+
|
| 254 |
+
if dataset_name == 'train':
|
| 255 |
+
self.train_metrics = metrics
|
| 256 |
+
elif dataset_name == 'val':
|
| 257 |
+
self.val_metrics = metrics
|
| 258 |
+
else:
|
| 259 |
+
self.test_metrics = metrics
|
| 260 |
+
|
| 261 |
+
self.predictions[dataset_name] = y_pred
|
| 262 |
+
self.residuals[dataset_name] = y - y_pred
|
| 263 |
+
|
| 264 |
+
return metrics
|
| 265 |
+
|
| 266 |
+
def _calculate_metrics(self, y_true, y_pred, X=None):
|
| 267 |
+
"""Calculate extended metrics."""
|
| 268 |
+
metrics = {}
|
| 269 |
+
|
| 270 |
+
try:
|
| 271 |
+
residuals = y_true - y_pred
|
| 272 |
+
|
| 273 |
+
metrics['mse'] = mean_squared_error(y_true, y_pred)
|
| 274 |
+
metrics['rmse'] = np.sqrt(metrics['mse'])
|
| 275 |
+
metrics['mae'] = mean_absolute_error(y_true, y_pred)
|
| 276 |
+
metrics['r2'] = r2_score(y_true, y_pred)
|
| 277 |
+
metrics['explained_variance'] = explained_variance_score(y_true, y_pred)
|
| 278 |
+
metrics['median_absolute_error'] = median_absolute_error(y_true, y_pred)
|
| 279 |
+
metrics['max_error'] = max_error(y_true, y_pred)
|
| 280 |
+
|
| 281 |
+
mask = y_true != 0
|
| 282 |
+
if mask.any():
|
| 283 |
+
metrics['mape'] = np.mean(np.abs(residuals[mask] / y_true[mask])) * 100
|
| 284 |
+
else:
|
| 285 |
+
metrics['mape'] = np.nan
|
| 286 |
+
|
| 287 |
+
if (y_true >= 0).all() and (y_pred >= 0).all():
|
| 288 |
+
try:
|
| 289 |
+
y_true_log = np.log1p(y_true)
|
| 290 |
+
y_pred_log = np.log1p(y_pred)
|
| 291 |
+
metrics['msle'] = mean_squared_log_error(y_true, y_pred)
|
| 292 |
+
metrics['rmsle'] = np.sqrt(metrics['msle'])
|
| 293 |
+
except:
|
| 294 |
+
metrics['msle'] = np.nan
|
| 295 |
+
metrics['rmsle'] = np.nan
|
| 296 |
+
else:
|
| 297 |
+
metrics['msle'] = np.nan
|
| 298 |
+
metrics['rmsle'] = np.nan
|
| 299 |
+
|
| 300 |
+
metrics['wmape'] = np.sum(np.abs(residuals)) / np.sum(np.abs(y_true)) * 100 if np.sum(np.abs(y_true)) > 0 else np.nan
|
| 301 |
+
|
| 302 |
+
metrics['residual_mean'] = np.mean(residuals)
|
| 303 |
+
metrics['residual_std'] = np.std(residuals)
|
| 304 |
+
metrics['residual_skewness'] = stats.skew(residuals) if len(residuals) > 2 else np.nan
|
| 305 |
+
metrics['residual_kurtosis'] = stats.kurtosis(residuals) if len(residuals) > 3 else np.nan
|
| 306 |
+
metrics['residual_min'] = np.min(residuals)
|
| 307 |
+
metrics['residual_max'] = np.max(residuals)
|
| 308 |
+
metrics['residual_q1'] = np.percentile(residuals, 25)
|
| 309 |
+
metrics['residual_q3'] = np.percentile(residuals, 75)
|
| 310 |
+
metrics['residual_iqr'] = metrics['residual_q3'] - metrics['residual_q1']
|
| 311 |
+
|
| 312 |
+
std_dev = metrics['residual_std']
|
| 313 |
+
if not np.isnan(std_dev) and std_dev > 0:
|
| 314 |
+
for n in [1, 2, 3]:
|
| 315 |
+
within_n_std = np.sum(np.abs(residuals) <= n * std_dev) / len(residuals) * 100
|
| 316 |
+
metrics[f'within_{n}_std'] = within_n_std
|
| 317 |
+
|
| 318 |
+
if len(y_true) > 1:
|
| 319 |
+
metrics['correlation'] = np.corrcoef(y_true, y_pred)[0, 1]
|
| 320 |
+
else:
|
| 321 |
+
metrics['correlation'] = np.nan
|
| 322 |
+
|
| 323 |
+
metrics['bias'] = np.mean(y_pred - y_true)
|
| 324 |
+
metrics['mpe'] = np.mean((y_pred - y_true) / y_true) * 100 if np.all(y_true != 0) else np.nan
|
| 325 |
+
|
| 326 |
+
n = len(y_true)
|
| 327 |
+
if self.feature_names is not None:
|
| 328 |
+
p = len(self.feature_names)
|
| 329 |
+
else:
|
| 330 |
+
p = 1
|
| 331 |
+
metrics['adjusted_r2'] = 1 - (1 - metrics['r2']) * (n - 1) / (n - p - 1) if n > p + 1 and metrics['r2'] <= 1 else np.nan
|
| 332 |
+
|
| 333 |
+
except Exception as e:
|
| 334 |
+
logger.warning(f"Error calculating metrics: {e}")
|
| 335 |
+
if self.config.debug:
|
| 336 |
+
traceback.print_exc()
|
| 337 |
+
|
| 338 |
+
return metrics
|
| 339 |
+
|
| 340 |
+
def cross_validate(self, X, y, cv=None, scoring=None):
|
| 341 |
+
"""Extended cross-validation."""
|
| 342 |
+
if scoring is None:
|
| 343 |
+
scoring = {
|
| 344 |
+
'neg_mse': 'neg_mean_squared_error',
|
| 345 |
+
'neg_rmse': make_scorer(lambda y_true, y_pred: -np.sqrt(mean_squared_error(y_true, y_pred))),
|
| 346 |
+
'neg_mae': 'neg_mean_absolute_error',
|
| 347 |
+
'r2': 'r2',
|
| 348 |
+
'explained_variance': 'explained_variance'
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
if cv is None:
|
| 352 |
+
if self.config.cv_method == 'timeseries':
|
| 353 |
+
cv = TimeSeriesSplit(n_splits=min(self.config.cv_folds, len(X) // 2))
|
| 354 |
+
elif self.config.cv_method == 'stratified':
|
| 355 |
+
if len(y) > 0:
|
| 356 |
+
y_bins = pd.qcut(y, q=min(5, len(y) // 10), labels=False, duplicates='drop')
|
| 357 |
+
cv = StratifiedKFold(n_splits=self.config.cv_folds, shuffle=True,
|
| 358 |
+
random_state=self.config.random_state)
|
| 359 |
+
else:
|
| 360 |
+
cv = self.config.cv_folds
|
| 361 |
+
elif self.config.cv_method == 'group':
|
| 362 |
+
if self.config.groups is not None:
|
| 363 |
+
groups = X[self.config.groups] if self.config.groups in X.columns else None
|
| 364 |
+
cv = GroupKFold(n_splits=self.config.cv_folds)
|
| 365 |
+
else:
|
| 366 |
+
cv = self.config.cv_folds
|
| 367 |
+
elif self.config.cv_method == 'loo':
|
| 368 |
+
cv = LeaveOneOut()
|
| 369 |
+
elif self.config.cv_method == 'shuffle':
|
| 370 |
+
cv = ShuffleSplit(n_splits=self.config.cv_folds, test_size=0.2,
|
| 371 |
+
random_state=self.config.random_state)
|
| 372 |
+
else:
|
| 373 |
+
cv = KFold(n_splits=self.config.cv_folds, shuffle=True,
|
| 374 |
+
random_state=self.config.random_state)
|
| 375 |
+
|
| 376 |
+
try:
|
| 377 |
+
X_clean = self.clean_feature_names(X)
|
| 378 |
+
|
| 379 |
+
if self.config.use_pipeline and self.pipeline is not None:
|
| 380 |
+
estimator = self.pipeline
|
| 381 |
+
else:
|
| 382 |
+
estimator = self.model
|
| 383 |
+
|
| 384 |
+
cv_results = cross_validate(
|
| 385 |
+
estimator, X_clean, y,
|
| 386 |
+
cv=cv,
|
| 387 |
+
scoring=scoring,
|
| 388 |
+
n_jobs=min(2, self.config.n_jobs),
|
| 389 |
+
return_train_score=True,
|
| 390 |
+
error_score='raise',
|
| 391 |
+
return_estimator=True
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
self.cv_results = {
|
| 395 |
+
'train_neg_mse_mean': -np.mean(cv_results['train_neg_mse']),
|
| 396 |
+
'train_neg_mse_std': np.std(cv_results['train_neg_mse']),
|
| 397 |
+
'test_neg_mse_mean': -np.mean(cv_results['test_neg_mse']),
|
| 398 |
+
'test_neg_mse_std': np.std(cv_results['test_neg_mse']),
|
| 399 |
+
'train_r2_mean': np.mean(cv_results['train_r2']),
|
| 400 |
+
'train_r2_std': np.std(cv_results['train_r2']),
|
| 401 |
+
'test_r2_mean': np.mean(cv_results['test_r2']),
|
| 402 |
+
'test_r2_std': np.std(cv_results['test_r2']),
|
| 403 |
+
'fit_time_mean': np.mean(cv_results['fit_time']),
|
| 404 |
+
'score_time_mean': np.mean(cv_results['score_time']),
|
| 405 |
+
'train_explained_variance_mean': np.mean(cv_results['train_explained_variance']),
|
| 406 |
+
'test_explained_variance_mean': np.mean(cv_results['test_explained_variance']),
|
| 407 |
+
'estimators': cv_results['estimator']
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
return self.cv_results
|
| 411 |
+
|
| 412 |
+
except Exception as e:
|
| 413 |
+
logger.warning(f"Cross-validation failed: {e}")
|
| 414 |
+
return {}
|
| 415 |
+
|
| 416 |
+
def get_feature_importance(self, feature_names=None, method='auto'):
|
| 417 |
+
"""Get feature importance using different methods."""
|
| 418 |
+
if not self.is_fitted:
|
| 419 |
+
return {}
|
| 420 |
+
|
| 421 |
+
if feature_names is None:
|
| 422 |
+
feature_names = self.feature_names
|
| 423 |
+
|
| 424 |
+
if feature_names is None:
|
| 425 |
+
return {}
|
| 426 |
+
|
| 427 |
+
importances = {}
|
| 428 |
+
|
| 429 |
+
try:
|
| 430 |
+
if method in ['auto', 'native']:
|
| 431 |
+
if hasattr(self.model, 'feature_importances_'):
|
| 432 |
+
importances = self.model.feature_importances_
|
| 433 |
+
elif hasattr(self.model, 'coef_'):
|
| 434 |
+
importances = np.abs(self.model.coef_)
|
| 435 |
+
if len(importances.shape) > 1:
|
| 436 |
+
importances = np.mean(importances, axis=0)
|
| 437 |
+
elif hasattr(self.model, 'estimators_'):
|
| 438 |
+
importances = np.mean([
|
| 439 |
+
tree.feature_importances_ for tree in self.model.estimators_
|
| 440 |
+
], axis=0)
|
| 441 |
+
|
| 442 |
+
if method in ['auto', 'permutation'] and len(feature_names) <= 50:
|
| 443 |
+
try:
|
| 444 |
+
if self.X_train_clean is not None:
|
| 445 |
+
perm_importance = permutation_importance(
|
| 446 |
+
self.model, self.X_train_clean,
|
| 447 |
+
np.random.randn(len(self.X_train_clean)),
|
| 448 |
+
n_repeats=10,
|
| 449 |
+
random_state=self.config.random_state,
|
| 450 |
+
n_jobs=min(2, self.config.n_jobs)
|
| 451 |
+
)
|
| 452 |
+
importances_perm = perm_importance.importances_mean
|
| 453 |
+
|
| 454 |
+
if method == 'permutation':
|
| 455 |
+
importances = importances_perm
|
| 456 |
+
else:
|
| 457 |
+
if len(importances) == 0:
|
| 458 |
+
importances = importances_perm
|
| 459 |
+
except:
|
| 460 |
+
pass
|
| 461 |
+
|
| 462 |
+
if len(importances) > 0 and len(importances) == len(feature_names):
|
| 463 |
+
self.feature_importances = dict(zip(feature_names, importances))
|
| 464 |
+
self.feature_importances = dict(sorted(
|
| 465 |
+
self.feature_importances.items(),
|
| 466 |
+
key=lambda x: x[1],
|
| 467 |
+
reverse=True
|
| 468 |
+
))
|
| 469 |
+
|
| 470 |
+
return self.feature_importances
|
| 471 |
+
|
| 472 |
+
except Exception as e:
|
| 473 |
+
logger.debug(f"Feature importance not available for {self.model_name}: {e}")
|
| 474 |
+
return {}
|
| 475 |
+
|
| 476 |
+
def calculate_shap_values(self, X, sample_size=1000):
|
| 477 |
+
"""Calculate SHAP values for model explanation with fixes."""
|
| 478 |
+
if not SHAP_AVAILABLE or not self.is_fitted:
|
| 479 |
+
return None
|
| 480 |
+
|
| 481 |
+
try:
|
| 482 |
+
X_clean = self.clean_feature_names(X)
|
| 483 |
+
|
| 484 |
+
if sample_size < len(X_clean):
|
| 485 |
+
indices = np.random.choice(len(X_clean), size=min(sample_size, len(X_clean)), replace=False)
|
| 486 |
+
X_sample = X_clean.iloc[indices]
|
| 487 |
+
else:
|
| 488 |
+
X_sample = X_clean
|
| 489 |
+
|
| 490 |
+
if len(X_sample) == 0:
|
| 491 |
+
return None
|
| 492 |
+
|
| 493 |
+
# Save sample for visualisation
|
| 494 |
+
self.X_sample_shap = X_sample
|
| 495 |
+
|
| 496 |
+
# Use different explainers for different model types
|
| 497 |
+
if hasattr(self.model, 'feature_importances_') or hasattr(self.model, 'feature_importance'):
|
| 498 |
+
# Tree-based models
|
| 499 |
+
try:
|
| 500 |
+
explainer = shap.TreeExplainer(self.model)
|
| 501 |
+
self.shap_values = explainer.shap_values(X_sample)
|
| 502 |
+
except:
|
| 503 |
+
# Alternative approach for complex cases
|
| 504 |
+
try:
|
| 505 |
+
explainer = shap.Explainer(self.model, X_sample)
|
| 506 |
+
self.shap_values = explainer(X_sample)
|
| 507 |
+
except Exception as e:
|
| 508 |
+
logger.warning(f"Failed to use TreeExplainer: {e}")
|
| 509 |
+
return None
|
| 510 |
+
elif hasattr(self.model, 'coef_'):
|
| 511 |
+
# Linear models
|
| 512 |
+
try:
|
| 513 |
+
explainer = shap.LinearExplainer(self.model, X_sample)
|
| 514 |
+
self.shap_values = explainer.shap_values(X_sample)
|
| 515 |
+
except:
|
| 516 |
+
explainer = shap.KernelExplainer(self.model.predict, X_sample)
|
| 517 |
+
self.shap_values = explainer.shap_values(X_sample)
|
| 518 |
+
else:
|
| 519 |
+
# Other models
|
| 520 |
+
try:
|
| 521 |
+
explainer = shap.KernelExplainer(self.model.predict, X_sample)
|
| 522 |
+
self.shap_values = explainer.shap_values(X_sample)
|
| 523 |
+
except Exception as e:
|
| 524 |
+
logger.warning(f"Failed to use KernelExplainer: {e}")
|
| 525 |
+
return None
|
| 526 |
+
|
| 527 |
+
return self.shap_values
|
| 528 |
+
|
| 529 |
+
except Exception as e:
|
| 530 |
+
logger.warning(f"Failed to calculate SHAP values: {e}")
|
| 531 |
+
if self.config.debug:
|
| 532 |
+
logger.debug(traceback.format_exc())
|
| 533 |
+
return None
|
| 534 |
+
|
| 535 |
+
def save_model(self, path: str = None, max_retries: int = 3):
|
| 536 |
+
"""
|
| 537 |
+
Reliable model saving that works for all model types.
|
| 538 |
+
Returns path to saved file or None on error.
|
| 539 |
+
"""
|
| 540 |
+
if not self.is_fitted:
|
| 541 |
+
logger.warning(f"Model {self.model_name} is not trained, cannot save")
|
| 542 |
+
return None
|
| 543 |
+
|
| 544 |
+
# Create unique filename
|
| 545 |
+
if path is None:
|
| 546 |
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 547 |
+
filename = f'{self.model_name}_{self.model_hash}_{timestamp}'
|
| 548 |
+
|
| 549 |
+
# Determine base path
|
| 550 |
+
if 'google.colab' in str(sys.modules):
|
| 551 |
+
base_path = '/content/drive/MyDrive/saved_models'
|
| 552 |
+
else:
|
| 553 |
+
base_path = self.config.models_dir if hasattr(self.config, 'models_dir') else 'saved_models'
|
| 554 |
+
|
| 555 |
+
Path(base_path).mkdir(parents=True, exist_ok=True)
|
| 556 |
+
path = f'{base_path}/{filename}'
|
| 557 |
+
|
| 558 |
+
try:
|
| 559 |
+
# Create directory if it doesn't exist
|
| 560 |
+
Path(os.path.dirname(path)).mkdir(parents=True, exist_ok=True)
|
| 561 |
+
|
| 562 |
+
# 1. Determine model type and use appropriate saving method
|
| 563 |
+
model_type = type(self.model).__name__.lower()
|
| 564 |
+
|
| 565 |
+
# 2. Use different saving strategies for different model types
|
| 566 |
+
saved_files = []
|
| 567 |
+
|
| 568 |
+
for attempt in range(max_retries):
|
| 569 |
+
try:
|
| 570 |
+
# STRATEGY 1: Models with built-in save_model() method
|
| 571 |
+
if hasattr(self.model, 'save_model') and callable(getattr(self.model, 'save_model')):
|
| 572 |
+
if 'lightgbm' in model_type:
|
| 573 |
+
# LightGBM
|
| 574 |
+
model_path = f"{path}_lightgbm.txt"
|
| 575 |
+
self.model.save_model(model_path)
|
| 576 |
+
saved_files.append(('lightgbm_model', model_path))
|
| 577 |
+
|
| 578 |
+
elif 'catboost' in model_type:
|
| 579 |
+
# CatBoost
|
| 580 |
+
model_path = f"{path}_catboost.cbm"
|
| 581 |
+
self.model.save_model(model_path, format='cbm')
|
| 582 |
+
saved_files.append(('catboost_model', model_path))
|
| 583 |
+
|
| 584 |
+
elif 'xgboost' in model_type:
|
| 585 |
+
# XGBoost
|
| 586 |
+
model_path = f"{path}_xgboost.json"
|
| 587 |
+
self.model.save_model(model_path)
|
| 588 |
+
saved_files.append(('xgboost_model', model_path))
|
| 589 |
+
|
| 590 |
+
else:
|
| 591 |
+
# Other models with save_model (Prophet etc.)
|
| 592 |
+
model_path = f"{path}_native.model"
|
| 593 |
+
self.model.save_model(model_path)
|
| 594 |
+
saved_files.append(('native_model', model_path))
|
| 595 |
+
|
| 596 |
+
# STRATEGY 2: Sklearn and other Python objects
|
| 597 |
+
else:
|
| 598 |
+
# For sklearn models and pipelines
|
| 599 |
+
model_path = f"{path}_sklearn.pkl"
|
| 600 |
+
|
| 601 |
+
# Remove problematic attributes before saving
|
| 602 |
+
model_to_save = self.model
|
| 603 |
+
|
| 604 |
+
# For pipelines, remove data references
|
| 605 |
+
if hasattr(model_to_save, 'named_steps'):
|
| 606 |
+
# This is a pipeline, clone it without data
|
| 607 |
+
from sklearn.base import clone
|
| 608 |
+
model_to_save = clone(model_to_save)
|
| 609 |
+
|
| 610 |
+
# Save model
|
| 611 |
+
joblib.dump(model_to_save, model_path, compress=3, protocol=pickle.HIGHEST_PROTOCOL)
|
| 612 |
+
saved_files.append(('sklearn_model', model_path))
|
| 613 |
+
|
| 614 |
+
# 3. Save metadata SEPARATELY (this is key)
|
| 615 |
+
metadata = {
|
| 616 |
+
'model_name': self.model_name,
|
| 617 |
+
'model_type': str(type(self.model)),
|
| 618 |
+
'model_hash': self.model_hash,
|
| 619 |
+
'best_params': self._convert_to_serializable(self.best_params),
|
| 620 |
+
'train_metrics': self._convert_to_serializable(self.train_metrics),
|
| 621 |
+
'test_metrics': self._convert_to_serializable(self.test_metrics),
|
| 622 |
+
'training_time': self.training_time,
|
| 623 |
+
'inference_time': self.inference_time,
|
| 624 |
+
'feature_names': self.feature_names,
|
| 625 |
+
'config': self.config.to_dict() if hasattr(self.config, 'to_dict') else str(self.config),
|
| 626 |
+
'saved_files': saved_files,
|
| 627 |
+
'python_version': sys.version,
|
| 628 |
+
'library_versions': self._get_library_versions(),
|
| 629 |
+
'saved_at': datetime.now().isoformat(),
|
| 630 |
+
'model_version': '2.0'
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
# Feature importance (if available, save separately)
|
| 634 |
+
if self.feature_importances:
|
| 635 |
+
importance_path = f"{path}_feature_importance.json"
|
| 636 |
+
with open(importance_path, 'w', encoding='utf-8') as f:
|
| 637 |
+
json.dump(self._convert_to_serializable(self.feature_importances),
|
| 638 |
+
f, indent=4, ensure_ascii=False)
|
| 639 |
+
saved_files.append(('feature_importance', importance_path))
|
| 640 |
+
|
| 641 |
+
# Save metadata
|
| 642 |
+
metadata_path = f"{path}_metadata.json"
|
| 643 |
+
with open(metadata_path, 'w', encoding='utf-8') as f:
|
| 644 |
+
json.dump(metadata, f, indent=4, default=str, ensure_ascii=False)
|
| 645 |
+
saved_files.append(('metadata', metadata_path))
|
| 646 |
+
|
| 647 |
+
# 4. Create file manifest
|
| 648 |
+
manifest = {
|
| 649 |
+
'model_name': self.model_name,
|
| 650 |
+
'main_model_file': saved_files[0][1] if saved_files else None,
|
| 651 |
+
'all_files': [file_info for _, file_info in saved_files],
|
| 652 |
+
'load_instructions': self._get_load_instructions(saved_files)
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
manifest_path = f"{path}_manifest.json"
|
| 656 |
+
with open(manifest_path, 'w', encoding='utf-8') as f:
|
| 657 |
+
json.dump(manifest, f, indent=4, ensure_ascii=False)
|
| 658 |
+
|
| 659 |
+
logger.info(f"✓ Model {self.model_name} successfully saved")
|
| 660 |
+
logger.info(f" Main file: {saved_files[0][1] if saved_files else 'N/A'}")
|
| 661 |
+
logger.info(f" Total files: {len(saved_files)}")
|
| 662 |
+
|
| 663 |
+
return manifest_path
|
| 664 |
+
|
| 665 |
+
except (pickle.PicklingError, TypeError) as e:
|
| 666 |
+
logger.warning(f"Attempt {attempt + 1}/{max_retries}: Serialisation error: {str(e)[:100]}")
|
| 667 |
+
|
| 668 |
+
if attempt == max_retries - 1: # Last attempt
|
| 669 |
+
# Try saving only parameters
|
| 670 |
+
return self._save_model_params_only(path)
|
| 671 |
+
|
| 672 |
+
time.sleep(1) # Wait before retry
|
| 673 |
+
|
| 674 |
+
except Exception as e:
|
| 675 |
+
logger.error(f"Attempt {attempt + 1}/{max_retries}: Error: {e}")
|
| 676 |
+
if attempt == max_retries - 1:
|
| 677 |
+
return self._save_model_params_only(path)
|
| 678 |
+
time.sleep(1)
|
| 679 |
+
|
| 680 |
+
except Exception as e:
|
| 681 |
+
logger.error(f"Critical error saving model {self.model_name}: {e}")
|
| 682 |
+
return None
|
| 683 |
+
|
| 684 |
+
def _convert_to_serializable(self, obj):
|
| 685 |
+
"""Recursively convert object to serializable format."""
|
| 686 |
+
if obj is None:
|
| 687 |
+
return None
|
| 688 |
+
elif isinstance(obj, (str, int, float, bool)):
|
| 689 |
+
return obj
|
| 690 |
+
elif isinstance(obj, dict):
|
| 691 |
+
return {k: self._convert_to_serializable(v) for k, v in obj.items()}
|
| 692 |
+
elif isinstance(obj, (list, tuple, set)):
|
| 693 |
+
return [self._convert_to_serializable(item) for item in obj]
|
| 694 |
+
elif isinstance(obj, np.integer):
|
| 695 |
+
return int(obj)
|
| 696 |
+
elif isinstance(obj, np.floating):
|
| 697 |
+
return float(obj)
|
| 698 |
+
elif isinstance(obj, np.ndarray):
|
| 699 |
+
return obj.tolist()
|
| 700 |
+
elif isinstance(obj, pd.Series):
|
| 701 |
+
return obj.to_dict()
|
| 702 |
+
elif isinstance(obj, pd.DataFrame):
|
| 703 |
+
return obj.to_dict('records')
|
| 704 |
+
elif hasattr(obj, 'tolist'):
|
| 705 |
+
return obj.tolist()
|
| 706 |
+
else:
|
| 707 |
+
return str(obj)
|
| 708 |
+
|
| 709 |
+
def _save_model_params_only(self, base_path):
|
| 710 |
+
"""Emergency save of model parameters only."""
|
| 711 |
+
try:
|
| 712 |
+
params_path = f"{base_path}_params_only.json"
|
| 713 |
+
|
| 714 |
+
# Try to get model parameters
|
| 715 |
+
model_params = {}
|
| 716 |
+
if hasattr(self.model, 'get_params'):
|
| 717 |
+
model_params = self.model.get_params()
|
| 718 |
+
|
| 719 |
+
# Save minimal information
|
| 720 |
+
minimal_data = {
|
| 721 |
+
'model_name': self.model_name,
|
| 722 |
+
'model_type': str(type(self.model)),
|
| 723 |
+
'model_params': self._convert_to_serializable(model_params),
|
| 724 |
+
'train_metrics': self._convert_to_serializable(self.train_metrics),
|
| 725 |
+
'test_metrics': self._convert_to_serializable(self.test_metrics),
|
| 726 |
+
'feature_names': self.feature_names,
|
| 727 |
+
'saved_at': datetime.now().isoformat(),
|
| 728 |
+
'note': 'Model parameters only (full model could not be saved)'
|
| 729 |
+
}
|
| 730 |
+
|
| 731 |
+
with open(params_path, 'w', encoding='utf-8') as f:
|
| 732 |
+
json.dump(minimal_data, f, indent=4, ensure_ascii=False)
|
| 733 |
+
|
| 734 |
+
logger.warning(f"⚠ Only model parameters saved: {params_path}")
|
| 735 |
+
return params_path
|
| 736 |
+
|
| 737 |
+
except Exception as e:
|
| 738 |
+
logger.error(f"Failed to save even model parameters: {e}")
|
| 739 |
+
return None
|
| 740 |
+
|
| 741 |
+
def _get_library_versions(self):
|
| 742 |
+
"""Get versions of used libraries."""
|
| 743 |
+
versions = {}
|
| 744 |
+
libs = ['sklearn', 'numpy', 'pandas', 'xgboost', 'lightgbm', 'catboost']
|
| 745 |
+
|
| 746 |
+
for lib in libs:
|
| 747 |
+
try:
|
| 748 |
+
module = __import__(lib)
|
| 749 |
+
versions[lib] = getattr(module, '__version__', 'unknown')
|
| 750 |
+
except:
|
| 751 |
+
pass
|
| 752 |
+
|
| 753 |
+
return versions
|
| 754 |
+
|
| 755 |
+
def _get_load_instructions(self, saved_files):
|
| 756 |
+
"""Generate loading instructions for this model type."""
|
| 757 |
+
instructions = {
|
| 758 |
+
'sklearn.pkl': 'joblib.load(file_path)',
|
| 759 |
+
'lightgbm.txt': 'lightgbm.Booster(model_file=file_path)',
|
| 760 |
+
'catboost.cbm': 'catboost.CatBoost().load_model(file_path)',
|
| 761 |
+
'xgboost.json': 'xgboost.XGBRegressor().load_model(file_path)',
|
| 762 |
+
'metadata.json': 'json.load(open(file_path))',
|
| 763 |
+
'params_only.json': 'Parameters only, full model cannot be restored'
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
return instructions
|
| 767 |
+
|
| 768 |
+
def load_model(self, manifest_path: str):
|
| 769 |
+
"""Universal model loading from saved files."""
|
| 770 |
+
try:
|
| 771 |
+
# Load manifest
|
| 772 |
+
with open(manifest_path, 'r', encoding='utf-8') as f:
|
| 773 |
+
manifest = json.load(f)
|
| 774 |
+
|
| 775 |
+
# Load metadata
|
| 776 |
+
metadata_path = None
|
| 777 |
+
model_file = None
|
| 778 |
+
|
| 779 |
+
for file_info in manifest.get('all_files', []):
|
| 780 |
+
if 'metadata' in file_info or file_info.endswith('_metadata.json'):
|
| 781 |
+
metadata_path = file_info
|
| 782 |
+
elif 'sklearn.pkl' in file_info or 'lightgbm.txt' in file_info or 'catboost.cbm' in file_info or 'xgboost.json' in file_info:
|
| 783 |
+
model_file = file_info
|
| 784 |
+
|
| 785 |
+
if not metadata_path:
|
| 786 |
+
raise ValueError("Metadata file not found")
|
| 787 |
+
|
| 788 |
+
# Load metadata
|
| 789 |
+
with open(metadata_path, 'r', encoding='utf-8') as f:
|
| 790 |
+
metadata = json.load(f)
|
| 791 |
+
|
| 792 |
+
# Restore attributes
|
| 793 |
+
self.model_name = metadata['model_name']
|
| 794 |
+
self.model_hash = metadata.get('model_hash', 'unknown')
|
| 795 |
+
self.best_params = metadata.get('best_params', {})
|
| 796 |
+
self.train_metrics = metadata.get('train_metrics', {})
|
| 797 |
+
self.test_metrics = metadata.get('test_metrics', {})
|
| 798 |
+
self.training_time = metadata.get('training_time', 0)
|
| 799 |
+
self.inference_time = metadata.get('inference_time', 0)
|
| 800 |
+
self.feature_names = metadata.get('feature_names', [])
|
| 801 |
+
self.feature_importances = {}
|
| 802 |
+
|
| 803 |
+
# Load model
|
| 804 |
+
if model_file:
|
| 805 |
+
if model_file.endswith('.pkl'):
|
| 806 |
+
# sklearn model
|
| 807 |
+
self.model = joblib.load(model_file)
|
| 808 |
+
elif model_file.endswith('.txt') and 'lightgbm' in str(metadata.get('model_type', '')):
|
| 809 |
+
# LightGBM
|
| 810 |
+
import lightgbm as lgb
|
| 811 |
+
self.model = lgb.Booster(model_file=model_file)
|
| 812 |
+
elif model_file.endswith('.cbm'):
|
| 813 |
+
# CatBoost
|
| 814 |
+
import catboost as cb
|
| 815 |
+
self.model = cb.CatBoostRegressor()
|
| 816 |
+
self.model.load_model(model_file)
|
| 817 |
+
elif model_file.endswith('.json') and 'xgboost' in str(metadata.get('model_type', '')):
|
| 818 |
+
# XGBoost
|
| 819 |
+
import xgboost as xgb
|
| 820 |
+
self.model = xgb.XGBRegressor()
|
| 821 |
+
self.model.load_model(model_file)
|
| 822 |
+
else:
|
| 823 |
+
raise ValueError(f"Unknown model format: {model_file}")
|
| 824 |
+
|
| 825 |
+
# Load feature importance if available
|
| 826 |
+
for file_info in manifest.get('all_files', []):
|
| 827 |
+
if 'feature_importance' in file_info or file_info.endswith('_feature_importance.json'):
|
| 828 |
+
with open(file_info, 'r', encoding='utf-8') as f:
|
| 829 |
+
self.feature_importances = json.load(f)
|
| 830 |
+
break
|
| 831 |
+
|
| 832 |
+
self.is_fitted = True
|
| 833 |
+
logger.info(f"✓ Model {self.model_name} successfully loaded")
|
| 834 |
+
return self
|
| 835 |
+
|
| 836 |
+
except Exception as e:
|
| 837 |
+
logger.error(f"Error loading model: {e}")
|
| 838 |
+
raise
|
| 839 |
+
|
| 840 |
+
def get_summary(self):
|
| 841 |
+
"""Get extended model summary."""
|
| 842 |
+
summary = {
|
| 843 |
+
'model_name': self.model_name,
|
| 844 |
+
'is_fitted': self.is_fitted,
|
| 845 |
+
'training_time': self.training_time,
|
| 846 |
+
'inference_time': self.inference_time,
|
| 847 |
+
'best_params': self.best_params,
|
| 848 |
+
'train_metrics': self.train_metrics,
|
| 849 |
+
'test_metrics': self.test_metrics,
|
| 850 |
+
'cv_results': {k: v for k, v in self.cv_results.items() if k != 'estimators'},
|
| 851 |
+
'feature_importance_count': len(self.feature_importances),
|
| 852 |
+
'model_hash': self.model_hash,
|
| 853 |
+
'has_shap_values': self.shap_values is not None,
|
| 854 |
+
'resource_usage': self.resource_monitor.get_summary()
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
+
return summary
|
| 858 |
+
|
| 859 |
+
def get_learning_curve(self, X_train, y_train, train_sizes=None):
|
| 860 |
+
"""Get learning curve."""
|
| 861 |
+
from sklearn.model_selection import learning_curve
|
| 862 |
+
|
| 863 |
+
if train_sizes is None:
|
| 864 |
+
train_sizes = np.linspace(0.1, 1.0, 10)
|
| 865 |
+
|
| 866 |
+
try:
|
| 867 |
+
if self.config.use_pipeline and self.pipeline is not None:
|
| 868 |
+
estimator = self.pipeline
|
| 869 |
+
else:
|
| 870 |
+
estimator = self.model
|
| 871 |
+
|
| 872 |
+
X_clean = self.clean_feature_names(X_train)
|
| 873 |
+
|
| 874 |
+
train_sizes_abs, train_scores, test_scores = learning_curve(
|
| 875 |
+
estimator, X_clean, y_train,
|
| 876 |
+
train_sizes=train_sizes,
|
| 877 |
+
cv=min(5, self.config.cv_folds),
|
| 878 |
+
scoring='neg_mean_squared_error',
|
| 879 |
+
n_jobs=min(2, self.config.n_jobs),
|
| 880 |
+
shuffle=True,
|
| 881 |
+
random_state=self.config.random_state,
|
| 882 |
+
error_score='raise'
|
| 883 |
+
)
|
| 884 |
+
|
| 885 |
+
return {
|
| 886 |
+
'train_sizes': train_sizes_abs,
|
| 887 |
+
'train_scores': train_scores,
|
| 888 |
+
'test_scores': test_scores
|
| 889 |
+
}
|
| 890 |
+
except Exception as e:
|
| 891 |
+
logger.warning(f"Failed to build learning curve: {e}")
|
| 892 |
+
return None
|
| 893 |
+
|
| 894 |
+
def get_sklearn_estimator(self):
|
| 895 |
+
"""Return sklearn-compatible model or pipeline."""
|
| 896 |
+
if not self.is_fitted:
|
| 897 |
+
raise ValueError("Model is not trained")
|
| 898 |
+
|
| 899 |
+
if self.config.use_pipeline and self.pipeline is not None:
|
| 900 |
+
return self.pipeline
|
| 901 |
+
else:
|
| 902 |
+
return self.model
|
| 903 |
+
|
| 904 |
+
def get_sklearn_model(self):
|
| 905 |
+
"""Return model without pipeline."""
|
| 906 |
+
if not self.is_fitted:
|
| 907 |
+
raise ValueError("Model is not trained")
|
| 908 |
+
|
| 909 |
+
return self.model
|
src/ml_pipeline/core/model_factory.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
| 5 |
+
|
| 6 |
+
from src.ml_pipeline.models.neighbors_models import KNNModel, RadiusNeighborsModel
|
| 7 |
+
from src.ml_pipeline.models.neural_models import GaussianProcessModel, MLPModel
|
| 8 |
+
from src.ml_pipeline.models.svm_models import LinearSVMModel, NuSVMModel, SVMModel
|
| 9 |
+
|
| 10 |
+
import logging
|
| 11 |
+
from typing import Dict, List
|
| 12 |
+
from ..config.ml_config import MLConfig
|
| 13 |
+
from .base_model import BaseMLModel
|
| 14 |
+
from ..models.linear_models import *
|
| 15 |
+
from ..models.tree_models import *
|
| 16 |
+
from ..models.boosting_models import *
|
| 17 |
+
from ..models.ensemble_models import *
|
| 18 |
+
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class ModelFactory:
|
| 23 |
+
"""Factory for creating ML model instances."""
|
| 24 |
+
|
| 25 |
+
@staticmethod
|
| 26 |
+
def create_model(model_type: str, config: MLConfig) -> BaseMLModel:
|
| 27 |
+
model_registry = {
|
| 28 |
+
# Linear models
|
| 29 |
+
'linear_regression': LinearRegressionModel,
|
| 30 |
+
'ridge': RidgeRegressionModel,
|
| 31 |
+
'lasso': LassoRegressionModel,
|
| 32 |
+
'elasticnet': ElasticNetModel,
|
| 33 |
+
'bayesian_ridge': BayesianRidgeModel,
|
| 34 |
+
'huber': HuberRegressionModel,
|
| 35 |
+
'kernel_ridge': KernelRidgeModel,
|
| 36 |
+
'pls': PLSRegressionModel,
|
| 37 |
+
'sgd': SGDRegressionModel,
|
| 38 |
+
'passive_aggressive': PassiveAggressiveRegressionModel,
|
| 39 |
+
'ransac': RANSACRegressionModel,
|
| 40 |
+
'theil_sen': TheilSenRegressionModel,
|
| 41 |
+
'orthogonal_matching_pursuit': OrthogonalMatchingPursuitModel,
|
| 42 |
+
'isotonic': IsotonicRegressionModel,
|
| 43 |
+
|
| 44 |
+
# Tree-based models
|
| 45 |
+
'random_forest': RandomForestModel,
|
| 46 |
+
'extra_trees': ExtraTreesModel,
|
| 47 |
+
'decision_tree': DecisionTreeModel,
|
| 48 |
+
|
| 49 |
+
# Gradient boosting models
|
| 50 |
+
'gradient_boosting': GradientBoostingModel,
|
| 51 |
+
'hist_gradient_boosting': HistGradientBoostingModel,
|
| 52 |
+
'xgboost': XGBoostModel,
|
| 53 |
+
'lightgbm': LightGBMModel,
|
| 54 |
+
'catboost': CatBoostModel,
|
| 55 |
+
|
| 56 |
+
# Ensemble models
|
| 57 |
+
'ada_boost': AdaBoostModel,
|
| 58 |
+
'bagging': BaggingModel,
|
| 59 |
+
|
| 60 |
+
# SVM models
|
| 61 |
+
'svr': SVMModel,
|
| 62 |
+
'linear_svr': LinearSVMModel,
|
| 63 |
+
'nu_svr': NuSVMModel,
|
| 64 |
+
|
| 65 |
+
# Nearest neighbours models
|
| 66 |
+
'knn': KNNModel,
|
| 67 |
+
'radius_neighbors': RadiusNeighborsModel,
|
| 68 |
+
|
| 69 |
+
# Neural network models
|
| 70 |
+
'mlp': MLPModel,
|
| 71 |
+
'gaussian_process': GaussianProcessModel
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
if model_type not in model_registry:
|
| 75 |
+
raise ValueError(f"Unknown model type: {model_type}")
|
| 76 |
+
|
| 77 |
+
return model_registry[model_type](config)
|
| 78 |
+
|
| 79 |
+
@staticmethod
|
| 80 |
+
def get_sklearn_compatible_estimator(model_wrapper: BaseMLModel):
|
| 81 |
+
"""Extract a scikit-learn compatible estimator from the model wrapper."""
|
| 82 |
+
try:
|
| 83 |
+
if hasattr(model_wrapper, 'get_sklearn_estimator'):
|
| 84 |
+
return model_wrapper.get_sklearn_estimator()
|
| 85 |
+
elif hasattr(model_wrapper, 'pipeline') and model_wrapper.pipeline is not None:
|
| 86 |
+
return model_wrapper.pipeline
|
| 87 |
+
elif hasattr(model_wrapper, 'model') and model_wrapper.model is not None:
|
| 88 |
+
return model_wrapper.model
|
| 89 |
+
else:
|
| 90 |
+
raise ValueError("Unable to retrieve a scikit-learn compatible estimator.")
|
| 91 |
+
except Exception as e:
|
| 92 |
+
logger.error(f"Error retrieving scikit-learn compatible estimator: {e}")
|
| 93 |
+
raise
|
| 94 |
+
|
| 95 |
+
@staticmethod
|
| 96 |
+
def get_all_models(config: MLConfig, model_types: List[str] = None) -> Dict[str, BaseMLModel]:
|
| 97 |
+
if model_types is None:
|
| 98 |
+
model_types = config.baseline_models
|
| 99 |
+
|
| 100 |
+
models = {}
|
| 101 |
+
for model_type in model_types:
|
| 102 |
+
try:
|
| 103 |
+
models[model_type] = ModelFactory.create_model(model_type, config)
|
| 104 |
+
logger.info(f"✓ Model created: {model_type}")
|
| 105 |
+
except Exception as e:
|
| 106 |
+
logger.warning(f"✗ Failed to create model {model_type}: {e}")
|
| 107 |
+
|
| 108 |
+
return models
|
| 109 |
+
|
| 110 |
+
@staticmethod
|
| 111 |
+
def get_model_categories() -> Dict[str, List[str]]:
|
| 112 |
+
return {
|
| 113 |
+
'linear': ['linear_regression', 'ridge', 'lasso', 'elasticnet',
|
| 114 |
+
'bayesian_ridge', 'huber', 'kernel_ridge', 'pls',
|
| 115 |
+
'sgd', 'passive_aggressive', 'ransac', 'theil_sen',
|
| 116 |
+
'orthogonal_matching_pursuit', 'isotonic'],
|
| 117 |
+
'tree': ['random_forest', 'extra_trees', 'decision_tree'],
|
| 118 |
+
'boosting': ['gradient_boosting', 'hist_gradient_boosting',
|
| 119 |
+
'xgboost', 'lightgbm', 'catboost', 'ada_boost'],
|
| 120 |
+
'svm': ['svr', 'linear_svr', 'nu_svr'],
|
| 121 |
+
'neighbors': ['knn', 'radius_neighbors'],
|
| 122 |
+
'neural': ['mlp', 'gaussian_process'],
|
| 123 |
+
'ensemble': ['bagging']
|
| 124 |
+
}
|
src/ml_pipeline/core/resource_monitor.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
import psutil
|
| 3 |
+
import numpy as np
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import matplotlib.pyplot as plt
|
| 6 |
+
from collections import defaultdict
|
| 7 |
+
import logging
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class ResourceMonitor:
|
| 13 |
+
"""Monitors system resource usage (CPU, memory, execution time)."""
|
| 14 |
+
|
| 15 |
+
def __init__(self):
|
| 16 |
+
self.start_time = None
|
| 17 |
+
self.start_memory = None
|
| 18 |
+
self.process = psutil.Process()
|
| 19 |
+
self.metrics = defaultdict(list)
|
| 20 |
+
|
| 21 |
+
def start(self):
|
| 22 |
+
"""Start monitoring resources."""
|
| 23 |
+
self.start_time = time.time()
|
| 24 |
+
self.start_memory = self.process.memory_info().rss / 1024 ** 2
|
| 25 |
+
self.metrics.clear()
|
| 26 |
+
return self
|
| 27 |
+
|
| 28 |
+
def snapshot(self, label: str = None):
|
| 29 |
+
"""Take a snapshot of current resource usage."""
|
| 30 |
+
current_time = time.time()
|
| 31 |
+
current_memory = self.process.memory_info().rss / 1024 ** 2
|
| 32 |
+
|
| 33 |
+
snapshot = {
|
| 34 |
+
'time': current_time - self.start_time,
|
| 35 |
+
'memory_mb': current_memory,
|
| 36 |
+
'memory_delta_mb': current_memory - self.start_memory,
|
| 37 |
+
'cpu_percent': self.process.cpu_percent(),
|
| 38 |
+
'threads': self.process.num_threads()
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
if label:
|
| 42 |
+
self.metrics[label].append(snapshot)
|
| 43 |
+
|
| 44 |
+
return snapshot
|
| 45 |
+
|
| 46 |
+
def get_summary(self):
|
| 47 |
+
"""Generate a summary report of collected metrics."""
|
| 48 |
+
if not self.metrics:
|
| 49 |
+
return {}
|
| 50 |
+
|
| 51 |
+
summary = {}
|
| 52 |
+
for label, snapshots in self.metrics.items():
|
| 53 |
+
if snapshots:
|
| 54 |
+
times = [s['time'] for s in snapshots]
|
| 55 |
+
memories = [s['memory_mb'] for s in snapshots]
|
| 56 |
+
|
| 57 |
+
summary[label] = {
|
| 58 |
+
'duration_sec': max(times) - min(times),
|
| 59 |
+
'avg_memory_mb': np.mean(memories),
|
| 60 |
+
'max_memory_mb': np.max(memories),
|
| 61 |
+
'min_memory_mb': np.min(memories),
|
| 62 |
+
'memory_peak_mb': max(memories),
|
| 63 |
+
'snapshots': len(snapshots)
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
return summary
|
| 67 |
+
|
| 68 |
+
def plot_usage(self, save_path: str = None):
|
| 69 |
+
"""Plot resource usage metrics."""
|
| 70 |
+
if not self.metrics:
|
| 71 |
+
logger.warning("No data available for plotting.")
|
| 72 |
+
return
|
| 73 |
+
|
| 74 |
+
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
|
| 75 |
+
|
| 76 |
+
all_labels = []
|
| 77 |
+
all_times = []
|
| 78 |
+
all_memories = []
|
| 79 |
+
all_cpus = []
|
| 80 |
+
|
| 81 |
+
for label, snapshots in self.metrics.items():
|
| 82 |
+
for snapshot in snapshots:
|
| 83 |
+
all_labels.append(label)
|
| 84 |
+
all_times.append(snapshot['time'])
|
| 85 |
+
all_memories.append(snapshot['memory_mb'])
|
| 86 |
+
all_cpus.append(snapshot['cpu_percent'])
|
| 87 |
+
|
| 88 |
+
if not all_times:
|
| 89 |
+
return
|
| 90 |
+
|
| 91 |
+
df = pd.DataFrame({
|
| 92 |
+
'label': all_labels,
|
| 93 |
+
'time': all_times,
|
| 94 |
+
'memory': all_memories,
|
| 95 |
+
'cpu': all_cpus
|
| 96 |
+
})
|
| 97 |
+
|
| 98 |
+
# Plot memory usage over time
|
| 99 |
+
for label in df['label'].unique():
|
| 100 |
+
label_data = df[df['label'] == label]
|
| 101 |
+
axes[0, 0].plot(label_data['time'], label_data['memory'],
|
| 102 |
+
'o-', label=label, alpha=0.7)
|
| 103 |
+
|
| 104 |
+
axes[0, 0].set_xlabel('Time (sec)')
|
| 105 |
+
axes[0, 0].set_ylabel('Memory (MB)')
|
| 106 |
+
axes[0, 0].set_title('Memory Usage Over Time')
|
| 107 |
+
axes[0, 0].legend(loc='best')
|
| 108 |
+
axes[0, 0].grid(True, alpha=0.3)
|
| 109 |
+
|
| 110 |
+
# Plot CPU usage over time
|
| 111 |
+
for label in df['label'].unique():
|
| 112 |
+
label_data = df[df['label'] == label]
|
| 113 |
+
axes[0, 1].plot(label_data['time'], label_data['cpu'],
|
| 114 |
+
's-', label=label, alpha=0.7)
|
| 115 |
+
|
| 116 |
+
axes[0, 1].set_xlabel('Time (sec)')
|
| 117 |
+
axes[0, 1].set_ylabel('CPU (%)')
|
| 118 |
+
axes[0, 1].set_title('CPU Usage Over Time')
|
| 119 |
+
axes[0, 1].legend(loc='best')
|
| 120 |
+
axes[0, 1].grid(True, alpha=0.3)
|
| 121 |
+
|
| 122 |
+
# Plot peak memory per operation
|
| 123 |
+
peak_memory = df.groupby('label')['memory'].max().sort_values()
|
| 124 |
+
axes[1, 0].bar(range(len(peak_memory)), peak_memory.values,
|
| 125 |
+
color='skyblue', edgecolor='black')
|
| 126 |
+
axes[1, 0].set_xticks(range(len(peak_memory)))
|
| 127 |
+
axes[1, 0].set_xticklabels(peak_memory.index, rotation=45, ha='right')
|
| 128 |
+
axes[1, 0].set_xlabel('Operation')
|
| 129 |
+
axes[1, 0].set_ylabel('Peak Memory (MB)')
|
| 130 |
+
axes[1, 0].set_title('Peak Memory Usage by Operation')
|
| 131 |
+
axes[1, 0].grid(True, alpha=0.3, axis='y')
|
| 132 |
+
|
| 133 |
+
# Plot memory distribution boxplot
|
| 134 |
+
memory_data = [df[df['label'] == label]['memory'].values
|
| 135 |
+
for label in df['label'].unique()]
|
| 136 |
+
axes[1, 1].boxplot(memory_data, labels=df['label'].unique())
|
| 137 |
+
axes[1, 1].set_xlabel('Operation')
|
| 138 |
+
axes[1, 1].set_ylabel('Memory (MB)')
|
| 139 |
+
axes[1, 1].set_title('Memory Usage Distribution')
|
| 140 |
+
axes[1, 1].grid(True, alpha=0.3, axis='y')
|
| 141 |
+
plt.setp(axes[1, 1].xaxis.get_majorticklabels(), rotation=45, ha='right')
|
| 142 |
+
|
| 143 |
+
plt.tight_layout()
|
| 144 |
+
|
| 145 |
+
if save_path:
|
| 146 |
+
plt.savefig(save_path, dpi=300, bbox_inches='tight')
|
| 147 |
+
logger.info(f"Resource usage plot saved: {save_path}")
|
| 148 |
+
|
| 149 |
+
plt.show()
|
| 150 |
+
plt.close(fig)
|
src/ml_pipeline/features/__init__.py
ADDED
|
File without changes
|
src/ml_pipeline/features/engineering.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import logging
|
| 4 |
+
from typing import List, Optional
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from ..config.ml_config import MLConfig
|
| 8 |
+
from sklearn.preprocessing import PolynomialFeatures
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class FeatureEngineering:
|
| 14 |
+
"""Class for advanced feature engineering with feature selection."""
|
| 15 |
+
|
| 16 |
+
def __init__(self, config: MLConfig):
|
| 17 |
+
self.config = config
|
| 18 |
+
self.feature_store = {}
|
| 19 |
+
self.selected_features = None
|
| 20 |
+
self.feature_selector = None
|
| 21 |
+
|
| 22 |
+
def create_temporal_features(self, df: pd.DataFrame):
|
| 23 |
+
"""Create temporal features from index."""
|
| 24 |
+
df_temp = df.copy()
|
| 25 |
+
|
| 26 |
+
try:
|
| 27 |
+
# Try to convert index to DatetimeIndex
|
| 28 |
+
index = pd.to_datetime(df_temp.index)
|
| 29 |
+
|
| 30 |
+
df_temp['year'] = index.year
|
| 31 |
+
df_temp['month'] = index.month
|
| 32 |
+
df_temp['day'] = index.day
|
| 33 |
+
df_temp['dayofweek'] = index.dayofweek
|
| 34 |
+
df_temp['dayofyear'] = index.dayofyear
|
| 35 |
+
df_temp['week'] = index.isocalendar().week.astype(int)
|
| 36 |
+
df_temp['quarter'] = index.quarter
|
| 37 |
+
df_temp['is_month_start'] = index.is_month_start.astype(int)
|
| 38 |
+
df_temp['is_month_end'] = index.is_month_end.astype(int)
|
| 39 |
+
df_temp['is_quarter_start'] = index.is_quarter_start.astype(int)
|
| 40 |
+
df_temp['is_quarter_end'] = index.is_quarter_end.astype(int)
|
| 41 |
+
df_temp['is_year_start'] = index.is_year_start.astype(int)
|
| 42 |
+
df_temp['is_year_end'] = index.is_year_end.astype(int)
|
| 43 |
+
df_temp['is_weekend'] = (index.dayofweek >= 5).astype(int)
|
| 44 |
+
|
| 45 |
+
logger.info("Created 15 temporal features from index")
|
| 46 |
+
return df_temp
|
| 47 |
+
|
| 48 |
+
except Exception as e:
|
| 49 |
+
logger.warning(f"Failed to create temporal features: {e}")
|
| 50 |
+
return df
|
| 51 |
+
|
| 52 |
+
def create_lag_features(self, df: pd.DataFrame, target_column: str,
|
| 53 |
+
lags: List[int] = None, group_by: str = None):
|
| 54 |
+
"""Create lag features."""
|
| 55 |
+
if lags is None:
|
| 56 |
+
lags = [1, 2, 3, 7, 14, 30]
|
| 57 |
+
|
| 58 |
+
df_lag = df.copy()
|
| 59 |
+
|
| 60 |
+
try:
|
| 61 |
+
if group_by and group_by in df_lag.columns:
|
| 62 |
+
for lag in lags:
|
| 63 |
+
df_lag[f'{target_column}_lag_{lag}'] = df_lag.groupby(group_by)[target_column].shift(lag)
|
| 64 |
+
else:
|
| 65 |
+
for lag in lags:
|
| 66 |
+
df_lag[f'{target_column}_lag_{lag}'] = df_lag[target_column].shift(lag)
|
| 67 |
+
|
| 68 |
+
logger.info(f"Created {len(lags)} lag features for {target_column}")
|
| 69 |
+
|
| 70 |
+
except Exception as e:
|
| 71 |
+
logger.warning(f"Error creating lag features: {e}")
|
| 72 |
+
|
| 73 |
+
return df_lag
|
| 74 |
+
|
| 75 |
+
def create_rolling_features(self, df: pd.DataFrame, target_column: str,
|
| 76 |
+
windows: List[int] = None, group_by: str = None):
|
| 77 |
+
"""Create rolling statistics."""
|
| 78 |
+
if windows is None:
|
| 79 |
+
windows = [3, 7, 14, 30]
|
| 80 |
+
|
| 81 |
+
df_rolling = df.copy()
|
| 82 |
+
|
| 83 |
+
try:
|
| 84 |
+
if group_by and group_by in df_rolling.columns:
|
| 85 |
+
for window in windows:
|
| 86 |
+
df_rolling[f'{target_column}_rolling_mean_{window}'] = (
|
| 87 |
+
df_rolling.groupby(group_by)[target_column]
|
| 88 |
+
.rolling(window=window, min_periods=1)
|
| 89 |
+
.mean()
|
| 90 |
+
.reset_index(level=0, drop=True)
|
| 91 |
+
)
|
| 92 |
+
df_rolling[f'{target_column}_rolling_std_{window}'] = (
|
| 93 |
+
df_rolling.groupby(group_by)[target_column]
|
| 94 |
+
.rolling(window=window, min_periods=1)
|
| 95 |
+
.std()
|
| 96 |
+
.reset_index(level=0, drop=True)
|
| 97 |
+
)
|
| 98 |
+
df_rolling[f'{target_column}_rolling_min_{window}'] = (
|
| 99 |
+
df_rolling.groupby(group_by)[target_column]
|
| 100 |
+
.rolling(window=window, min_periods=1)
|
| 101 |
+
.min()
|
| 102 |
+
.reset_index(level=0, drop=True)
|
| 103 |
+
)
|
| 104 |
+
df_rolling[f'{target_column}_rolling_max_{window}'] = (
|
| 105 |
+
df_rolling.groupby(group_by)[target_column]
|
| 106 |
+
.rolling(window=window, min_periods=1)
|
| 107 |
+
.max()
|
| 108 |
+
.reset_index(level=0, drop=True)
|
| 109 |
+
)
|
| 110 |
+
else:
|
| 111 |
+
for window in windows:
|
| 112 |
+
df_rolling[f'{target_column}_rolling_mean_{window}'] = (
|
| 113 |
+
df_rolling[target_column].rolling(window=window, min_periods=1).mean()
|
| 114 |
+
)
|
| 115 |
+
df_rolling[f'{target_column}_rolling_std_{window}'] = (
|
| 116 |
+
df_rolling[target_column].rolling(window=window, min_periods=1).std()
|
| 117 |
+
)
|
| 118 |
+
df_rolling[f'{target_column}_rolling_min_{window}'] = (
|
| 119 |
+
df_rolling[target_column].rolling(window=window, min_periods=1).min()
|
| 120 |
+
)
|
| 121 |
+
df_rolling[f'{target_column}_rolling_max_{window}'] = (
|
| 122 |
+
df_rolling[target_column].rolling(window=window, min_periods=1).max()
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
logger.info(f"Created {len(windows) * 4} rolling features for {target_column}")
|
| 126 |
+
|
| 127 |
+
except Exception as e:
|
| 128 |
+
logger.warning(f"Error creating rolling features: {e}")
|
| 129 |
+
|
| 130 |
+
return df_rolling
|
| 131 |
+
|
| 132 |
+
def create_interaction_features(self, df: pd.DataFrame,
|
| 133 |
+
numeric_columns: List[str] = None):
|
| 134 |
+
"""Create interaction features with limits."""
|
| 135 |
+
if numeric_columns is None:
|
| 136 |
+
numeric_columns = df.select_dtypes(include=[np.number]).columns.tolist()
|
| 137 |
+
|
| 138 |
+
if len(numeric_columns) < 2:
|
| 139 |
+
return df
|
| 140 |
+
|
| 141 |
+
df_interaction = df.copy()
|
| 142 |
+
interactions_created = 0
|
| 143 |
+
max_interactions = min(100, len(numeric_columns) * (len(numeric_columns) - 1) // 2)
|
| 144 |
+
|
| 145 |
+
try:
|
| 146 |
+
# Limit number of created interaction features
|
| 147 |
+
for i in range(min(len(numeric_columns), 20)):
|
| 148 |
+
for j in range(i + 1, min(len(numeric_columns), 20)):
|
| 149 |
+
if interactions_created >= max_interactions:
|
| 150 |
+
break
|
| 151 |
+
|
| 152 |
+
col1, col2 = numeric_columns[i], numeric_columns[j]
|
| 153 |
+
|
| 154 |
+
# Create only if features are not too highly correlated
|
| 155 |
+
corr = abs(df_interaction[col1].corr(df_interaction[col2]))
|
| 156 |
+
if corr < 0.95: # Don't create for highly correlated features
|
| 157 |
+
df_interaction[f'{col1}_x_{col2}'] = df_interaction[col1] * df_interaction[col2]
|
| 158 |
+
interactions_created += 1
|
| 159 |
+
|
| 160 |
+
logger.info(f"Created {interactions_created} interaction features")
|
| 161 |
+
|
| 162 |
+
except Exception as e:
|
| 163 |
+
logger.warning(f"Error creating interaction features: {e}")
|
| 164 |
+
|
| 165 |
+
return df_interaction
|
| 166 |
+
|
| 167 |
+
def create_polynomial_features(self, df: pd.DataFrame,
|
| 168 |
+
numeric_columns: List[str] = None,
|
| 169 |
+
degree: int = 2):
|
| 170 |
+
"""Create polynomial features."""
|
| 171 |
+
if not self.config.create_polynomial_features:
|
| 172 |
+
return df
|
| 173 |
+
|
| 174 |
+
if numeric_columns is None:
|
| 175 |
+
numeric_columns = df.select_dtypes(include=[np.number]).columns.tolist()
|
| 176 |
+
|
| 177 |
+
df_poly = df.copy()
|
| 178 |
+
|
| 179 |
+
try:
|
| 180 |
+
# Limit number of features for polynomial transformation
|
| 181 |
+
if len(numeric_columns) > 10:
|
| 182 |
+
numeric_columns = numeric_columns[:10]
|
| 183 |
+
logger.info(f"Limited number of features for polynomial transformation to 10")
|
| 184 |
+
|
| 185 |
+
poly = PolynomialFeatures(
|
| 186 |
+
degree=min(degree, 3), # Limit degree
|
| 187 |
+
include_bias=False,
|
| 188 |
+
interaction_only=False
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
poly_features = poly.fit_transform(df_poly[numeric_columns])
|
| 192 |
+
poly_feature_names = poly.get_feature_names_out(numeric_columns)
|
| 193 |
+
|
| 194 |
+
poly_df = pd.DataFrame(
|
| 195 |
+
poly_features,
|
| 196 |
+
columns=poly_feature_names,
|
| 197 |
+
index=df_poly.index
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
df_poly = pd.concat([df_poly, poly_df], axis=1)
|
| 201 |
+
|
| 202 |
+
logger.info(f"Created {len(poly_feature_names)} polynomial features of degree {degree}")
|
| 203 |
+
|
| 204 |
+
except Exception as e:
|
| 205 |
+
logger.warning(f"Error creating polynomial features: {e}")
|
| 206 |
+
|
| 207 |
+
return df_poly
|
| 208 |
+
|
| 209 |
+
def select_features(self, X: pd.DataFrame, y: pd.Series, method: str = None):
|
| 210 |
+
"""Select most important features."""
|
| 211 |
+
if not self.config.use_feature_selection:
|
| 212 |
+
self.selected_features = list(X.columns)
|
| 213 |
+
return X
|
| 214 |
+
|
| 215 |
+
if method is None:
|
| 216 |
+
method = self.config.feature_selection_method
|
| 217 |
+
|
| 218 |
+
try:
|
| 219 |
+
if method == 'importance':
|
| 220 |
+
# Use random forest for feature importance assessment
|
| 221 |
+
from sklearn.ensemble import RandomForestRegressor
|
| 222 |
+
rf = RandomForestRegressor(n_estimators=100, random_state=self.config.random_state, n_jobs=-1)
|
| 223 |
+
rf.fit(X, y)
|
| 224 |
+
importances = rf.feature_importances_
|
| 225 |
+
|
| 226 |
+
# Determine number of features to select
|
| 227 |
+
if isinstance(self.config.n_features_to_select, float):
|
| 228 |
+
n_features = int(X.shape[1] * self.config.n_features_to_select)
|
| 229 |
+
else:
|
| 230 |
+
n_features = min(self.config.n_features_to_select, X.shape[1])
|
| 231 |
+
|
| 232 |
+
# Select top-n features
|
| 233 |
+
indices = np.argsort(importances)[-n_features:]
|
| 234 |
+
self.selected_features = list(X.columns[indices])
|
| 235 |
+
self.feature_selector = lambda X_sel: X_sel.iloc[:, indices]
|
| 236 |
+
|
| 237 |
+
logger.info(f"Selected {len(self.selected_features)} features using importance method")
|
| 238 |
+
|
| 239 |
+
elif method == 'correlation':
|
| 240 |
+
# Select based on correlation with target
|
| 241 |
+
correlations = X.apply(lambda col: col.corr(y))
|
| 242 |
+
|
| 243 |
+
if isinstance(self.config.n_features_to_select, float):
|
| 244 |
+
n_features = int(X.shape[1] * self.config.n_features_to_select)
|
| 245 |
+
else:
|
| 246 |
+
n_features = min(self.config.n_features_to_select, X.shape[1])
|
| 247 |
+
|
| 248 |
+
# Select features with highest absolute correlation
|
| 249 |
+
indices = np.argsort(np.abs(correlations))[-n_features:]
|
| 250 |
+
self.selected_features = list(X.columns[indices])
|
| 251 |
+
self.feature_selector = lambda X_sel: X_sel.iloc[:, indices]
|
| 252 |
+
|
| 253 |
+
logger.info(f"Selected {len(self.selected_features)} features using correlation method")
|
| 254 |
+
|
| 255 |
+
elif method == 'variance':
|
| 256 |
+
# Select based on variance
|
| 257 |
+
from sklearn.feature_selection import VarianceThreshold
|
| 258 |
+
selector = VarianceThreshold(threshold=0.01)
|
| 259 |
+
selector.fit(X)
|
| 260 |
+
self.selected_features = list(X.columns[selector.get_support()])
|
| 261 |
+
self.feature_selector = selector
|
| 262 |
+
|
| 263 |
+
logger.info(f"Selected {len(self.selected_features)} features using variance method")
|
| 264 |
+
|
| 265 |
+
# Apply selection
|
| 266 |
+
if self.selected_features:
|
| 267 |
+
X_selected = X[self.selected_features]
|
| 268 |
+
return X_selected
|
| 269 |
+
else:
|
| 270 |
+
return X
|
| 271 |
+
|
| 272 |
+
except Exception as e:
|
| 273 |
+
logger.warning(f"Error selecting features: {e}")
|
| 274 |
+
return X
|
| 275 |
+
|
| 276 |
+
def apply_feature_engineering(self, df: pd.DataFrame, target_column: str):
|
| 277 |
+
"""Apply all feature engineering methods with constraints."""
|
| 278 |
+
if not self.config.feature_engineering:
|
| 279 |
+
return df
|
| 280 |
+
|
| 281 |
+
logger.info("Applying feature engineering...")
|
| 282 |
+
|
| 283 |
+
df_engineered = df.copy()
|
| 284 |
+
|
| 285 |
+
try:
|
| 286 |
+
# Save original number of features
|
| 287 |
+
original_features = df_engineered.shape[1]
|
| 288 |
+
|
| 289 |
+
# Create temporal features from index
|
| 290 |
+
df_engineered = self.create_temporal_features(df_engineered)
|
| 291 |
+
|
| 292 |
+
# Limit polynomial feature creation
|
| 293 |
+
if self.config.create_polynomial_features:
|
| 294 |
+
df_engineered = self.create_polynomial_features(
|
| 295 |
+
df_engineered,
|
| 296 |
+
degree=self.config.polynomial_degree
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
# Limit interaction feature creation
|
| 300 |
+
if self.config.create_interaction_features:
|
| 301 |
+
df_engineered = self.create_interaction_features(df_engineered)
|
| 302 |
+
|
| 303 |
+
# Remove duplicate columns
|
| 304 |
+
df_engineered = df_engineered.loc[:, ~df_engineered.columns.duplicated()]
|
| 305 |
+
|
| 306 |
+
# Remove features with constant values
|
| 307 |
+
constant_cols = [col for col in df_engineered.columns if df_engineered[col].nunique() <= 1]
|
| 308 |
+
if constant_cols:
|
| 309 |
+
df_engineered = df_engineered.drop(columns=constant_cols)
|
| 310 |
+
logger.info(f"Removed {len(constant_cols)} features with constant values")
|
| 311 |
+
|
| 312 |
+
logger.info(f"After feature engineering: {df_engineered.shape[1]} features (was {original_features})")
|
| 313 |
+
|
| 314 |
+
return df_engineered
|
| 315 |
+
|
| 316 |
+
except Exception as e:
|
| 317 |
+
logger.error(f"Feature engineering error: {e}")
|
| 318 |
+
return df
|
| 319 |
+
|
| 320 |
+
def save_features(self, features: pd.DataFrame, name: str):
|
| 321 |
+
"""Save features to feature store."""
|
| 322 |
+
path = f"{self.config.feature_store_dir}/{name}.parquet"
|
| 323 |
+
features.to_parquet(path)
|
| 324 |
+
self.feature_store[name] = path
|
| 325 |
+
logger.info(f"Features saved to feature store: {path}")
|
| 326 |
+
|
| 327 |
+
def load_features(self, name: str):
|
| 328 |
+
"""Load features from feature store."""
|
| 329 |
+
if name in self.feature_store:
|
| 330 |
+
return pd.read_parquet(self.feature_store[name])
|
| 331 |
+
return None
|
src/ml_pipeline/features/selection.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import logging
|
| 4 |
+
from typing import List, Optional
|
| 5 |
+
from sklearn.feature_selection import VarianceThreshold
|
| 6 |
+
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class FeatureSelector:
|
| 11 |
+
"""Feature selection using various methods."""
|
| 12 |
+
|
| 13 |
+
def __init__(self, config):
|
| 14 |
+
self.config = config
|
| 15 |
+
self.selected_features = None
|
| 16 |
+
self.feature_selector = None
|
| 17 |
+
|
| 18 |
+
def select_features(self, X: pd.DataFrame, y: pd.Series, method: str = None) -> pd.DataFrame:
|
| 19 |
+
"""Select the most relevant features using the specified method."""
|
| 20 |
+
if not self.config.use_feature_selection:
|
| 21 |
+
self.selected_features = list(X.columns)
|
| 22 |
+
return X
|
| 23 |
+
|
| 24 |
+
if method is None:
|
| 25 |
+
method = self.config.feature_selection_method
|
| 26 |
+
|
| 27 |
+
try:
|
| 28 |
+
if method == 'importance':
|
| 29 |
+
# Use random forest for feature importance estimation
|
| 30 |
+
from sklearn.ensemble import RandomForestRegressor
|
| 31 |
+
rf = RandomForestRegressor(
|
| 32 |
+
n_estimators=100,
|
| 33 |
+
random_state=self.config.random_state,
|
| 34 |
+
n_jobs=-1
|
| 35 |
+
)
|
| 36 |
+
rf.fit(X, y)
|
| 37 |
+
importances = rf.feature_importances_
|
| 38 |
+
|
| 39 |
+
# Determine number of features to select
|
| 40 |
+
if isinstance(self.config.n_features_to_select, float):
|
| 41 |
+
n_features = int(X.shape[1] * self.config.n_features_to_select)
|
| 42 |
+
else:
|
| 43 |
+
n_features = min(self.config.n_features_to_select, X.shape[1])
|
| 44 |
+
|
| 45 |
+
# Select top-n features
|
| 46 |
+
indices = np.argsort(importances)[-n_features:]
|
| 47 |
+
self.selected_features = list(X.columns[indices])
|
| 48 |
+
self.feature_selector = lambda X_sel: X_sel.iloc[:, indices]
|
| 49 |
+
|
| 50 |
+
logger.info(f"Selected {len(self.selected_features)} features using importance method")
|
| 51 |
+
|
| 52 |
+
elif method == 'correlation':
|
| 53 |
+
# Selection based on correlation with target variable
|
| 54 |
+
correlations = X.apply(lambda col: col.corr(y))
|
| 55 |
+
|
| 56 |
+
if isinstance(self.config.n_features_to_select, float):
|
| 57 |
+
n_features = int(X.shape[1] * self.config.n_features_to_select)
|
| 58 |
+
else:
|
| 59 |
+
n_features = min(self.config.n_features_to_select, X.shape[1])
|
| 60 |
+
|
| 61 |
+
# Select features with highest absolute correlation
|
| 62 |
+
indices = np.argsort(np.abs(correlations))[-n_features:]
|
| 63 |
+
self.selected_features = list(X.columns[indices])
|
| 64 |
+
self.feature_selector = lambda X_sel: X_sel.iloc[:, indices]
|
| 65 |
+
|
| 66 |
+
logger.info(f"Selected {len(self.selected_features)} features using correlation method")
|
| 67 |
+
|
| 68 |
+
elif method == 'variance':
|
| 69 |
+
# Selection based on feature variance
|
| 70 |
+
selector = VarianceThreshold(threshold=0.01)
|
| 71 |
+
selector.fit(X)
|
| 72 |
+
self.selected_features = list(X.columns[selector.get_support()])
|
| 73 |
+
self.feature_selector = selector
|
| 74 |
+
|
| 75 |
+
logger.info(f"Selected {len(self.selected_features)} features using variance method")
|
| 76 |
+
|
| 77 |
+
# Apply selection
|
| 78 |
+
if self.selected_features:
|
| 79 |
+
X_selected = X[self.selected_features]
|
| 80 |
+
return X_selected
|
| 81 |
+
else:
|
| 82 |
+
return X
|
| 83 |
+
|
| 84 |
+
except Exception as e:
|
| 85 |
+
logger.warning(f"Feature selection error: {e}")
|
| 86 |
+
return X
|
src/ml_pipeline/models/__init__.py
ADDED
|
File without changes
|
src/ml_pipeline/models/boosting_models.py
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import re
|
| 4 |
+
import logging
|
| 5 |
+
import inspect
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
import traceback
|
| 8 |
+
|
| 9 |
+
from src.ml_pipeline.config.ml_config import MLConfig
|
| 10 |
+
from src.ml_pipeline.utils.helpers import _filter_fit_kwargs, _filter_kwargs_for_callable
|
| 11 |
+
|
| 12 |
+
from ..core.base_model import BaseMLModel
|
| 13 |
+
|
| 14 |
+
# Import boosting libraries
|
| 15 |
+
try:
|
| 16 |
+
import xgboost as xgb
|
| 17 |
+
XGBOOST_AVAILABLE = True
|
| 18 |
+
except ImportError:
|
| 19 |
+
XGBOOST_AVAILABLE = False
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
import lightgbm as lgb
|
| 23 |
+
LIGHTGBM_AVAILABLE = True
|
| 24 |
+
except ImportError:
|
| 25 |
+
LIGHTGBM_AVAILABLE = False
|
| 26 |
+
|
| 27 |
+
try:
|
| 28 |
+
from catboost import CatBoostRegressor
|
| 29 |
+
CATBOOST_AVAILABLE = True
|
| 30 |
+
except ImportError:
|
| 31 |
+
CATBOOST_AVAILABLE = False
|
| 32 |
+
|
| 33 |
+
from sklearn.ensemble import GradientBoostingRegressor, HistGradientBoostingRegressor
|
| 34 |
+
|
| 35 |
+
logger = logging.getLogger(__name__)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# Gradient Boosting
|
| 39 |
+
class GradientBoostingModel(BaseMLModel):
|
| 40 |
+
def __init__(self, config: MLConfig):
|
| 41 |
+
super().__init__(config, 'gradient_boosting')
|
| 42 |
+
|
| 43 |
+
def create_model(self, **kwargs):
|
| 44 |
+
params = {
|
| 45 |
+
'loss': 'squared_error',
|
| 46 |
+
'learning_rate': 0.1,
|
| 47 |
+
'n_estimators': 100,
|
| 48 |
+
'subsample': 1.0,
|
| 49 |
+
'criterion': 'friedman_mse',
|
| 50 |
+
'min_samples_split': 2,
|
| 51 |
+
'min_samples_leaf': 1,
|
| 52 |
+
'max_depth': 3,
|
| 53 |
+
'min_impurity_decrease': 0.0,
|
| 54 |
+
'init': None,
|
| 55 |
+
'random_state': self.config.random_state,
|
| 56 |
+
'max_features': None,
|
| 57 |
+
'alpha': 0.9,
|
| 58 |
+
'verbose': 0,
|
| 59 |
+
'max_leaf_nodes': None,
|
| 60 |
+
'warm_start': False,
|
| 61 |
+
'validation_fraction': 0.1,
|
| 62 |
+
'n_iter_no_change': None,
|
| 63 |
+
'tol': 0.0001,
|
| 64 |
+
'ccp_alpha': 0.0
|
| 65 |
+
}
|
| 66 |
+
params.update(kwargs)
|
| 67 |
+
self.model = GradientBoostingRegressor(**params)
|
| 68 |
+
return self.model
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
class HistGradientBoostingModel(BaseMLModel):
|
| 72 |
+
def __init__(self, config: MLConfig):
|
| 73 |
+
super().__init__(config, 'hist_gradient_boosting')
|
| 74 |
+
|
| 75 |
+
def create_model(self, **kwargs):
|
| 76 |
+
params = {
|
| 77 |
+
'loss': 'squared_error',
|
| 78 |
+
'learning_rate': 0.1,
|
| 79 |
+
'max_iter': 100,
|
| 80 |
+
'max_leaf_nodes': 31,
|
| 81 |
+
'max_depth': None,
|
| 82 |
+
'min_samples_leaf': 20,
|
| 83 |
+
'l2_regularization': 0.0,
|
| 84 |
+
'max_bins': 255,
|
| 85 |
+
'categorical_features': None,
|
| 86 |
+
'monotonic_cst': None,
|
| 87 |
+
'warm_start': False,
|
| 88 |
+
'early_stopping': 'auto',
|
| 89 |
+
'scoring': 'loss',
|
| 90 |
+
'validation_fraction': 0.1,
|
| 91 |
+
'n_iter_no_change': 10,
|
| 92 |
+
'tol': 1e-07,
|
| 93 |
+
'verbose': 0,
|
| 94 |
+
'random_state': self.config.random_state
|
| 95 |
+
}
|
| 96 |
+
params.update(kwargs)
|
| 97 |
+
self.model = HistGradientBoostingRegressor(**params)
|
| 98 |
+
return self.model
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
# LightGBM Model
|
| 102 |
+
class LightGBMModel(BaseMLModel):
|
| 103 |
+
def __init__(self, config: MLConfig):
|
| 104 |
+
super().__init__(config, 'lightgbm')
|
| 105 |
+
self.feature_names_ = None # Store feature names for LGBM
|
| 106 |
+
self._original_to_clean_map = None
|
| 107 |
+
|
| 108 |
+
def clean_feature_names_for_lgbm(self, X):
|
| 109 |
+
"""
|
| 110 |
+
Clean feature names for LightGBM.
|
| 111 |
+
Replace prohibited characters with '_' and make names unique.
|
| 112 |
+
Logs changes and saves original->clean mapping.
|
| 113 |
+
"""
|
| 114 |
+
if X is None:
|
| 115 |
+
return None
|
| 116 |
+
|
| 117 |
+
if not hasattr(X, 'columns'):
|
| 118 |
+
return X # non-DataFrame - do nothing
|
| 119 |
+
|
| 120 |
+
X_clean = X.copy()
|
| 121 |
+
orig_names = list(X_clean.columns)
|
| 122 |
+
clean_names = []
|
| 123 |
+
orig_to_clean = {}
|
| 124 |
+
|
| 125 |
+
for i, name in enumerate(orig_names):
|
| 126 |
+
s = str(name)
|
| 127 |
+
# Replace special characters with underscore
|
| 128 |
+
for ch in ['[', ']', '<', '>', '=', ' ', ',', ';', ':', '(', ')', '{', '}', '/', '\\', '\t', '\n']:
|
| 129 |
+
s = s.replace(ch, '_')
|
| 130 |
+
# Keep only alphanumeric, underscores, and hyphens
|
| 131 |
+
while '__' in s:
|
| 132 |
+
s = s.replace('__', '_')
|
| 133 |
+
s = s.strip('_')
|
| 134 |
+
if not s:
|
| 135 |
+
s = f'feature_{i}'
|
| 136 |
+
clean_names.append(s)
|
| 137 |
+
orig_to_clean[orig_names[i]] = s
|
| 138 |
+
|
| 139 |
+
# Make names unique (add suffixes for collisions)
|
| 140 |
+
seen = {}
|
| 141 |
+
final_names = []
|
| 142 |
+
for name in clean_names:
|
| 143 |
+
base = name
|
| 144 |
+
cnt = seen.get(base, 0)
|
| 145 |
+
if cnt:
|
| 146 |
+
name = f"{base}_{cnt}"
|
| 147 |
+
seen[base] = cnt + 1
|
| 148 |
+
final_names.append(name)
|
| 149 |
+
|
| 150 |
+
# Log changes if any
|
| 151 |
+
if orig_names != final_names:
|
| 152 |
+
logger.debug(
|
| 153 |
+
f"Feature names adapted for LightGBM compatibility. "
|
| 154 |
+
f"Example changes: {list(orig_to_clean.items())[:10]}"
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
X_clean.columns = final_names
|
| 158 |
+
self.feature_names_ = final_names
|
| 159 |
+
self._original_to_clean_map = orig_to_clean
|
| 160 |
+
return X_clean
|
| 161 |
+
|
| 162 |
+
def create_model(self, **kwargs):
|
| 163 |
+
"""Create LightGBM model with only constructor-compatible kwargs."""
|
| 164 |
+
base_params = {
|
| 165 |
+
'boosting_type': 'gbdt',
|
| 166 |
+
'num_leaves': 31,
|
| 167 |
+
'max_depth': -1,
|
| 168 |
+
'learning_rate': 0.1,
|
| 169 |
+
'n_estimators': 100,
|
| 170 |
+
'objective': 'regression',
|
| 171 |
+
'random_state': self.config.random_state,
|
| 172 |
+
'importance_type': 'split',
|
| 173 |
+
'verbosity': -1, # use verbosity instead of verbose
|
| 174 |
+
'n_jobs': getattr(self.config, "n_jobs", None)
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
# GPU options (if needed)
|
| 178 |
+
if getattr(self.config, 'use_gpu', False):
|
| 179 |
+
base_params.update({
|
| 180 |
+
'device': 'gpu',
|
| 181 |
+
'gpu_platform_id': 0,
|
| 182 |
+
'gpu_device_id': 0
|
| 183 |
+
})
|
| 184 |
+
|
| 185 |
+
# Filter kwargs, keeping only those accepted by __init__
|
| 186 |
+
init_filtered, removed_init = _filter_kwargs_for_callable(lgb.LGBMRegressor.__init__, kwargs, logger=logger, obj_name="LGBMRegressor.__init__")
|
| 187 |
+
if removed_init:
|
| 188 |
+
logger.debug(f"Parameters not suitable for LGBMRegressor constructor and omitted: {removed_init}")
|
| 189 |
+
|
| 190 |
+
# Merge constructor parameters, priority to init_filtered
|
| 191 |
+
for k, v in init_filtered.items():
|
| 192 |
+
base_params[k] = v
|
| 193 |
+
|
| 194 |
+
# Ensure 'verbose' is not passed to constructor or fit
|
| 195 |
+
if 'verbose' in base_params:
|
| 196 |
+
del base_params['verbose']
|
| 197 |
+
|
| 198 |
+
# Create model
|
| 199 |
+
self.model = lgb.LGBMRegressor(**{k: v for k, v in base_params.items() if v is not None})
|
| 200 |
+
return self.model
|
| 201 |
+
|
| 202 |
+
def fit(self, X_train, y_train, X_val=None, y_val=None, **kwargs):
|
| 203 |
+
"""Train LightGBM model with proper constructor and fit parameter handling."""
|
| 204 |
+
if 'verbose' in kwargs:
|
| 205 |
+
logger.debug("Parameter 'verbose' removed from fit() for LightGBM")
|
| 206 |
+
kwargs.pop('verbose', None)
|
| 207 |
+
|
| 208 |
+
start_time = datetime.now()
|
| 209 |
+
try:
|
| 210 |
+
# Clean feature names
|
| 211 |
+
X_train_clean = self.clean_feature_names_for_lgbm(X_train)
|
| 212 |
+
|
| 213 |
+
# Prepare validation parameters
|
| 214 |
+
fit_specific = {}
|
| 215 |
+
if X_val is not None and y_val is not None:
|
| 216 |
+
X_val_clean = self.clean_feature_names_for_lgbm(X_val)
|
| 217 |
+
fit_specific['eval_set'] = [(X_val_clean, y_val)]
|
| 218 |
+
fit_specific['eval_names'] = ['valid']
|
| 219 |
+
|
| 220 |
+
eval_metric = kwargs.pop('eval_metric', 'l2')
|
| 221 |
+
if eval_metric == 'l2':
|
| 222 |
+
fit_specific['eval_metric'] = 'l2'
|
| 223 |
+
elif eval_metric == 'mae':
|
| 224 |
+
fit_specific['eval_metric'] = 'l1'
|
| 225 |
+
else:
|
| 226 |
+
fit_specific['eval_metric'] = eval_metric
|
| 227 |
+
|
| 228 |
+
# Early stopping
|
| 229 |
+
early_stopping_rounds = kwargs.pop('early_stopping_rounds', None)
|
| 230 |
+
if early_stopping_rounds and X_val is not None:
|
| 231 |
+
fit_specific['early_stopping_rounds'] = early_stopping_rounds
|
| 232 |
+
|
| 233 |
+
# Callbacks
|
| 234 |
+
callbacks = kwargs.pop('callbacks', None)
|
| 235 |
+
if callbacks:
|
| 236 |
+
fit_specific['callbacks'] = callbacks
|
| 237 |
+
|
| 238 |
+
# Separate kwargs into constructor parameters and remaining (for fit)
|
| 239 |
+
init_kwargs, removed_init = _filter_kwargs_for_callable(lgb.LGBMRegressor.__init__, kwargs, logger=logger, obj_name="LGBMRegressor.__init__")
|
| 240 |
+
|
| 241 |
+
# Create model (if not already created) with init_kwargs
|
| 242 |
+
if self.model is None:
|
| 243 |
+
self.create_model(**init_kwargs)
|
| 244 |
+
else:
|
| 245 |
+
# If model already exists, log that some init_kwargs were ignored
|
| 246 |
+
if init_kwargs:
|
| 247 |
+
logger.debug(f"Constructor parameters passed after model creation: {list(init_kwargs.keys())}")
|
| 248 |
+
|
| 249 |
+
# Remaining kwargs (since init_kwargs were taken from kwargs) - potentially fit parameters
|
| 250 |
+
remaining_kwargs = {k: v for k, v in kwargs.items() if k not in init_kwargs}
|
| 251 |
+
|
| 252 |
+
# Combine parameters for fit and filter against model.fit signature
|
| 253 |
+
all_fit_kwargs = {**remaining_kwargs, **fit_specific}
|
| 254 |
+
filtered_fit_kwargs, removed_fit = _filter_kwargs_for_callable(self.model.fit, all_fit_kwargs, logger=logger, obj_name=f"{self.model.__class__.__name__}.fit")
|
| 255 |
+
|
| 256 |
+
# Ensure 'verbose' is removed - LightGBM expects 'verbosity' in constructor
|
| 257 |
+
if 'verbose' in filtered_fit_kwargs:
|
| 258 |
+
filtered_fit_kwargs.pop('verbose', None)
|
| 259 |
+
# Also remove constructor parameters that might have been passed by mistake
|
| 260 |
+
for p in ['boosting_type', 'num_leaves', 'learning_rate', 'n_estimators', 'objective', 'verbosity', 'n_jobs']:
|
| 261 |
+
if p in filtered_fit_kwargs:
|
| 262 |
+
filtered_fit_kwargs.pop(p, None)
|
| 263 |
+
|
| 264 |
+
if removed_fit:
|
| 265 |
+
logger.debug(f"Parameters removed from fit call for {self.model.__class__.__name__}: {removed_fit}")
|
| 266 |
+
|
| 267 |
+
# Actual training call
|
| 268 |
+
self.model.fit(X_train_clean, y_train, **filtered_fit_kwargs)
|
| 269 |
+
|
| 270 |
+
# Save training information
|
| 271 |
+
self.training_time = (datetime.now() - start_time).total_seconds()
|
| 272 |
+
self.is_fitted = True
|
| 273 |
+
|
| 274 |
+
# Predictions on training data
|
| 275 |
+
self.predictions['train'] = self.predict(X_train)
|
| 276 |
+
self.residuals['train'] = y_train - self.predictions['train']
|
| 277 |
+
|
| 278 |
+
# Predictions on validation data
|
| 279 |
+
if X_val is not None and y_val is not None:
|
| 280 |
+
self.predictions['val'] = self.predict(X_val)
|
| 281 |
+
self.residuals['val'] = y_val - self.predictions['val']
|
| 282 |
+
|
| 283 |
+
logger.info(f"✓ LightGBM trained in {self.training_time:.2f} seconds")
|
| 284 |
+
|
| 285 |
+
# Save feature importance
|
| 286 |
+
if hasattr(self.model, 'feature_importances_'):
|
| 287 |
+
self.feature_importances_ = self.model.feature_importances_
|
| 288 |
+
|
| 289 |
+
return self
|
| 290 |
+
|
| 291 |
+
except Exception as e:
|
| 292 |
+
logger.error(f"✗ Error training LightGBM: {str(e)}")
|
| 293 |
+
if getattr(self.config, "debug", False):
|
| 294 |
+
logger.error(traceback.format_exc())
|
| 295 |
+
raise
|
| 296 |
+
|
| 297 |
+
def predict(self, X):
|
| 298 |
+
"""Prediction with feature name cleaning."""
|
| 299 |
+
if not self.is_fitted:
|
| 300 |
+
raise ValueError("Model is not trained")
|
| 301 |
+
|
| 302 |
+
start_time = datetime.now()
|
| 303 |
+
X_clean = self.clean_feature_names_for_lgbm(X)
|
| 304 |
+
|
| 305 |
+
# If pipeline exists and expects original/cleaned names - use it
|
| 306 |
+
if hasattr(self, 'pipeline') and self.pipeline is not None:
|
| 307 |
+
preds = self.pipeline.predict(X_clean)
|
| 308 |
+
else:
|
| 309 |
+
preds = self.model.predict(X_clean)
|
| 310 |
+
|
| 311 |
+
# Save inference time
|
| 312 |
+
inference_time_ms = (datetime.now() - start_time).total_seconds() * 1000
|
| 313 |
+
try:
|
| 314 |
+
n = len(X)
|
| 315 |
+
except Exception:
|
| 316 |
+
n = 1
|
| 317 |
+
self.inference_time = inference_time_ms / n if n > 0 else inference_time_ms
|
| 318 |
+
|
| 319 |
+
return preds
|
| 320 |
+
|
| 321 |
+
def get_feature_importance(self, feature_names=None):
|
| 322 |
+
"""Return feature importance."""
|
| 323 |
+
if not self.is_fitted:
|
| 324 |
+
raise ValueError("Model is not trained")
|
| 325 |
+
|
| 326 |
+
if hasattr(self.model, 'feature_importances_'):
|
| 327 |
+
importances = self.model.feature_importances_
|
| 328 |
+
|
| 329 |
+
if feature_names is None and self.feature_names_ is not None:
|
| 330 |
+
feature_names = self.feature_names_
|
| 331 |
+
|
| 332 |
+
if feature_names is not None and len(feature_names) == len(importances):
|
| 333 |
+
return dict(zip(feature_names, importances))
|
| 334 |
+
else:
|
| 335 |
+
return importances
|
| 336 |
+
|
| 337 |
+
return None
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
class XGBoostModel(BaseMLModel):
|
| 341 |
+
def __init__(self, config: MLConfig):
|
| 342 |
+
super().__init__(config, 'xgboost')
|
| 343 |
+
|
| 344 |
+
def create_model(self, **params):
|
| 345 |
+
params.setdefault('n_estimators', 100)
|
| 346 |
+
params.setdefault('max_depth', 3)
|
| 347 |
+
params.setdefault('learning_rate', 0.1)
|
| 348 |
+
params.setdefault('verbosity', 0)
|
| 349 |
+
params.setdefault('objective', 'reg:squarederror')
|
| 350 |
+
params.setdefault('booster', 'gbtree')
|
| 351 |
+
params.setdefault('tree_method', 'auto')
|
| 352 |
+
params.setdefault('n_jobs', self.config.n_jobs)
|
| 353 |
+
params.setdefault('gamma', 0)
|
| 354 |
+
params.setdefault('min_child_weight', 1)
|
| 355 |
+
params.setdefault('subsample', 1)
|
| 356 |
+
params.setdefault('colsample_bytree', 1)
|
| 357 |
+
params.setdefault('reg_alpha', 0)
|
| 358 |
+
params.setdefault('reg_lambda', 1)
|
| 359 |
+
params.setdefault('scale_pos_weight', 1)
|
| 360 |
+
params.setdefault('random_state', self.config.random_state)
|
| 361 |
+
params.setdefault('missing', np.nan)
|
| 362 |
+
|
| 363 |
+
if self.config.use_gpu:
|
| 364 |
+
params['tree_method'] = 'gpu_hist'
|
| 365 |
+
params['predictor'] = 'gpu_predictor'
|
| 366 |
+
|
| 367 |
+
self.model = xgb.XGBRegressor(**params)
|
| 368 |
+
return self.model
|
| 369 |
+
|
| 370 |
+
def fit(self, X_train, y_train, X_val=None, y_val=None, **kwargs):
|
| 371 |
+
start_time = datetime.now()
|
| 372 |
+
try:
|
| 373 |
+
# Extract typical fit-only parameters (won't be passed to create_model)
|
| 374 |
+
early_stopping = kwargs.pop('early_stopping_rounds', None)
|
| 375 |
+
fit_verbose = kwargs.pop('fit_verbose', False) # User setting for fit verbosity
|
| 376 |
+
|
| 377 |
+
# Clean features
|
| 378 |
+
X_train_clean = self.clean_feature_names(X_train)
|
| 379 |
+
X_val_clean = None
|
| 380 |
+
if X_val is not None:
|
| 381 |
+
X_val_clean = self.clean_feature_names(X_val)
|
| 382 |
+
|
| 383 |
+
# Create model (remaining kwargs are constructor parameters)
|
| 384 |
+
if self.model is None:
|
| 385 |
+
self.create_model(**kwargs)
|
| 386 |
+
|
| 387 |
+
# Collect arguments for fit
|
| 388 |
+
fit_args = {}
|
| 389 |
+
if X_val_clean is not None and y_val is not None:
|
| 390 |
+
fit_args['eval_set'] = [(X_val_clean, y_val)]
|
| 391 |
+
# Try to use standard 'verbose' if model supports it
|
| 392 |
+
fit_args['verbose'] = fit_verbose
|
| 393 |
+
|
| 394 |
+
# Handle early stopping - prefer direct parameter, otherwise use callback
|
| 395 |
+
if early_stopping and 'eval_set' in fit_args:
|
| 396 |
+
# If model.fit supports 'early_stopping_rounds' - pass directly
|
| 397 |
+
try_fit_args = fit_args.copy()
|
| 398 |
+
try_fit_args['early_stopping_rounds'] = early_stopping
|
| 399 |
+
filtered_try = _filter_fit_kwargs(self.model, try_fit_args)
|
| 400 |
+
if 'early_stopping_rounds' in filtered_try:
|
| 401 |
+
fit_args = filtered_try
|
| 402 |
+
else:
|
| 403 |
+
# Fallback: use callback
|
| 404 |
+
callbacks = try_fit_args.get('callbacks', [])
|
| 405 |
+
callbacks = list(callbacks) # Copy
|
| 406 |
+
try:
|
| 407 |
+
callbacks.append(xgb.callback.EarlyStopping(rounds=early_stopping, save_best=True))
|
| 408 |
+
fit_args['callbacks'] = callbacks
|
| 409 |
+
except Exception:
|
| 410 |
+
# If callback unavailable - log only (train without early stopping)
|
| 411 |
+
logger.warning("Failed to add xgb.callback.EarlyStopping - early stopping not applied")
|
| 412 |
+
else:
|
| 413 |
+
# If early stopping not specified but in config and eval_set exists
|
| 414 |
+
cfg_es = getattr(self.config, 'early_stopping_rounds', None)
|
| 415 |
+
if cfg_es and 'eval_set' in fit_args:
|
| 416 |
+
try_fit_args = fit_args.copy()
|
| 417 |
+
try_fit_args['early_stopping_rounds'] = cfg_es
|
| 418 |
+
filtered_try = _filter_fit_kwargs(self.model, try_fit_args)
|
| 419 |
+
if 'early_stopping_rounds' in filtered_try:
|
| 420 |
+
fit_args = filtered_try
|
| 421 |
+
else:
|
| 422 |
+
callbacks = try_fit_args.get('callbacks', [])
|
| 423 |
+
callbacks = list(callbacks)
|
| 424 |
+
try:
|
| 425 |
+
callbacks.append(xgb.callback.EarlyStopping(rounds=cfg_es, save_best=True))
|
| 426 |
+
fit_args['callbacks'] = callbacks
|
| 427 |
+
except Exception:
|
| 428 |
+
logger.debug("Failed to set early stopping callback for xgboost")
|
| 429 |
+
|
| 430 |
+
# Final filtering of fit_args against model.fit signature
|
| 431 |
+
fit_args = _filter_fit_kwargs(self.model, fit_args)
|
| 432 |
+
|
| 433 |
+
# Fit call
|
| 434 |
+
self.model.fit(X_train_clean, y_train, **fit_args)
|
| 435 |
+
|
| 436 |
+
self.training_time = (datetime.now() - start_time).total_seconds()
|
| 437 |
+
self.is_fitted = True
|
| 438 |
+
|
| 439 |
+
# Predictions on cleaned data
|
| 440 |
+
self.predictions['train'] = self.model.predict(X_train_clean)
|
| 441 |
+
self.residuals['train'] = y_train - self.predictions['train']
|
| 442 |
+
|
| 443 |
+
if X_val_clean is not None and y_val is not None:
|
| 444 |
+
self.predictions['val'] = self.model.predict(X_val_clean)
|
| 445 |
+
self.residuals['val'] = y_val - self.predictions['val']
|
| 446 |
+
|
| 447 |
+
logger.info(f"✓ XGBoost trained in {self.training_time:.2f} seconds")
|
| 448 |
+
|
| 449 |
+
if hasattr(self.model, 'best_iteration'):
|
| 450 |
+
self.best_params['best_iteration'] = self.model.best_iteration
|
| 451 |
+
if hasattr(self.model, 'best_score'):
|
| 452 |
+
self.best_params['best_score'] = self.model.best_score
|
| 453 |
+
|
| 454 |
+
return self
|
| 455 |
+
|
| 456 |
+
except Exception as e:
|
| 457 |
+
logger.error(f"✗ Error training XGBoost: {e}")
|
| 458 |
+
if self.config.debug:
|
| 459 |
+
logger.error(traceback.format_exc())
|
| 460 |
+
raise
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
class CatBoostModel(BaseMLModel):
|
| 464 |
+
def __init__(self, config: MLConfig):
|
| 465 |
+
super().__init__(config, 'catboost')
|
| 466 |
+
|
| 467 |
+
def create_model(self, **kwargs):
|
| 468 |
+
params = {
|
| 469 |
+
'iterations': 100,
|
| 470 |
+
'learning_rate': 0.1,
|
| 471 |
+
'depth': 6,
|
| 472 |
+
'l2_leaf_reg': 3,
|
| 473 |
+
'loss_function': 'RMSE',
|
| 474 |
+
'verbose': False,
|
| 475 |
+
'random_seed': self.config.random_state,
|
| 476 |
+
'bagging_temperature': 1,
|
| 477 |
+
'border_count': 254,
|
| 478 |
+
'thread_count': self.config.n_jobs if self.config.n_jobs != -1 else -1,
|
| 479 |
+
'task_type': 'GPU' if self.config.use_gpu else 'CPU',
|
| 480 |
+
'early_stopping_rounds': self.config.early_stopping_rounds
|
| 481 |
+
}
|
| 482 |
+
params.update(kwargs)
|
| 483 |
+
self.model = CatBoostRegressor(**params)
|
| 484 |
+
return self.model
|
| 485 |
+
|
| 486 |
+
def fit(self, X_train, y_train, X_val=None, y_val=None, **kwargs):
|
| 487 |
+
start_time = datetime.now()
|
| 488 |
+
|
| 489 |
+
try:
|
| 490 |
+
X_train_clean = self.clean_feature_names(X_train)
|
| 491 |
+
|
| 492 |
+
if X_val is not None and y_val is not None:
|
| 493 |
+
X_val_clean = self.clean_feature_names(X_val)
|
| 494 |
+
kwargs['eval_set'] = (X_val_clean, y_val)
|
| 495 |
+
|
| 496 |
+
self.model.fit(X_train_clean, y_train, **kwargs)
|
| 497 |
+
|
| 498 |
+
self.training_time = (datetime.now() - start_time).total_seconds()
|
| 499 |
+
self.is_fitted = True
|
| 500 |
+
|
| 501 |
+
self.predictions['train'] = self.predict(X_train)
|
| 502 |
+
self.residuals['train'] = y_train - self.predictions['train']
|
| 503 |
+
|
| 504 |
+
if X_val is not None and y_val is not None:
|
| 505 |
+
self.predictions['val'] = self.predict(X_val)
|
| 506 |
+
self.residuals['val'] = y_val - self.predictions['val']
|
| 507 |
+
|
| 508 |
+
logger.info(f"✓ CatBoost trained in {self.training_time:.2f} seconds")
|
| 509 |
+
|
| 510 |
+
return self
|
| 511 |
+
|
| 512 |
+
except Exception as e:
|
| 513 |
+
logger.error(f"✗ Error training CatBoost: {e}")
|
| 514 |
+
raise
|