ArabovMK commited on
Commit
5c04586
·
1 Parent(s): 1761a59

implement core ML pipeline and unified Streamlit app for data and modeling tasks.

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +8 -0
  2. README.md +177 -355
  3. app.py +0 -0
  4. fix_streamlit_api.py +48 -0
  5. pipeline/main_pipeline.py → main_pipeline.py +13 -14
  6. requirements.txt +0 -0
  7. {config → src/enhanced_preprocessing/config}/__init__.py +0 -0
  8. {config → src/enhanced_preprocessing/config}/config.py +0 -0
  9. {config → src/enhanced_preprocessing/config}/default_config.json +0 -0
  10. {config → src/enhanced_preprocessing/config}/settings.py +0 -0
  11. {correlations → src/enhanced_preprocessing/correlations}/__init__.py +0 -0
  12. {correlations → src/enhanced_preprocessing/correlations}/correlation_analyzer.py +3 -1
  13. {data_loader → src/enhanced_preprocessing/data_loader}/__init__.py +0 -0
  14. {data_loader → src/enhanced_preprocessing/data_loader}/data_loader.py +3 -1
  15. {decomposition → src/enhanced_preprocessing/decomposition}/__init__.py +0 -0
  16. {decomposition → src/enhanced_preprocessing/decomposition}/decomposer.py +1 -1
  17. {feature_selection → src/enhanced_preprocessing/feature_selection}/__init__.py +0 -0
  18. {feature_selection → src/enhanced_preprocessing/feature_selection}/feature_selector.py +1 -1
  19. {features → src/enhanced_preprocessing/features}/__init__.py +0 -0
  20. {features → src/enhanced_preprocessing/features}/feature_engineer.py +1 -4
  21. {missing_values → src/enhanced_preprocessing/missing_values}/__init__.py +0 -0
  22. {missing_values → src/enhanced_preprocessing/missing_values}/missing_analyzer.py +1 -1
  23. {outliers → src/enhanced_preprocessing/outliers}/__init__.py +0 -0
  24. {outliers → src/enhanced_preprocessing/outliers}/outlier_analyzer.py +1 -1
  25. {pipeline → src/enhanced_preprocessing/pipeline}/__init__.py +0 -0
  26. run_pipeline.py → src/enhanced_preprocessing/run_pipeline.py +17 -7
  27. {scaling → src/enhanced_preprocessing/scaling}/__init__.py +0 -0
  28. {scaling → src/enhanced_preprocessing/scaling}/data_scaler.py +1 -1
  29. {splitting → src/enhanced_preprocessing/splitting}/__init__.py +0 -0
  30. {splitting → src/enhanced_preprocessing/splitting}/data_splitter.py +1 -1
  31. {stationarity → src/enhanced_preprocessing/stationarity}/__init__.py +0 -0
  32. {stationarity → src/enhanced_preprocessing/stationarity}/stationarity_checker.py +1 -1
  33. {validation → src/enhanced_preprocessing/validation}/__init__.py +0 -0
  34. {validation → src/enhanced_preprocessing/validation}/data_validator.py +1 -2
  35. {visualization → src/enhanced_preprocessing/visualization}/__init__.py +0 -0
  36. {visualization → src/enhanced_preprocessing/visualization}/visualization_manager.py +1 -3
  37. src/ml_pipeline/Readme.md +1 -0
  38. src/ml_pipeline/__init__.py +0 -0
  39. src/ml_pipeline/combine_files.py +127 -0
  40. src/ml_pipeline/config/__init__.py +0 -0
  41. src/ml_pipeline/config/ml_config.py +503 -0
  42. src/ml_pipeline/core/__init__.py +0 -0
  43. src/ml_pipeline/core/base_model.py +909 -0
  44. src/ml_pipeline/core/model_factory.py +124 -0
  45. src/ml_pipeline/core/resource_monitor.py +150 -0
  46. src/ml_pipeline/features/__init__.py +0 -0
  47. src/ml_pipeline/features/engineering.py +331 -0
  48. src/ml_pipeline/features/selection.py +86 -0
  49. src/ml_pipeline/models/__init__.py +0 -0
  50. 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: ML Pipeline PRO Dashboard
3
- emoji: 🤖
4
- colorFrom: purple
5
  colorTo: indigo
6
- sdk: streamlit
7
  pinned: true
8
  app_file: app.py
9
- sdk_version: 1.53.1
10
- license: mit
11
- short_description: Full ML pipeline dashboard with 28+ algorithms and ensembles
12
  ---
13
 
14
- # 🤖 ML Pipeline PRO Dashboard
15
 
16
  <div align="center">
17
 
18
- **Complete Machine Learning Pipeline with 28+ Algorithms - From Data to Deployment**
19
 
20
- *Advanced ML workflow automation with full model comparison and ensemble capabilities*
21
 
22
- [![Hugging Face](https://img.shields.io/badge/🤗-Hugging%20Face%20Space-blue)](https://huggingface.co/spaces/your-username/ml-pipeline-pro-dashboard)
23
  [![Streamlit](https://img.shields.io/badge/Interface-Streamlit-FF4B4B)](https://streamlit.io)
24
- [![Python](https://img.shields.io/badge/Python-3.8+-blue)](https://python.org)
25
- [![Scikit-learn](https://img.shields.io/badge/ML-Scikit--learn-orange)](https://scikit-learn.org)
26
 
27
  </div>
28
 
29
- ## 🚀 Overview
30
 
31
- **ML Pipeline PRO Dashboard** is a comprehensive web application that provides a complete machine learning workflow in one interface. It seamlessly integrates 28+ regression algorithms with advanced preprocessing, hyperparameter tuning, ensemble methods, and model deployment capabilities. Designed for data scientists, ML engineers, and analysts, it eliminates the complexity of switching between multiple tools.
32
 
33
- ## ✨ Key Features
34
 
35
- ### 📊 **Complete ML Workflow**
36
- - **End-to-End Pipeline**: Data loading → preprocessing → model training → evaluation → deployment
37
- - **28+ Regression Algorithms**: From linear models to gradient boosting ensembles
38
- - **Interactive Dashboard**: Real-time visualization of model performance
39
- - **One-Click Operations**: Complex workflows simplified to single clicks
40
 
41
- ### 🤖 **Advanced Model Library**
42
- - **Linear Models**: Linear Regression, Ridge, Lasso, ElasticNet, Bayesian Ridge
43
- - **Tree-Based Models**: Random Forest, XGBoost, LightGBM, CatBoost, Gradient Boosting
44
- - **Ensemble Methods**: Voting, Stacking, Weighted Averaging, Bagging
45
- - **Advanced Algorithms**: SVR, MLP, Gaussian Process, Theil-Sen, RANSAC
46
 
47
- ### ⚙️ **Smart Automation**
48
- - **Auto Feature Engineering**: Automatic handling of missing values, scaling, feature selection
49
- - **Hyperparameter Tuning**: Grid Search, Random Search with intelligent defaults
50
- - **Model Comparison**: Side-by-side performance metrics and visualization
51
- - **Best Model Selection**: Automatic identification of top-performing algorithms
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. **Launch the Dashboard**
62
- ```bash
63
- # Option 1: Use Hugging Face Space (Recommended)
64
- https://huggingface.co/spaces/your-username/ml-pipeline-pro-dashboard
65
 
66
- # Option 2: Run locally
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
- # Auto-configured settings based on your data
78
  config = {
79
- 'target_column': 'auto-detected',
80
  'test_size': 0.2,
81
- 'models': 'all_algorithms',
82
- 'tuning': 'auto',
83
- 'ensembles': 'enabled'
84
  }
85
  ```
86
 
87
- ### 4. **Train & Compare Models**
88
- - One-click training of all 28+ algorithms
89
- - Real-time progress tracking
90
- - Interactive comparison dashboard
91
- - Export results with one click
92
 
93
- ## 🏗️ Technical Architecture
94
 
95
- ### 🔧 **Pipeline Structure**
96
  ```
97
- Data Layer → Preprocessing → Feature Engineering → Model Training
98
  ↓
99
- Hyperparameter Tuning → Ensemble Creation → Evaluation → Deployment
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
- ## 📊 Feature Details
117
-
118
- ### **Data Preparation**
119
- - Smart missing value imputation (mean, median, mode)
120
- - Automatic feature scaling (Standard, MinMax, Robust)
121
- - Outlier detection and treatment
122
- - Feature selection and dimensionality reduction
123
-
124
- ### **Model Training**
125
- - Batch training of all selected models
126
- - Memory-efficient processing
127
- - Progress tracking with visual indicators
128
- - Automatic error handling and fallbacks
129
-
130
- ### **Evaluation & Comparison**
131
- - Interactive metrics dashboard
132
- - Feature importance visualization
133
- - Residual analysis and error distribution
134
- - Model performance heatmaps
135
-
136
- ### **Advanced Features**
137
- - Hyperparameter optimization (Grid/Random Search)
138
- - Multiple ensemble methods (Voting, Stacking, Weighted)
139
- - Model persistence (Pickle, Joblib, Dill)
140
- - Comprehensive export capabilities
141
 
142
- ## 🎯 Use Cases
143
 
144
- ### 🔬 **Research & Development**
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/ml-pipeline-pro-dashboard
175
- cd ml-pipeline-pro-dashboard
176
 
177
- # Install requirements
178
  pip install -r requirements.txt
179
 
180
- # Launch application
181
  streamlit run app.py
182
  ```
183
 
184
- ### **Docker Deployment**
185
- ```dockerfile
186
- FROM python:3.9-slim
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
- ### **Tuning Settings**
251
- ```yaml
252
- tuning:
253
- enable_hyperparameter_tuning: true
254
- tuning_method: grid
255
- n_trials: 30
256
- cv_folds: 3
257
  ```
258
 
259
- ## 📋 Supported Models
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
- # Example: Using the pipeline programmatically
299
- from ml_pipeline_dashboard import StreamlitMLPipeline
300
 
301
- # Initialize pipeline
302
- pipeline = StreamlitMLPipeline()
303
- pipeline.initialize()
304
 
305
- # Load and prepare data
306
- pipeline.load_data('demo')
307
- pipeline.prepare_features()
308
-
309
- # Train all models
310
- results = pipeline.train_models(pipeline.get_all_model_names())
 
311
 
312
- # Get best model
313
- best_model = pipeline.best_model
314
- predictions = pipeline.predict(new_data, [best_model])
315
 
316
- # Export results
317
- pipeline.export_results('export_directory')
 
318
  ```
319
 
320
- ## 📊 Visualization Examples
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
- ### **Real-time Analytics**
329
- - Live training progress
330
- - Instant metric updates
331
- - Dynamic chart rendering
332
- - Export-ready visualizations
333
 
334
- ## 🔒 Security & Privacy
335
 
336
- ### **Data Protection**
337
- - Local processing only (no data sent externally)
338
- - Session-based data isolation
339
- - Automatic temporary file cleanup
340
- - No persistent storage of uploaded data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
 
342
- ### **Best Practices**
343
- - Memory-efficient processing
344
- - Secure model serialization
345
- - Input validation and sanitization
346
- - Error handling without data leakage
 
347
 
348
  ## 🤝 Contributing
349
 
350
- We welcome contributions from the community!
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
- - New model algorithms
361
- - Additional visualization types
362
- - Performance optimizations
363
- - Documentation improvements
364
- - Bug fixes and enhancements
365
 
366
- ### **Development Setup**
367
  ```bash
368
- # Set up development environment
369
- git clone https://huggingface.co/spaces/your-username/ml-pipeline-pro-dashboard
370
- cd ml-pipeline-pro-dashboard
371
- python -m venv venv
372
- source venv/bin/activate # On Windows: venv\Scripts\activate
373
- pip install -r requirements.txt
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
- ### **Core Technologies**
384
- - **Streamlit** for the amazing web framework
385
- - **Scikit-learn** for comprehensive ML algorithms
386
- - **Plotly** for interactive visualizations
387
- - **Hugging Face** for hosting and community
388
 
389
- ### **Special Thanks**
390
- - All open-source contributors
391
- - The data science community
392
- - Early testers and feedback providers
393
- - Everyone who helped improve this tool
 
394
 
395
  ## 📞 Support & Contact
396
 
397
- ### **Get Help**
398
  - 📧 **Email**: cool.araby@gmail.com
399
- - 💬 **Issues**: [Report on Hugging Face](https://huggingface.co/spaces/your-username/ml-pipeline-pro-dashboard/discussions)
400
- - 🐦 **Twitter**: [@YourHandle](https://twitter.com/yourhandle)
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
- - Watch for updates on Hugging Face
410
- - Follow for new feature announcements
411
- - Join the community discussions
412
 
413
  ---
414
 
415
  <div align="center">
416
 
417
- **Transform Your ML Workflow with One Dashboard**
418
 
419
- *From Data to Deployment - 28+ Algorithms, Infinite Possibilities*
420
 
421
  [![Follow on Hugging Face](https://img.shields.io/badge/Follow%20on-🤗%20Hugging%20Face-yellow)](https://huggingface.co/your-username)
422
- [![Try on Hugging Face](https://img.shields.io/badge/Try%20It-🤗%20Space-blue)](https://huggingface.co/spaces/your-username/ml-pipeline-pro-dashboard)
423
-
424
- **Empower Your Data Science Projects with Complete ML Automation**
425
-
426
- </div>
427
 
428
- ## 🎯 Pro Tips
429
-
430
- ### **For Best Results**
431
- 1. **Start with demo data** to understand the workflow
432
- 2. **Use auto-configuration** for initial experiments
433
- 3. **Compare all models** before selecting the best
434
- 4. **Experiment with ensembles** for improved accuracy
435
- 5. **Export and save** your best configurations
436
-
437
- ### **Performance Optimization**
438
- - Use appropriate data size for your needs
439
- - Enable parallel processing for large datasets
440
- - Utilize GPU acceleration when available
441
- - Monitor memory usage during training
442
-
443
- ### **Workflow Recommendations**
444
- 1. **Exploratory Phase**: Use demo data, test all models
445
- 2. **Optimization Phase**: Tune hyperparameters, create ensembles
446
- 3. **Production Phase**: Export best model, deploy with confidence
 
1
  ---
2
+ title: TimeFlow Pro
3
+ emoji: 📊
4
+ colorFrom: blue
5
  colorTo: indigo
6
+ sdk: docker
7
  pinned: true
8
  app_file: app.py
9
+ sdk_version: 1.52.2
 
 
10
  ---
11
 
12
+ # 📊 TimeFlow Pro
13
 
14
  <div align="center">
15
 
16
+ **Intelligent Time Series Data Analysis and Preprocessing Platform**
17
 
18
+ *Advanced pipeline for data preparation and feature engineering*
19
 
20
+ [![Hugging Face](https://img.shields.io/badge/🤗-Hugging%20Face%20Space-blue)](https://huggingface.co/spaces/your-username/timeflow-pro)
21
  [![Streamlit](https://img.shields.io/badge/Interface-Streamlit-FF4B4B)](https://streamlit.io)
22
+ [![Python](https://img.shields.io/badge/Python-3.9+-blue)](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
  [![Follow on Hugging Face](https://img.shields.io/badge/Follow%20on-🤗%20Hugging%20Face-yellow)](https://huggingface.co/your-username)
266
+ [![GitHub Stars](https://img.shields.io/github/stars/your-username/timeflow-pro?style=social)](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 visualization.visualization_manager import VisualisationManager
 
 
 
 
 
 
 
 
 
 
 
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
- from config.config import Config
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
- from config.config import Config, DataType
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
- from config.config import Config
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
- processed_data.to_csv('enhanced_preprocessing_results\processed_data\enhanced_final_processed_data.csv',
61
- index=True if isinstance(processed_data.index, pd.DatetimeIndex) else False)
62
- print(f"\n✓ Final data saved to 'enhanced_final_processed_data.csv'")
 
 
 
 
 
 
 
 
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