Python · 第 4 篇
实战:数据清洗与分析
学了这么多,能不能做一个完整项目? 前三篇我们学了 Python 基础、Pandas 数据处理、可视化。现在把它们全部串起来,模拟一个真实的数据分析工作场景:产品经理给你一份原始数据,要求你产出分析结论和可视化报告。
项目背景
你是一家电商平台的数据分析师,产品经理提出以下需求:
“最近 3 个月的销售数据我导出来了,你帮我看看:
- 整体销售趋势怎么样?
- 哪些品类卖得好?哪些在下滑?
- 不同渠道的获客效率如何?
- 有没有什么值得关注的异常?”
第一步:加载与初探
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# 中文字体设置
plt.rcParams["font.sans-serif"] = ["SimHei", "Arial Unicode MS"]
plt.rcParams["axes.unicode_minus"] = False
sns.set_theme(style="whitegrid", font_scale=1.1)
# 模拟原始数据(实际项目中用 pd.read_csv 读取)
np.random.seed(42)
n = 5000
dates = pd.date_range("2024-01-01", "2024-03-31", freq="h")
sample_dates = np.random.choice(dates, n)
raw_data = pd.DataFrame({
"order_id": [f"ORD{str(i).zfill(6)}" for i in range(1, n + 1)],
"order_date": sample_dates,
"user_id": [f"U{np.random.randint(1000, 3000)}" for _ in range(n)],
"category": np.random.choice(
["服装", "数码", "食品", "美妆", "家居", "图书"], n,
p=[0.25, 0.15, 0.20, 0.18, 0.12, 0.10]
),
"channel": np.random.choice(
["微信", "抖音", "小红书", "百度SEM", "自然搜索"], n,
p=[0.30, 0.25, 0.20, 0.10, 0.15]
),
"amount": np.round(np.random.lognormal(5, 1, n), 2),
"quantity": np.random.randint(1, 6, n),
"city": np.random.choice(
["北京", "上海", "广州", "深圳", "杭州", "成都", "武汉", "南京"], n
),
})
# 故意注入一些脏数据
raw_data.loc[np.random.choice(n, 150), "amount"] = np.nan
raw_data.loc[np.random.choice(n, 80), "city"] = None
raw_data.loc[np.random.choice(n, 20), "amount"] = -abs(np.random.normal(500, 200, 20))
raw_data.loc[np.random.choice(n, 5), "amount"] = np.random.uniform(50000, 100000, 5)
print(f"原始数据量: {raw_data.shape[0]} 行, {raw_data.shape[1]} 列")
raw_data.head(10)
数据初探 5 步法
# 1. 基本信息
print("=" * 50)
print("【数据基本信息】")
print(raw_data.info())
# 2. 数值列统计
print("\n【数值列统计】")
print(raw_data.describe())
# 3. 缺失值
print("\n【缺失值统计】")
missing = raw_data.isnull().sum()
missing_pct = (missing / len(raw_data) * 100).round(1)
print(pd.DataFrame({"缺失数": missing, "缺失率%": missing_pct}))
# 4. 各分类字段的唯一值
print("\n【分类字段分布】")
for col in ["category", "channel", "city"]:
print(f"\n{col} ({raw_data[col].nunique()} 个唯一值):")
print(raw_data[col].value_counts().head())
# 5. 日期范围
print(f"\n日期范围: {raw_data['order_date'].min()} ~ {raw_data['order_date'].max()}")
第二步:数据清洗
df = raw_data.copy()
# 2.1 处理缺失值
print(f"清洗前: {len(df)} 行")
# 金额缺失 → 删除(无法合理填充)
df = df.dropna(subset=["amount"])
print(f"删除金额缺失后: {len(df)} 行")
# 城市缺失 → 填充为"未知"
df["city"] = df["city"].fillna("未知")
# 2.2 处理异常值
# 金额为负数 → 可能是退货,标记后排除
df = df[df["amount"] > 0]
print(f"排除负数金额后: {len(df)} 行")
# 金额极端值 → 使用 IQR 方法检测
Q1 = df["amount"].quantile(0.25)
Q3 = df["amount"].quantile(0.75)
IQR = Q3 - Q1
upper_bound = Q3 + 3 * IQR # 使用 3 倍 IQR,更宽松
outliers = df[df["amount"] > upper_bound]
print(f"\n极端高额订单(>{upper_bound:.0f}元): {len(outliers)} 条")
print(outliers[["order_id", "amount", "category"]].head())
# 将极端值截断
df["amount_clean"] = df["amount"].clip(upper=upper_bound)
# 2.3 日期处理
df["order_date"] = pd.to_datetime(df["order_date"])
df["date"] = df["order_date"].dt.date
df["month"] = df["order_date"].dt.to_period("M")
df["weekday"] = df["order_date"].dt.day_name()
df["hour"] = df["order_date"].dt.hour
print(f"\n最终清洗后数据量: {len(df)} 行")
print("新增字段: date, month, weekday, hour, amount_clean")
第三步:探索性分析
3.1 整体销售趋势
daily = df.groupby("date").agg(
订单量=("order_id", "count"),
GMV=("amount_clean", "sum"),
客单价=("amount_clean", "mean"),
用户数=("user_id", "nunique"),
).reset_index()
fig, axes = plt.subplots(2, 2, figsize=(16, 10))
# 每日订单量趋势
axes[0, 0].plot(daily["date"], daily["订单量"], color="#4F46E5", linewidth=1.5)
axes[0, 0].set_title("每日订单量趋势")
axes[0, 0].set_ylabel("订单量")
# 每日 GMV 趋势
axes[0, 1].plot(daily["date"], daily["GMV"], color="#10B981", linewidth=1.5)
axes[0, 1].set_title("每日 GMV 趋势")
axes[0, 1].set_ylabel("GMV (元)")
# 客单价趋势
axes[1, 0].plot(daily["date"], daily["客单价"], color="#F59E0B", linewidth=1.5)
axes[1, 0].set_title("客单价趋势")
axes[1, 0].set_ylabel("客单价 (元)")
# 每日独立用户数
axes[1, 1].plot(daily["date"], daily["用户数"], color="#EF4444", linewidth=1.5)
axes[1, 1].set_title("每日独立用户数")
axes[1, 1].set_ylabel("用户数")
fig.suptitle("电商平台 Q1 销售概览", fontsize=18, fontweight="bold")
plt.tight_layout()
plt.savefig("01_daily_overview.png", dpi=150, bbox_inches="tight")
plt.show()
3.2 品类分析
category_stats = df.groupby("category").agg(
订单量=("order_id", "count"),
GMV=("amount_clean", "sum"),
客单价=("amount_clean", "mean"),
用户数=("user_id", "nunique"),
).sort_values("GMV", ascending=False).reset_index()
category_stats["GMV占比"] = (category_stats["GMV"] / category_stats["GMV"].sum() * 100).round(1)
print("=== 品类销售排名 ===")
print(category_stats.to_string(index=False))
# 可视化
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# 品类 GMV 柱状图
bars = axes[0].barh(
category_stats["category"],
category_stats["GMV"],
color=plt.cm.Set2(np.linspace(0, 1, len(category_stats)))
)
axes[0].set_xlabel("GMV (元)")
axes[0].set_title("各品类 GMV 排名")
for bar, pct in zip(bars, category_stats["GMV占比"]):
axes[0].text(bar.get_width() + 1000, bar.get_y() + bar.get_height()/2,
f"{pct}%", va="center", fontsize=10)
# 品类占比饼图
axes[1].pie(
category_stats["GMV"],
labels=category_stats["category"],
autopct="%1.1f%%",
startangle=90,
colors=plt.cm.Set2(np.linspace(0, 1, len(category_stats)))
)
axes[1].set_title("GMV 品类占比")
plt.tight_layout()
plt.savefig("02_category_analysis.png", dpi=150, bbox_inches="tight")
plt.show()
3.3 月度品类变化趋势
monthly_cat = df.groupby(["month", "category"]).agg(
GMV=("amount_clean", "sum")
).reset_index()
monthly_cat["month"] = monthly_cat["month"].astype(str)
plt.figure(figsize=(12, 6))
sns.barplot(data=monthly_cat, x="month", y="GMV", hue="category", palette="Set2")
plt.title("各品类月度 GMV 变化", fontsize=16, fontweight="bold")
plt.xlabel("月份")
plt.ylabel("GMV (元)")
plt.legend(title="品类", bbox_to_anchor=(1.02, 1), loc="upper left")
plt.tight_layout()
plt.savefig("03_monthly_category.png", dpi=150, bbox_inches="tight")
plt.show()
3.4 渠道分析
channel_stats = df.groupby("channel").agg(
订单量=("order_id", "count"),
GMV=("amount_clean", "sum"),
用户数=("user_id", "nunique"),
客单价=("amount_clean", "mean"),
).reset_index()
# 计算人均贡献
channel_stats["人均GMV"] = channel_stats["GMV"] / channel_stats["用户数"]
channel_stats = channel_stats.sort_values("GMV", ascending=False)
print("=== 渠道效率对比 ===")
print(channel_stats.to_string(index=False, float_format="{:.0f}".format))
# 可视化:渠道效率气泡图
plt.figure(figsize=(10, 7))
scatter = plt.scatter(
channel_stats["用户数"],
channel_stats["客单价"],
s=channel_stats["GMV"] / 500, # 气泡大小 = GMV
c=range(len(channel_stats)),
cmap="Set1",
alpha=0.7,
edgecolors="black",
linewidth=0.5,
)
for _, row in channel_stats.iterrows():
plt.annotate(
row["channel"],
(row["用户数"], row["客单价"]),
fontsize=12,
fontweight="bold",
ha="center",
va="bottom",
)
plt.xlabel("用户数", fontsize=12)
plt.ylabel("客单价 (元)", fontsize=12)
plt.title("渠道效率气泡图(气泡大小 = GMV)", fontsize=16, fontweight="bold")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("04_channel_bubble.png", dpi=150, bbox_inches="tight")
plt.show()
3.5 时间维度分析
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 每小时订单分布
hourly = df.groupby("hour")["order_id"].count()
axes[0].bar(hourly.index, hourly.values, color="#4F46E5", alpha=0.8)
axes[0].set_title("24 小时订单分布")
axes[0].set_xlabel("小时")
axes[0].set_ylabel("订单量")
axes[0].set_xticks(range(0, 24, 2))
# 星期几订单分布
weekday_order = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
weekday_cn = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
weekly = df.groupby("weekday")["order_id"].count().reindex(weekday_order)
axes[1].bar(weekday_cn, weekly.values, color="#10B981", alpha=0.8)
axes[1].set_title("每周订单分布")
axes[1].set_ylabel("订单量")
plt.tight_layout()
plt.savefig("05_time_analysis.png", dpi=150, bbox_inches="tight")
plt.show()
第四步:深度分析
4.1 RFM 用户分层
# 以 2024-03-31 为基准日期
snapshot_date = pd.Timestamp("2024-03-31")
rfm = df.groupby("user_id").agg(
R=("order_date", lambda x: (snapshot_date - x.max()).days), # 最近一次消费距今天数
F=("order_id", "count"), # 消费频次
M=("amount_clean", "sum"), # 消费总金额
).reset_index()
# 打分(1-4 分)
rfm["R_score"] = pd.qcut(rfm["R"], 4, labels=[4, 3, 2, 1]) # R 越小越好
rfm["F_score"] = pd.qcut(rfm["F"].rank(method="first"), 4, labels=[1, 2, 3, 4])
rfm["M_score"] = pd.qcut(rfm["M"].rank(method="first"), 4, labels=[1, 2, 3, 4])
# 综合标签
def rfm_label(row):
r, f, m = int(row["R_score"]), int(row["F_score"]), int(row["M_score"])
if r >= 3 and f >= 3 and m >= 3:
return "高价值用户"
elif r >= 3 and f < 3:
return "新用户/潜力用户"
elif r < 3 and f >= 3:
return "沉睡忠诚用户"
elif r < 3 and m >= 3:
return "流失高价值用户"
else:
return "低价值用户"
rfm["用户标签"] = rfm.apply(rfm_label, axis=1)
# 统计各群体
segment_stats = rfm.groupby("用户标签").agg(
用户数=("user_id", "count"),
平均消费频次=("F", "mean"),
平均消费金额=("M", "mean"),
).round(1)
segment_stats["占比%"] = (segment_stats["用户数"] / segment_stats["用户数"].sum() * 100).round(1)
print("=== RFM 用户分层 ===")
print(segment_stats)
4.2 品类交叉购买分析
from itertools import combinations
# 找出同一用户购买了哪些品类
user_cats = df.groupby("user_id")["category"].apply(set).reset_index()
user_cats = user_cats[user_cats["category"].apply(len) >= 2] # 至少购买 2 个品类
# 统计品类对的共同购买次数
pair_count = {}
for cats in user_cats["category"]:
for pair in combinations(sorted(cats), 2):
pair_count[pair] = pair_count.get(pair, 0) + 1
cross_buy = pd.DataFrame(
[(p[0], p[1], c) for p, c in pair_count.items()],
columns=["品类A", "品类B", "共同购买用户数"]
).sort_values("共同购买用户数", ascending=False)
print("=== 品类交叉购买 Top 10 ===")
print(cross_buy.head(10).to_string(index=False))
业务建议:交叉购买频率高的品类对可以做联合营销。例如”服装+美妆”共同购买率高,可以在服装详情页推荐美妆产品,或设计跨品类满减活动。
第五步:输出分析报告
关键指标汇总
total_orders = len(df)
total_gmv = df["amount_clean"].sum()
total_users = df["user_id"].nunique()
avg_aov = df["amount_clean"].mean()
avg_orders_per_user = total_orders / total_users
print("=" * 50)
print("电商平台 2024 Q1 销售分析报告")
print("=" * 50)
print(f"分析周期: 2024-01-01 ~ 2024-03-31")
print(f"总订单量: {total_orders:,}")
print(f"总 GMV: ¥{total_gmv:,.0f}")
print(f"独立用户: {total_users:,}")
print(f"客单价: ¥{avg_aov:,.0f}")
print(f"人均下单: {avg_orders_per_user:.1f} 次")
print("=" * 50)
导出完整报告
# 导出到 Excel(多 Sheet)
with pd.ExcelWriter("Q1_analysis_report.xlsx", engine="openpyxl") as writer:
daily.to_excel(writer, sheet_name="每日汇总", index=False)
category_stats.to_excel(writer, sheet_name="品类分析", index=False)
channel_stats.to_excel(writer, sheet_name="渠道分析", index=False)
segment_stats.to_excel(writer, sheet_name="RFM分层")
cross_buy.head(20).to_excel(writer, sheet_name="交叉购买", index=False)
print("分析报告已导出: Q1_analysis_report.xlsx")
分析结论模板
一份完整的数据分析结论应该包含以下要素:
结论一:整体销售稳中有升 Q1 总 GMV 达到 XXX 万元,环比 Q4 增长 X%。其中 3 月表现最好,日均 GMV 达到 XXX 元,主要由”XXX”品类拉动。
结论二:品类表现分化明显 服装品类贡献了 XX% 的 GMV,是绝对主力。数码品类客单价最高(¥XXX),但订单量偏低,建议加大流量投入。图书品类 GMV 占比仅 X%,且环比下滑,需关注。
结论三:渠道效率差异大 微信渠道贡献了最多用户和 GMV,但客单价较低(¥XXX);小红书渠道客单价最高(¥XXX),用户质量好但流量偏小,建议加大投放预算。
建议:
- 加大小红书渠道投放,提升高质量用户占比
- 服装 + 美妆做联合营销,提升交叉购买率
- 关注 20-22 点订单高峰期,在此时段推送促销活动
项目总结
本项目完整演示了数据分析师的日常工作流程:
| 步骤 | 核心操作 | 使用的工具 |
|---|---|---|
| 加载与初探 | 读取数据、5 步探索法 | Pandas |
| 数据清洗 | 缺失值、异常值、类型转换 | Pandas / NumPy |
| 探索性分析 | 趋势、品类、渠道、时间维度 | Pandas + Matplotlib |
| 深度分析 | RFM 分层、交叉购买 | Pandas |
| 输出报告 | 关键指标、图表、结论建议 | Pandas + Excel |
放进简历的建议:将这个项目稍作修改(使用真实公开数据集如 Kaggle 的 E-Commerce 数据),上传到 GitHub,在简历中描述为”独立完成电商平台 Q1 销售分析,产出品类优化和渠道策略建议,协助运营团队提升 GMV”。
恭喜你完成了 Python 数据分析模块的全部学习!接下来进入 可视化 & BI 模块,学习用 Tableau 和 Power BI 制作交互式数据看板。