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

看到了吗?AgePurchase 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。这差距也很大,后面做聚类时,这些特征得做标准化处理。
关键发现: 从描述性统计来看,咱们的数据集有1960条有效记录,包含年龄、收入、消费评分、购买金额四个数值特征,以及性别这个分类特征。数据分布比较健康,没有明显的偏态。这为后续的聚类分析打下了好基础。

最后,咱们再看看性别分布:

# 性别分布可视化
plt.figure(figsize=(6, 4))
sns.countplot(x='Gender', data=df_clean)
plt.title('客户性别分布')
plt.show()

从输出看,男女比例大概是6:4,男性略多。这个信息在后续做客户分群时,可以作为辅助分析的维度。

好了,数据初探就到这里。咱们把数据洗得干干净净,也摸清了它的脾气。下一节,咱们就要开始做特征工程,为聚类算法准备「食材」了。