本案例sklearn.cluster 包中的 KMeans 来实现K-Means聚类算法,基于青少年的社交网络数据对青少年市场细分。

本案例主要包括以下内容:

1. 数据源
2. 数据探索和预处理
    2.1 通过虚拟编码来处理分类变量的缺失值
    2.2 使用填补方法来处理数值变量的缺失值
    2.3 数据标准化
3. 模型训练
4. 聚类结果分析   

使用K-Means聚类进行青少年市场细分

市场细分(market segmentation)是指营销者通过市场调研,依据消费者的需要和欲望、购买行为和购买习惯等方面的差异,把某一产品的市场整体划分为若干消费者群的市场分类过程。 每一个消费者群就是一个细分市场,每一个细分市场都是具有类似需求倾向的消费者构成的群体。 市场细分能够对企业的生产、营销起到极其重要的作用:

  • 有利于选择目标市场和制定市场营销策略
  • 有利于发掘市场机会,开拓新市场
  • 有利于集中人力、物力投入目标市场
  • 有利于企业提高经济效益

随着 Facebook, Twitter等社交网络平台的流行,越来越多的青少年用户会在这些平台发布消息。 这些文本数据能够反映青少年的行为、兴趣爱好,结合社交网络平台上用户的性别、年龄、好友数等信息,对于挖掘青少年细分市场具有很大的价值。

细分市场都是具有类似需求倾向的消费者,而聚类算法很适合用来完成这一任务的。 本案例中,我们将使用一份从社交网络平台抽取的描述青少年基本信息和兴趣爱好的数据集,利用K-Means聚类算法来进行青少年市场细分。

1 数据源

我们使用一份包含30000个样本的美国高中生社交网络信息数据集。 数据均匀采样于2006年到2009年,对应的高中生年级有高中一年级、二年级、三年级和四年级。 每个样本包含40个变量,其中 gradyear,gender,age和friends四个变量代表高中生的毕业年份、性别、年龄和好友数等基本信息。 其余36个变量代表36个词语,这36个词语代表高中生的5大兴趣类:课外活动、时尚、宗教、浪漫和反社会行为。 每个词语变量的取值代表对应词语在高中生的社交网络服务平台发布的消息中出现的频次。 36个词语的列表如下:

  • basketball (篮球)
  • football (足球)
  • soccer (英式足球)
  • softball (垒球)
  • volleyball (排球)
  • swimming (游泳)
  • cheerleading (带领拉拉队)
  • baseball (棒球)
  • tennis (网球)
  • sports (运动)
  • cute (可爱的)
  • sex (性)
  • sexy (性感)
  • hot (火辣)
  • kissed (吻)
  • dance (跳舞)
  • band (乐队)
  • marching (游行)
  • music (音乐)
  • rock (摇滚)
  • god (上帝)
  • church (教堂)
  • jesus (耶稣)
  • bible (圣经)
  • hair (头发)
  • dress (服装)
  • blonde (金发女郎)
  • mall (商业街)
  • shopping (购物)
  • clothes (衣服)
  • hollister (hollister品牌,美国时尚休闲大牌)
  • abercrombie (abercrombie品牌,美国青少年最青睐的品牌)
  • die (死亡)
  • deat (死亡)
  • drunk (醉酒)
  • drugs (毒品)
In [1]:
# 忽略警告信息
import warnings
warnings.filterwarnings("ignore")

2 数据探索和预处理

首先,使用pandas中的 read_csv() 函数将数据加载到数据框中:

In [2]:
import pandas as pd
teenager_sns = pd.read_csv("./input/teenager_sns.csv")
teenager_sns.head(5)
Out[2]:
gradyear gender age friends basketball football soccer softball volleyball swimming ... blonde mall shopping clothes hollister abercrombie die death drunk drugs
0 2006 M 18.980 7 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
1 2006 F 18.801 0 0 1 0 0 0 0 ... 0 1 0 0 0 0 0 0 0 0
2 2006 M 18.335 69 0 1 0 0 0 0 ... 0 0 0 0 0 0 0 1 0 0
3 2006 F 18.875 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
4 2006 NaN 18.995 10 0 0 0 0 0 0 ... 0 0 2 0 0 0 0 0 1 1

5 rows × 40 columns

通过观察,发现 $gender$和 $age$ 变量存在缺失值(missing value)。 很多机器学习模型不能直接处理带有缺失值的数据,例如我们将要使用的 K-Means聚类算法。 因此在正式构建模型之前,需要对缺失值进行处理:删除或者以某种方法进行填补。 在对缺失值进行处理之前,我们先分别统计$gender$和 $age$这两个变量存在缺失值的样本数量。 对于 gender 变量,我们使用pandas.Series的 value_counts() 函数来完成这一分析,注意 value_counts() 函数默认不对缺失值进行统计,需要将 dropna 参数设置成 False。 对于连续型变量 age,使用 count() 函数能够统计非缺失值的数量,从而能够计算缺失值数量。 使用 describe() 函数则可以对 age 变量进行描述性统计。

In [3]:
teenager_sns["gender"].value_counts(dropna = False)
Out[3]:
F      22054
M       5222
NaN     2724
Name: gender, dtype: int64
In [4]:
print 'age变量缺失值数目:', len(teenager_sns["age"]) - teenager_sns["age"].count()
teenager_sns["age"].describe()
age变量缺失值数目: 5086
Out[4]:
count    24914.000000
mean        17.993949
std          7.858054
min          3.086000
25%               NaN
50%               NaN
75%               NaN
max        106.927000
Name: age, dtype: float64

可见,在30000个样本中,有2724个样本(约9%)缺少性别数据,5086个样本(约17%)缺少年龄数据。 进一步观察 age 变量的描述统计信息发现,年龄的最小值为3.086,最大值为106.9。 因为我们的样本是青少年样本,所以该最小值和最大值似乎不可信,因为现实中不太可能会有一个3岁或者106岁的人就读高中。 这种异常数据往往会影响最终的建模分析结果,因此需要进行异常值处理。 高中生的合理年龄区间为13~20岁,因此对于我们的数据集,如果年龄在13~20岁之外,我们将其标记为空值NaN。 我们通过pandas.Series的 map() 函数来完成这一个处理:

In [5]:
import numpy as np

def tag_nan(value):
    if (value >= 13) & (value < 20):
        return value
    else:
        return np.NaN

teenager_sns["age"]  = teenager_sns["age"].map(tag_nan)

teenager_sns["age"].describe()
Out[5]:
count    24477.000000
mean        17.252429
std          1.157465
min         13.027000
25%               NaN
50%               NaN
75%               NaN
max         19.995000
Name: age, dtype: float64

现在,对于异常值的处理完毕,但是这样引入了更多的缺失值,下一步我们需要对缺失值进行处理。

2.1 通过虚拟编码来处理分类变量的缺失值

对于样本中的缺失值,一个最简单的方法是删除带有缺失值的样本。 然而直接删除缺失值会使我们的数据变少,特别是我们的数据中一共有40个变量,只有两个变量存在缺失值。 缺失值在数据中整体不多,但是如果直接删除会导致我们失去很多的可用数据。

对于 gender 这种分类变量,如果缺失值的样本跟其他样本的差别明显,我们可以为 gender 变量增加一个单独的分类 "unkown"。 可以通过 replace() 函数将空值替换为“unkown”。

In [6]:
teenager_sns["gender"] = teenager_sns["gender"].replace(np.NaN, "unkown")
teenager_sns["gender"].value_counts()
Out[6]:
F         22054
M          5222
unkown     2724
Name: gender, dtype: int64

由于K-Means聚类算法需要计算样本之间的距离,因此我们还需要对分类变量进行虚拟编码(也成为OneHot编码)。 虚拟编码将一个有K个取值的分类变量转换成K个二元变量。 我们的 gender 变量现在有“M”,“F”和“unkown”三种取值,我们可以将其转换成三个变量:gender_M, gender_F 和 gender_unkown。 这三个变量取值为0和1,分别代表某一个高中生是否是某一性别类型。 对于一个样本,在这三个变量下同时只能一个变量取值为1,其他变量取值为0。 pandas包提供了一个很方便的函数帮助我们完成虚拟编码:

In [7]:
gender_dummies = pd.get_dummies(teenager_sns["gender"], prefix="gender")
gender_dummies.head(5)
Out[7]:
gender_F gender_M gender_unkown
0 0.0 1.0 0.0
1 1.0 0.0 0.0
2 0.0 1.0 0.0
3 1.0 0.0 0.0
4 0.0 0.0 1.0

可见,我们已经正确完成了虚拟编码,gender变量转换成了3个二元变量 gender_F、gender_M 和 gender_unkown。 现在,将上述虚拟编码结果合并到数据集 teenager_sns。 我们使用 pandas 的 concat() 函数将 teenager_sns 和 gender_dummies 两个数据框进行水平拼接。

In [8]:
teenager_sns = pd.concat([teenager_sns, gender_dummies], axis = 1)
teenager_sns.head(5)
Out[8]:
gradyear gender age friends basketball football soccer softball volleyball swimming ... clothes hollister abercrombie die death drunk drugs gender_F gender_M gender_unkown
0 2006 M 18.980 7 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0.0 1.0 0.0
1 2006 F 18.801 0 0 1 0 0 0 0 ... 0 0 0 0 0 0 0 1.0 0.0 0.0
2 2006 M 18.335 69 0 1 0 0 0 0 ... 0 0 0 0 1 0 0 0.0 1.0 0.0
3 2006 F 18.875 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 1.0 0.0 0.0
4 2006 unkown 18.995 10 0 0 0 0 0 0 ... 0 0 0 0 0 1 1 0.0 0.0 1.0

5 rows × 43 columns

2.2 使用填补方法来处理数值变量的缺失值

对于 age 这种数值变量的缺失值,我们可以使用一个特殊的值对缺失值进行填补(imputation),常用的填补值包括给定值、均值、中位数等。 在案例中,我们使用最具有代表性的均值填补法。 所以,我们需要先计算 age 变量的均值。 均值的计算可以使用R中的 mean() 函数,但是需要注意默认情况下 mean() 是无法对包含缺失值的数据计算均值的。 我们需要给 mean() 函数传入一个额外的参数 na.rm,将其值设置为 TRUE。

In [9]:
age_mean = teenager_sns["age"].mean()
age_mean
Out[9]:
17.252428851574948

现在,我们使用 pandas.Series的 fillna() 函数使用上述均值对 age 变量的缺失值进行填补:

In [10]:
teenager_sns["age_avg_imputated"] = teenager_sns["age"].fillna(value = age_mean)
teenager_sns.head(5)
Out[10]:
gradyear gender age friends basketball football soccer softball volleyball swimming ... hollister abercrombie die death drunk drugs gender_F gender_M gender_unkown age_avg_imputated
0 2006 M 18.980 7 0 0 0 0 0 0 ... 0 0 0 0 0 0 0.0 1.0 0.0 18.980
1 2006 F 18.801 0 0 1 0 0 0 0 ... 0 0 0 0 0 0 1.0 0.0 0.0 18.801
2 2006 M 18.335 69 0 1 0 0 0 0 ... 0 0 0 1 0 0 0.0 1.0 0.0 18.335
3 2006 F 18.875 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 1.0 0.0 0.0 18.875
4 2006 unkown 18.995 10 0 0 0 0 0 0 ... 0 0 0 0 1 1 0.0 0.0 1.0 18.995

5 rows × 44 columns

观察上表中的第6行的“age”变量和“age_avg_imputated”变量, 发现该样本的age缺失值已经被正确填补为均值17.2523。

2.3 数据标准化

因为K-means聚类算法需要计算样本的距离,在构建模型之前,我们需要进行数据标准化。 常用的标准化方法有 min-max 标准化和 z-score 标准化等。 在本例中,我们直接采用 z-score 标准化方法。

In [11]:
from sklearn import preprocessing
filtered_columns = ["gradyear","friends","basketball",
                     "football","soccer","softball","volleyball","swimming",
                     "cheerleading","baseball","tennis","sports","cute","sex",
                     "sexy","hot","kissed","dance","band","marching","music",
                     "rock","god","church",
                      "jesus","bible","hair","dress","blonde","mall","shopping","clothes",
                     "hollister","abercrombie","die","death","drunk","drugs",
                     "gender_M","gender_F","gender_unkown","age_avg_imputated"]

teenager_sns_zscore = pd.DataFrame(preprocessing.scale(teenager_sns[filtered_columns]),\
                                   columns = teenager_sns[filtered_columns].columns)
teenager_sns_zscore.head(5)
Out[11]:
gradyear friends basketball football soccer softball volleyball swimming cheerleading baseball ... hollister abercrombie die death drunk drugs gender_M gender_F gender_unkown age_avg_imputated
0 -1.341641 -0.634528 -0.332217 -0.357697 -0.242874 -0.217928 -0.22367 -0.259971 -0.207327 -0.201131 ... -0.201476 -0.183032 -0.294793 -0.261530 -0.220403 -0.174908 2.178285 -1.665979 -0.316019 1.652413
1 -1.341641 -0.826150 -0.332217 1.060049 -0.242874 -0.217928 -0.22367 -0.259971 -0.207327 -0.201131 ... -0.201476 -0.183032 -0.294793 -0.261530 -0.220403 -0.174908 -0.459077 0.600248 -0.316019 1.481200
2 -1.341641 1.062695 -0.332217 1.060049 -0.242874 -0.217928 -0.22367 -0.259971 -0.207327 -0.201131 ... -0.201476 -0.183032 -0.294793 2.027908 -0.220403 -0.174908 2.178285 -1.665979 -0.316019 1.035474
3 -1.341641 -0.826150 -0.332217 -0.357697 -0.242874 -0.217928 -0.22367 -0.259971 -0.207327 -0.201131 ... -0.201476 -0.183032 -0.294793 -0.261530 -0.220403 -0.174908 -0.459077 0.600248 -0.316019 1.551981
4 -1.341641 -0.552404 -0.332217 -0.357697 -0.242874 -0.217928 -0.22367 -0.259971 -0.207327 -0.201131 ... -0.201476 -0.183032 -0.294793 -0.261530 2.285122 2.719316 -0.459077 -1.665979 3.164367 1.666760

5 rows × 42 columns

3 模型训练

为了将我们的青少年数据进行市场细分,我们使用 sklearn.cluster 包中的 KMeans 类。 KMeans 的 n_clusters 参数为聚类数目。 在本案例中,我们将细分市场的个数 n_clusters 设置为5。

In [12]:
from sklearn.cluster import KMeans
teenager_cluster_model = KMeans(n_clusters = 5)
teenager_cluster_model.fit(teenager_sns_zscore)
Out[12]:
KMeans(algorithm='auto', copy_x=True, init='k-means++', max_iter=300,
    n_clusters=5, n_init=10, n_jobs=1, precompute_distances='auto',
    random_state=None, tol=0.0001, verbose=0)

4 聚类结果分析

聚类结果的定量性能评价指标有互信息同质性完备性等,但是这些指标并不能指示聚类结果是否达到我们的预期分析目标。 在本案例中,我们的分析目标是确定具有相似特质和兴趣爱好的青少年的分类,以达到向不同类的青少年做区别营销的目的。 因此,很大程度上,我们需要的不是定量的评价指标结果,而是定性地对聚类结果进行分析。 首先,我们来观察我们K-Means聚类出来的每一个类中样本的数目。

In [13]:
teenager_clusters = pd.Series(teenager_cluster_model.labels_)
teenager_clusters.value_counts().sort_index()
Out[13]:
0    19786
1     1602
2     2610
3     5019
4      983
dtype: int64

在我们的聚类的5个类中,最大的类中有10530名青少年,最小的类中有1181名青少年。 需要注意的是,因为K-Means聚类会随机选取初始的聚类中心,因此每次运行的结果可能会不同。 为了更好地理解每一个类所代表的青少年群体的特点,我们观察每一个类的聚类中心(cluster center)。 聚类中心结果保存在 teenager_cluster_model 的 centers 属性。

In [14]:
centers = pd.DataFrame(teenager_cluster_model.cluster_centers_, \
                       columns = teenager_sns_zscore.columns)
centers
Out[14]:
gradyear friends basketball football soccer softball volleyball swimming cheerleading baseball ... hollister abercrombie die death drunk drugs gender_M gender_F gender_unkown age_avg_imputated
0 0.007278 0.028112 -0.045815 -0.100603 -0.012655 0.040278 0.019584 -0.005693 0.009252 -0.098347 ... -0.136765 -0.140545 -0.071509 -0.038210 -0.074903 -0.107035 -0.459077 0.600248 -0.316019 -0.015605
1 0.158004 0.081790 0.514872 0.497198 0.213103 0.225958 0.242594 0.367894 0.289064 0.364800 ... 0.099137 0.125114 1.217514 0.803159 1.288855 1.814194 -0.228596 0.318738 -0.187840 -0.151346
2 -0.068538 -0.121281 -0.146049 -0.169751 -0.052809 -0.104492 -0.075785 -0.077657 -0.063553 -0.096848 ... -0.113086 -0.140544 -0.091721 -0.125567 -0.103286 -0.120572 -0.459077 -1.665979 3.164367 -0.009560
3 -0.115033 -0.153714 0.049831 0.272789 -0.017610 -0.206615 -0.173542 -0.108510 -0.192994 0.313286 ... -0.156086 -0.150959 -0.063488 -0.063559 -0.075632 -0.099366 2.178285 -1.665979 -0.316019 0.177282
4 0.365323 0.407711 0.216445 0.272572 0.137553 0.153400 0.297749 0.275259 0.496814 0.042606 ... 3.688473 3.768956 0.022854 0.118102 0.067618 0.025303 -0.290050 0.328208 -0.121287 -0.319039

5 rows × 42 columns

因为数据已经使用Z-score方法进行标准化,我们可以直接通过观察聚类中心在每一个变量上的取值情况来分析每一个聚类中心的含义。 如果聚类中心在某一个变量取值大于0,代表该聚类所代表的群体在该变量取值大于群体平均水平。 首先对上述聚类结果数据框进行转置,然后对每一个聚类中心的变量取值从大到小进行排序。 通过观察每个聚类前10个变量来分析聚类所代表的群体:

In [15]:
centers_t = centers.T
centers_t.columns = ["cluster_0","cluster_1","cluster_2","cluster_3","cluster_4"]
centers_t["cluster_0"].sort(ascending = False, inplace = False).head(10)

#sort(centers_t[,1],decreasing = TRUE)[1:10]
Out[15]:
gender_F        0.600248
shopping        0.045598
softball        0.040278
friends         0.028112
volleyball      0.019584
cheerleading    0.009252
gradyear        0.007278
cute            0.006031
dress           0.001396
dance           0.000420
Name: cluster_0, dtype: float64

第一个聚类所代表的青少年群体特点为:浪漫、爱好音乐,酗酒。

In [16]:
centers_t["cluster_1"].sort(ascending = False, inplace = False).head(10)
Out[16]:
hair       2.150805
kissed     2.062237
drugs      1.814194
sex        1.509894
clothes    1.312942
drunk      1.288855
die        1.217514
rock       1.195975
music      1.114010
cute       0.868749
Name: cluster_1, dtype: float64

第二个聚类所代表的青少年群体的特点为:爱好运动和购物,爱好体育运动,女生居多。

In [17]:
centers_t["cluster_2"].sort(ascending = False, inplace = False).head(10)
Out[17]:
gender_unkown        3.164367
age_avg_imputated   -0.009560
tennis              -0.022190
blonde              -0.026870
bible               -0.029357
jesus               -0.044456
soccer              -0.052809
marching            -0.056007
cheerleading        -0.063553
gradyear            -0.068538
Name: cluster_2, dtype: float64

第三个聚类性别为未知,大部分变量取值为负,这一人群可能对应在社交网络平台资料不全,且很少发布内容的群体。

In [18]:
centers_t["cluster_3"].sort(ascending = False, inplace = False).head(10)
Out[18]:
gender_M             2.178285
baseball             0.313286
football             0.272789
age_avg_imputated    0.177282
sports               0.109760
basketball           0.049831
tennis               0.025312
band                 0.024275
marching             0.011263
bible                0.002851
Name: cluster_3, dtype: float64

第四个聚类所代表的青少年群体为高年级、爱好购物,其中以女生居多。

In [19]:
centers_t["cluster_4"].sort(ascending = False, inplace = False).head(10)
Out[19]:
abercrombie     3.768956
hollister       3.688473
shopping        1.071534
mall            0.792505
clothes         0.635011
cheerleading    0.496814
hair            0.462919
hot             0.442794
cute            0.436902
friends         0.407711
Name: cluster_4, dtype: float64

第五个聚类所代表的青少年群体特点为:喜欢体育运动的高年级男生。