🕐 预计用时:3-4 小时 | 🎯 目标:用 NumPy + Pandas + Matplotlib + Seaborn 完成完整的数据分析报告
假设你是一家电商公司的数据分析师,老板给你一份销售数据,要求你回答以下问题:
我们先用代码生成一份模拟电商数据,方便后续分析。
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
np.random.seed(42)
# 生成 10000 条订单数据
n_orders = 10000
# 用户信息(500 个用户)
n_users = 500
user_ids = np.random.randint(1001, 1001 + n_users, n_orders)
genders = np.random.choice(['男', '女'], n_orders, p=[0.55, 0.45])
ages = np.random.normal(30, 8, n_orders).astype(int)
ages = np.clip(ages, 18, 65)
# 商品品类和价格
categories = ['电子产品', '服装', '食品', '家居', '美妆', '图书']
cat_probs = [0.25, 0.20, 0.15, 0.15, 0.15, 0.10]
order_categories = np.random.choice(categories, n_orders, p=cat_probs)
# 品类价格范围
price_ranges = {
'电子产品': (200, 8000),
'服装': (50, 2000),
'食品': (10, 300),
'家居': (30, 3000),
'美妆': (30, 1500),
'图书': (15, 200),
}
amounts = []
for cat in order_categories:
low, high = price_ranges[cat]
amounts.append(np.random.uniform(low, high))
amounts = np.round(amounts, 2)
# 数量
quantities = np.random.choice([1, 1, 1, 2, 2, 3], n_orders)
# 日期(2025年全年,含随机下单小时)
start_date = datetime(2025, 1, 1)
end_date = datetime(2025, 12, 31)
days_range = (end_date - start_date).days
order_dates = [start_date + timedelta(days=np.random.randint(0, days_range + 1),
hours=np.random.randint(0, 24)) for _ in range(n_orders)]
# 地区
regions = ['华东', '华南', '华北', '华中', '西南', '西北', '东北']
region_probs = [0.30, 0.20, 0.18, 0.12, 0.08, 0.07, 0.05]
order_regions = np.random.choice(regions, n_orders, p=region_probs)
# 创建 DataFrame
df = pd.DataFrame({
'订单号': range(100001, 100001 + n_orders),
'用户ID': user_ids,
'日期': order_dates,
'品类': order_categories,
'单价': amounts,
'数量': quantities,
'性别': genders,
'年龄': ages,
'地区': order_regions,
})
# 计算总金额
df['总金额'] = df['单价'] * df['数量']
# 保存为 CSV
df.to_csv('ecommerce_sales.csv', index=False, encoding='utf-8-sig')
print(f'数据生成完成!共 {len(df)} 条订单')
print(df.head(10))import pandas as pd
import numpy as np
df = pd.read_csv('ecommerce_sales.csv', encoding='utf-8-sig')
# === 1. 基本信息 ===
print(df.shape) # (10000, 10)
print(df.dtypes) # 查看数据类型
print(df.info()) # 概览
# === 2. 缺失值检查 ===
print(df.isnull().sum())
# 本数据无缺失值(模拟数据很干净)
# 如果有缺失值:
# df['年龄'].fillna(df['年龄'].median(), inplace=True)
# df['地区'].fillna('未知', inplace=True)
# === 3. 重复值检查 ===
print(f'重复行数: {df.duplicated().sum()}')
df.drop_duplicates(inplace=True)
# === 4. 异常值检查 ===
print(f'单价范围: {df["单价"].min():.2f} ~ {df["单价"].max():.2f}')
print(f'数量范围: {df["数量"].min()} ~ {df["数量"].max()}')
print(f'年龄范围: {df["年龄"].min()} ~ {df["年龄"].max()}')
# 删除异常值
df = df[df['单价'] > 0]
df = df[df['数量'] > 0]
# === 5. 数据类型转换 ===
df['日期'] = pd.to_datetime(df['日期'])
df['年'] = df['日期'].dt.year
df['月'] = df['日期'].dt.month
df['星期'] = df['日期'].dt.day_name()
df['小时'] = df['日期'].dt.hour
# === 6. 最终确认 ===
print(f'清洗后数据: {df.shape[0]} 行, {df.shape[1]} 列')
print(df.dtypes)import matplotlib.pyplot as plt
import seaborn as sns
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
sns.set_theme(style='whitegrid', palette='Set2')
# === 核心指标 ===
total_sales = df['总金额'].sum()
total_orders = len(df)
total_users = df['用户ID'].nunique()
avg_order = total_sales / total_orders
print('=' * 50)
print('📊 电商销售数据 — 整体概览')
print('=' * 50)
print(f'总销售额: ¥{total_sales:,.2f}')
print(f'总订单数: {total_orders:,}')
print(f'独立用户: {total_users:,}')
print(f'客单价: ¥{avg_order:,.2f}')
print('=' * 50)
# === KPI 卡片图 ===
fig, axes = plt.subplots(1, 4, figsize=(20, 5))
kpis = [
('总销售额', f'¥{total_sales/10000:,.1f}万', '#07c160'),
('总订单数', f'{total_orders:,}', '#4d96ff'),
('独立用户', f'{total_users}', '#ffd93d'),
('客单价', f'¥{avg_order:,.0f}', '#ff6b6b'),
]
for ax, (title, value, color) in zip(axes, kpis):
ax.text(0.5, 0.5, value, ha='center', va='center',
fontsize=28, fontweight='bold', color=color,
transform=ax.transAxes)
ax.text(0.5, 0.15, title, ha='center', va='center',
fontsize=14, color='#666', transform=ax.transAxes)
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.axis('off')
ax.set_facecolor('#f8f9fa')
for spine in ax.spines.values():
spine.set_visible(True)
spine.set_color('#e0e0e0')
fig.suptitle('📊 电商销售 KPI 看板', fontsize=20, fontweight='bold', y=1.02)
plt.tight_layout()
plt.show()# === 月度销售趋势 ===
monthly = df.groupby('月')['总金额'].agg(['sum', 'count', 'mean']).reset_index()
monthly.columns = ['月份', '销售额', '订单数', '客单价']
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
# 销售额趋势
axes[0].plot(monthly['月份'], monthly['销售额'], marker='o',
color='#07c160', linewidth=2, markersize=8)
axes[0].fill_between(monthly['月份'], monthly['销售额'], alpha=0.1, color='#07c160')
axes[0].set_title('月度销售额趋势', fontsize=14)
axes[0].set_xlabel('月份')
axes[0].set_ylabel('销售额(元)')
for i, row in monthly.iterrows():
axes[0].annotate(f'¥{row["销售额"]/10000:.1f}万',
(row['月份'], row['销售额']),
textcoords="offset points", xytext=(0, 10), ha='center', fontsize=9)
# 订单数趋势
axes[1].bar(monthly['月份'], monthly['订单数'], color='#4d96ff', alpha=0.8)
axes[1].set_title('月度订单数趋势', fontsize=14)
axes[1].set_xlabel('月份')
axes[1].set_ylabel('订单数')
# 客单价趋势
axes[2].plot(monthly['月份'], monthly['客单价'], marker='s',
color='#ff6b6b', linewidth=2, markersize=8)
axes[2].set_title('月度客单价趋势', fontsize=14)
axes[2].set_xlabel('月份')
axes[2].set_ylabel('客单价(元)')
for ax in axes:
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# === 星期分布 ===
weekday_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
weekday_labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
weekday_sales = df.groupby('星期')['总金额'].sum().reindex(weekday_order)
fig, ax = plt.subplots(figsize=(10, 6))
bars = ax.bar(weekday_labels, weekday_sales.values,
color=['#4d96ff' if i < 5 else '#ff6b6b' for i in range(7)])
ax.set_title('一周销售额分布(工作日 vs 周末)', fontsize=14)
ax.set_ylabel('销售额(元)')
for bar, val in zip(bars, weekday_sales.values):
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 500,
f'¥{val/10000:.1f}万', ha='center', fontsize=10)
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()# === 品类销售排名 ===
cat_sales = df.groupby('品类')['总金额'].agg(['sum', 'count', 'mean']).reset_index()
cat_sales.columns = ['品类', '销售额', '订单数', '客单价']
cat_sales = cat_sales.sort_values('销售额', ascending=False)
cat_sales['占比'] = cat_sales['销售额'] / cat_sales['销售额'].sum() * 100
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
# 品类销售额柱状图
colors = ['#07c160', '#4d96ff', '#ffd93d', '#ff6b6b', '#6bcb77', '#cccccc']
axes[0].barh(cat_sales['品类'], cat_sales['销售额'], color=colors[:len(cat_sales)])
axes[0].set_xlabel('销售额(元)')
axes[0].set_title('品类销售额排名', fontsize=14)
for i, row in cat_sales.iterrows():
axes[0].text(row['销售额'] + 1000, i, f'¥{row["销售额"]/10000:.1f}万',
va='center', fontsize=10)
# 品类占比饼图
axes[1].pie(cat_sales['占比'], labels=cat_sales['品类'], autopct='%1.1f%%',
colors=colors[:len(cat_sales)], startangle=90, pctdistance=0.85)
axes[1].set_title('品类销售占比', fontsize=14)
# 品类订单数 vs 客单价散点图
scatter = axes[2].scatter(cat_sales['订单数'], cat_sales['客单价'],
s=cat_sales['销售额']/100, alpha=0.7,
c=colors[:len(cat_sales)], edgecolors='gray')
for i, row in cat_sales.iterrows():
axes[2].annotate(row['品类'], (row['订单数'], row['客单价']),
textcoords="offset points", xytext=(5, 5), fontsize=10)
axes[2].set_xlabel('订单数')
axes[2].set_ylabel('客单价(元)')
axes[2].set_title('品类订单数 vs 客单价(气泡=销售额)', fontsize=14)
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# === 品类×月份热力图 ===
cat_month = df.pivot_table(values='总金额', index='品类', columns='月', aggfunc='sum', fill_value=0)
fig, ax = plt.subplots(figsize=(14, 6))
sns.heatmap(cat_month, annot=True, fmt='.0f', cmap='YlOrRd',
linewidths=0.5, ax=ax)
ax.set_title('品类 × 月份 销售额热力图', fontsize=14)
ax.set_xlabel('月份')
ax.set_ylabel('品类')
plt.tight_layout()
plt.show()# === 性别分布 ===
gender_sales = df.groupby('性别')['总金额'].agg(['sum', 'count']).reset_index()
gender_sales.columns = ['性别', '销售额', '订单数']
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
axes[0].pie(gender_sales['销售额'], labels=gender_sales['性别'],
autopct='%1.1f%%', colors=['#4d96ff', '#ff6b6b'], startangle=90)
axes[0].set_title('男女销售额占比', fontsize=14)
# 年龄分布
axes[1].hist(df['年龄'], bins=20, color='#07c160', edgecolor='white', alpha=0.8)
axes[1].axvline(df['年龄'].mean(), color='red', linestyle='--', linewidth=2,
label=f'平均年龄: {df["年龄"].mean():.1f}岁')
axes[1].set_title('用户年龄分布', fontsize=14)
axes[1].set_xlabel('年龄')
axes[1].set_ylabel('订单数')
axes[1].legend()
plt.tight_layout()
plt.show()
# === 地区分析 ===
region_sales = df.groupby('地区')['总金额'].sum().sort_values(ascending=False)
fig, ax = plt.subplots(figsize=(10, 6))
bars = ax.bar(region_sales.index, region_sales.values, color=colors[:len(region_sales)])
ax.set_title('各地区销售额', fontsize=14)
ax.set_ylabel('销售额(元)')
for bar, val in zip(bars, region_sales.values):
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 500,
f'¥{val/10000:.1f}万', ha='center', fontsize=10)
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()
# === 年龄段分析 ===
df['年龄段'] = pd.cut(df['年龄'], bins=[0, 20, 25, 30, 35, 40, 50, 100],
labels=['20以下', '20-25', '25-30', '30-35', '35-40', '40-50', '50以上'])
age_sales = df.groupby('年龄段', observed=False)['总金额'].agg(['sum', 'mean', 'count']).reset_index()
age_sales.columns = ['年龄段', '总消费', '平均消费', '订单数']
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
axes[0].bar(age_sales['年龄段'], age_sales['总消费'], color='#07c160')
axes[0].set_title('各年龄段总消费', fontsize=14)
axes[0].set_ylabel('总消费(元)')
axes[1].bar(age_sales['年龄段'], age_sales['平均消费'], color='#4d96ff')
axes[1].set_title('各年龄段平均消费', fontsize=14)
axes[1].set_ylabel('平均消费(元)')
axes[2].bar(age_sales['年龄段'], age_sales['订单数'], color='#ffd93d')
axes[2].set_title('各年龄段订单数', fontsize=14)
axes[2].set_ylabel('订单数')
for ax in axes:
ax.tick_params(axis='x', rotation=45)
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()# === 品类×性别的交叉分析 ===
cross = pd.crosstab(df['品类'], df['性别'], values=df['总金额'], aggfunc='sum')
fig, ax = plt.subplots(figsize=(10, 6))
cross.plot(kind='bar', ax=ax, color=['#ff6b6b', '#4d96ff'], alpha=0.8)
ax.set_title('品类 × 性别 销售额对比', fontsize=14)
ax.set_ylabel('销售额(元)')
ax.set_xticklabels(ax.get_xticklabels(), rotation=45)
ax.legend(title='性别')
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()
# === 价格区间分析 ===
df['价格区间'] = pd.cut(df['单价'], bins=[0, 50, 100, 300, 500, 1000, 5000, 10000],
labels=['0-50', '50-100', '100-300', '300-500',
'500-1000', '1000-5000', '5000+'])
price_dist = df['价格区间'].value_counts().sort_index()
fig, ax = plt.subplots(figsize=(10, 6))
ax.bar(price_dist.index, price_dist.values, color='#07c160', alpha=0.8)
ax.set_title('订单价格区间分布', fontsize=14)
ax.set_xlabel('价格区间(元)')
ax.set_ylabel('订单数')
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()
# === RFM 分析(简化版)===
# R (Recency): 最近一次购买距今天数
# F (Frequency): 购买频次
# M (Monetary): 消费总额
max_date = df['日期'].max()
rfm = df.groupby('用户ID').agg({
'日期': lambda x: (max_date - x.max()).days, # R
'订单号': 'count', # F
'总金额': 'sum' # M
}).reset_index()
rfm.columns = ['用户ID', 'R', 'F', 'M']
# 打分(简化版:按分位数打 1-5 分)
# 注意:用 rank(method='first') 避免重复值导致 qcut 报错
rfm['R_score'] = pd.qcut(rfm['R'].rank(method='first'), 5, labels=[5, 4, 3, 2, 1]).astype(int)
rfm['F_score'] = pd.qcut(rfm['F'].rank(method='first'), 5, labels=[1, 2, 3, 4, 5]).astype(int)
rfm['M_score'] = pd.qcut(rfm['M'].rank(method='first'), 5, labels=[1, 2, 3, 4, 5]).astype(int)
rfm['RFM_score'] = rfm['R_score'] + rfm['F_score'] + rfm['M_score']
# 用户分层
def rfm_label(row):
if row['RFM_score'] >= 12:
return '高价值用户'
elif row['RFM_score'] >= 9:
return '中价值用户'
elif row['RFM_score'] >= 6:
return '低价值用户'
else:
return '流失风险用户'
rfm['用户等级'] = rfm.apply(rfm_label, axis=1)
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
# 用户等级分布
level_counts = rfm['用户等级'].value_counts()
axes[0].pie(level_counts, labels=level_counts.index, autopct='%1.1f%%',
colors=['#07c160', '#4d96ff', '#ffd93d', '#ff6b6b'])
axes[0].set_title('RFM 用户分层', fontsize=14)
# RFM 散点图
scatter = axes[1].scatter(rfm['F'], rfm['M'], c=rfm['R_score'],
cmap='RdYlGn', s=30, alpha=0.6)
axes[1].set_xlabel('购买频次 (F)')
axes[1].set_ylabel('消费总额 (M)')
axes[1].set_title('RFM 散点图(颜色=R_score)', fontsize=14)
plt.colorbar(scatter, ax=axes[1], label='R_score')
# 各等级平均消费
level_monetary = rfm.groupby('用户等级', observed=False)['M'].mean()
axes[2].bar(level_monetary.index, level_monetary.values,
color=['#07c160', '#4d96ff', '#ffd93d', '#ff6b6b'])
axes[2].set_title('各等级用户平均消费', fontsize=14)
axes[2].set_ylabel('平均消费(元)')
axes[2].grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()
print('\n📊 RFM 用户分层统计:')
print(rfm['用户等级'].value_counts())# 将所有分析整合为一份完整的 HTML 报告
# 以下为报告框架,实际项目中可生成 HTML 或 PDF
print("""
╔══════════════════════════════════════════════════╗
║ 📊 2025年度电商销售分析报告 ║
╠══════════════════════════════════════════════════╣
║ ║
║ 一、整体概览 ║
║ • 总销售额:¥XXX 万 ║
║ • 总订单数:XX,XXX ║
║ • 独立用户:XXX ║
║ • 客单价:¥XXX ║
║ ║
║ 二、销售趋势 ║
║ • 月度趋势:X月最高,X月最低 ║
║ • 周度规律:周X销售最好 ║
║ • 同比增长:XX% ║
║ ║
║ 三、品类分析 ║
║ • Top1品类:XXX(占比XX%) ║
║ • 客单价最高:XXX(¥XXXX) ║
║ • 订单数最多:XXX ║
║ ║
║ 四、用户画像 ║
║ • 男女比例:X:X ║
║ • 核心年龄段:XX-XX岁 ║
║ • Top地区:XX(占比XX%) ║
║ ║
║ 五、RFM 分析 ║
║ • 高价值用户:XX人(XX%) ║
║ • 流失风险用户:XX人 ║
║ • 建议:针对XX用户群做XX活动 ║
║ ║
╚══════════════════════════════════════════════════╝
""")🎉 数据科学综合项目完成!
今天你走完了一个完整的数据分析流程:
数据生成 → 清洗 → 概览 → 趋势 → 品类 → 用户 → 关联 → RFM → 报告
这就是数据分析师的日常工作!接下来进入 自动化 阶段(Day 79-84)—— Selenium 自动化、Excel 处理、邮件发送、定时任务。