探索性数据分析(EDA)的核心价值,远不止于绘制图表或计算统计量,而在于避免被自己的数据所蒙蔽。在诸多数据类型中,分类列往往是最容易“埋雷”的地方。
像 city、category、product、department、role、customer_type 这样的列,表面上看起来很简单,新手通常跑个 value_counts() 画个柱状图就以为完成了任务。
但实际上,分类变量常常隐藏着复杂的内部层次结构。这些关系潜藏在类别内部,如果不主动去挖掘,你根本无法察觉。一旦忽略,由此得出的结论、构建的特征或生成的报表,很可能就是错误的,甚至是误导性的。
这篇文章将介绍如何在 EDA 阶段,系统性地找出这些隐藏的结构。我们将通过真实的步骤与案例,辅以可以直接复用的 Python 代码,帮助你提升分析段位。
什么是“隐藏层次结构”?
简单来说,就是一个分类变量表面看起来是扁平的、无序的列表,但其内部却存在着分层、分组或优先级关系。这就是隐藏的层次结构。
让我们看几个典型的例子:City 背后可能隐藏着收入水平、门店类型或客户行为模式;Product Category 背后可能对应着不同的价格层级和利润模式;Customer Type 可能暗示着不同的忠诚度阶段或消费能力;Department 则可能隐含了员工的资历或责任级别。
如果我们在分析时将所有类别一视同仁,那么 EDA 就失去了意义,因为现实世界中的这些类别,从来都不是平等的。
示例数据集
为了保持连贯性,我们继续使用同一份销售数据进行分析。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style("whitegrid")
df = pd.read_csv("sales_data.csv")
df['order_date'] = pd.to_datetime(df['order_date'])
df.head()
扁平类别的假象
初学者通常是这样处理分类变量的:
df['city'].value_counts()
输出可能显示:Delhi: 3, Mumbai: 1, Bangalore: 1。
于是得出结论:“Delhi 的销售量最多。”
从技术上讲,这个结论没错。但从分析角度来看,它几乎毫无价值。
真正的 EDA 应该追问更好的问题:Delhi 的客户是因为购买更频繁,还是单次购买金额更高?Delhi 的数据优势是否仅仅由某一个超级客户所驱动?不同城市的商品品类结构是否存在显著差异?
扁平的频数统计,恰恰把这些真正有价值的结构给掩埋了。
频率不等于重要性
让我们比较一下频率(订单数)和价值(销售额):
df.groupby('city')['amount'].sum().sort_values(ascending=False)
再来看看客单价均值:
df.groupby('city')['amount'].mean().sort_values(ascending=False)
你很可能会发现:某个城市虽然订单数量少,但客单价极高;而另一个城市订单量巨大,但贡献的总收入反而一般。这就是我们挖掘出的第一个隐藏层次结构:数量主导型 vs. 价值主导型。
出现频率高的类别,并不自动意味着它在业务上更重要。理解这一点,是进行有效数据分析的第一步。
嵌套类别
在真实的数据场景中,类别很少孤立存在。让我们看看 city 与 category 之间的关系:
pd.crosstab(df['city'], df['category'], normalize='index')
为了更直观,我们可以将其可视化:
pd.crosstab(df['city'], df['category'], normalize='index')\
.plot(kind='bar', stacked=True, figsize=(8,5))
plt.title("Category Distribution Within Each City")
plt.show()
此时,模式开始浮现:有的城市电子产品销售占绝对大头,有的城市家具品类更为突出,还有的城市品类分布相对均衡。
这里的隐藏层次结构是:“城市”本身不是一个孤立的标签,而是一个包含着不同品类偏好的“容器”。忽略了这种嵌套关系,你的市场细分策略就可能失效,报表也只会流于表面。
主导类别背后的子群组
让我们再看看 category 这一列:
df['category'].value_counts(normalize=True)
假设结果显示“电子产品”占据主导地位。但先别急着下结论,我们继续向下拆解:
df.groupby(['category', 'product'])['amount'].sum()
结果很可能让你惊讶:整个“电子产品”大类的绝大部分收入,其实是由某一个或某几个特定产品(比如“高端智能手机”)贡献的,其他产品只是“凑数”的。
这意味着,一个大类别可能完全由一个小子群组在支撑。这个洞察对特征工程(是否要深入到产品级)、库存规划以及识别模型偏差,都有着直接的影响。
客户层级
客户 ID 本质上也是一个分类变量,而且它蕴含的层次可能非常深。
df.groupby('customer_id')['amount'].sum().sort_values(ascending=False)
你可能会发现,收入的大部分由少数几个客户贡献,或者存在同一个人反复购买的模式。
如果再叠加城市维度:
df.groupby(['customer_id', 'city'])['amount'].sum()
一个更残酷的真相可能浮出水面:某个城市在报表上的“领先地位”,其实完全依靠一两个大客户在支撑。由此得出的任何地理战略结论都将是脆弱且不可靠的。
因此,在分析分类数据时,我们必须时刻检查:一个类别的表现,是由众多贡献者共同驱动的,还是被某个或某几个异常个体拉高的。
时间带来的层次
时间维度天然会为数据带来层次结构。
df['month'] = df['order_date'].dt.month
df.groupby(['city', 'month'])['amount'].sum().unstack()
我们可以用图表更清晰地展示这种模式:
sns.lineplot(data=df, x='month', y='amount', hue='city', marker='o')
plt.show()
你可能会发现,不同城市在不同月份达到销售峰值,季节性的主导权在品类之间轮换。这些动态规律,是静态的频数柱状图永远无法揭示的。
类别与数值的交互
处理分类数据时,分析其与数值变量的交互效应是最关键的一环。
先看单一维度下,不同品类的销售额分布:
sns.boxplot(x='category', y='amount', data=df)
plt.show()
然后,我们将城市维度也加进来,进行更细致的观察:
sns.boxplot(x='city', y='amount', hue='category', data=df)
plt.xticks(rotation=45)
plt.show()
这个分析可能揭示:同一个品类(比如“家具”)在不同城市的表现天差地别——消费金额的分布不同,隐藏的高端细分市场也藏匿其中。许多有价值的特征工程创意,往往就诞生于对这些交互作用的深入探索之中。
隐藏层次结构如何破坏你的模型?
如果在 EDA 阶段没有识别出这些隐藏结构,就直接进行 One-Hot 编码并投入模型,很可能会导致严重问题:高价值和低价值的子群组被混为一谈,重要的客户集中度信息被丢失,噪声被无谓放大。
而好的 EDA 可以为建模提供关键的修补思路。例如,基于对客户贡献度的分析,我们可以创建这样一个特征:
df['high_value_customer'] = (
df.groupby('customer_id')['amount']
.transform('sum') > df['amount'].median()
).astype(int)
这个“高价值客户”标签特征的存在和价值,完全依赖于前文对客户层级这一隐藏结构的挖掘。它体现了从原始大数据中提取洞见的能力。
分类数据 EDA 实用清单
为了确保分析质量,建议你对每个分类列都过一遍以下检查清单:
- 频率检查:基础的
value_counts()。
- 价值聚合:按该类别分组,对核心指标(如销售额、利润)进行求和、求均值。
- 跨类别交互:使用交叉表或分组聚合,分析该类别与其他关键类别(如城市 vs 品类)的关系。
- 时间维度拆分:观察该类别下的数据随时间的变化模式。
- 异常值/主导个体检查:分析类别内的分布,识别是否由少数个体主导。
跳过这些步骤,你的 EDA 很可能只是在“做做样子”,无法触及数据真正的脉搏。
总结
分类数据从来都不是扁平的、均质的列表。EDA 存在的核心意义之一,就是去证明这个天真的假设是错误的。
那些隐藏的层次结构,能够解释许多令人困惑的现象:为什么看报表总觉得哪里不对?为什么模型在训练集上表现完美,上线却一塌糊涂?为什么业务决策总是让人一头雾水?
一旦你开始有意识地在 EDA 中寻找这些结构,就再也回不去了。你的分析视角和实操能力,将会直接拉升一个档次。记住,EDA 的目的不是更快地画出更多的图表,而是在你相信任何图表之前,先运用思考和技术工具,把数据“看”得更清楚、更透彻。
掌握这些方法,能让你在面对复杂业务数据时游刃有余。如果你想查看更多类似的数据分析实战技巧与深度讨论,欢迎访问 云栈社区 ,与更多同行交流。
