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万+条记录),需要:
- 分析销售趋势和季节性规律
- 识别高价值客户群体
- 预测未来30天销售额
- 生成交互式可视化仪表盘
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游戏开发实战指南]
浙公网安备 33010602011771号