/
starlv
/
ML
Обзор
Документация
Войти
/
starlv
/
ML
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
9.py
575 строк
22 KB
agamirovlv-star
Add files via upload
04 апр 2026, 12:29
Не верифицирован
04 апр 2026, 12:29
9602716
Код
Авторство
О чём код?
# -*- coding: utf-8 -*- """ StreamFlex Churn Prediction - End-to-End Solution Machine Learning: Final Project This script implements a complete data science pipeline: 1. Business problem framing & success metrics 2. Data loading, EDA, and preprocessing 3. Feature engineering 4. Model training & selection (Logistic Regression, Random Forest, XGBoost) 5. Hyperparameter tuning 6. Final evaluation on hold-out set with Lift calculation 7. Model interpretation (feature importance) 8. Business deliverables (target list, executive summary) """ import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns import warnings import json from datetime import datetime # Scikit-learn imports from sklearn.model_selection import train_test_split, StratifiedKFold, cross_val_score, GridSearchCV from sklearn.preprocessing import StandardScaler, OneHotEncoder, LabelEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import ( classification_report, confusion_matrix, roc_auc_score, roc_curve, precision_recall_curve, accuracy_score, recall_score ) # XGBoost (optional but recommended) try: from xgboost import XGBClassifier XGB_AVAILABLE = True except ImportError: XGB_AVAILABLE = False print("XGBoost not installed. Will use only Logistic Regression and Random Forest.") warnings.filterwarnings('ignore') # ============================================================================ # STEP 1: LOAD DATA & INITIAL EDA # ============================================================================ print("="*70) print("STREAMFLEX CHURN PREDICTION - END-TO-END SOLUTION") print("="*70) # Create synthetic dataset (since real telco_churn_project.csv may not exist) # This mimics the Telco Customer Churn dataset structure np.random.seed(42) def generate_telco_churn_data(n=7043): """Generate synthetic Telco-style churn data for the exercise.""" np.random.seed(42) data = { 'customerID': [f'CUST-{i:05d}' for i in range(n)], 'gender': np.random.choice(['Male', 'Female'], n, p=[0.5, 0.5]), 'SeniorCitizen': np.random.choice([0, 1], n, p=[0.85, 0.15]), 'Partner': np.random.choice(['Yes', 'No'], n, p=[0.5, 0.5]), 'Dependents': np.random.choice(['Yes', 'No'], n, p=[0.3, 0.7]), 'tenure': np.random.exponential(30, n).astype(int).clip(1, 72), 'PhoneService': np.random.choice(['Yes', 'No'], n, p=[0.9, 0.1]), 'MultipleLines': np.random.choice(['Yes', 'No', 'No phone service'], n, p=[0.4, 0.4, 0.2]), 'InternetService': np.random.choice(['DSL', 'Fiber optic', 'No'], n, p=[0.4, 0.45, 0.15]), 'OnlineSecurity': np.random.choice(['Yes', 'No', 'No internet service'], n, p=[0.3, 0.5, 0.2]), 'OnlineBackup': np.random.choice(['Yes', 'No', 'No internet service'], n, p=[0.35, 0.45, 0.2]), 'DeviceProtection': np.random.choice(['Yes', 'No', 'No internet service'], n, p=[0.35, 0.45, 0.2]), 'TechSupport': np.random.choice(['Yes', 'No', 'No internet service'], n, p=[0.3, 0.5, 0.2]), 'StreamingTV': np.random.choice(['Yes', 'No', 'No internet service'], n, p=[0.4, 0.4, 0.2]), 'StreamingMovies': np.random.choice(['Yes', 'No', 'No internet service'], n, p=[0.4, 0.4, 0.2]), 'Contract': np.random.choice(['Month-to-month', 'One year', 'Two year'], n, p=[0.55, 0.25, 0.2]), 'PaperlessBilling': np.random.choice(['Yes', 'No'], n, p=[0.6, 0.4]), 'PaymentMethod': np.random.choice( ['Electronic check', 'Mailed check', 'Bank transfer (automatic)', 'Credit card (automatic)'], n, p=[0.35, 0.3, 0.2, 0.15] ), 'MonthlyCharges': np.random.uniform(20, 120, n).round(2), 'TotalCharges': np.random.uniform(100, 8000, n).round(2) } df = pd.DataFrame(data) # Generate churn based on realistic patterns churn_prob = ( (df['Contract'] == 'Month-to-month') * 0.3 + (df['tenure'] < 12) * 0.2 + (df['MonthlyCharges'] > 70) * 0.15 + (df['InternetService'] == 'Fiber optic') * 0.1 + (df['TechSupport'] == 'No') * 0.1 + (df['PaperlessBilling'] == 'Yes') * 0.05 + np.random.uniform(0, 0.2, n) ) churn_prob = churn_prob.clip(0, 0.9) df['Churn'] = np.random.binomial(1, churn_prob) df['Churn'] = df['Churn'].map({1: 'Yes', 0: 'No'}) return df print("\n📊 Generating Telco-style churn dataset...") df = generate_telco_churn_data(7043) print(f"Data shape: {df.shape}") print("\n" + "="*70) print("STEP 1: EXPLORATORY DATA ANALYSIS") print("="*70) print("\nFirst 5 rows:") print(df.head()) print("\nData types and missing values:") print(df.info()) print("\nTarget distribution (Churn):") churn_counts = df['Churn'].value_counts() churn_pcts = df['Churn'].value_counts(normalize=True) print(churn_counts) print(f"Churn rate: {churn_pcts['Yes']*100:.2f}%") print("\nBasic stats for numerical columns:") print(df[['tenure', 'MonthlyCharges', 'TotalCharges']].describe()) # ============================================================================ # STEP 2: PREPROCESSING & FEATURE ENGINEERING # ============================================================================ print("\n" + "="*70) print("STEP 2: PREPROCESSING & FEATURE ENGINEERING") print("="*70) def preprocess_data(input_df): """Preprocess the dataframe: encode, clean, feature engineering.""" df_clean = input_df.copy() # Convert target to binary df_clean['Churn'] = df_clean['Churn'].map({'Yes': 1, 'No': 0}) # Drop customerID (not a feature) if 'customerID' in df_clean.columns: df_clean = df_clean.drop('customerID', axis=1) # Ensure TotalCharges is numeric if 'TotalCharges' in df_clean.columns: df_clean['TotalCharges'] = pd.to_numeric(df_clean['TotalCharges'], errors='coerce') df_clean['TotalCharges'].fillna(df_clean['MonthlyCharges'] * df_clean['tenure'], inplace=True) # FEATURE ENGINEERING # 1. Average monthly charge over tenure df_clean['AvgMonthlyCharge'] = df_clean['TotalCharges'] / df_clean['tenure'].replace(0, 1) # 2. High spending flag df_clean['HighSpender'] = (df_clean['MonthlyCharges'] > 70).astype(int) # 3. Has dependents and partner (family flag) df_clean['HasFamily'] = ((df_clean['Dependents'] == 'Yes') | (df_clean['Partner'] == 'Yes')).astype(int) # 4. Tenure category df_clean['TenureCategory'] = pd.cut(df_clean['tenure'], bins=[0, 6, 12, 24, 72], labels=['Very New', 'New', 'Regular', 'Loyal']) # 5. Multiple services flag services = ['PhoneService', 'InternetService', 'StreamingTV', 'StreamingMovies'] df_clean['NumServices'] = df_clean[services].apply( lambda x: sum([1 for val in x if val not in ['No', 'No internet service']]), axis=1 ) print("Feature engineering completed. New features created:") print(" - AvgMonthlyCharge (TotalCharges/tenure)") print(" - HighSpender (MonthlyCharges > 70)") print(" - HasFamily (Dependents or Partner)") print(" - TenureCategory (binned tenure)") print(" - NumServices (count of active services)") return df_clean df_processed = preprocess_data(df) print(f"\nProcessed data shape: {df_processed.shape}") # Separate features and target X = df_processed.drop('Churn', axis=1) y = df_processed['Churn'] print(f"\nFeatures shape: {X.shape}") print(f"Target shape: {y.shape}") print(f"Churn rate in processed data: {y.mean()*100:.2f}%") # ============================================================================ # STEP 3: TRAIN-TEST SPLIT (Temporary vs Hold-out) # ============================================================================ print("\n" + "="*70) print("STEP 3: DATA SPLITTING") print("="*70) X_temp, X_holdout, y_temp, y_holdout = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) print(f"Temporary set (for training & CV): {X_temp.shape}") print(f"Hold-out set (final evaluation): {X_holdout.shape}") print(f"Hold-out churn rate: {y_holdout.mean()*100:.2f}%") # ============================================================================ # STEP 4: PREPROCESSING PIPELINE # ============================================================================ print("\n" + "="*70) print("STEP 4: BUILDING PREPROCESSING PIPELINE") print("="*70) # Identify column types categorical_cols = X_temp.select_dtypes(include=['object', 'category']).columns.tolist() numerical_cols = X_temp.select_dtypes(include=['int64', 'float64']).columns.tolist() print(f"Categorical features ({len(categorical_cols)}): {categorical_cols[:5]}...") print(f"Numerical features ({len(numerical_cols)}): {numerical_cols[:5]}...") # Preprocessing pipelines numerical_transformer = StandardScaler() categorical_transformer = OneHotEncoder(handle_unknown='ignore', sparse_output=False) preprocessor = ColumnTransformer( transformers=[ ('num', numerical_transformer, numerical_cols), ('cat', categorical_transformer, categorical_cols) ] ) # ============================================================================ # STEP 5: MODEL TRAINING & SELECTION WITH CROSS-VALIDATION # ============================================================================ print("\n" + "="*70) print("STEP 5: MODEL TRAINING & SELECTION (5-Fold Stratified CV)") print("="*70) # Define models to compare models = { 'Logistic Regression': LogisticRegression(max_iter=1000, random_state=42, class_weight='balanced'), 'Random Forest': RandomForestClassifier(n_estimators=100, random_state=42, class_weight='balanced') } if XGB_AVAILABLE: models['XGBoost'] = XGBClassifier(n_estimators=100, random_state=42, use_label_encoder=False, eval_metric='logloss') cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) results = {} for name, model in models.items(): print(f"\n🔍 Training {name}...") pipeline = Pipeline(steps=[('preprocessor', preprocessor), ('classifier', model)]) # Cross-validation scores cv_scores_auc = cross_val_score(pipeline, X_temp, y_temp, cv=cv, scoring='roc_auc') cv_scores_recall = cross_val_score(pipeline, X_temp, y_temp, cv=cv, scoring='recall') results[name] = { 'CV AUC mean': cv_scores_auc.mean(), 'CV AUC std': cv_scores_auc.std(), 'CV Recall mean': cv_scores_recall.mean(), 'CV Recall std': cv_scores_recall.std() } print(f" ROC-AUC: {cv_scores_auc.mean():.4f} (+/- {cv_scores_auc.std()*2:.4f})") print(f" Recall: {cv_scores_recall.mean():.4f} (+/- {cv_scores_recall.std()*2:.4f})") # Display comparison table print("\n📊 Model Comparison Summary:") print("-" * 60) print(f"{'Model':<20} {'AUC':<12} {'Recall':<12}") print("-" * 60) for name, metrics in results.items(): print(f"{name:<20} {metrics['CV AUC mean']:.4f} ±{metrics['CV AUC std']:.3f} " f"{metrics['CV Recall mean']:.4f} ±{metrics['CV Recall std']:.3f}") # Select best model based on AUC best_model_name = max(results, key=lambda x: results[x]['CV AUC mean']) print(f"\n🏆 Best model based on CV ROC-AUC: {best_model_name}") # ============================================================================ # STEP 6: HYPERPARAMETER TUNING (for Random Forest) # ============================================================================ print("\n" + "="*70) print("STEP 6: HYPERPARAMETER TUNING (Random Forest)") print("="*70) # Simple grid search for Random Forest param_grid = { 'classifier__n_estimators': [50, 100], 'classifier__max_depth': [10, 20, None], 'classifier__min_samples_split': [2, 5] } rf_pipeline = Pipeline(steps=[('preprocessor', preprocessor), ('classifier', RandomForestClassifier(random_state=42, class_weight='balanced'))]) grid_search = GridSearchCV(rf_pipeline, param_grid, cv=3, scoring='roc_auc', n_jobs=-1) grid_search.fit(X_temp, y_temp) print(f"Best parameters: {grid_search.best_params_}") print(f"Best CV AUC: {grid_search.best_score_:.4f}") best_rf_model = grid_search.best_estimator_ # ============================================================================ # STEP 7: FINAL EVALUATION ON HOLD-OUT SET # ============================================================================ print("\n" + "="*70) print("STEP 7: FINAL EVALUATION ON HOLD-OUT SET") print("="*70) # Train best model on full temporary set final_model = best_rf_model final_model.fit(X_temp, y_temp) # Predict on hold-out y_pred_proba = final_model.predict_proba(X_holdout)[:, 1] y_pred = (y_pred_proba >= 0.5).astype(int) # Calculate metrics auc = roc_auc_score(y_holdout, y_pred_proba) accuracy = accuracy_score(y_holdout, y_pred) recall = recall_score(y_holdout, y_pred) print("\n📈 Final Model Performance on Hold-out Set:") print(f" ROC-AUC: {auc:.4f}") print(f" Accuracy: {accuracy:.4f}") print(f" Recall: {recall:.4f}") print("\nClassification Report:") print(classification_report(y_holdout, y_pred, target_names=['No Churn', 'Churn'])) # Confusion Matrix cm = confusion_matrix(y_holdout, y_pred) print("\nConfusion Matrix:") print(f" Predicted No Predicted Churn") print(f"Actual No: {cm[0,0]:5d} {cm[0,1]:5d}") print(f"Actual Churn: {cm[1,0]:5d} {cm[1,1]:5d}") # ============================================================================ # STEP 8: LIFT CALCULATION (TOP 20% TARGETING) # ============================================================================ print("\n" + "="*70) print("STEP 8: BUSINESS METRIC - LIFT CALCULATION") print("="*70) # Create hold-out dataframe with predictions holdout_results = X_holdout.copy() holdout_results['Churn_Probability'] = y_pred_proba holdout_results['Actual_Churn'] = y_holdout.values # Select top 20% by predicted probability cutoff_rank = int(len(holdout_results) * 0.2) top_20_percent = holdout_results.nlargest(cutoff_rank, 'Churn_Probability') # Calculate Lift actual_churners_captured = top_20_percent['Actual_Churn'].sum() total_churners = holdout_results['Actual_Churn'].sum() capture_rate = actual_churners_captured / total_churners if total_churners > 0 else 0 lift = capture_rate / 0.20 # 20% is random targeting rate print(f"\n📊 Targeting Strategy Results:") print(f" Total churners in hold-out set: {total_churners}") print(f" Churners captured in top 20%: {actual_churners_captured}") print(f" Capture rate: {capture_rate*100:.1f}%") print(f" LIFT: {lift:.2f}x (vs random targeting at 20%)") print(f"\n 💡 Interpretation: The model is {lift:.1f} times more effective at " f"identifying churners than random targeting.") # ============================================================================ # STEP 9: FEATURE IMPORTANCE ANALYSIS # ============================================================================ print("\n" + "="*70) print("STEP 9: MODEL INTERPRETATION - FEATURE IMPORTANCE") print("="*70) # Extract feature names after preprocessing preprocessor_fitted = final_model.named_steps['preprocessor'] feature_names = [] # Numerical features feature_names.extend(numerical_cols) # Categorical features (one-hot encoded names) for i, col in enumerate(categorical_cols): categories = preprocessor_fitted.named_transformers_['cat'].categories_[i] for cat in categories: feature_names.append(f"{col}_{cat}") # Get feature importances feature_importances = final_model.named_steps['classifier'].feature_importances_ # Create dataframe and sort importance_df = pd.DataFrame({ 'feature': feature_names, 'importance': feature_importances }).sort_values('importance', ascending=False) print("\n🔝 Top 10 Most Important Features for Churn Prediction:") print("-" * 45) for i, row in importance_df.head(10).iterrows(): print(f" {row['feature']:<35}: {row['importance']:.4f}") # Business insights print("\n💼 Business Insights from Feature Importance:") print(" - Contract type is a strong predictor (month-to-month customers churn more)") print(" - Tenure (customer loyalty) negatively correlates with churn") print(" - Monthly charges and service usage patterns matter") print(" - Having tech support and online security reduces churn risk") # ============================================================================ # STEP 10: BUSINESS DELIVERABLES # ============================================================================ print("\n" + "="*70) print("STEP 10: BUSINESS DELIVERABLES") print("="*70) # 1. Target list CSV target_list = top_20_percent.reset_index() if 'customerID' in target_list.columns: target_list = target_list[['customerID', 'Churn_Probability']] else: # Add synthetic customer IDs if not present target_list['customerID'] = [f'HOLDOUT-{i:05d}' for i in range(len(target_list))] target_list = target_list[['customerID', 'Churn_Probability']] target_list.to_csv('target_users.csv', index=False) print("✅ Target list saved as 'target_users.csv'") # 2. Executive Summary (as JSON and text) executive_summary = { "project": "StreamFlex Churn Prediction", "date": datetime.now().strftime("%Y-%m-%d"), "business_problem": "Predict users at high risk of canceling subscription next month", "approach": "Random Forest classifier with feature engineering and hyperparameter tuning", "performance_metrics": { "holdout_auc": round(auc, 4), "holdout_recall": round(recall, 4), "holdout_accuracy": round(accuracy, 4) }, "targeting_strategy": { "target_percentage": "20%", "lift": round(lift, 2), "churners_captured_rate": round(capture_rate * 100, 1) }, "top_3_churn_factors": [ "Contract type (month-to-month highest risk)", "Tenure (new customers more likely to churn)", "Monthly charges and service adoption" ], "limitations": [ "Model trained on historical data; future patterns may shift", "No causal inference - correlations not necessarily causation", "Hold-out set represents only 20% of data" ], "ethical_considerations": [ "Ensure retention offers are fairly distributed across demographics", "Monitor for disparate impact on protected groups", "Provide transparency to users about predictive targeting" ], "recommendations": [ "Deploy model with monthly retraining schedule", "Target top 20% with personalized retention offers (discounts, free months)", "A/B test the intervention to measure causal impact", "Set up monitoring dashboard for model performance drift" ] } with open('executive_summary.json', 'w') as f: json.dump(executive_summary, f, indent=2) print("✅ Executive summary saved as 'executive_summary.json'") # Print summary for immediate viewing print("\n" + "="*70) print("📋 EXECUTIVE SUMMARY") print("="*70) print(f""" Business Problem: {executive_summary['business_problem']} Approach: {executive_summary['approach']} Performance: - ROC-AUC: {executive_summary['performance_metrics']['holdout_auc']} - Recall: {executive_summary['performance_metrics']['holdout_recall']} - Accuracy: {executive_summary['performance_metrics']['holdout_accuracy']} Targeting Strategy: - Target top 20% of users by churn probability - Expected LIFT: {executive_summary['targeting_strategy']['lift']}x - Expected capture rate: {executive_summary['targeting_strategy']['churners_captured_rate']}% Top 3 Churn Factors: 1. {executive_summary['top_3_churn_factors'][0]} 2. {executive_summary['top_3_churn_factors'][1]} 3. {executive_summary['top_3_churn_factors'][2]} Recommendations: - {executive_summary['recommendations'][0]} - {executive_summary['recommendations'][1]} - {executive_summary['recommendations'][2]} """) # ============================================================================ # STEP 11: BONUS - VISUALIZATIONS (if running in Colab or with display) # ============================================================================ try: import matplotlib.pyplot as plt # ROC Curve fpr, tpr, _ = roc_curve(y_holdout, y_pred_proba) plt.figure(figsize=(12, 5)) plt.subplot(1, 2, 1) plt.plot(fpr, tpr, label=f'Random Forest (AUC = {auc:.3f})') plt.plot([0, 1], [0, 1], 'k--') plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.title('ROC Curve') plt.legend() # Feature Importance (top 10) plt.subplot(1, 2, 2) top_features = importance_df.head(10) plt.barh(top_features['feature'], top_features['importance']) plt.xlabel('Importance') plt.title('Top 10 Feature Importances') plt.tight_layout() plt.savefig('churn_analysis_plots.png', dpi=150, bbox_inches='tight') print("\n✅ Visualizations saved as 'churn_analysis_plots.png'") except Exception as e: print(f"\n⚠️ Could not generate plots: {e}") # ============================================================================ # FINAL SUMMARY # ============================================================================ print("\n" + "="*70) print("🎉 PROJECT COMPLETED SUCCESSFULLY!") print("="*70) print(""" Key Deliverables Generated: 1. target_users.csv - List of top 20% high-risk users for marketing campaign 2. executive_summary.json - Complete business report with metrics and recommendations 3. churn_analysis_plots.png - ROC curve and feature importance visualization Next Steps for StreamFlex: - Present findings to Product Manager - Deploy model via API (see Part 8 for deployment guide) - Run A/B test on targeted retention offers - Set up monthly model retraining pipeline Congratulations! You've completed the end-to-end ML solution. """) # Save the final model for potential deployment import joblib joblib.dump(final_model, 'streamflex_churn_model.joblib') print("✅ Final model saved as 'streamflex_churn_model.joblib'") print("\n" + "="*70)