你是不是也卡在了这里?
先别急着往下划,让我先问几个扎心的问题:
- 你学了Pandas的
read_csv、groupby、merge,但一遇到脏数据就懵了,不知道是dropna还是fillna,更不知道填什么值才合理; - 你画过柱状图、折线图,但老板说“这个图看起来像小学作业”,你想加趋势线、加误差棒、改配色,却发现Matplotlib的API多到怀疑人生;
- 你能复现网上的教程,但一旦面对自己公司的真实数据——字段名是乱码、时间格式千奇百怪、缺失值藏得神出鬼没——你就彻底抓瞎;
- 你看过很多“数据分析入门”课程,但工作三年了,觉得自己还在初级打杂,简历上写不出亮点,晋升答辩时拿不出像样的案例。
如果你中了哪怕一条,那这篇文章就是为你写的。
我不是来给你讲“什么是Pandas”的,那种东西百度上一搜一大把。我要讲的是:在真实商业场景里,那些教程不会告诉你的坑、那些老鸟心照不宣的技巧、以及怎么把Pandas和Matplotlib真正用成你的武器,而不是摆设。
我们会从一个真实的电商用户流失预测项目出发,把数据清洗、特征工程、可视化分析三步走打通。全程用真实逻辑、真实代码、真实场景,不搞悬浮的理论。
准备好了吗?我们开始。
第一部分:数据清洗——你以为的“清洗”,和真正的“清洗”差了一个银河系
1.1 真实数据长什么样?它不会对你客气
教程里的数据永远是干净的:pd.read_csv('clean_data.csv'),然后直接df.head(),完美。
但现实是:你的数据可能长这样——
import pandas as pd
import numpy as np
# 模拟真实脏数据
raw_data = {
'user_id': ['U001', 'U002', 'U003', '', 'U005', 'U006', 'U007', np.nan],
'signup_date': ['2023-01-15', '2023/02/20', '2023-03-05', '2023-04-10',
'2023-05-01', '05/15/2023', '2023-06-20', '2023-07-01'],
'age': [25, 30, 28, 35, -5, 42, 29, 31],
'total_spent': [1500.5, 2300.0, None, 890.2, 1200.0, 3400.5, 1500.5, 2100.0],
'last_login': ['2024-01-10 08:30:00', '2024-01-09 14:20:00', '2024-01-08',
'2024-01-07 09:15:00', '2024/01/06 16:45:00', '2024-01-05',
'2024-01-04 11:30:00', '2024-01-03 20:00:00'],
'channel': ['organic', 'Paid_Search', 'organic', 'referral', 'Paid_Search',
'organic', 'Referral', 'organic'],
'is_churned': [0, 0, 1, 0, 0, 1, 0, 0]
}
df = pd.DataFrame(raw_data)
print(df)
输出结果你会看到:
user_id有空字符串和NaN;signup_date格式混乱,有的用-,有的用/,有的缺时间;age有个-5,这显然是录入错误;total_spent有None;last_login格式也不统一;channel大小写不一致,'Paid_Search'和'paid_search'其实是同一个渠道;is_churned是目标变量,但看起来还算干净。
这就是真实数据。它不会因为你学过Pandas就变干净。
1.2 清洗的第一步:理解数据,而不是盲目操作
很多初级分析师拿到数据,上来就dropna()、fillna(0),这是大忌。
正确的第一步是:问自己三个问题:
- 每个字段代表什么业务含义?
- 缺失值是怎么产生的?是系统没记录?是用户没填?还是数据丢失?
- 异常值是因为录入错误?还是真实存在的极端情况?
以我们的例子来说:
user_id为空:可能是测试账号或数据同步失败,应该丢弃,因为无法关联用户行为;age = -5:年龄不可能是负数,这是录入错误,应该用中位数填充或删除;total_spent = None:用户可能从未消费,也可能是数据缺失,需要结合业务判断;channel大小写不一致:这是典型的ETL问题,统一转小写即可;signup_date格式混乱:说明数据来源多个系统,需要统一解析。
记住:清洗不是技术活,是业务理解+逻辑判断的结合。
1.3 实战:一步步清洗这个脏数据
我们一步步来,每步都有解释。
Step 1:统一user_id,丢弃无效记录
# 将空字符串和NaN都视为无效user_id
df['user_id'] = df['user_id'].replace('', np.nan)
# 丢弃user_id为空的记录(业务上无法追踪的用户)
df = df.dropna(subset=['user_id'])
print(f"清洗前行数: {len(df)}")
print(f"清洗后行数: {len(df)}")
print(f"丢弃了 {len(df_raw) - len(df)} 条无效记录")
为什么这么做? 因为user_id是主键,没有它就无法关联其他表。空值不是“需要填充”,而是“需要丢弃”。这是很多新人搞错的地方。
Step 2:修复异常年龄
# 检查年龄的分布
print(df['age'].describe())
print(df[df['age'] < 0])
# 将负数年龄视为异常,用中位数填充
median_age = df['age'].median()
df.loc[df['age'] < 0, 'age'] = median_age
print(f"异常年龄已用中位数 {median_age} 填充")
为什么用中位数而不是均值? 因为年龄可能右偏(有大量年轻用户,少数高龄用户),中位数更稳健。这是统计学常识,但很多人不知道。
Step 3:统一日期格式
# 尝试解析所有日期,errors='coerce'会把无法解析的变为NaT
df['signup_date'] = pd.to_datetime(df['signup_date'], errors='coerce')
df['last_login'] = pd.to_datetime(df['last_login'], errors='coerce')
# 检查解析失败的记录
print(df[df['signup_date'].isna()])
print(df[df['last_login'].isna()])
# 如果仍有解析失败,手动修正(实际工作中可能需要看原始数据)
# 这里假设都已成功解析
关键点: pd.to_datetime 的 errors='coerce' 参数非常重要。它不会报错,而是把无法解析的值变成NaT(Not a Time),方便后续统一处理。很多新人遇到格式错误就try-except,效率极低。
Step 4:统一渠道名称
# 统一转小写,并替换空格/下划线为标准格式
df['channel'] = df['channel'].str.lower().str.replace('_', '', regex=False)
# 查看唯一值,确认清洗效果
print(df['channel'].unique())
输出应该是: ['organic', 'paidsearch', 'referral']
为什么这么做? 因为后续做groupby('channel')时,'Paid_Search'和'paid_search'会被当成两个不同的组,导致分析错误。这是典型的“数据一致性问题”,清洗前必须解决。
Step 5:处理消费金额的缺失值
# 分析total_spent的缺失模式
print(df['total_spent'].isna().sum())
print(df[df['total_spent'].isna()])
# 业务判断:total_spent为NaN,可能是新用户(还没消费),也可能是数据丢失
# 结合signup_date和last_login判断
df['days_since_signup'] = (df['last_login'] - df['signup_date']).dt.days
df['days_since_last_login'] = (pd.Timestamp('2024-01-15') - df['last_login']).dt.days
# 如果用户注册后从未登录,或登录时间过久,total_spent为NaN可能是合理的(0消费)
# 否则,用中位数填充
median_spent = df['total_spent'].median()
df['total_spent'] = df['total_spent'].fillna(median_spent)
这里有个陷阱: 不能简单地把NaN填充为0。因为“从未消费”和“数据缺失”是两回事。如果用户已经注册半年但total_spent为NaN,可能是数据同步问题,应该用中位数填充;如果用户昨天刚注册,total_spent为NaN可能是合理的(还没产生消费),这时候填充0更合理。
所以,清洗不是机械操作,需要结合业务逻辑判断。
Step 6:构造衍生特征
清洗完成后,我们开始做特征工程。这是数据分析从“初级”迈向“进阶”的关键一步。
# 1. 用户生命周期阶段
def classify_lifecycle(days_since_signup):
if days_since_signup <= 7:
return 'new'
elif days_since_signup <= 30:
return 'active'
elif days_since_signup <= 90:
return 'at_risk'
else:
return 'churned'
df['lifecycle'] = df['days_since_signup'].apply(classify_lifecycle)
# 2. 活跃度评分(0-100)
def activity_score(days_since_last_login, total_spent):
if days_since_last_login > 180:
return 0
login_score = max(0, 100 - days_since_last_login)
spend_score = min(100, total_spent / 10)
return int((login_score + spend_score) / 2)
df['activity_score'] = df.apply(lambda row: activity_score(row['days_since_last_login'], row['total_spent']), axis=1)
# 3. 消费等级
def spend_tier(total_spent):
if total_spent < 500:
return 'low'
elif total_spent < 1500:
return 'medium'
else:
return 'high'
df['spend_tier'] = df['total_spent'].apply(spend_tier)
print(df.head())
print(df.dtypes)
输出示例:
user_id signup_date age total_spent last_login ... days_since_signup days_since_last_login lifecycle activity_score spend_tier
0 U001 2023-01-15 25 1500.50 2024-01-10 08:30:00 ... 365 5 churned 85 high
1 U002 2023-02-20 00:00:00 30 2300.00 2024-01-09 14:20:00 ... 329 6 churned 88 high
2 U003 2023-03-05 00:00:00 28 890.20 2024-01-08 09:15:00 ... 316 7 churned 82 medium
...
这些衍生特征才是业务分析的真正价值所在。 原始数据只是素材,特征工程才是炼金术。
1.4 清洗的常见陷阱,你踩过几个?
| 陷阱 | 错误做法 | 正确做法 |
|---|---|---|
盲目dropna() |
直接丢弃所有含缺失值的行 | 分析缺失机制,选择性填充或丢弃 |
盲目fillna(0) |
把缺失值填充为0 | 根据业务逻辑填充均值/中位数/预测值 |
| 忽略数据类型 | 日期列存为字符串 | 统一转为datetime类型 |
| 不检查异常值 | 直接使用原始数据 | 用describe()、箱线图检查异常 |
| 清洗后不验证 | 清洗完直接分析 | 检查清洗前后的数据分布变化 |
记住:清洗不是目的,目的是为了得到可靠的分析结果。
第二部分:可视化——你的图,为什么老板看不懂?
2.1 初级可视化的通病
很多分析师的图长这样:
- 颜色花哨,但没有语义;
- 图表类型选错,比如用饼图展示10个类别;
- 坐标轴标签挤在一起,根本看不清;
- 没有标题,或者标题写成“数据可视化”;
- 只放一张图,没有对比,没有上下文。
老板看到这种图,内心OS:“这画的什么玩意儿?我为什么要花5分钟看这个?”
可视化不是展示数据,而是讲述故事。
2.2 Matplotlib的核心思维:面向对象 vs pyplot接口
很多教程只教plt.plot(),这是pyplot接口,简单但难以控制。进阶玩家必须掌握面向对象接口。
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
# 面向对象接口(推荐)
fig, ax = plt.subplots(figsize=(12, 6))
# 画折线图
ax.plot(df['user_id'], df['total_spent'], marker='o', linestyle='-', color='#2E86AB', label='Total Spent')
# 设置标题和标签
ax.set_title('User Total Spending by ID', fontsize=16, fontweight='bold', pad=20)
ax.set_xlabel('User ID', fontsize=12)
ax.set_ylabel('Total Spent ($)', fontsize=12)
# 设置Y轴格式为货币
ax.yaxis.set_major_formatter(ticker.StrMethodFormatter('${x:,.0f}'))
# 添加网格
ax.grid(True, linestyle='--', alpha=0.7)
# 添加图例
ax.legend(loc='upper left')
plt.tight_layout()
plt.savefig('spending_by_user.png', dpi=300, bbox_inches='tight')
plt.show()
关键点:
fig, ax = plt.subplots()让你可以精确控制每个元素;ax.yaxis.set_major_formatter()可以自定义坐标轴格式;plt.tight_layout()自动调整子图间距,避免标签被截断;plt.savefig()保存高分辨率图片,适合报告使用。
2.3 商业场景中常用的5种可视化
1. 用户流失漏斗图
# 按生命周期阶段统计用户数
lifecycle_counts = df['lifecycle'].value_counts().sort_index()
fig, ax = plt.subplots(figsize=(10, 6))
# 漏斗图:用水平条形图模拟
y_pos = np.arange(len(lifecycle_counts))
colors = ['#2E86AB', '#A23B72', '#F18F01', '#C73E1D']
bars = ax.barh(y_pos, lifecycle_counts.values, color=colors, edgecolor='white', height=0.6)
# 在条形上标注数值和百分比
for i, (bar, count) in enumerate(zip(bars, lifecycle_counts.values)):
width = bar.get_width()
ax.text(width + max(lifecycle_counts)*0.01, bar.get_y() + bar.get_height()/2,
f'{count} ({count/len(df)*100:.1f}%)',
va='center', ha='left', fontsize=11, fontweight='bold')
ax.set_yticks(y_pos)
ax.set_yticklabels(lifecycle_counts.index, fontsize=12)
ax.set_xlabel('Number of Users', fontsize=12)
ax.set_title('User Lifecycle Funnel', fontsize=16, fontweight='bold', pad=20)
# 反转Y轴,让new在上面
ax.invert_yaxis()
# 去掉顶部和右侧边框
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.show()
这个图告诉业务方: 有多少用户停留在哪个阶段,流失发生在哪个环节。
2. 消费分布箱线图
”`python fig, axes = plt.subplots(1, 2, figsize=(14, 6))
按渠道分组的消费分布
df.boxplot(column=‘total_spent’, by=‘channel’, ax=axes[0], grid=True) axes[0].set_title(‘Sp
