nkds

导航

 

MonkeyCode数据分析与可视化实战:从数据到洞察的智能之旅

前言

数据是新时代的石油,但原始数据本身并无价值——只有经过清洗、分析、可视化后,才能转化为驱动决策的智慧燃料。MonkeyCode作为AI编程助手,在数据分析与可视化领域同样展现出强大的能力。本文将通过多个实战案例,展示如何利用MonkeyCode高效完成从数据获取到可视化的全流程工作。


一、数据分析的完整链路

1.1 数据分析工作流概览

┌─────────────────────────────────────────────────────────────────┐
│                  MonkeyCode 数据分析工作流                        │
├──────────┬──────────┬──────────┬──────────┬────────────────────┤
│  数据采集   │ 数据清洗   │ 探索性分析  │ 建模预测   │ 可视化呈现          │
├──────────┼──────────┼──────────┼──────────┼────────────────────┤
│ API/爬虫  │ 缺失值处理 │ 统计描述   │ ML模型    │ 图表/仪表盘         │
│ 数据库查询 │ 异常值检测 │ 相关分析   │ 深度学习   │ 交互式报告          │
│ 文件读取   │ 格式统一   │ 分布检验   │ 时间序列   │ 自动化报表          │
│ 实时流     │ 去重合并   │ 聚类分析   │ NLP      │ 部署分享            │
└──────────┴──────────┴──────────┴──────────┴────────────────────┘

1.2 MonkeyCode在数据分析中的核心优势

能力维度 传统方式 MonkeyCode辅助 效率提升
代码编写 手写每行代码 自然语言→代码 5-10x
库选择 查文档/试错 智能推荐最优方案 3x
调试排错 逐行排查 AI定位+修复建议 4x
可视化 手调参数 一键生成+智能优化 3-5x
报告生成 手动拼装 自动化完整报告 10x

二、实战案例一:销售数据深度分析

2.1 场景背景

某电商公司积累了近三年的销售数据(100万+条记录),需要:

  1. 分析销售趋势和季节性规律
  2. 识别高价值客户群体
  3. 预测未来30天销售额
  4. 生成交互式可视化仪表盘

2.2 MonkeyCode生成的完整分析代码

"""
电商销售数据分析 - MonkeyCode生成
功能:数据加载、清洗、探索性分析、预测建模、可视化
"""

import warnings
warnings.filterwarnings('ignore')

import numpy as np
import pandas as pd
from datetime import datetime, timedelta
from pathlib import Path

# 可视化库
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import seaborn as sns
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
sns.set_style('whitegrid')

# 机器学习
from sklearn.model_selection import train_test_split, TimeSeriesSplit, cross_val_score
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.linear_model import Ridge, Lasso
from sklearn.metrics import (mean_absolute_error, mean_squared_error,
                             r2_score, mean_absolute_percentage_error)
from sklearn.cluster import KMeans, DBSCAN
from sklearn.decomposition import PCA

# 统计
from scipy import stats
from scipy.stats import normaltest, shapiro, pearsonr, spearmanr


class SalesDataAnalyzer:
    """电商销售数据分析器 - MonkeyCode最佳实践实现"""
    
    def __init__(self, data_path: str):
        self.data_path = Path(data_path)
        self.df_raw = None
        self.df_clean = None
        self.feature_df = None
        self.models = {}
        self.results = {}
        
    # ──────────────────────────────────────────────
    # 第一阶段:数据加载与清洗
    # ──────────────────────────────────────────────
    
    def load_data(self) -> pd.DataFrame:
        """加载数据并自动推断格式"""
        print("📂 加载数据...")
        
        suffix = self.data_path.suffix.lower()
        if suffix == '.csv':
            self.df_raw = pd.read_csv(
                self.data_path,
                parse_dates=['order_date', 'payment_date'],
                dtype={
                    'order_id': 'string',
                    'customer_id': 'string',
                    'product_id': 'string',
                    'category': 'category',
                    'quantity': 'int32',
                    'unit_price': 'float32',
                    'discount': 'float32',
                    'region': 'category',
                    'channel': 'category',
                }
            )
        elif suffix in ('.xlsx', '.xls'):
            self.df_raw = pd.read_excel(self.data_path)
        else:
            raise ValueError(f"不支持的文件格式: {suffix}")
            
        print(f"   ✅ 加载完成: {len(self.df_raw):,} 条记录")
        return self.df_raw
    
    def clean_data(self) -> pd.DataFrame:
        """数据清洗管道"""
        print("🧹 清洗数据...")
        df = self.df_raw.copy()
        
        # 计算实际金额
        df['total_amount'] = df['quantity'] * df['unit_price'] * (1 - df['discount'])
        
        # 提取时间特征
        df['year'] = df['order_date'].dt.year
        df['month'] = df['order_date'].dt.month
        df['day'] = df['order_date'].dt.day
        df['weekday'] = df['order_date'].dt.weekday
        df['is_weekend'] = df['weekday'].isin([5, 6]).astype(int)
        df['quarter'] = df['order_date'].dt.quarter
        
        # 处理缺失值
        missing_report = df.isnull().sum()
        if missing_report.any():
            print(f"   ⚠️ 发现缺失值:\n{missing_report[missing_report > 0]}")
            numeric_cols = df.select_dtypes(include=[np.number]).columns
            df[numeric_cols] = df[numeric_cols].fillna(df[numeric_cols].median())
            cat_cols = df.select_dtypes(include=['category', 'object']).columns
            for col in cat_cols:
                df[col] = df[col].fillna(df[col].mode()[0])
        
        # 异常值检测与处理 (IQR方法)
        for col in ['total_amount', 'quantity']:
            Q1 = df[col].quantile(0.25)
            Q3 = df[col].quantile(0.75)
            IQR = Q3 - Q1
            lower = Q1 - 3 * IQR
            upper = Q3 + 3 * IQR
            outliers = ((df[col] < lower) | (df[col] > upper)).sum()
            if outliers > 0:
                print(f"   📊 {col}: 检测到 {outliers} 个极端值")
                df[col] = df[col].clip(lower, upper)
        
        # 去重
        before_dedup = len(df)
        df = df.drop_duplicates(subset=['order_id'])
        after_dedup = len(df)
        if before_dedup != after_dedup:
            print(f"   🔁 移除 {before_dedup - after_dedup} 条重复记录")
        
        self.df_clean = df
        print(f"   ✅ 清洗完成: {len(df):,} 条有效记录")
        return df
    
    # ──────────────────────────────────────────────
    # 第二阶段:探索性数据分析 (EDA)
    # ──────────────────────────────────────────────
    
    def eda_summary(self) -> dict:
        """生成完整的EDA统计摘要"""
        print("\n📈 执行探索性数据分析...")
        df = self.df_clean
        summary = {}
        
        # 基础统计
        summary['basic_stats'] = {
            '总销售额': f"¥{df['total_amount'].sum():,.2f}",
            '平均订单额': f"¥{df['total_amount'].mean():,.2f}",
            '中位数订单额': f"¥{df['total_amount'].median():,.2f}",
            '总订单数': f"{len(df):,}",
            '独立客户数': f"{df['customer_id'].nunique():,}",
            '独立商品数': f"{df['product_id'].nunique():,}",
            '时间跨度': f"{df['order_date'].min().date()} ~ {df['order_date'].max().date()}",
        }
        
        # 时间趋势
        daily_sales = df.groupby('order_date')['total_amount'].agg(['sum', 'count'])
        daily_sales.columns = ['sales_amount', 'order_count']
        summary['daily_trend'] = daily_sales
        
        # 月度趋势
        monthly_sales = df.groupby(['year', 'month'])['total_amount'].agg([
            'sum', 'mean', 'count'
        ]).reset_index()
        monthly_sales.columns = ['year', 'month', 'total_sales', 'avg_order', 'order_count']
        summary['monthly_trend'] = monthly_sales
        
        # 分类分布
        category_stats = df.groupby('category').agg({
            'total_amount': ['sum', 'mean', 'count'],
            'quantity': 'sum'
        }).round(2)
        category_stats.columns = ['总销售额', '平均订单额', '订单数', '销量']
        category_stats = category_stats.sort_values('总销售额', ascending=False)
        summary['category_stats'] = category_stats
        
        # 地区分布
        region_stats = df.groupby('region').agg({
            'total_amount': 'sum',
            'customer_id': 'nunique',
            'order_id': 'count'
        }).sort_values('total_amount', ascending=False)
        region_stats.columns = ['销售额', '客户数', '订单数']
        summary['region_stats'] = region_stats
        
        # 渠道对比
        channel_stats = df.groupby('channel').agg({
            'total_amount': ['sum', 'mean'],
            'order_id': 'count'
        })
        channel_stats.columns = ['总销售额', '客单价', '订单数']
        channel_stats['占比%'] = (channel_stats['总销售额'] / channel_stats['总销售额'].sum() * 100).round(1)
        summary['channel_stats'] = channel_stats
        
        self.results['eda_summary'] = summary
        self._print_eda_summary(summary)
        return summary
    
    def _print_eda_summary(self, summary: dict):
        """打印格式化的EDA摘要"""
        s = summary['basic_stats']
        print("\n" + "="*50)
        print("📊 销售数据核心指标")
        print("="*50)
        for k, v in s.items():
            print(f"  {k:12s}: {v}")
    
    # ──────────────────────────────────────────────
    # 第三阶段:高级分析
    # ──────────────────────────────────────────────
    
    def customer_segmentation(self, n_clusters: int = 4) -> pd.DataFrame:
        """RFM客户分群"""
        print("\n👥 执行RFM客户分群...")
        df = self.df_clean
        
        reference_date = df['order_date'].max() + timedelta(days=1)
        rfm = df.groupby('customer_id').agg({
            'order_date': lambda x: (reference_date - x.max()).days,
            'order_id': 'count',
            'total_amount': 'sum'
        })
        rfm.columns = ['recency', 'frequency', 'monetary']
        
        rfm['R_score'] = pd.qcut(rfm['recency'], q=5, labels=[5,4,3,2,1], duplicates='drop').astype(int)
        rfm['F_score'] = pd.qcut(rfm['frequency'].rank(method='first'), q=5, labels=[1,2,3,4,5], duplicates='drop').astype(int)
        rfm['M_score'] = pd.qcut(rfm['monetary'], q=5, labels=[1,2,3,4,5], duplicates='drop').astype(int)
        rfm['rfm_score'] = rfm['R_score'].astype(str) + rfm['F_score'].astype(str) + rfm['M_score'].astype(str)
        
        scaler = StandardScaler()
        rfm_scaled = scaler.fit_transform(rfm[['recency', 'frequency', 'monetary']])
        
        kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
        rfm['cluster'] = kmeans.fit_predict(rfm_scaled)
        
        cluster_profile = rfm.groupby('cluster').agg({
            'recency': 'mean',
            'frequency': 'mean',
            'monetary': 'mean',
            'customer_id': 'count'
        }).round(1)
        cluster_profile.columns = ['平均Recency', '平均Frequency', '平均Monetary', '客户数']
        
        cluster_labels = {
            cluster_profile.sort_values('平均Monetary', ascending=False).index[0]: '💎 高价值VIP',
            cluster_profile.sort_values('average_recency').index[0]: '🔄 流失风险',
            cluster_profile.sort_values('average_frequency').index[0]: '🌱 新兴潜力',
        }
        rfm['segment'] = rfm['cluster'].map(cluster_labels).fillna('👤 一般客户')
        
        self.results['rfm'] = rfm
        self._print_segmentation(rfm, cluster_profile)
        return rfm
    
    def _print_segmentation(self, rfm: pd.DataFrame, profile: pd.DataFrame):
        """打印分群结果"""
        print("\n" + "="*60)
        print("🎯 客户分群结果")
        print("="*60)
        seg_counts = rfm['segment'].value_counts()
        for seg, count in seg_counts.items():
            pct = count / len(rfm) * 100
            print(f"  {seg:16s} : {count:>8,} 客户 ({pct:.1f}%)")
    
    def seasonality_analysis(self) -> dict:
        """季节性分析"""
        print("\n📅 执行季节性分析...")
        df = self.df_clean
        
        monthly_pattern = df.groupby('month').agg({
            'total_amount': ['sum', 'mean'],
            'order_id': 'count'
        })
        monthly_pattern.columns = ['总销售额', '平均日销', '订单数']
        monthly_pattern.index.name = '月份'
        
        weekday_names = ['周一','周二','周三','周四','周五','周六','周日']
        weekday_pattern = df.groupby('weekday').agg({
            'total_amount': ['sum', 'mean']
        })
        weekday_pattern.index = [weekday_names[i] for i in weekday_pattern.index]
        weekday_pattern.columns = ['总销售额', '平均订单额']
        
        quarterly = df.groupby(['year', 'quarter'])['total_amount'].sum().unstack(level=0)
        
        result = {'monthly': monthly_pattern, 'weekday': weekday_pattern, 'quarterly': quarterly}
        self.results['seasonality'] = result
        return result
    
    # ──────────────────────────────────────────────
    # 第四阶段:预测建模
    # ──────────────────────────────────────────────
    
    def build_prediction_models(self) -> dict:
        """构建多种预测模型并进行比较"""
        print("\n🤖 构建预测模型...")
        df = self.df_clean.copy()
        
        daily = df.groupby('order_date').agg({
            'total_amount': 'sum',
            'order_id': 'count',
            'quantity': 'sum',
            'customer_id': 'nunique',
            'discount': 'mean'
        }).reset_index()
        daily.columns = ['date', 'sales', 'orders', 'qty', 'customers', 'avg_discount']
        
        daily['dayofyear'] = daily['date'].dt.dayofyear
        daily['weekofyear'] = daily['date'].dt.isocalendar().week.astype(int)
        daily['month'] = daily['date'].dt.month
        day_of_week = daily['date'].dt.weekday
        for i in range(7):
            daily[f'dow_{i}'] = (day_of_week == i).astype(int)
        
        for lag in [1, 7, 14, 28]:
            daily[f'sales_lag{lag}'] = daily['sales'].shift(lag)
            daily[f'orders_lag{lag}'] = daily['orders'].shift(lag)
        
        for window in [7, 14, 30]:
            daily[f'sales_ma{window}'] = daily['sales'].rolling(window=window).mean()
            daily[f'sales_std{window}'] = daily['sales'].rolling(window=window).std()
        
        daily = daily.dropna()
        
        feature_cols = [c for c in daily.columns if c not in ['date', 'sales']]
        X = daily[feature_cols]
        y = daily['sales']
        
        tscv = TimeSeriesSplit(n_splits=5)
        
        models = {
            'Ridge': Ridge(alpha=10),
            'GradientBoosting': GradientBoostingRegressor(
                n_estimators=200, max_depth=5, learning_rate=0.05,
                subsample=0.8, random_state=42
            ),
            'RandomForest': RandomForestRegressor(
                n_estimators=150, max_depth=10, min_samples_leaf=5,
                n_jobs=-1, random_state=42
            ),
        }
        
        model_results = {}
        for name, model in models.items():
            cv_scores = cross_val_score(model, X, y, cv=tscv,
                                        scoring='neg_mean_absolute_percentage_error')
            mape_scores = -cv_scores
            
            X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.15, shuffle=False)
            model.fit(X_train, y_train)
            y_pred = model.predict(X_test)
            
            model_results[name] = {
                'model': model,
                'cv_mape_mean': mape_scores.mean(),
                'cv_mape_std': mape_scores.std(),
                'test_mae': mean_absolute_error(y_test, y_pred),
                'test_rmse': np.sqrt(mean_squared_error(y_test, y_pred)),
                'test_r2': r2_score(y_test, y_pred),
                'predictions': pd.Series(y_pred, index=y_test.index),
            }
        
        best_model_name = min(model_results.keys(), key=lambda k: model_results[k]['cv_mape_mean'])
        best_result = model_results[best_model_name]
        
        self.models = model_results
        self.feature_df = daily
        self.results['prediction'] = {
            'all_models': {k: {kk: vv for kk, vv in v.items() if kk != 'model'}
                          for k, v in model_results.items()},
            'best_model': best_model_name,
            'best_metrics': {k: v for k, v in best_result.items() if k != 'model'},
        }
        
        self._print_model_comparison(model_results, best_model_name)
        return model_results
    
    def _print_model_comparison(self, results: dict, best: str):
        """打印模型比较结果"""
        print("\n" + "="*65)
        print("🏆 预测模型性能对比 (MAPE↓)")
        print("="*65)
        print(f"{'模型':<20s} {'CV-MAPE':>10s} {'±Std':>8s} {'Test-R²':>10s} {'排名':>4s}")
        print("-"*65)
        sorted_models = sorted(results.keys(), key=lambda k: results[k]['cv_mape_mean'])
        for rank, name in enumerate(sorted_models, 1):
            r = results[name]
            marker = " 👑" if name == best else ""
            print(f"{name:<20s} {r['cv_mape_mean']:>9.2f}% {r['cv_mape_std']:>7.2f}% "
                  f"{r['test_r2']:>9.3f} {rank:>3d}{marker}")
    
    def forecast_next_days(self, days: int = 30) -> pd.DataFrame:
        """使用最佳模型预测未来N天"""
        print(f"\n🔮 预测未来 {days} 天销售额...")
        
        best_name = self.results['prediction']['best_model']
        model = self.models[best_name]['model']
        daily = self.feature_df
        
        last_date = daily['date'].max()
        future_dates = [last_date + timedelta(days=i+1) for i in range(days)]
        
        last_row = daily.iloc[-1:].copy()
        forecasts = []
        for i, date in enumerate(future_dates):
            row = last_row.copy()
            row['date'] = date
            row['dayofyear'] = date.timetuple().tm_yday
            row['weekofyear'] = date.isocalendar()[1]
            row['month'] = date.month
            
            dow = date.weekday()
            for d in range(7):
                row[f'dow_{d}'] = 1 if d == dow else 0
            
            for lag in [1, 7, 14, 28]:
                idx = len(daily) - lag + i
                if idx >= 0 and idx < len(daily):
                    row[f'sales_lag{lag}'] = daily['sales'].iloc[idx]
                    row[f'orders_lag{lag}'] = daily['orders'].iloc[idx]
            
            for window in [7, 14, 30]:
                recent = daily['sales'].iloc[-window:]
                row[f'sales_ma{window}'] = recent.mean()
                row[f'sales_std{window}'] = recent.std()
            
            feature_cols = [c for c in daily.columns if c not in ['date', 'sales']]
            pred = model.predict(row[feature_cols])[0]
            forecasts.append({'date': date, 'predicted_sales': max(0, pred)})
            last_row = row
            last_row['sales'] = pred
        
        forecast_df = pd.DataFrame(forecasts)
        total_forecast = forecast_df['predicted_sales'].sum()
        avg_daily = forecast_df['predicted_sales'].mean()
        
        print(f"   📊 预测期间: {future_dates[0].date()} ~ {future_dates[-1].date()}")
        print(f"   💰 预测总销售额: ¥{total_forecast:,.2f}")
        print(f"   📈 平均日销售额: ¥{avg_daily:,.2f}")
        
        self.results['forecast'] = forecast_df
        return forecast_df
    
    # ──────────────────────────────────────────────
    # 第五阶段:可视化
    # ──────────────────────────────────────────────
    
    def create_visualizations(self, output_dir: str = './output') -> list[str]:
        """生成全套可视化图表"""
        print("\n🎨 生成可视化图表...")
        out = Path(output_dir)
        out.mkdir(exist_ok=True)
        generated = []
        
        # 图1: 销售趋势图
        fig, axes = plt.subplots(2, 1, figsize=(14, 10))
        daily = self.results['eda_summary']['daily_trend']
        ax = axes[0]
        ax.plot(daily.index, daily['sales_amount'], color='#2E86AB', linewidth=0.8, alpha=0.7)
        ma7 = daily['sales_amount'].rolling(7).mean()
        ma30 = daily['sales_amount'].rolling(30).mean()
        ax.plot(ma7.index, ma7, color='#E94F37', linewidth=2, label='7日均线')
        ax.plot(ma30.index, ma30, color='#F39C12', linewidth=2, label='30日均线')
        ax.set_title('📈 日销售额趋势及移动平均线', fontsize=14, fontweight='bold')
        ax.set_ylabel('销售额 (¥)')
        ax.legend(); ax.grid(True, alpha=0.3)
        
        ax = axes[1]
        monthly = self.results['eda_summary']['monthly_trend']
        monthly['period'] = monthly['year'].astype(str) + '-' + monthly['month'].astype(str).str.zfill(2)
        bars = ax.bar(range(len(monthly)), monthly['total_sales'], color='#3498DB', alpha=0.8)
        ax.set_xticks(range(len(monthly)))
        ax.set_xticklabels(monthly['period'], rotation=45, ha='right', fontsize=8)
        ax.set_title('📊 月度销售额柱状图', fontsize=14, fontweight='bold')
        ax.set_ylabel('销售额 (¥)')
        ax.grid(axis='y', alpha=0.3)
        plt.tight_layout()
        p = out / '01_sales_trend.png'
        fig.savefig(p, dpi=150, bbox_inches='tight')
        plt.close(fig); generated.append(str(p))
        
        # 图2: 客户分群图
        fig, axes = plt.subplots(1, 2, figsize=(14, 6))
        rfm = self.results['rfm']
        seg_counts = rfm['segment'].value_counts()
        colors = ['#E74C3C', '#3498DB', '#2ECC71', '#F39C12']
        ax = axes[0]
        wedges, texts, autotexts = ax.pie(seg_counts.values, labels=None, autopct='%1.1f%%',
            colors=colors[:len(seg_counts)], startangle=90, explode=[0.02]*len(seg_counts), shadow=True)
        ax.legend(wedges, seg_counts.index, loc='center left', bbox_to_anchor=(1, 0.5))
        ax.set_title('👥 客户分群占比', fontsize=14, fontweight='bold')
        
        ax = axes[1]
        rfm_means = rfm.groupby('segment')[['recency', 'frequency', 'monetary']].mean()
        rfm_means.columns = ['Recency(天)', 'Frequency(次)', 'Monetary(¥)']
        x = np.arange(len(rfm_means)); width = 0.25
        for i, col in enumerate(rfm_means.columns):
            offset = (i-1)*width; vals = rfm_means[col].values; normalized = vals/vals.max()
            bars = ax.bar(x+offset, normalized, width, label=col, alpha=0.8)
        ax.set_xticks(x); ax.set_xticklabels([s.replace(' ','\n') for s in rfm_means.index], fontsize=9)
        ax.set_title('🎯 各分群RFM特征对比(归一化)', fontsize=14, fontweight='bold')
        ax.legend(fontsize=9); ax.grid(axis='y', alpha=0.3)
        plt.tight_layout()
        p = out / '02_customer_segments.png'
        fig.savefig(p, dpi=150, bbox_inches='tight'); plt.close(fig); generated.append(str(p))
        
        # 图3: 季节性与渠道分析
        fig, axes = plt.subplots(2, 2, figsize=(14, 11))
        ax = axes[0][0]; monthly_pat = self.results['seasonality']['monthly']
        colors_month = plt.cm.RdYlGn(np.linspace(0.2, 0.8, 12))
        bars = ax.bar(monthly_pat.index, monthly_pat['总销售额'], color=colors_month)
        ax.set_title('📅 月度销售季节性规律', fontsize=13, fontweight='bold'); ax.set_xlabel('月份'); ax.set_xticks(range(1,13))
        
        ax = axes[0][1]; wd_pat = self.results['seasonality']['weekday']
        colors_wd = ['#95A5A6']*5 + ['#E74C3C','#E74C3C']
        ax.bar(wd_pat.index, wd_pat['总销售额'], color=colors_wd)
        ax.set_title('📆 星期销售分布', fontsize=13, fontweight='bold'); ax.tick_params(axis='x', rotation=45)
        
        ax = axes[1][0]; ch_stats = self.results['eda_summary']['channel_stats']
        wedges, texts, autotexts = ax.pie(ch_stats['总销售额'], labels=None, autopct='%1.1f%%',
            colors=plt.cm.Set3.colors[:len(ch_stats)], startangle=90)
        ax.legend(wedges, ch_stats.index, title='渠道', loc='center left', bbox_to_anchor=(1, 0.5))
        ax.set_title('🛒 渠道销售额占比', fontsize=13, fontweight='bold')
        
        ax = axes[1][1]; reg_stats = self.results['eda_summary']['region_stats']
        y_pos = range(len(reg_stats)); bars = ax.barh(y_pos, reg_stats['销售额'],
            color=plt.cm.Blues(np.linspace(0.4, 0.9, len(reg_stats))))
        ax.set_yticks(y_pos); ax.set_yticklabels(reg_stats.index)
        ax.set_title('🗺️ 地区销售额排行', fontsize=13, fontweight='bold'); ax.invert_yaxis()
        for bar, val in zip(bars, reg_stats['销售额']):
            ax.text(bar.get_width(), bar.get_y()+bar.get_height()/2, f'¥{val/10000:.0f}万', va='center', fontsize=8)
        plt.tight_layout()
        p = out / '03_seasonality_channels.png'
        fig.savefig(p, dpi=150, bbox_inches='tight'); plt.close(fig); generated.append(str(p))
        
        # 图4: 预测结果图
        if 'forecast' in self.results:
            fig, axes = plt.subplots(2, 1, figsize=(14, 9))
            ax = axes[0]; daily = self.results['eda_summary']['daily_trend']
            ax.plot(daily.index, daily['sales_amount'], color='#3498DB', linewidth=0.8, alpha=0.7, label='历史数据')
            forecast = self.results['forecast']
            ax.plot(forecast['date'], forecast['predicted_sales'], color='#E74C3C', linewidth=2,
                   marker='o', markersize=3, label='预测数据')
            ax.axvline(x=daily.index[-1], color='gray', linestyle='--', alpha=0.5)
            ax.set_title('🔮 销售额历史趋势与未来预测', fontsize=14, fontweight='bold')
            ax.legend(); ax.grid(alpha=0.3)
            
            ax = axes[1]; pred_info = self.results['prediction']
            model_names = list(pred_info['all_models'].keys())
            mapes = [pred_info['all_models'][m]['cv_mape_mean'] for m in model_names]
            x = np.arange(len(model_names)); ax.bar(x, mapes,
                color=['#E74C3C' if m==pred_info['best_model'] else '#95A5A6' for m in model_names])
            ax.set_xticks(x); ax.set_xticklabels(model_names, rotation=15)
            ax.set_ylabel('CV-MAPE (%) ↓')
            ax.set_title('⚖️ 预测模型误差对比 (越低越好)', fontsize=14, fontweight='bold')
            for i, (m, v) in enumerate(zip(mapes, model_names)):
                marker = " ★最佳" if v == pred_info['best_model'] else ""
                ax.text(i, m+0.3, f'{m:.1f}%{marker}', ha='center', fontsize=9)
            ax.grid(axis='y', alpha=0.3)
            plt.tight_layout()
            p = out / '04_predictions.png'
            fig.savefig(p, dpi=150, bbox_inches='tight'); plt.close(fig); generated.append(str(p))
        
        # 图5: 相关性热力图
        fig, ax = plt.subplots(figsize=(12, 8))
        df = self.df_clean; numeric_df = df[['total_amount','quantity','unit_price','discount','month','weekday']].copy()
        corr = numeric_df.corr(); mask = np.triu(np.ones_like(corr, dtype=bool), k=1)
        sns.heatmap(corr, mask=mask, annot=True, fmt='.2f', cmap='RdBu_r', center=0,
                   square=True, linewidths=0.5, annot_kws={'size':10}, ax=ax)
        ax.set_title('🔗 变量相关性矩阵', fontsize=14, fontweight='bold')
        plt.tight_layout()
        p = out / '05_correlation_heatmap.png'
        fig.savefig(p, dpi=150, bbox_inches='tight'); plt.close(fig); generated.append(str(p))
        
        print(f"   ✅ 已生成 {len(generated)} 张图表 → {out}/")
        return generated
    
    # ──────────────────────────────────────────────
    # 主流程
    # ──────────────────────────────────────────────
    
    def run_full_analysis(self, output_dir: str = './output') -> dict:
        """执行完整分析流程"""
        print("="*60); print("🚀 MonkeyCode 销售数据分析引擎 启动"); print("="*60)
        self.load_data(); self.clean_data(); self.eda_summary()
        self.customer_segmentation(n_clusters=4); self.seasonality_analysis()
        self.build_prediction_models(); self.forecast_next_days(days=30)
        charts = self.create_visualizations(output_dir=output_dir)
        print("\n"+"="*60); print("✅ 全部分析完成!"); print("="*60)
        return {
            'summary': self.results['eda_summary']['basic_stats'],
            'charts': charts,
            'best_model': self.results['prediction']['best_model'],
            'forecast_total': self.results['forecast']['predicted_sales'].sum(),
        }


if __name__ == '__main__':
    analyzer = SalesDataAnalyzer('./data/sales_data.csv')
    report = analyzer.run_full_analysis(output_dir='./reports')

三、实战案例二:用户行为漏斗分析

"""
用户行为漏斗分析 - MonkeyCode生成
适用于:产品运营、增长分析、转化率优化
"""

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch
from dataclasses import dataclass
from typing import Optional


@dataclass
class FunnelStage:
    """漏斗阶段定义"""
    name: str; description: str; users: int; color: str = '#3498DB'


class FunnelAnalyzer:
    """用户行为漏斗分析器"""
    
    def __init__(self):
        self.stages: list[FunnelStage] = []
        self.conversion_rates: dict[str, float] = {}
        
    def add_stage(self, name: str, users: int, desc: str = '', color: str = '') -> None:
        if not color:
            colors = ['#2980B9','#27AE60','#F39C12','#E67E22','#E74C3C','#9B59B6','#1ABC9C','#34495E']
            color = colors[len(self.stages)%len(colors)]
        stage = FunnelStage(name=name, description=desc, users=users, color=color)
        self.stages.append(stage)
    
    def calculate_metrics(self) -> pd.DataFrame:
        records = []
        for i, stage in enumerate(self.stages):
            prev_users = self.stages[i-1].users if i > 0 else stage.users
            overall_prev = self.stages[0].users
            step_rate = (stage.users/prev_users*100) if prev_users > 0 else 0
            overall_rate = (stage.users/overall_prev*100) if overall_prev > 0 else 0
            dropoff = (100-step_rate) if i > 0 else 0
            records.append({
                '阶段': stage.name, '用户数': stage.users,
                '阶段转化率': f'{step_rate:.1f}%', '总体转化率': f'{overall_rate:.1f}%',
                '流失率': f'{dropoff:.1f}%' if i > 0 else '-',
            })
            self.conversion_rates[stage.name] = overall_rate
        df = pd.DataFrame(records); self.metrics_df = df; return df
    
    def visualize_funnel(self, save_path: Optional[str] = None) -> None:
        fig, ax = plt.subplots(figsize=(12, max(6, len(self.stages)*1.2)))
        max_users = max(s.users for s in self.stages); height = 0.8
        for i, stage in enumerate(self.stages):
            width_ratio = stage.users/max_users; left = (1-width_ratio)/2
            rect = FancyBboxPatch((left, len(self.stages)-1-i-height/2), width_ratio, height,
                boxstyle="round,pad=0.02", facecolor=stage.color, edgecolor='white', linewidth=2, alpha=0.85)
            ax.add_patch(rect)
            ax.text(0.5, len(self.stages)-1-i,
                f'{stage.name}\n{stage.users:,} 用户 ({stage.users/max_users*100:.1f}%)',
                ha='center', va='center', fontsize=11, fontweight='bold', color='white')
            if i > 0:
                prev = self.stages[i-1].users; curr = stage.users; drop_pct = (prev-curr)/prev*100
                mid_y = len(self.stages)-i; ax.annotate(f'-{drop_pct:.1f}%', xy=(1.02,mid_y),
                    fontsize=10, color='#E74C3C', fontweight='bold')
        ax.set_xlim(-0.05, 1.15); ax.set_ylim(-0.3, len(self.stages)); ax.axis('off')
        ax.set_title('🔄 用户行为转化漏斗', fontsize=16, fontweight='bold', pad=20)
        plt.tight_layout()
        if save_path:
            fig.savefig(save_path, dpi=150, bbox_inches='tight', facecolor='white')
            print(f"💾 漏斗图已保存: {save_path}")
        plt.show()


funnel = FunnelAnalyzer()
funnel.add_stage("页面访问", 100000, "用户打开页面")
funnel.add_stage("浏览商品", 65000, "查看至少一个商品")
funnel.add_stage("加入购物车", 28000, "将商品加入购物车")
funnel.add_stage("开始结算", 15000, "进入结算流程")
funnel.add_stage("支付成功", 8500, "完成支付")

metrics = funnel.calculate_metrics()
print(metrics.to_string(index=False))
funnel.visualize_funnel(save_path='./funnel_chart.png')

四、实战案例三:A/B测试统计分析

"""
A/B测试统计分析 - MonkeyCode生成
包含:假设检验、效应量计算、置信区间、可视化
"""

import numpy as np
import pandas as pd
from scipy import stats
from scipy.stats import norm
import matplotlib.pyplot as plt
import seaborn as sns


class ABTestAnalyzer:
    """A/B测试分析器"""
    
    def __init__(self, group_a: np.ndarray, group_b: np.ndarray, metric_name: str = '转化率'):
        self.A = np.array(group_a); self.B = np.array(group_b)
        self.metric_name = metric_name; self.results = {}
    
    def run_full_analysis(self, alpha: float = 0.05) -> dict:
        print("="*55); print(f"🧪 A/B 测试分析: {self.metric_name}"); print("="*55)
        self._descriptive_stats(); self._normality_test()
        is_binary = set(np.unique(self.A)).issubset({0,1})
        if is_binary: self._proportion_test()
        else: self._t_test_or_mannwhitney()
        self._effect_size(); self._confidence_interval()
        self._visualize(); self._conclusion(alpha)
        return self.results
    
    def _descriptive_stats(self):
        r = self.results; r['n_A']=len(self.A); r['n_B']=len(self.B)
        r['mean_A']=np.mean(self.A); r['mean_B']=np.mean(self.B)
        r['std_A']=np.std(self.A,ddof=1); r['std_B']=np.std(self.B,ddof=1)
        r['lift']=(r['mean_B']-r['mean_A'])/r['mean_A']*100
        print(f"\n📊 描述性统计:")
        print(f"  组A (对照): N={r['n_A']:,}, 均值={r['mean_A']:.4f}, 标准差={r['std_A']:.4f}")
        print(f"  组B (实验): N={r['n_B']:,}, 均值={r['mean_B']:.4f}, 标准差={r['std_B']:.4f}")
        print(f"  提升幅度: {r['lift']:+.2f}%")
    
    def _normality_test(self):
        sample_size = min(5000, len(self.A), len(self.B))
        _,p_a = shapiro(self.A[np.random.choice(len(self.A),sample_size,replace=False)])
        _,p_b = shapiro(self.B[np.random.choice(len(self.B),sample_size,replace=False)])
        self.results['normality_p_A']=p_a; self.results['normality_p_B']=p_b
        self.results['is_normal']=p_a>0.05 and p_b>0.05
    
    def _t_test_or_mannwhitney(self):
        if self.results.get('is_normal', False):
            stat,p_value = stats.ttest_ind(self.A,self.B,equal_var=False); method="Welch's t-test"
        else:
            stat,p_value = stats.mannwhitneyu(self.A,self.B,alternative='two-sided'); method="Mann-Whitney U"
        self.results['test_statistic']=stat; self.results['p_value']=p_value; self.results['test_method']=method
        print(f"\n🔬 统计检验 ({method}): 统计量={stat:.4f}, p-value={p_value:.6f}")
    
    def _proportion_test(self):
        n1,n2=len(self.A),len(self.B); p1,p2=np.mean(self.A),np.mean(self.B)
        pooled_p=(np.sum(self.A)+np.sum(self.B))/(n1+n2); se=np.sqrt(pooled_p*(1-pooled_p)*(1/n1+1/n2))
        z=(p2-p1)/se; p_value=2*(1-norm.cdf(abs(z)))
        self.results['test_statistic']=z; self.results['p_value']=p_value; self.results['test_method']="Two-proportion Z-test"
        print(f"\n🔬 比例Z检验: Z={z:.4f}, p-value={p_value:.6f}")
    
    def _effect_size(self):
        pooled_std=np.sqrt((self.results['std_A']**2+self.results['std_B']**2)/2)
        cohens_d=(self.results['mean_B']-self.results['mean_A'])/pooled_std
        interp=["极小","小","中等","大"][sum(abs(cohens_d)>=t for t in [0.2,0.5,0.8])]
        self.results['cohens_d']=cohens_d; self.results['effect_interpretation']=interp
        print(f"\n📐 效应量: Cohen's d = {cohens_d:.4f} ({interp})")
    
    def _confidence_interval(self, ci_level=0.95):
        mean_diff=self.results['mean_B']-self.results['mean_A']
        se=np.sqrt(self.results['std_A']**2/self.results['n_A']+self.results['std_B']**2/self.results['n_B'])
        z_crit=norm.ppf((1+ci_level)/2); margin=z_crit*se
        ci_lower,ci_upper=mean_diff-margin,mean_diff+margin
        lift_lower=ci_lower/self.results['mean_A']*100; lift_upper=ci_upper/self.results['mean_A']*100
        self.results['ci_95']=(ci_lower,ci_upper); self.results['lift_ci_95']=(lift_lower,lift_upper)
        print(f"\n📏 95% 置信区间: 差异=[{ci_lower:.6f},{ci_upper:.6f}] 提升=[{lift_lower:+.2f}%,{lift_upper:+.2f}%]")
    
    def _visualize(self):
        fig,axes=plt.subplots(1,3,figsize=(15,5))
        ax=axes[0]; ax.hist(self.A,bins=50,alpha=0.6,label=f'A(n={len(self.A)})',color='#3498DB',density=True)
        ax.hist(self.B,bins=50,alpha=0.6,label=f'B(n={len(self.B)})',color='#E74C3C',density=True)
        ax.set_title('两组分布对比',fontweight='bold'); ax.legend(); ax.set_xlabel(self.metric_name)
        ax=axes[1]; means=[self.results['mean_A'],self.results['mean_B']]
        ses=[self.results['std_A']/np.sqrt(self.results['n_A']),self.results['std_B']/np.sqrt(self.results['n_B'])]
        ax.bar(['组A\n(对照)','组B\n(实验)],means,yerr=1.96*np.array(ses),capsize=8,color=['#3498DB','#E74C3C'],alpha=0.8,width=0.5)
        ax.set_title('均值 ± 95% CI',fontweight='bold'); ax.set_ylabel(self.metric_name)
        ax=axes[2]; ax.barh(["Cohen's d"],[self.results['cohens_d']],color='#27AE60' if self.results['cohens_d']>0 else '#E74C3C')
        ax.axvline(x=0,color='gray',linestyle='--')
        for t,l in [(0.2,'小效应'),(0.5,'中效应'),(0.8,'大效应')]: ax.axvline(x=t,color='orange',linestyle=':',alpha=0.5,label=l)
        ax.set_title('效应量',fontweight='bold'); ax.legend(fontsize=8)
        plt.suptitle(f'A/B Test Results: {self.metric_name}',fontsize=14,fontweight='bold')
        plt.tight_layout(); plt.savefig('./ab_test_results.png',dpi=150,bbox_inches='tight'); plt.show()
        print("📊 图表已保存: ./ab_test_results.png")
    
    def _conclusion(self, alpha):
        p=self.results['p_value']; sig="✅ 显著" if p<alpha else "❌ 不显著"
        decision="拒绝H₀" if p<alpha else "无法拒绝H₀"
        print(f"\n{'='*55}\n📋 结论 [{sig}] (α={alpha})\n{'='*55}")
        print(f"  决策: {decision} | P值: {p:.6f} | 提升: {self.results['lift']:+.2f}%")
        print(f"  95% CI: [{self.results['lift_ci_95'][0]:+.2f}%,{self.results['lift_ci_95'][1]:+.2f}%]")
        print(f"  效应量: Cohen's d={self.results['cohens_d']:.3f} ({self.results['effect_interpretation']})")


np.random.seed(42)
group_a=np.random.binomial(1,0.120,10000)
group_b=np.random.binomial(1,0.138,10500)

ab_test=ABTestAnalyzer(group_a,group_b,metric_name="购买转化率")
results=ab_test.run_full_analysis(alpha=0.05)

五、MonkeyCode数据分析最佳实践

5.1 数据分析提示词模板

📝 MonkeyCode 数据分析提示词模板:

【基础分析】
"请对以下数据进行完整的探索性分析(EDA),包括:
 - 数据质量检查和清洗建议
 - 关键统计指标汇总
 - 分布分析和异常值识别
 - 变量间相关性分析
 输出Python代码"

【建模预测】
"基于上述数据,构建预测模型:
 - 进行特征工程(时间特征、滞后特征、滚动窗口)
 - 尝试至少3种不同的机器学习算法
 - 使用时间序列交叉验证评估
 - 选择最佳模型并解释原因
 - 预测未来30天的趋势"

【可视化】
"为以上分析结果创建专业级可视化:
 - 销售趋势折线图(含移动平均线)
 - 多维度的组合图表
 - 配色方案采用商务蓝色系
 - 中文标签支持
 - 高分辨率输出(DPI>=150)"

5.2 常见问题与解决方案

问题 MonkeyCode解决方案
不知道用什么库 "推荐最适合当前任务的Python数据分析库"
代码报错 直接粘贴错误信息,MonkeyCode会定位并修复
图表不美观 "优化这个图表的配色、字体和布局"
性能太慢 "优化这段数据处理代码的性能"
不懂统计方法 "解释这段统计代码的含义和适用场景"

六、总结

MonkeyCode在数据分析领域的能力远超简单的代码补全:

  • 🔄 全流程覆盖 — 从数据采集到可视化报告的一站式支持
  • 🧠 智能决策 — 自动选择最优算法和参数配置
  • 📊 专业级输出 — 生产环境可直接使用的代码和图表
  • 🚀 效率飞跃 — 将数天的工作压缩到数小时
  • 📚 知识传递 — 在生成代码的同时解释背后的统计学原理

"好的数据分析工具不只是帮你画图,而是帮你理解数据讲述的故事。MonkeyCode正是这样的伙伴。"


本文最后更新:2026年7月16日
作者:MonkeyCode团队

相关阅读:

下一篇预告:[MonkeyCode游戏开发实战指南]

posted on 2026-07-16 18:36  MonkeyCode  阅读(3)  评论(0)    收藏  举报