3. 数据初探:加载电商客户数据集、数据清洗与描述性统计分析
好,咱们进入实战环节。数据挖掘这行当,有一句老话叫「垃圾进,垃圾出」。你模型再花哨,数据本身有问题,结果就是白搭。所以,拿到任何数据集,第一步不是急着跑算法,而是先跟数据「打个招呼」。
我个人习惯,先看看数据长什么样,心里有个底。今天咱们用的是一份电商客户数据集,里面记录了客户的各种行为信息。说白了,就是一堆用户买了什么、花了多少钱、啥时候买的这些事。
3.1 加载数据集:先看看「食材」
我一般用 Pandas 来加载数据,它就像一把瑞士军刀,啥都能干。咱们先把它请出来。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# 设置中文显示,不然画图全是框框
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 加载数据
df = pd.read_csv('ecommerce_data.csv', encoding='utf-8')
# 快速看一眼
print('数据集形状:', df.shape)
print('\n前5行数据:')
print(df.head())
print('\n数据基本信息:')
print(df.info())
嗯,这里要注意。加载的时候,encoding 参数经常出问题。我在项目中遇到过,有些 CSV 文件是 gbk 编码的,你直接读就会报错。所以,如果报编码错误,试试 encoding='gbk' 或者 encoding='ISO-8859-1'。
跑完上面的代码,你会看到类似这样的输出:
| 列名 | 非空计数 | 数据类型 |
|---|---|---|
| CustomerID | 2000 | int64 |
| Gender | 2000 | object |
| Age | 1985 | float64 |
| Annual Income (k$) | 2000 | int64 |
| Spending Score (1-100) | 2000 | int64 |
| Purchase Amount (k$) | 1990 | float64 |
| Last Purchase Date | 2000 | object |
看到了吗?Age 和 Purchase Amount 这两列,非空计数比总行数少。这说明什么?有缺失值了。另外,Last Purchase Date 是 object 类型,也就是字符串,咱们后面得把它转成日期格式。
df.describe() 快速看数值列的统计量,比如均值、标准差、最大最小值。这能帮你一眼发现异常值。
3.2 数据清洗:把「烂菜叶」摘掉
数据清洗,说白了就是处理缺失值和异常值。这是最磨人的一步,但也是最关键的。
3.2.1 缺失值处理
咱们先看看缺失值到底有多少:
# 检查缺失值
print('缺失值统计:')
print(df.isnull().sum())
# 缺失值占比
print('\n缺失值占比:')
print((df.isnull().sum() / len(df)) * 100)
输出结果:
| 列名 | 缺失数量 | 缺失占比 |
|---|---|---|
| Age | 15 | 0.75% |
| Purchase Amount (k$) | 10 | 0.50% |
缺失比例很小,不到1%。对于这种情况,我一般直接删掉这些行,因为对整体影响不大。但如果缺失比例超过5%,你就得想想办法了,比如用均值、中位数填充。
# 删除缺失值
df_clean = df.dropna(subset=['Age', 'Purchase Amount (k$)'])
# 验证一下
print('清洗后数据集形状:', df_clean.shape)
print('还有缺失值吗?', df_clean.isnull().sum().sum())
3.2.2 异常值处理
异常值,就是那些「离谱」的数据。比如年龄填了200岁,或者消费金额是负数。咱们用箱线图来揪出它们。
# 画箱线图
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
sns.boxplot(y=df_clean['Age'], ax=axes[0])
axes[0].set_title('年龄分布')
sns.boxplot(y=df_clean['Annual Income (k$)'], ax=axes[1])
axes[1].set_title('年收入分布')
sns.boxplot(y=df_clean['Spending Score (1-100)'], ax=axes[2])
axes[2].set_title('消费评分分布')
plt.tight_layout()
plt.show()
你想想看,箱线图里那些「小圆点」就是异常值。对于年龄,如果出现小于0或大于120的,那肯定是异常。对于年收入,如果出现负数,也得处理。
我处理异常值的策略是这样的:
- 年龄异常: 小于18岁或大于100岁的,我直接剔除。因为电商客户一般不会这么极端。
- 收入异常: 小于0的,剔除。如果出现特别大的值(比如超过99%分位数),我会用99%分位数替换,而不是直接删掉。
- 消费评分: 这个范围是1-100,超出这个范围的,直接删掉。
# 处理异常值
# 年龄:保留18-100岁
df_clean = df_clean[(df_clean['Age'] >= 18) & (df_clean['Age'] <= 100)]
# 年收入:保留大于0,且用99%分位数封顶
income_99 = df_clean['Annual Income (k$)'].quantile(0.99)
df_clean = df_clean[df_clean['Annual Income (k$)'] > 0]
df_clean['Annual Income (k$)'] = df_clean['Annual Income (k$)'].clip(upper=income_99)
# 消费评分:保留1-100
df_clean = df_clean[(df_clean['Spending Score (1-100)'] >= 1) &
(df_clean['Spending Score (1-100)'] <= 100)]
print('最终数据集形状:', df_clean.shape)
3.3 描述性统计分析:给数据「把把脉」
数据洗干净了,咱们得看看它到底长什么样。描述性统计,就是给数据做个「体检报告」。
# 描述性统计
print('数值列的统计描述:')
print(df_clean.describe())
# 分类列的统计
print('\n性别分布:')
print(df_clean['Gender'].value_counts())
print('\n性别占比:')
print(df_clean['Gender'].value_counts(normalize=True) * 100)
输出结果:
| 统计量 | Age | Annual Income (k$) | Spending Score | Purchase Amount (k$) |
|---|---|---|---|---|
| count | 1960 | 1960 | 1960 | 1960 |
| mean | 38.85 | 60.56 | 50.20 | 62.34 |
| std | 13.97 | 26.26 | 25.82 | 36.78 |
| min | 18.00 | 15.00 | 1.00 | 1.02 |
| 25% | 28.00 | 41.50 | 34.75 | 32.15 |
| 50% | 36.00 | 61.50 | 50.00 | 61.80 |
| 75% | 49.00 | 78.00 | 73.00 | 91.20 |
| max | 100.00 | 137.00 | 100.00 | 189.00 |
从这张表里,咱们能读出不少信息:
- 年龄: 平均38.85岁,中位数36岁。说明客户偏年轻,25%的客户在28岁以下。嗯,这很电商。
- 年收入: 平均60.56k,但标准差有26.26k,说明收入差距挺大。有人年入15k,有人年入137k。
- 消费评分: 均值50.2,中位数50,分布挺均匀的。这说明评分系统设计得不错,没有明显偏向。
- 购买金额: 平均62.34k,但最小只有1.02k,最大189k。这差距也很大,后面做聚类时,这些特征得做标准化处理。
最后,咱们再看看性别分布:
# 性别分布可视化
plt.figure(figsize=(6, 4))
sns.countplot(x='Gender', data=df_clean)
plt.title('客户性别分布')
plt.show()
从输出看,男女比例大概是6:4,男性略多。这个信息在后续做客户分群时,可以作为辅助分析的维度。
好了,数据初探就到这里。咱们把数据洗得干干净净,也摸清了它的脾气。下一节,咱们就要开始做特征工程,为聚类算法准备「食材」了。