Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| Model Testing and Evaluation Script | |
| This script loads the trained models and performs comprehensive testing: | |
| 1. Load saved models | |
| 2. Test on new data | |
| 3. Generate predictions | |
| 4. Visualize results | |
| 5. Cross-validation analysis | |
| """ | |
| import pandas as pd | |
| import numpy as np | |
| import matplotlib | |
| matplotlib.use('Agg') # Use non-interactive backend | |
| import matplotlib.pyplot as plt | |
| import seaborn as sns | |
| import joblib | |
| import torch | |
| import torch.nn as nn | |
| import xgboost as xgb | |
| from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error | |
| from sklearn.model_selection import cross_val_score | |
| import warnings | |
| import os | |
| warnings.filterwarnings('ignore') | |
| class DataPreprocessor: | |
| """Data preprocessing and feature engineering class.""" | |
| def __init__(self): | |
| self.label_encoders = {} | |
| self.scaler = None | |
| self.imputer = None | |
| self.feature_names = None | |
| def prepare_features(self, df): | |
| """Prepare features for machine learning.""" | |
| print("Preparing features...") | |
| # Create a copy to avoid modifying original data | |
| data = df.copy() | |
| # Remove records with zero or negative yield (invalid data) | |
| data = data[data['Yield'] > 0].copy() | |
| # Feature engineering | |
| data['Area_Production_Ratio'] = data['Area'] / (data['Production'] + 1e-6) | |
| data['Yield_Area_Interaction'] = data['Yield'] * data['Area'] | |
| data['Production_Per_Area'] = data['Production'] / (data['Area'] + 1e-6) | |
| # Create season dummies | |
| season_dummies = pd.get_dummies(data['Season'], prefix='Season') | |
| data = pd.concat([data, season_dummies], axis=1) | |
| # Handle categorical variables | |
| categorical_cols = ['State', 'District', 'Crop'] | |
| for col in categorical_cols: | |
| if col in data.columns: | |
| # Use label encoding for high cardinality features | |
| if col not in self.label_encoders: | |
| from sklearn.preprocessing import LabelEncoder | |
| self.label_encoders[col] = LabelEncoder() | |
| data[f'{col}_encoded'] = self.label_encoders[col].fit_transform(data[col].astype(str)) | |
| else: | |
| # Handle unseen categories | |
| unique_values = set(data[col].astype(str)) | |
| known_values = set(self.label_encoders[col].classes_) | |
| new_values = unique_values - known_values | |
| if new_values: | |
| # Add new categories to the encoder | |
| all_values = list(known_values) + list(new_values) | |
| self.label_encoders[col].classes_ = np.array(all_values) | |
| data[f'{col}_encoded'] = self.label_encoders[col].transform(data[col].astype(str)) | |
| # Select features for modeling | |
| feature_cols = ['Crop_Year', 'Area', 'Production', 'Annual_Rainfall', | |
| 'Fertilizer', 'Pesticide', 'State_encoded', 'Crop_encoded', | |
| 'Area_Production_Ratio', 'Yield_Area_Interaction', | |
| 'Production_Per_Area'] + list(season_dummies.columns) | |
| # Add District_encoded if available | |
| if 'District_encoded' in data.columns: | |
| feature_cols.append('District_encoded') | |
| # Select only available columns | |
| available_cols = [col for col in feature_cols if col in data.columns] | |
| X = data[available_cols].copy() | |
| y = data['Yield'].copy() | |
| print(f"Selected features: {available_cols}") | |
| print(f"Dataset shape after preprocessing: {X.shape}") | |
| return X, y, data | |
| def transform(self, X): | |
| """Transform new data using fitted preprocessors.""" | |
| # Handle missing values | |
| X_imputed = pd.DataFrame( | |
| self.imputer.transform(X), | |
| columns=X.columns, | |
| index=X.index | |
| ) | |
| # Scale features | |
| X_scaled = pd.DataFrame( | |
| self.scaler.transform(X_imputed), | |
| columns=X.columns, | |
| index=X.index | |
| ) | |
| return X_scaled | |
| class PyTorchYieldPredictor(nn.Module): | |
| """PyTorch Neural Network for yield prediction (same as training script).""" | |
| def __init__(self, input_dim, hidden_dims=[256, 128, 64], dropout_rate=0.3): | |
| super(PyTorchYieldPredictor, self).__init__() | |
| layers = [] | |
| prev_dim = input_dim | |
| for hidden_dim in hidden_dims: | |
| layers.extend([ | |
| nn.Linear(prev_dim, hidden_dim), | |
| nn.BatchNorm1d(hidden_dim), | |
| nn.ReLU(), | |
| nn.Dropout(dropout_rate) | |
| ]) | |
| prev_dim = hidden_dim | |
| # Output layer | |
| layers.append(nn.Linear(prev_dim, 1)) | |
| self.model = nn.Sequential(*layers) | |
| def forward(self, x): | |
| return self.model(x) | |
| class ModelTester: | |
| """Class for testing and evaluating trained models.""" | |
| def __init__(self, models_dir='trained_models'): | |
| self.models_dir = models_dir | |
| self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') | |
| print(f"Using device: {self.device}") | |
| self.models = {} | |
| self.preprocessor = None | |
| self.results = {} | |
| def load_models(self): | |
| """Load all trained models and preprocessor.""" | |
| print("Loading trained models...") | |
| try: | |
| # Load preprocessor | |
| preprocessor_path = os.path.join(self.models_dir, 'preprocessor.pkl') | |
| if os.path.exists(preprocessor_path): | |
| self.preprocessor = joblib.load(preprocessor_path) | |
| print("✅ Preprocessor loaded") | |
| else: | |
| raise FileNotFoundError("Preprocessor not found") | |
| # Load Random Forest | |
| rf_path = os.path.join(self.models_dir, 'random_forest_model.pkl') | |
| if os.path.exists(rf_path): | |
| self.models['RandomForest'] = joblib.load(rf_path) | |
| print("✅ Random Forest model loaded") | |
| # Load XGBoost | |
| xgb_path = os.path.join(self.models_dir, 'xgboost_model.json') | |
| if os.path.exists(xgb_path): | |
| xgb_model = xgb.XGBRegressor() | |
| xgb_model.load_model(xgb_path) | |
| self.models['XGBoost'] = xgb_model | |
| print("✅ XGBoost model loaded") | |
| # Load PyTorch model | |
| pytorch_path = os.path.join(self.models_dir, 'pytorch_model.pth') | |
| if os.path.exists(pytorch_path): | |
| # We need to know the input dimension - get it from preprocessor | |
| # This is a bit tricky - we'll determine it from the data | |
| print("✅ PyTorch model path found (will load after determining input size)") | |
| except Exception as e: | |
| print(f"Error loading models: {e}") | |
| raise | |
| def load_pytorch_model(self, input_dim): | |
| """Load PyTorch model with known input dimension.""" | |
| pytorch_path = os.path.join(self.models_dir, 'pytorch_model.pth') | |
| if os.path.exists(pytorch_path): | |
| pytorch_model = PyTorchYieldPredictor(input_dim).to(self.device) | |
| pytorch_model.load_state_dict(torch.load(pytorch_path, map_location=self.device)) | |
| pytorch_model.eval() | |
| self.models['PyTorch'] = pytorch_model | |
| print("✅ PyTorch model loaded") | |
| def prepare_test_data(self, data_file='combined_crop_data.csv', sample_size=1000): | |
| """Prepare test data for evaluation.""" | |
| print(f"Preparing test data from {data_file}...") | |
| # Load data | |
| df = pd.read_csv(data_file) | |
| # Sample data for testing if too large | |
| if len(df) > sample_size: | |
| df = df.sample(n=sample_size, random_state=42) | |
| print(f"Sampled {sample_size} records for testing") | |
| # Prepare features using the same preprocessor | |
| X, y, processed_data = self.preprocessor.prepare_features(df) | |
| # Transform using fitted preprocessor | |
| X_processed = self.preprocessor.transform(X) | |
| print(f"Test data shape: {X_processed.shape}") | |
| # Now we can load PyTorch model | |
| input_dim = X_processed.shape[1] | |
| self.load_pytorch_model(input_dim) | |
| return X_processed, y, processed_data | |
| def test_models(self, X_test, y_test): | |
| """Test all loaded models and calculate metrics.""" | |
| print("\\n" + "="*50) | |
| print("TESTING MODELS") | |
| print("="*50) | |
| for model_name, model in self.models.items(): | |
| print(f"\\nTesting {model_name}...") | |
| try: | |
| if model_name == 'PyTorch': | |
| # PyTorch model prediction | |
| X_tensor = torch.FloatTensor(X_test.values).to(self.device) | |
| with torch.no_grad(): | |
| predictions = model(X_tensor).cpu().numpy().flatten() | |
| else: | |
| # Sklearn/XGBoost prediction | |
| predictions = model.predict(X_test) | |
| # Calculate metrics | |
| mse = mean_squared_error(y_test, predictions) | |
| rmse = np.sqrt(mse) | |
| mae = mean_absolute_error(y_test, predictions) | |
| r2 = r2_score(y_test, predictions) | |
| self.results[model_name] = { | |
| 'predictions': predictions, | |
| 'mse': mse, | |
| 'rmse': rmse, | |
| 'mae': mae, | |
| 'r2': r2 | |
| } | |
| print(f" RMSE: {rmse:.4f}") | |
| print(f" MAE: {mae:.4f}") | |
| print(f" R²: {r2:.4f}") | |
| except Exception as e: | |
| print(f" ❌ Error testing {model_name}: {e}") | |
| def visualize_results(self, y_test): | |
| """Create visualizations of model performance.""" | |
| print("\\nCreating visualizations...") | |
| # Create subplots for each model | |
| n_models = len(self.results) | |
| fig, axes = plt.subplots(2, n_models, figsize=(5*n_models, 10)) | |
| if n_models == 1: | |
| axes = axes.reshape(-1, 1) | |
| for i, (model_name, results) in enumerate(self.results.items()): | |
| predictions = results['predictions'] | |
| # Actual vs Predicted scatter plot | |
| axes[0, i].scatter(y_test, predictions, alpha=0.6) | |
| axes[0, i].plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', lw=2) | |
| axes[0, i].set_xlabel('Actual Yield') | |
| axes[0, i].set_ylabel('Predicted Yield') | |
| axes[0, i].set_title(f'{model_name} - Actual vs Predicted\\nR² = {results["r2"]:.4f}') | |
| # Residuals plot | |
| residuals = y_test - predictions | |
| axes[1, i].scatter(predictions, residuals, alpha=0.6) | |
| axes[1, i].axhline(y=0, color='r', linestyle='--') | |
| axes[1, i].set_xlabel('Predicted Yield') | |
| axes[1, i].set_ylabel('Residuals') | |
| axes[1, i].set_title(f'{model_name} - Residuals Plot') | |
| plt.tight_layout() | |
| plt.savefig('model_test_results.png', dpi=300, bbox_inches='tight') | |
| plt.close() | |
| print("✅ Visualization saved as model_test_results.png") | |
| # Create comparison chart | |
| self.plot_model_comparison() | |
| def plot_model_comparison(self): | |
| """Plot model comparison metrics.""" | |
| comparison_data = [] | |
| for model_name, results in self.results.items(): | |
| comparison_data.append({ | |
| 'Model': model_name, | |
| 'RMSE': results['rmse'], | |
| 'MAE': results['mae'], | |
| 'R²': results['r2'] | |
| }) | |
| comparison_df = pd.DataFrame(comparison_data) | |
| # Create comparison plots | |
| fig, axes = plt.subplots(1, 3, figsize=(15, 5)) | |
| # RMSE comparison | |
| axes[0].bar(comparison_df['Model'], comparison_df['RMSE'], alpha=0.7, color='blue') | |
| axes[0].set_title('RMSE Comparison') | |
| axes[0].set_ylabel('RMSE') | |
| axes[0].tick_params(axis='x', rotation=45) | |
| # MAE comparison | |
| axes[1].bar(comparison_df['Model'], comparison_df['MAE'], alpha=0.7, color='orange') | |
| axes[1].set_title('MAE Comparison') | |
| axes[1].set_ylabel('MAE') | |
| axes[1].tick_params(axis='x', rotation=45) | |
| # R² comparison | |
| axes[2].bar(comparison_df['Model'], comparison_df['R²'], alpha=0.7, color='green') | |
| axes[2].set_title('R² Comparison') | |
| axes[2].set_ylabel('R² Score') | |
| axes[2].tick_params(axis='x', rotation=45) | |
| plt.tight_layout() | |
| plt.savefig('model_performance_comparison.png', dpi=300, bbox_inches='tight') | |
| plt.close() | |
| print("✅ Comparison chart saved as model_performance_comparison.png") | |
| # Print comparison table | |
| print("\\n" + "="*50) | |
| print("MODEL PERFORMANCE COMPARISON") | |
| print("="*50) | |
| print(comparison_df.to_string(index=False, float_format='%.4f')) | |
| def cross_validate_models(self, X, y, cv=5): | |
| """Perform cross-validation on models that support it.""" | |
| print("\\n" + "="*50) | |
| print("CROSS-VALIDATION RESULTS") | |
| print("="*50) | |
| cv_results = {} | |
| for model_name, model in self.models.items(): | |
| if model_name != 'PyTorch': # Skip PyTorch for CV (more complex to implement) | |
| try: | |
| print(f"\\nCross-validating {model_name}...") | |
| cv_scores = cross_val_score(model, X, y, cv=cv, scoring='neg_mean_squared_error') | |
| cv_rmse = np.sqrt(-cv_scores) | |
| cv_results[model_name] = { | |
| 'cv_rmse_mean': cv_rmse.mean(), | |
| 'cv_rmse_std': cv_rmse.std(), | |
| 'cv_scores': cv_rmse | |
| } | |
| print(f" CV RMSE: {cv_rmse.mean():.4f} ± {cv_rmse.std():.4f}") | |
| except Exception as e: | |
| print(f" ❌ Error in cross-validation for {model_name}: {e}") | |
| return cv_results | |
| def generate_predictions_for_new_data(self, new_data_file=None): | |
| """Generate predictions for new data.""" | |
| if new_data_file is None: | |
| print("\\nNo new data file provided for prediction.") | |
| return | |
| print(f"\\nGenerating predictions for {new_data_file}...") | |
| try: | |
| # Load new data | |
| new_df = pd.read_csv(new_data_file) | |
| # Prepare features | |
| X_new, _, _ = self.preprocessor.prepare_features(new_df) | |
| X_new_processed = self.preprocessor.transform(X_new) | |
| predictions_df = new_df.copy() | |
| # Generate predictions from each model | |
| for model_name, model in self.models.items(): | |
| if model_name == 'PyTorch': | |
| X_tensor = torch.FloatTensor(X_new_processed.values).to(self.device) | |
| with torch.no_grad(): | |
| preds = model(X_tensor).cpu().numpy().flatten() | |
| else: | |
| preds = model.predict(X_new_processed) | |
| predictions_df[f'Predicted_Yield_{model_name}'] = preds | |
| # Save predictions | |
| output_file = 'new_data_predictions.csv' | |
| predictions_df.to_csv(output_file, index=False) | |
| print(f"✅ Predictions saved to {output_file}") | |
| return predictions_df | |
| except Exception as e: | |
| print(f"❌ Error generating predictions: {e}") | |
| def run_comprehensive_test(self, data_file='combined_crop_data.csv', new_data_file=None): | |
| """Run comprehensive testing pipeline.""" | |
| print("🧪 Starting Comprehensive Model Testing...") | |
| try: | |
| # Load models | |
| self.load_models() | |
| # Prepare test data | |
| X_test, y_test, processed_data = self.prepare_test_data(data_file) | |
| # Test models | |
| self.test_models(X_test, y_test) | |
| # Create visualizations | |
| self.visualize_results(y_test) | |
| # Cross-validation | |
| cv_results = self.cross_validate_models(X_test, y_test) | |
| # Generate predictions for new data if provided | |
| if new_data_file: | |
| self.generate_predictions_for_new_data(new_data_file) | |
| print("\\n🎉 Comprehensive testing completed successfully!") | |
| print("📊 Check the generated plots and results files.") | |
| return self.results, cv_results | |
| except Exception as e: | |
| print(f"❌ Error in testing pipeline: {e}") | |
| raise | |
| def main(): | |
| """Main function to run model testing.""" | |
| tester = ModelTester() | |
| # Check if trained models exist | |
| if not os.path.exists('trained_models'): | |
| print("❌ No trained models found. Please run the training script first.") | |
| return | |
| # Run comprehensive testing | |
| results, cv_results = tester.run_comprehensive_test() | |
| return tester, results, cv_results | |
| if __name__ == "__main__": | |
| tester, results, cv_results = main() | |