分类数据 EDA 实战:如何发现隐藏的层次结构
探索性数据分析(EDA)的本质不是画图和算统计量,而是不被自己的数据欺骗。 分类列是最容易出问题的地方。 、 、 、 、 、 ——这些列看起来很简单,跑个 画个柱状图搞定了。 其实分类变量往往藏着隐藏的层次结构。这些关系存在于类别内部,不主动挖掘根本看不出来。一旦忽略那么就会得到错误的结论、垃圾特征、误导性的报表。 这篇文章讲的是如何在 EDA 阶段把这些隐藏结构找出来,用实际的步骤、真实的案例,外加可以直接复用的 Python 代码。 一个分类变量表面看起来是扁平的,实际上却是分层的:这就是隐藏层次结构。 举几个常见例子: 背后藏着收入水平、门店类型、客户行为; 背后是价格层级和利润模式; 对应着忠诚度阶段或消费能力; 则可能隐含资历或责任级别。 把所有类别一视同仁EDA 就废了,因为它们从来都不平等。 继续使用同一份销售数据,保持系列的连贯性。 初学者通常这么干: 输出:Delhi: 3,Mumbai: 1,Bangalore: 1。 结论:"Delhi 销售最多。" 技术上没错,分析上毫无价值。 EDA 应该问更好的问题:Delhi 的客户是买得更频繁,还是买得更贵?Delhi 的数据是不是被某一个客户撑起来的?不同城市的品类结构有没有差异? 扁平的计数把真正的结构埋了起来。 比较一下频率和价值: 再看均值: 你很可能发现:某个城市订单少但客单价高,另一个城市量大但贡献的收入反而一般。 这就是第一个隐藏层次结构:数量主导 vs 价值主导。 出现频率高的类别,并不自动意味着更重要。 类别很少孤立存在。看看 的关系: 可视化一下: 模式开始出现了:有的城市电子产品占大头,有的城市家具更突出,还有的城市品类分布比较均匀。 这里的隐藏层次结构是:城市不是一个类别,而是一个容器。 忽略这一点,细分就做不好,报表也只是走过场。 看看 : 电子产品占主导。但继续拆解: 很可能发现某一个产品贡献了绝大部分收入,其他产品只是凑数的。 一个大类别可能完全由一个小子群组撑着。这对特征工程、库存规划、模型偏差都有直接影响。 客户 ID 本质上也是分类变量,而且层次很深。 你可能会看到某个客户贡献了大部分收入,或者同一个人反复购买。 再叠加城市维度: 真相可能是:某个城市的"领先地位"其实就靠一个客户撑着。由此得出的地理结论完全站不住脚。 永远要检查:一个类别是由众多贡献者驱动的,还是被某个异常个体拉高的。 时间天然会产生层次结构。 画出来: 你可能会发现不同城市在不同月份达到峰值,季节性主导权在品类之间轮换。 静态的柱状图永远看不到这些。 处理分类数据时,交互分析是最关键的一环。 先看单一维度: 加上城市: 同一个品类在不同城市的表现可能天差地别,消费分布不一样,隐藏的高端细分市场也藏在里面。 特征创意往往就是这么来的。 不做 EDA 就直接 one-hot 编码会出大问题,因为高价值和低价值的子群组被混在一起,客户集中度信息泄露,噪声被放大。 EDA 阶段可以这样修补: 这个特征的存在,完全依赖于对层次结构的挖掘。 每个分类列都应该过一遍:频率检查、基于价值的聚合、跨类别交互、时间维度拆分、异常值主导检查。 跳过这些,EDA 就只是做做样子。 不要说"我检查了分类分布"。 要说:"我通过结合频率、价值贡献以及与时间和数值变量的交互,分析了分类变量的隐藏层次结构,识别出主导子群组,避免了建模时的误导性结论。" 面试官一听就知道你是明白人。 分类数据从来都不是扁平的。EDA 存在的意义,就是证明这个假设是错的。 隐藏的层次结构能解释很多事:为什么报表会骗人,为什么模型会过拟合,为什么业务决策让人一头雾水。 一旦开始有意识地寻找这些结构,就再也回不去了。分析的段位会直接拉升一个档次。 EDA 的目的不是更快地出图,而是在相信图表之前,先想清楚。 https://avoid.overfit.cn/post/829701eeb5dc40d094b0f69df05c3b15 by Gitanjalicitycategoryproductdepartmentrolecustomer_typevalue_counts()
什么是"隐藏层次结构"?
CityProduct CategoryCustomer TypeDepartment示例数据集
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()频率不等于重要性
df.groupby('city')['amount'].sum().sort_values(ascending=False) df.groupby('city')['amount'].mean().sort_values(ascending=False)嵌套类别
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()客户层级
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()隐藏层次结构如何破坏模型
df['high_value_customer'] = (
df.groupby('customer_id')['amount']
.transform('sum') > df['amount'].median()
).astype(int)分类数据的 EDA 清单
面试时怎么说
总结