数据的预处理

  • 删除答题时间小于1分钟的
  • 对异常数据进行一些修改
  • 检查有没有重复的问卷
  • 保留研究所需要的列
  • 处理公共题目缺失值
  • 把数据分为总数据df,来过游客的数据df_gone,没有来过游客的数据df_not_gone,所有原始信息保留
  • 对于df_gone和df_not_gone分别应用孤立森林清洗异常问卷
  • 得到最后的数据集df, df_gone, df_not_gone
import pandas as pd

df = pd.read_csv('乌蒙大草原旅游问卷调查_final.csv')

print(df.shape)
df.head()

这两段代码主要是排除答题时间小于60s的问卷,不过问卷网上可以直接进行筛选

# 先把答题时间转换为时间格式
def convert_to_seconds(time_str):
    if '分' in time_str:
        minutes, seconds = time_str[:-1].split('分')
        return int(minutes) * 60 + int(seconds)
    else:
        return int(time_str[:-1])

df['答题时长'] = df['答题时长'].astype(str)
df['答题时长'] = pd.to_timedelta(df['答题时长'].apply(convert_to_seconds), unit='s')
# 删除答题时间小于1分钟的数据
df = df[df['答题时长'] > '00:01:00']

print(df.shape)
df.head()

针对Q2年龄的一些数据问题进行处理

import seaborn as sns
import matplotlib.pyplot as plt

# 查看Q2有没有非数字的数据
df['Q2'].unique()

# 修改数据
df['Q2'] = df['Q2'].replace('1995', 29)
df['Q2'] = df['Q2'].replace('50\n\n50', '50')

df['Q2'].unique()

df['Q2'] = pd.to_numeric(df['Q2'], errors='coerce')
print(df['Q2'].dtypes)

这里用使用 pandas 的 duplicated 函数来找出重复的行

# 检查df中是否有重复的行
df_duplicates = df.duplicated().sum()
print(f"df中有{df_duplicates}行重复的数据。")

# 列名列表
columns = ['Q1', 'Q2', 'Q3', 'Q3|open', 'Q4', 'Q5', 'Q6|1|open', 'Q6|2|open', 'Q6|3|open', 'Q7|1', 'Q7|4', 'Q7|2', 'Q7|3', 'Q7|5', 'Q7|6', 'Q8', 'Q9', 'Q10', 'Q11', 'Q12', 'Q13', 'Q14', 'Q14|open', 'Q15', 'Q16|6', 'Q16|1', 'Q16|2', 'Q16|3', 'Q16|7', 'Q16|4', 'Q16|10', 'Q16|11', 'Q16|9', 'Q16|9|open', 'Q17', 'Q18', 'Q19', 'Q20', 'Q21|R1', 'Q21|R2', 'Q21|R3', 'Q21|R4', 'Q21|R5', 'Q22|1', 'Q23', 'Q24|R1', 'Q24|R2', 'Q24|R3', 'Q24|R4', 'Q24|R5', 'Q24|R6', 'Q24|R7', 'Q24|R8', 'Q24|R9', 'Q24|R10', 'Q24|R11', 'Q24|R12', 'Q24|R13', 'Q24|R14', 'Q24|R18', 'Q24|R15', 'Q24|R16', 'Q24|R17', 'Q25|R5', 'Q25|R8', 'Q25|R10', 'Q25|R13', 'Q25|R14', 'Q25|R15', 'Q25|R16', 'Q25|R17', 'Q25|R18', 'Q26|1', 'Q27|1', 'Q28|1', 'Q28|23', 'Q28|21', 'Q28|22', 'Q28|24', 'Q28|25', 'Q28|4', 'Q28|2', 'Q28|26', 'Q28|27', 'Q28|28', 'Q28|29', 'Q28|35', 'Q28|34', 'Q28|34|open', 'Q29|R1', 'Q29|R2', 'Q29|R3', 'Q29|R4', 'Q30|R1', 'Q30|R2', 'Q30|R3', 'Q30|R4', 'Q30|R5', 'Q30|R6', 'Q31|R1', 'Q31|R2', 'Q31|R8', 'Q31|R4', 'Q31|R7', 'Q31|R9', 'Q31|R6', 'Q31|R5', 'Q32|R3', 'Q32|R7', 'Q32|R4', 'Q32|R5', 'Q32|R1', 'Q32|R2', 'Q32|R6', 'Q33|R10', 'Q33|R12', 'Q33|R11', 'Q33|R4', 'Q33|R5', 'Q33|R1', 'Q33|R8', 'Q33|R7', 'Q34', 'IP地址']

# 找出重复的行
duplicates = df.duplicated(subset=columns)

# 只保留没有重复的行
df = df[~duplicates]

# 打印数据的形状
print(df.shape)

去除不关心的列

答题序号 来源 开始时间 提交时间 答题时长 IP省份 IP城市 IP地址 浏览器 操作系统

columns_to_drop = ['答题序号', '来源', '开始时间', '提交时间', '答题时长', 'IP省份', 'IP城市', 'IP地址', '浏览器', '操作系统']
df = df.drop(columns_to_drop, axis=1)
print(df.shape)
df.head()

检查公共题目有没有缺失值

公共题目:‘Q1’, ‘Q2’, ‘Q3’, ‘Q3|open’, ‘Q4’, ‘Q5’, ‘Q6|1|open’, ‘Q6|2|open’, ‘Q6|3|open’, ‘Q7|1’, ‘Q7|4’, ‘Q7|2’, ‘Q7|3’, ‘Q7|5’, ‘Q7|6’, ‘Q8’, ‘Q9’, ‘Q10’, ‘Q11’, ‘Q12’, ‘Q13’, ‘Q14’, ‘Q14|open’, ‘Q15’, ‘Q16|6’, ‘Q16|1’, ‘Q16|2’, ‘Q16|3’, ‘Q16|7’, ‘Q16|4’, ‘Q16|10’, ‘Q16|11’, ‘Q16|9’, ‘Q16|9|open’, ‘Q17’,Q30|R1,Q30|R2,Q30|R3,Q30|R4,Q30|R5,Q30|R6,Q33|R10,Q33|R12,Q33|R11,Q33|R4,Q33|R5,Q33|R1,Q33|R8,Q33|R7

common_columns = ['Q1', 'Q2', 'Q3', 'Q3|open', 'Q4', 'Q5', 'Q6|1|open', 'Q6|2|open', 'Q6|3|open', 'Q7|1', 'Q7|4', 'Q7|2', 'Q7|3', 'Q7|5', 'Q7|6', 'Q8', 'Q9', 'Q10', 'Q11', 'Q12', 'Q13', 'Q14', 'Q14|open', 'Q15', 'Q16|6', 'Q16|1', 'Q16|2', 'Q16|3', 'Q16|7', 'Q16|4', 'Q16|10', 'Q16|11', 'Q16|9', 'Q16|9|open', 'Q17', 'Q30|R1', 'Q30|R2', 'Q30|R3', 'Q30|R4', 'Q30|R5', 'Q30|R6', 'Q33|R10', 'Q33|R12', 'Q33|R11', 'Q33|R4', 'Q33|R5', 'Q33|R1', 'Q33|R8', 'Q33|R7']
missing_values = df[common_columns].isnull().any()
print(missing_values)

missing_rows = df[df['Q2'].isnull()]
print(missing_rows)

依照Q17的选项1(去过),2(没去过)把数据分为去过的和没有去过的,同时保留原始数据

# 依照Q17的选项1(去过),2(没去过)把数据分为去过的和没有去过的,同时保留原始数据

print(df['Q17'].value_counts())

# 去过的数据
df_gone = df[df['Q17'] == 1 ]

# 没去过的数据
df_not_gone = df[df['Q17'] == 2]

print("-"*100)
print("df_gone shape"+str(df_gone.shape))
print("df_not_gone shape"+str(df_not_gone.shape))

孤立森林

尝试使用孤立森林找出异常的问卷数据,并删除异常数据

# 使用孤立森林算法检测异常值
from sklearn.ensemble import IsolationForest

# 对于去过的
# 屏蔽开放性问题
df_gone_IsolationForest = df_gone.drop(['Q3|open', 'Q6|1|open', 'Q6|2|open', 'Q6|3|open', 'Q14|open', 'Q16|9|open', 'Q28|34|open','Q34'], axis=1)
print("df_gone_IsolationForest shape"+str(df_gone_IsolationForest.shape))
# 屏蔽没有去过的问题,会出现nan
df_gone_IsolationForest = df_gone_IsolationForest.drop(['Q29|R1','Q29|R2','Q29|R3','Q29|R4'], axis=1)
print("df_gone_IsolationForest shape"+str(df_gone_IsolationForest.shape))

# 使用中位数填充数值列的空值
for col in df_gone_IsolationForest.columns:
    if df_gone_IsolationForest[col].dtype == 'int64' or df_gone_IsolationForest[col].dtype == 'float64':
         df_gone_IsolationForest[col] = df_gone_IsolationForest[col].fillna(df[col].median())

# 使用最常见的值填充类别列的空值
for col in df_gone_IsolationForest.columns:
    if df_gone_IsolationForest[col].dtype == 'object':
        df_gone_IsolationForest[col] = df_gone_IsolationForest[col].fillna(df_gone_IsolationForest[col].mode()[0])
        
# 对于没有去过的
# 屏蔽开放性问题
df_not_gone_IsolationForest = df_not_gone.drop(['Q3|open', 'Q6|1|open', 'Q6|2|open', 'Q6|3|open', 'Q14|open', 'Q16|9|open', 'Q28|34|open','Q34',], axis=1)
print("df_not_gone_IsolationForest shape"+str(df_not_gone_IsolationForest.shape))
# 屏蔽去过的问题,会出现nan
columns_to_drop_1 = ['Q19','Q20','Q21|R1','Q21|R2','Q21|R3','Q21|R4','Q21|R5']
columns_to_drop_2 = ['Q24|R1', 'Q24|R2', 'Q24|R3', 'Q24|R4', 'Q24|R5', 'Q24|R6', 'Q24|R7', 'Q24|R8', 'Q24|R9', 'Q24|R10', 'Q24|R11', 'Q24|R12', 'Q24|R13', 'Q24|R14', 'Q24|R18', 'Q24|R15', 'Q24|R16', 'Q24|R17']
columns_to_drop_3 = ['Q25|R5', 'Q25|R8', 'Q25|R10', 'Q25|R13', 'Q25|R14', 'Q25|R15', 'Q25|R16', 'Q25|R17', 'Q25|R18', 'Q26|1', 'Q27|1']
columns_to_drop_4 = ['Q28|1', 'Q28|23', 'Q28|21', 'Q28|22', 'Q28|24', 'Q28|25', 'Q28|4', 'Q28|2', 'Q28|26', 'Q28|27', 'Q28|28', 'Q28|29', 'Q28|35', 'Q28|34']
columns_to_drop_5 = ['Q31|R1', 'Q31|R2', 'Q31|R8', 'Q31|R4', 'Q31|R7', 'Q31|R9', 'Q31|R6', 'Q31|R5', 'Q32|R3', 'Q32|R7', 'Q32|R4', 'Q32|R5', 'Q32|R1', 'Q32|R2', 'Q32|R6']
columns_to_drop = columns_to_drop_1 + columns_to_drop_2 + columns_to_drop_3 + columns_to_drop_4 + columns_to_drop_5


df_not_gone_IsolationForest = df_not_gone_IsolationForest.drop(columns_to_drop, axis=1)
print("df_not_gone_IsolationForest shape"+str(df_not_gone_IsolationForest.shape))

# 使用中位数填充数值列的空值
for col in df_not_gone_IsolationForest.columns:
    if df_not_gone_IsolationForest[col].dtype == 'int64' or df_not_gone_IsolationForest[col].dtype == 'float64':
         df_not_gone_IsolationForest[col] = df_not_gone_IsolationForest[col].fillna(df[col].median())

# 使用最常见的值填充类别列的空值
for col in df_not_gone_IsolationForest.columns:
    if df_not_gone_IsolationForest[col].dtype == 'object':
        df_not_gone_IsolationForest[col] = df_not_gone_IsolationForest[col].fillna(df_not_gone_IsolationForest[col].mode()[0])

print(df_gone_IsolationForest.head(100))
print(df_not_gone_IsolationForest.head(100))
X = df_gone_IsolationForest.values
Y = df_not_gone_IsolationForest.values

# 创建孤立森林模型,contamination参数表示你预计的异常点的比例
clf_X = IsolationForest(contamination=0.01)
clf_Y = IsolationForest(contamination=0.01)

# 训练模型
clf_X.fit(X)
clf_Y.fit(Y)

# 预测异常点,返回1表示正常点,-1表示异常点
pred_X = clf_X.predict(X)
pred_Y = clf_Y.predict(Y)

# 将预测结果添加到数据框中
df_gone_IsolationForest['anomaly'] = pred_X
df_not_gone_IsolationForest['anomaly'] = pred_Y

# 打印出预测结果
print("-"*100+"\n去过的数据异常值预测结果:\n")
print(df_gone_IsolationForest['anomaly'].value_counts())
print("-"*100+"\n异常数据:\n")
print(df_gone_IsolationForest[df_gone_IsolationForest['anomaly'] == -1])
print("-"*100+"\n没有去过的数据异常值预测结果:\n")
print(df_not_gone_IsolationForest['anomaly'].value_counts())
print("-"*100+"\n异常数据:\n")
print(df_not_gone_IsolationForest[df_not_gone_IsolationForest['anomaly'] == -1])


# 在原来的df上删除异常值,先找到异常值的序号
print("删除异常值前df的形状"+str(df.shape))
df = df.drop(df_gone_IsolationForest[df_gone_IsolationForest['anomaly'] == -1].index)
df = df.drop(df_not_gone_IsolationForest[df_not_gone_IsolationForest['anomaly'] == -1].index)
print("删除异常值后df的形状"+str(df.shape))
# 在df_gone和df_not_gone上删除异常值
print("删除异常值前df_gone的形状"+str(df_gone.shape))
df_gone = df_gone.drop(df_gone_IsolationForest[df_gone_IsolationForest['anomaly'] == -1].index)
print("删除异常值后df_gone的形状"+str(df_gone.shape))
print("删除异常值前df_not_gone的形状"+str(df_not_gone.shape))
df_not_gone = df_not_gone.drop(df_not_gone_IsolationForest[df_not_gone_IsolationForest['anomaly'] == -1].index)
print("删除异常值后df_not_gone的形状"+str(df_not_gone.shape))


# 删除异常值
df_gone_IsolationForest = df_gone_IsolationForest[df_gone_IsolationForest['anomaly'] == 1]
df_not_gone_IsolationForest = df_not_gone_IsolationForest[df_not_gone_IsolationForest['anomaly'] == 1]

再次检查有没有重复的行

# 检查df中是否有重复的行
df_duplicates = df.duplicated().sum()
print(f"df中有{df_duplicates}行重复的数据。")

# 检查df_gone中是否有重复的行
df_gone_duplicates = df_gone.duplicated().sum()
print(f"df_gone中有{df_gone_duplicates}行重复的数据。")

# 检查df_not_gone中是否有重复的行
df_not_gone_duplicates = df_not_gone.duplicated().sum()
print(f"df_not_gone中有{df_not_gone_duplicates}行重复的数据。")

最终数据集

  • df,全体的数据集
  • df_gone,去过的游客的数据集
  • df_not_gone,没有去过的游客数据集

PS:所有的数据都在

print(df.shape)
df.head()
print(df_gone.shape)
df_gone.head()
print(df_not_gone.shape)
df_not_gone.head()

数据分析


对所有游客

基本游客画像 Q1-Q16

[未来打造项目的喜爱程度] Q30

聚类方法:

  • k-prototypes

    感觉对于分类的效果不是很好,另外地理位置信息因为分成了三列,所以在聚类之后会出现不匹配的现象; 使用肘部图确定的n=6;使用轮廓系数确定n=1;

  • 基于密度聚类DBSCAN


对去过的游客进行分析

游玩乌蒙大草原的基本情况:Q18 Q19 Q20

  • 因子分析 Q21 (交给wyx了)
  • 摆渡车 Q22 Q23
  • 结构方程 Q24 Q25 (交给wyx了)

再次游玩意愿 Q26

  • 关联分析[推荐意愿] Q27 Q28
  • [盘州特产、非遗文化] Q31 Q32
  • [改进建议] Q34

针对没有去过的游客

  • 决策树[未前往原因] Q29

结构方程

结构方程模型(Structural Equation Modeling, SEM)是一种复杂的统计分析方法,用于研究变量之间的因果关系。它能够同时处理测量误差和多变量之间的关系,非常适用于社会科学、行为科学、营销研究、教育研究等领域。

基础知识

需要的数据类型

  • 量表数据:SEM通常需要量表数据,即通过问卷调查收集的、用于测量潜在变量(如态度、满意度、感知质量等)的多个观察指标。
  • 连续数据:SEM分析通常假定数据是连续的,特别是当使用最大似然估计方法时。
  • 正态分布的数据:SEM的某些估计方法(如最大似然估计)要求数据近似正态分布。

作用

  • 因果关系分析:SEM允许研究者构建和测试变量之间的因果关系模型。
  • 潜在变量建模:它可以用来测量不易直接观察的潜在变量,并分析这些潜在变量之间的关系。
  • 同时测试多个假设:SEM能够在单一模型中同时测试多个假设,提供全面的分析视角。

实现方法

  • 协方差基础的SEM(如LISREL,AMOS):通过构建协方差矩阵并使用最大似然估计或其他方法来估计模型参数。
  • 基于方差的SEM(如PLS-SEM):主要关注构建模型的预测能力,适用于理论发展阶段较早、样本较小或数据分布假设不严格的情况。

聚类分析

基础知识

方法分类

聚类分析是一种无监督学习方法,旨在将数据集中的对象分组成若干个簇,使得同一簇内的对象相似度较高,而不同簇内的对象相似度较低。它在市场细分、社会网络分析、生物信息学、图像分割等领域有广泛应用。根据聚类的方法和原理,主要可以分为以下几类:

  1. 划分方法:这种方法将数据集划分成若干个非重叠的簇,使得每个数据对象恰好属于一个簇。最著名的划分方法是K-均值聚类(K-means clustering),它通过迭代地将数据点分配给最近的簇中心,然后更新簇中心,直到满足停止条件。

  2. 层次方法:层次聚类方法根据对象之间的相似性逐渐合并或分裂簇。它们可以是凝聚的(自底向上),从每个对象作为单个簇开始,逐步合并到一个全体簇;或是分裂的(自顶向下),从所有对象组成一个簇开始,逐步分裂成更小的簇。层次方法的一个优点是可以形成簇的层次结构,非常适合解释性分析。

  3. 基于密度的方法:这些方法根据数据空间的密度分布来形成簇。它们能够识别任意形状的簇,并且对噪声和孤立点有良好的鲁棒性。著名的算法包括DBSCAN(Density-Based Spatial Clustering of Applications with Noise)和OPTICS(Ordering Points To Identify the Clustering Structure)。

  4. 基于网格的方法:网格聚类算法将数据空间划分为有限数量的单元,形成一个网格结构,并在这个结构上进行聚类。这些方法的优点是计算效率高,特别适合处理大数据集。STING(Statistical Information Grid)和WaveCluster是两种典型的基于网格的聚类方法。

  5. 基于模型的方法:这些方法基于统计模型将数据分配给簇,最常见的是基于高斯混合模型(Gaussian Mixture Models, GMM)的聚类。这类方法的优点是可以提供丰富的统计信息,对簇的形状和大小有更灵活的假设。

比较 K-prototypes & K-means

K-原型(K-prototypes)聚类算法是K-均值(K-means)聚类算法的扩展,专门设计来处理同时包含数值数据和分类数据的情况。这两种算法的主要区别在于它们处理数据类型的方式和计算簇中心(或称为原型)的方法。

K-means聚类算法

  • 数据类型:K-means主要适用于数值型数据。它通过计算数据点与簇中心之间的欧氏距离来划分簇,以最小化簇内距离的总和。
  • 簇中心:K-means的簇中心是簇内所有点的均值。
  • 限制:K-means对分类数据处理不佳,因为分类数据(如性别、职业等)没有自然的数值中心,且不能通过常规的欧氏距离来衡量相似性。

K-原型聚类算法

  • 数据类型:K-原型算法可以同时处理数值数据和分类数据。这使得它非常适用于现实世界的数据集,这些数据集通常包含这两种类型的数据。
  • 簇中心和距离计算:K-原型算法通过结合数值数据的均值和分类数据的众数来计算簇中心。同时,它使用特殊的距离度量来计算数值数据和分类数据的相似性,常见的方法是将数值数据的欧式距离和分类数据的汉明距离(Hamming distance)进行组合。
  • 优势:K-原型能够处理混合数据类型,使其更适合多种数据集,尤其是那些同时包含数值和分类属性的数据集。

应用场景对比

  • 当数据集仅包含数值型数据时,K-means是一个简单且有效的选择。
  • 当数据集包含数值数据和分类数据时,K-原型算法是更合适的选择,因为它能够同时处理这两种数据类型,并考虑它们在数据集中的相互作用。

总结来说,K-原型聚类算法是对K-means的重要补充,它扩展了聚类分析的应用范围,使之能够有效处理更加复杂和多样化的数据集。

K-Prototypes 聚类:所有游客/来过的游客画像

总的题目: ‘Q1’, ‘Q2’, ‘Q3’, ‘Q3|open’, ‘Q4’, ‘Q5’, ‘Q6|1|open’, ‘Q6|2|open’, ‘Q6|3|open’, ‘Q7|1’, ‘Q7|4’, ‘Q7|2’, ‘Q7|3’, ‘Q7|5’, ‘Q7|6’, ‘Q8’, ‘Q9’, ‘Q10’, ‘Q11’, ‘Q12’, ‘Q13’, ‘Q14’, ‘Q14|open’, ‘Q15’, ‘Q16|6’, ‘Q16|1’, ‘Q16|2’, ‘Q16|3’, ‘Q16|7’, ‘Q16|4’, ‘Q16|10’, ‘Q16|11’, ‘Q16|9’, ‘Q16|9|open’

对于Q3,Q14,Q16的其他选项对于聚类来说意义不大且不好处理,直接忽略

确定聚类簇数

使用肘部图确定最佳簇数
# 去过的游客 df_gone

from kmodes.kprototypes import KPrototypes
import matplotlib.pyplot as plt

columns = ['Q1', 'Q2', 'Q3', 'Q4', 'Q5',  'Q7|1', 'Q7|4', 'Q7|2', 'Q7|3', 'Q7|5', 'Q7|6', 'Q8', 'Q9', 'Q10', 'Q11', 'Q12', 'Q13', 'Q14', 'Q15', 'Q19', 'Q20','Q21|R1','Q21|R2','Q21|R3','Q21|R4','Q21|R5']
numerical_columns = ['Q2', 'Q4', 'Q5','Q7|1', 'Q7|4', 'Q7|2', 'Q7|3', 'Q7|5', 'Q7|6']

# 类别型列
categorical_columns = [col for col in columns if col not in numerical_columns]

# 将类别型列的索引转换为数字
categorical_columns_indices = [columns.index(col) for col in categorical_columns]

costs = []
for n_clusters in range(1, 10):
    kproto = KPrototypes(n_clusters=n_clusters, init='Cao', verbose=2)
    kproto.fit_predict(df_gone[columns].values, categorical=categorical_columns_indices)
    costs.append(kproto.cost_)

# 绘制成本随簇数变化的图
plt.plot(range(1, 10), costs, 'o-')
plt.title('Elbow Method (visited tourists)')
plt.xlabel('Number of clusters')
plt.ylabel('Cost')
plt.show()
使用轮廓系数确定簇数的方法
# 轮廓图,去过的游客

from kmodes.kprototypes import KPrototypes
from sklearn.metrics import silhouette_score
import matplotlib.pyplot as plt

columns = ['Q1', 'Q2', 'Q3', 'Q4', 'Q5',  'Q7|1', 'Q7|4', 'Q7|2', 'Q7|3', 'Q7|5', 'Q7|6', 'Q8', 'Q9', 'Q10', 'Q11', 'Q12', 'Q13', 'Q14', 'Q15', 'Q19', 'Q20','Q21|R1','Q21|R2','Q21|R3','Q21|R4','Q21|R5']
numerical_columns = ['Q2', 'Q4', 'Q5','Q7|1', 'Q7|4', 'Q7|2', 'Q7|3', 'Q7|5', 'Q7|6']

# 类别型列
categorical_columns = [col for col in columns if col not in numerical_columns]

# 将类别型列的索引转换为数字
categorical_columns_indices = [columns.index(col) for col in categorical_columns]

silhouette_scores = []
for n_clusters in range(2, 10):  # 轮廓系数至少需要2个簇
    kproto = KPrototypes(n_clusters=n_clusters, init='Cao', verbose=2)
    clusters = kproto.fit_predict(df_gone[columns].values, categorical=categorical_columns_indices)
    score = silhouette_score(df_gone[columns], clusters)
    silhouette_scores.append(score)

# 绘制轮廓系数随簇数变化的图
plt.plot(range(2, 10), silhouette_scores, 'o-')
plt.title('Silhouette Method (visited tourists)')
plt.xlabel('Number of clusters')
plt.ylabel('Silhouette Score')
plt.show()

开始聚类

from kmodes.kprototypes import KPrototypes

# 加入乌蒙大草原游客的基本信息,只能对已经来过的游客进行聚类
columns = ['Q1', 'Q2', 'Q3', 'Q4', 'Q5',  'Q7|1', 'Q7|4', 'Q7|2', 'Q7|3', 'Q7|5', 'Q7|6', 'Q8', 'Q9', 'Q10', 'Q11', 'Q12', 'Q13', 'Q14', 'Q15', 'Q19', 'Q20','Q21|R1','Q21|R2','Q21|R3','Q21|R4','Q21|R5']

# 数值型列
numerical_columns = ['Q2', 'Q4', 'Q5','Q7|1', 'Q7|4', 'Q7|2', 'Q7|3', 'Q7|5', 'Q7|6']

# 类别型列
categorical_columns = [col for col in columns if col not in numerical_columns]

# 将类别型列的索引转换为数字
categorical_columns_indices = [columns.index(col) for col in categorical_columns]

# 初始化 K-原型聚类器
kproto = KPrototypes(n_clusters=3, init='Cao', verbose=2)

clusters = kproto.fit_predict(df_gone[columns].values, categorical=categorical_columns_indices)

# 将聚类结果添加到数据集作为新的特征
df_gone['k-prototypes cluster'] = clusters

# 想查看每个簇的中心点
print(kproto.cluster_centroids_)

# 查看每个簇的数量
print(df_gone['k-prototypes cluster'].value_counts())

df_gone.head()
# 把'k-prototypes cluster'为0的数据保存到csv文件
df_gone[df_gone['k-prototypes cluster'] == 0].to_csv('cluster_0.csv', index=False)

特征选择:通过伪标签进行方差分析和卡方检验

from scipy.stats import f_oneway, chi2_contingency

# 对 numerical_columns 进行方差分析
for column in numerical_columns:
    groups = df_gone.groupby('k-prototypes cluster')[column].apply(list)
    f_val, p_val = f_oneway(*groups)
    if p_val < 0.05:
        print(f'Column: {column}, F-value: {f_val}, p-value: {p_val}. significant.')
    else:
        print(f'Column: {column}, F-value: {f_val}, p-value: {p_val}. NO.')

# 对 categorical_columns 进行卡方检验
for column in categorical_columns:
    contingency_table = pd.crosstab(df_gone[column], df_gone['k-prototypes cluster'])
    chi2, p_val, dof, expected = chi2_contingency(contingency_table)
    if p_val < 0.05:
        print(f'Column: {column}, Chi2: {chi2}, p-value: {p_val}. significant.')
    else:
        print(f'Column: {column}, Chi2: {chi2}, p-value: {p_val}. NO.')
from kmodes.kprototypes import KPrototypes

# # 列表中的列名
# columns = ['Q1', 'Q2', 'Q3', 'Q4', 'Q5', 'Q7|1', 'Q7|4', 'Q7|2', 'Q7|3', 'Q7|5', 'Q7|6', 'Q8', 'Q9', 'Q10', 'Q11', 'Q12', 'Q13', 'Q14', 'Q15']
# columns = ['Q2', 'Q4', 'Q5', 'Q8', 'Q10', 'Q11', 'Q12', 'Q13', 'Q14']
# 加入乌蒙大草原游客的基本信息,只能对已经来过的游客进行聚类
columns_again = ['Q4', 'Q5', 'Q7|5', 'Q10', 'Q19']

# # 数值型列
numerical_columns = ['Q4', 'Q5','Q7|5']
# numerical_columns = ['Q2', 'Q4', 'Q5']

# 类别型列
categorical_columns = [col for col in columns_again if col not in numerical_columns]

# 将类别型列的索引转换为数字
categorical_columns_indices = [columns_again.index(col) for col in categorical_columns]

# 初始化 K-原型聚类器
kproto = KPrototypes(n_clusters=2, init='Cao', verbose=2)

# 对所有游客进行聚类
# clusters = kproto.fit_predict(df[columns].values, categorical=categorical_columns_indices)
clusters = kproto.fit_predict(df_gone[columns_again].values, categorical=categorical_columns_indices)


# 将聚类结果添加到数据集作为新的特征
# df['k-prototypes cluster'] = clusters
df_gone['k-prototypes cluster'] = clusters

# 想查看每个簇的中心点
print(kproto.cluster_centroids_)

# 查看每个簇的数量
# print(df['k-prototypes cluster'].value_counts())
print(df_gone['k-prototypes cluster'].value_counts())


# df.head()
df_gone.head()

分布分析

对每个聚类中的:

  • 数值数据进行最小值、最大值(范围)、平均值、中位数(50%分位数)、四分位数(25%和75%分位数)、标准差的分析;
  • 分类数据进行各个类别的计数和比例
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd

df_selected = df_gone

df_cluster_1 = df_selected[df_selected['k-prototypes cluster'] == 0]
df_cluster_2 = df_selected[df_selected['k-prototypes cluster'] == 1]
df_cluster_3 = df_selected[df_selected['k-prototypes cluster'] == 2]

clusters_list = [df_cluster_1, df_cluster_2, df_cluster_3]
cluster_names = ['人群 1', '人群 2','人群 3']

# 数值数据的描述性统计
desc_stats = []
for i, df_cluster in enumerate(clusters_list):
    desc_stats.append(df_cluster[numerical_columns].describe())

desc_stats_df = pd.concat(desc_stats, axis=1, keys=cluster_names)
print(desc_stats_df)

# 分类数据的各个类别的计数和比例
cat_counts = []
for i, df_cluster in enumerate(clusters_list):
    cat_counts.append(df_cluster[categorical_columns].apply(lambda x: x.value_counts()).T.stack() / df_cluster.shape[0])

cat_counts_df = pd.concat(cat_counts, axis=1, keys=cluster_names)
print(cat_counts_df)

####################
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np

# 设置全局字体
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

# 计算子图的行数和列数
n = len(numerical_columns)
ncols = 3
nrows = n // ncols if n % ncols == 0 else n // ncols + 1

# 创建一个大图和子图的数组,每个子图都有自己独立的y轴刻度
fig, axs = plt.subplots(nrows, ncols, figsize=(15, 5*nrows), sharey=False)

# 创建一个ExcelWriter对象
with pd.ExcelWriter('output.xlsx') as writer:
    # 描述性统计的可视化
    for idx, question in enumerate(numerical_columns):
        # 计算子图的位置
        row = idx // ncols
        col = idx % ncols

        # 创建一个空的数据框来存储所有的簇的数据
        all_clusters = pd.DataFrame()
        for i, df_cluster in enumerate(clusters_list):
            # 添加一个新的列来标识簇
            df_cluster = df_cluster.copy()
            df_cluster['Cluster'] = f'Cluster {i+1}'
            all_clusters = pd.concat([all_clusters, df_cluster])

        # 在子图上绘制小提琴图,这里将使用各自的数据范围设置y轴刻度
        sns.violinplot(x='Cluster', y=question, data=all_clusters, ax=axs[row, col])
        axs[row, col].set_title(f'各簇{question}的小提琴图')
        axs[row, col].set_ylabel('')

        # 将数据写入Excel文件的一个新工作表
        all_clusters.to_excel(writer, sheet_name=question)

# 删除多余的子图
if n % ncols != 0:
    for col in range(n % ncols, ncols):
        fig.delaxes(axs[nrows-1, col])

plt.tight_layout()
plt.show()
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np

# 设置全局字体
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

# 计算子图的行数和列数
n = len(categorical_columns)
ncols = 5
nrows = n // ncols if n % ncols == 0 else n // ncols + 1

# 创建一个大图和子图的数组
fig, axs = plt.subplots(nrows, ncols, figsize=(15, 5*nrows))

# 创建一个ExcelWriter对象
with pd.ExcelWriter('output.xlsx') as writer:
    # 描述性统计的可视化
    for idx, question in enumerate(categorical_columns):
        # 计算子图的位置
        row = idx // ncols
        col = idx % ncols

        # 创建一个空的数据框来存储所有的簇的数据
        all_clusters = pd.DataFrame()
        for i, df_cluster in enumerate(clusters_list):
            # 添加一个新的列来标识簇
            df_cluster = df_cluster.copy()
            df_cluster['Cluster'] = f'Cluster {i+1}'
            all_clusters = pd.concat([all_clusters, df_cluster], ignore_index=True)

        # 数据透视以便于绘图,计算每个簇每个分类的数量
        pivot_df = all_clusters.pivot_table(index='Cluster', columns=question, aggfunc='size').fillna(0)

        # 将数量转换为占比
        pivot_df_percent = pivot_df.div(pivot_df.sum(axis=1), axis=0) * 100  # 计算占总数的百分比

        # 绘制堆叠条形图,展示百分比
        pivot_df_percent.plot(kind='bar', stacked=True, ax=axs[row, col], legend=True)

        axs[row, col].set_title(f'各簇{question}的堆叠条形图')
        axs[row, col].set_ylabel('百分比 (%)')

        # 将数据写入Excel文件的一个新工作表
        pivot_df_percent.to_excel(writer, sheet_name=question)

# 删除多余的子图
if n % ncols != 0:
    for col in range(n % ncols, ncols):
        fig.delaxes(axs[nrows-1, col])

plt.tight_layout()
plt.show()

聚类结果可视化

使用PCA,t-SNE,UMAP方法进行降维,得到2D和3D的图像,选择效果最好的可视化。

六合一可视化
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
from umap import UMAP
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

# 数据
# X = df[columns].values
X = df_gone[columns].values

# 创建PCA,t-SNE,UMAP对象
pca = PCA(n_components=2)
tsne = TSNE(n_components=2)
umap = UMAP(n_components=2)

# 降维
X_pca = pca.fit_transform(X)
X_tsne = tsne.fit_transform(X)
X_umap = umap.fit_transform(X)

# 创建图形和子图
fig, axs = plt.subplots(3, 2, figsize=(15, 20))

# 绘制二维版本
sc = axs[0, 0].scatter(X_pca[:, 0], X_pca[:, 1], c=clusters, cmap='viridis', alpha=0.5)
axs[0, 0].set_title('PCA 2D')
fig.colorbar(sc, ax=axs[0, 0], shrink=0.6)

sc = axs[1, 0].scatter(X_tsne[:, 0], X_tsne[:, 1], c=clusters, cmap='viridis', alpha=0.5)
axs[1, 0].set_title('t-SNE 2D')
fig.colorbar(sc, ax=axs[1, 0], shrink=0.6)

sc = axs[2, 0].scatter(X_umap[:, 0], X_umap[:, 1], c=clusters, cmap='viridis', alpha=0.5)
axs[2, 0].set_title('UMAP 2D')
fig.colorbar(sc, ax=axs[2, 0], shrink=0.6)

# 创建PCA,t-SNE,UMAP对象
pca = PCA(n_components=3)
tsne = TSNE(n_components=3)
umap = UMAP(n_components=3)

# 降维
X_pca = pca.fit_transform(X)
X_tsne = tsne.fit_transform(X)
X_umap = umap.fit_transform(X)

# 绘制三维版本
axs[0, 1] = fig.add_subplot(3, 2, 2, projection='3d')
sc = axs[0, 1].scatter(X_pca[:, 0], X_pca[:, 1], X_pca[:, 2], c=clusters, cmap='viridis', alpha=0.5)
axs[0, 1].set_title('PCA 3D')
fig.colorbar(sc, ax=axs[0, 1], shrink=0.6, pad=0.2)  # 修改的代码

axs[1, 1] = fig.add_subplot(3, 2, 4, projection='3d')
sc = axs[1, 1].scatter(X_tsne[:, 0], X_tsne[:, 1], X_tsne[:, 2], c=clusters, cmap='viridis', alpha=0.5)
axs[1, 1].set_title('t-SNE 3D')
fig.colorbar(sc, ax=axs[1, 1], shrink=0.6, pad=0.2)  # 修改的代码

axs[2, 1] = fig.add_subplot(3, 2, 6, projection='3d')
sc = axs[2, 1].scatter(X_umap[:, 0], X_umap[:, 1], X_umap[:, 2], c=clusters, cmap='viridis', alpha=0.5)
axs[2, 1].set_title('UMAP 3D')
fig.colorbar(sc, ax=axs[2, 1], shrink=0.6, pad=0.2)  # 修改的代码
# 调整子图之间的间隙
plt.subplots_adjust(wspace=0.5, hspace=0.5)

plt.tight_layout()
plt.show()
PCA主成分降维
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

df_selected = df_gone

# 使用PCA进行降维
pca = PCA(n_components=2)
X_pca = pca.fit_transform(df_selected[columns].values)

# 绘制散点图
plt.figure(figsize=(8, 6))
plt.scatter(X_pca[:, 0], X_pca[:, 1], c=clusters, cmap='viridis', alpha=0.5)
plt.title('K-Prototypes Clustering (PCA-reduced data)')
plt.xlabel('Principal Component 1')
plt.ylabel('Principal Component 2')
plt.colorbar(label='Cluster')
plt.show()
PCA主成分降维 3D
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

df_selected = df_gone

# 使用PCA进行降维
pca = PCA(n_components=3)
X_pca = pca.fit_transform(df_selected[columns].values)

# 创建一个3D图形
fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111, projection='3d')

# 绘制散点图
sc = ax.scatter(X_pca[:, 0], X_pca[:, 1], X_pca[:, 2], c=clusters, cmap='viridis', alpha=0.5)

plt.title('K-Prototypes Clustering (PCA-reduced data)')
ax.set_xlabel('Principal Component 1')
ax.set_ylabel('Principal Component 2')
ax.set_zlabel('Principal Component 3')
fig.colorbar(sc, label='Cluster', pad=0.1)

plt.show()
df.head()
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import imageio
import os

df_selected = df_gone

# 使用PCA进行降维
pca = PCA(n_components=3)
X_pca = pca.fit_transform(df_selected[columns].values)

# 创建GIF的帧
filenames = []

for azim in range(-90, 90, 2):  # 从0度旋转到360度,每次增加10度
    # 创建一个3D图形
    fig = plt.figure(figsize=(8, 6))
    ax = fig.add_subplot(111, projection='3d')

    # 绘制散点图
    sc = ax.scatter(X_pca[:, 0], X_pca[:, 1], X_pca[:, 2], c=clusters, cmap='viridis', alpha=0.5)

    ax.set_title('K-Prototypes Clustering (PCA-reduced data)')
    ax.set_xlabel('Principal Component 1')
    ax.set_ylabel('Principal Component 2')
    ax.set_zlabel('Principal Component 3')
    fig.colorbar(sc, label='Cluster', pad=0.1)

    # 设置视角
    ax.view_init(elev=20., azim=azim)

    # 保存帧
    filename = f'frame_{azim}.png'
    plt.savefig(filename)
    filenames.append(filename)

    plt.close(fig)  # 关闭图形,防止在notebook中显示

# 使用所有的帧创建一个GIF
with imageio.get_writer('mygif.gif', mode='I') as writer:
    for filename in filenames:
        image = imageio.imread(filename)
        writer.append_data(image)

# 删除所有的帧
for filename in filenames:
    os.remove(filename)
t-SNE降维
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

# 使用t-SNE进行降维
tsne = TSNE(n_components=3)
X_tsne = tsne.fit_transform(df[columns].values)

# 创建一个3D图形
fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111, projection='3d')

# 绘制散点图
sc = ax.scatter(X_tsne[:, 0], X_tsne[:, 1], X_tsne[:, 2], c=clusters, cmap='viridis', alpha=0.5)

plt.title('K-Prototypes Clustering')
ax.set_xlabel('t-SNE Dimension 1')
ax.set_ylabel('t-SNE Dimension 2')
ax.set_zlabel('t-SNE Dimension 3')
fig.colorbar(sc, label='Cluster')
plt.show()
df.head()
制作t-SNE gif版本
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import imageio
from sklearn.manifold import TSNE

# 初始化t-SNE
tsne = TSNE(n_components=3)
#, init='random', random_state=0, n_iter=1000, method='exact'

# 保存每次迭代的图像
frames = []
for i in range(250, 1000, 50):
    tsne.n_iter = i
    X_tsne = tsne.fit_transform(df[columns].values)
    
    fig = plt.figure(figsize=(8, 6))
    ax = fig.add_subplot(111, projection='3d')
    scatter = ax.scatter(X_tsne[:, 0], X_tsne[:, 1], X_tsne[:, 2], c=clusters, cmap='viridis', alpha=0.5)
    ax.set_title(f't-SNE with {i} iterations')
    ax.set_xlabel('t-SNE Dimension 1')
    ax.set_ylabel('t-SNE Dimension 2')
    ax.set_zlabel('t-SNE Dimension 3')
    fig.colorbar(scatter, label='Cluster')
    plt.savefig(f'tsne_{i}.png')
    plt.close(fig)
    
    frames.append(imageio.imread(f'tsne_{i}.png'))

# 保存为GIF
imageio.mimsave('tsne.gif', frames, 'GIF', duration=1)
df.head()
使用UMAP降维
import umap
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

# 假设df是你的数据框,columns是你想要降维的列
reducer = umap.UMAP(n_components=3, random_state=8)
embedding = reducer.fit_transform(df[columns].values)

# 创建一个3D图形
fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111, projection='3d')

# 绘制散点图
sc = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2], c=clusters, cmap='viridis', alpha=0.5)

plt.title('UMAP projection')
ax.set_xlabel('Dimension 1')
ax.set_ylabel('Dimension 2')
ax.set_zlabel('Dimension 3')
fig.colorbar(sc, label='Cluster')

# 调整布局
plt.tight_layout()
plt.show()
df.head()
UMAP GIF
import umap
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import imageio

# 假设df是你的数据框,columns是你想要降维的列
frames = []
for n in range(10, 51, 5):
    reducer = umap.UMAP(n_components=3, random_state=8, n_neighbors=n)
    embedding = reducer.fit_transform(df[columns].values)

    # 创建一个3D图形
    fig = plt.figure(figsize=(8, 6))
    ax = fig.add_subplot(111, projection='3d')

    # 绘制散点图
    sc = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2], c=clusters, cmap='viridis', alpha=0.5)

    plt.title(f'UMAP projection with {n} neighbors')
    ax.set_xlabel('Dimension 1')
    ax.set_ylabel('Dimension 2')
    ax.set_zlabel('Dimension 3')
    fig.colorbar(sc, label='Cluster')

    # 调整布局
    plt.tight_layout()

    # 保存图像
    plt.savefig(f'umap_{n}.png')
    plt.close(fig)
    
    frames.append(imageio.imread(f'umap_{n}.png'))

# 保存为GIF
imageio.mimsave('umap.gif', frames, 'GIF', duration=1)
df.head()

输出聚类结果

import csv
# 聚类中心
centroids = kproto.cluster_centroids_

# show_columns = ['Q2', 'Q4', 'Q5','Q7|1', 'Q7|4', 'Q7|2', 'Q7|3', 'Q7|5', 'Q7|6','Q1', 'Q3', 'Q8', 'Q9', 'Q10', 'Q11', 'Q12', 'Q13', 'Q14', 'Q15', 'Q16|6', 'Q16|1', 'Q16|2', 'Q16|3', 'Q16|7', 'Q16|4', 'Q16|10', 'Q16|11', 'Q16|9']
show_columns = columns

# 打开你想要写入的文件
with open('centroids.csv', 'w', newline='') as file:
    writer = csv.writer(file)
    # 写入题号作为第一行
    writer.writerow(show_columns)
    # 写入数据
    writer.writerows(centroids)

# 打印题号和聚类中心
print("Question IDs:")
print(show_columns)
print("Cluster centroids:")
print(centroids)


# 聚类标签
labels = kproto.labels_
print("Labels of first 10 data points:")
print(labels[:10])
df.head()

对照文本编码

读取centroids.csv匹配structured_questions.json中的题号把对应的单选选项换掉,并添加上题目

结果在centrios.csv中

import csv
import json
import re

# 读取 structured_questions.json 文件
with open('structured_questions.json', 'r') as json_file:
    structured_questions = json.load(json_file)

# 读取 centroids.csv 文件
with open('centroids.csv', 'r') as csv_file:
    csv_reader = csv.reader(csv_file)
    data = list(csv_reader)

# 先替换选项
for i, row in enumerate(data):
    if i == 0:  
        continue
    for j, value in enumerate(row):  # 遍历每一列
        question_id = data[0][j]  # 获取题号,只有单选和填空的格式是Q1这种,可以匹配上
        if question_id in structured_questions:  # 如果题号在 structured_questions 中
            # 替换选项
            data[i][j] = structured_questions[question_id]['options'].get(value, value)
            
# 再替换题目
for i, row in enumerate(data):
    if i == 0:  # 指针对第一行替换题目
        for j, value in enumerate(row):
            if value in structured_questions:
                data[i][j] = value +" "+structured_questions[value]['text']
            
            # 对于题目为Q7 | 1这种格式,把Q7替换为text的内容,1替换为options中的内容
            # 用正则表达式匹配
            if re.match(r"^Q\d+\|\d+$", value):
                question_id, option_id = value.split('|', 1)
                if question_id in structured_questions:
                    data[i][j] = question_id +" "+ structured_questions[question_id]['text'] + ": " + structured_questions[question_id]['options'][option_id]            
    if i!=0:
        break

# 将更新后的数据写回 centroids.csv 文件
with open('centroids.csv', 'w', newline='') as csv_file:
    csv_writer = csv.writer(csv_file)
    csv_writer.writerows(data)
    
df.head()

基于密度聚类 DBSCAN

from sklearn.cluster import DBSCAN
from sklearn.preprocessing import StandardScaler
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
import pandas as pd
print(df)

# 列表中的列名
columns = ['Q1', 'Q2', 'Q3', 'Q4', 'Q5', 'Q7|1', 'Q7|4', 'Q7|2', 'Q7|3', 'Q7|5', 'Q7|6', 'Q8', 'Q9', 'Q10', 'Q11', 'Q12', 'Q13', 'Q14', 'Q15', 'Q16|6', 'Q16|1', 'Q16|2', 'Q16|3', 'Q16|7', 'Q16|4', 'Q16|10', 'Q16|11', 'Q16|9']

# 数值型列
numerical_columns = ['Q2', 'Q4', 'Q5','Q7|1', 'Q7|4', 'Q7|2', 'Q7|3', 'Q7|5', 'Q7|6']

# 对数值型列进行标准化
scaler = StandardScaler()
df[numerical_columns] = scaler.fit_transform(df[numerical_columns])

X = df[columns].values

# 初始化 DBSCAN 聚类器
dbscan = DBSCAN(eps=4.7, min_samples=7)

# 进行聚类
clusters = dbscan.fit_predict(X)

# 添加数据标签
df['dbscan cluster'] = clusters


# 使用t-SNE进行降维
tsne = TSNE(n_components=2, random_state=42)
X_tsne = tsne.fit_transform(X)

# 绘制散点图
plt.figure(figsize=(8, 6))
plt.scatter(X_tsne[:, 0], X_tsne[:, 1], c=clusters, cmap='viridis', alpha=0.5)

plt.title('t-SNE projection')
plt.xlabel('Dimension 1')
plt.ylabel('Dimension 2')
plt.colorbar(label='Cluster')

plt.show()
df.head()
from sklearn.cluster import DBSCAN
from sklearn.preprocessing import StandardScaler
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import pandas as pd

print(df.columns)
# 列表中的列名
columns = ['Q1', 'Q2', 'Q3', 'Q4', 'Q5', 'Q7|1', 'Q7|4', 'Q7|2', 'Q7|3', 'Q7|5', 'Q7|6', 'Q8', 'Q9', 'Q10', 'Q11', 'Q12', 'Q13', 'Q14', 'Q15', 'Q16|6', 'Q16|1', 'Q16|2', 'Q16|3', 'Q16|7', 'Q16|4', 'Q16|10', 'Q16|11', 'Q16|9']

# 数值型列
numerical_columns = ['Q2', 'Q4', 'Q5','Q7|1', 'Q7|4', 'Q7|2', 'Q7|3', 'Q7|5', 'Q7|6']

# 对数值型列进行标准化
scaler = StandardScaler()
df[numerical_columns] = scaler.fit_transform(df[numerical_columns])

X = df[columns].values

# 初始化 DBSCAN 聚类器
dbscan = DBSCAN(eps=4.7, min_samples=7)

# 进行聚类
clusters = dbscan.fit_predict(X)

# 使用t-SNE进行降维
tsne = TSNE(n_components=3, random_state=42)
X_tsne = tsne.fit_transform(X)

# 创建一个3D图形
fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111, projection='3d')

# 绘制散点图
sc = ax.scatter(X_tsne[:, 0], X_tsne[:, 1], X_tsne[:, 2], c=clusters, cmap='viridis', alpha=0.5)

plt.title('t-SNE projection')
ax.set_xlabel('Dimension 1')
ax.set_ylabel('Dimension 2')
ax.set_zlabel('Dimension 3')
fig.colorbar(sc, label='Cluster')

plt.show()
df.head()

决策树

基础知识

决策树是一种常用的数据挖掘技术,用于分类和回归任务。它通过构造一个树状结构来模拟决策过程。主要的决策树算法包括:

  1. ID3(Iterative Dichotomiser 3):基于信息增益来选择特征。
  2. C4.5:ID3的改进版,使用增益率来选择特征。
  3. CART(Classification and Regression Trees):同时支持分类和回归任务,基于基尼不纯度(对于分类)或最小二乘偏差(对于回归)来选择特征。
  4. CHAID(Chi-squared Automatic Interaction Detector):使用卡方检验来选择最佳特征,可以处理分类变量。

这些方法在不同的情况下各有优势。例如,CART是最常用的算法之一,因为它简单且适用于各种类型的数据。

CART决策树:决定游客来不来的画像因素分析

针对没有来过的游客,分析他们没有来过的原因

决策树处理

核心问题是Q29,可以结合游客的基本信息一起分析 Q29|R1,Q29|R2,Q29|R3,Q29|R4

k-prototypes cluster 之前聚类的结果

feature_columns是问卷的一组问题,目标变量是去了还是没有去

from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import classification_report
from sklearn.tree import plot_tree
from sklearn.feature_selection import SelectPercentile, chi2
import matplotlib.pyplot as plt

# 定义特征和目标变量
feature_columns = ['Q8','Q9','Q10','Q11','Q12','Q13','Q14','Q15']
X_unencoded = df[feature_columns]
target_column = df['Q17']

# 对特征进行0-1编码
X_encoded = pd.get_dummies(X_unencoded, columns=feature_columns)

# 对目标变量进行0-1编码
y_encoded = target_column

# 特征选择
selector = SelectPercentile(chi2, percentile=10)
X_new = selector.fit_transform(X_encoded, y_encoded)

# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X_new, y_encoded, test_size=0.1, random_state=42)

# Best feature selector: SelectPercentile
# Best parameters: {'criterion': 'gini', 'max_depth': 5, 'min_samples_leaf': 3, 'min_samples_split': 2, 'splitter': 'best'}

# 初始化决策树分类器
classifier = DecisionTreeClassifier(random_state=42, max_depth=5,criterion='gini',splitter='best',min_samples_split=2,min_samples_leaf=3)

# 训练模型
classifier.fit(X_train, y_train)

# 预测测试集
y_pred = classifier.predict(X_test)

# 打印分类报告
print(classification_report(y_test, y_pred))

# 获取目标变量的类别名称
class_names = target_column.unique().tolist()

# 将类别名称转换为字符串
class_names = [str(name) for name in class_names]

# 绘制决策树
plt.figure(figsize=(80,40))
plot_tree(classifier, filled=True, class_names=class_names)
plt.show()

超参数调优

from sklearn.feature_selection import SelectKBest, chi2, SelectPercentile, mutual_info_classif, f_classif, SelectFromModel
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import f1_score

# 定义特征选择方法
feature_selectors = [
    ('SelectKBest_chi2', SelectKBest(chi2, k=10)),
    ('SelectPercentile', SelectPercentile(chi2, percentile=10)),
    ('mutual_info_classif', SelectKBest(mutual_info_classif, k=10)),
    ('f_classif', SelectKBest(f_classif, k=10)),
    ('SelectFromModel', SelectFromModel(LogisticRegression(penalty="l1", solver='liblinear'))),
]

# 定义要搜索的参数范围
param_grid = {
    'max_depth': [3, 4, 5, 6, 7, 8, 9, 10],
    'criterion': ['gini', 'entropy'],
    'splitter': ['best', 'random'],
    'min_samples_split': [2, 3, 4, 5],
    'min_samples_leaf': [1, 2, 3, 4, 5],
}

# 初始化决策树分类器
classifier = DecisionTreeClassifier(random_state=42)

# 初始化网格搜索
grid_search = GridSearchCV(classifier, param_grid, cv=5, scoring='f1_macro')

best_score = 0
best_selector = None
best_params = None

# 对每个特征选择方法进行迭代
for name, selector in feature_selectors:
    # 执行特征选择
    X_new = selector.fit_transform(X_encoded, y_encoded)
    
    # 执行网格搜索
    grid_search.fit(X_new, y_encoded)
    
    # 如果这个得分比之前的得分好,就更新最好的得分和最好的参数
    if grid_search.best_score_ > best_score:
        best_score = grid_search.best_score_
        best_selector = name
        best_params = grid_search.best_params_

# 打印最好的特征选择方法和最好的参数
print('Best feature selector:', best_selector)
print('Best parameters:', best_params)

CART决策树:游客满意度因素分析

数据集:df_cart_satisfaction

目标变量是’满意度’,分类变量:Q24|R1,Q24|R2,Q24|R3,Q24|R4,Q24|R5,Q24|R6,Q24|R7,Q24|R8,Q24|R9,Q24|R10,Q24|R11,Q24|R12,Q24|R13,Q24|R14,Q24|R18,Q24|R15,Q24|R16,Q24|R17

import pandas as pd

# 从"熵权满意度.xlsx"中读取数据
df_cart_satisfaction = pd.read_excel('熵权满意度.xlsx')
df_cart_satisfaction.head()
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import mean_squared_error
from sklearn.tree import plot_tree
import matplotlib.pyplot as plt

# 定义特征和目标变量
feature_columns =['Q24|R1', 'Q24|R2', 'Q24|R3', 'Q24|R4', 'Q24|R5', 'Q24|R6', 'Q24|R7', 'Q24|R8', 'Q24|R9', 'Q24|R10', 'Q24|R11', 'Q24|R12', 'Q24|R13', 'Q24|R14', 'Q24|R18', 'Q24|R15', 'Q24|R16', 'Q24|R17']
X = df_cart_satisfaction[feature_columns]
y = df_cart_satisfaction['满意度']

# Best feature selector: mutual_info_regression
# Best parameters: {'max_depth': 10, 'min_samples_leaf': 1, 'min_samples_split': 3, 'splitter': 'random'}

# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)

# 初始化决策树回归器
regressor = DecisionTreeRegressor(random_state=42, max_depth=3,min_samples_split=3,min_samples_leaf=1,splitter='random')

# 训练模型
regressor.fit(X_train, y_train)

# 预测测试集
y_pred = regressor.predict(X_test)

# 打印均方误差
print("Mean Squared Error: ", mean_squared_error(y_test, y_pred))

# 绘制决策树
plt.figure(figsize=(80,40))
plot_tree(regressor, filled=True)
plt.show()
from sklearn.feature_selection import SelectKBest, f_regression, mutual_info_regression, SelectFromModel
from sklearn.linear_model import Lasso
from sklearn.model_selection import GridSearchCV
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import make_scorer, mean_squared_error

# 定义特征选择方法
feature_selectors = [
    ('SelectKBest_f_regression', SelectKBest(f_regression, k=10)),
    ('mutual_info_regression', SelectKBest(mutual_info_regression, k=10)),
    ('SelectFromModel', SelectFromModel(Lasso(alpha=0.1))),
]

# 定义要搜索的参数范围
param_grid = {
    'max_depth': [3, 4, 5, 6, 7, 8, 9, 10],
    'splitter': ['best', 'random'],
    'min_samples_split': [2, 3, 4, 5],
    'min_samples_leaf': [1, 2, 3, 4, 5],
}

# 初始化决策树回归器
regressor = DecisionTreeRegressor(random_state=42)

# 初始化网格搜索
grid_search = GridSearchCV(regressor, param_grid, cv=5, scoring=make_scorer(mean_squared_error, greater_is_better=False))

best_score = float('inf')
best_selector = None
best_params = None

# 对每个特征选择方法进行迭代
for name, selector in feature_selectors:
    # 执行特征选择
    X_new = selector.fit_transform(X, y)
    
    # 执行网格搜索
    grid_search.fit(X_new, y)
    
    # 如果这个得分比之前的得分好,就更新最好的得分和最好的参数
    if grid_search.best_score_ < best_score:
        best_score = grid_search.best_score_
        best_selector = name
        best_params = grid_search.best_params_

# 打印最好的特征选择方法和最好的参数
print('Best feature selector:', best_selector)
print('Best parameters:', best_params)
import pandas as pd

# Load the dataset
df_cart_satisfaction = pd.read_excel('熵权满意度.xlsx')

from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import mean_squared_error, r2_score

# Feature and target columns
feature_columns = ['Q24|R1', 'Q24|R2', 'Q24|R3', 'Q24|R4', 'Q24|R5', 'Q24|R6', 'Q24|R7', 'Q24|R8', 'Q24|R9', 'Q24|R10', 'Q24|R11', 'Q24|R12', 'Q24|R13', 'Q24|R14', 'Q24|R18', 'Q24|R15', 'Q24|R16', 'Q24|R17']
X = df_cart_satisfaction[feature_columns]
y = df_cart_satisfaction['满意度']

# Splitting data into training and testing sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Initialize and train Decision Tree Regressor
dt_regressor = DecisionTreeRegressor(random_state=42)
dt_regressor.fit(X_train, y_train)

# Predicting the Test set results
y_pred = dt_regressor.predict(X_test)

# Evaluate the model
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)

mse, r2
# Getting feature importances
feature_importances = pd.Series(dt_regressor.feature_importances_, index=feature_columns)

# Sort the feature importances in descending order
feature_importances_sorted = feature_importances.sort_values(ascending=False)

feature_importances_sorted

from sklearn.tree import plot_tree
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap

# 为决策树的不同深度生成一个绿色渐变颜色映射
cmap = ListedColormap(["#e5f5e0", "#a1d99b", "#31a354"])

# Plotting the Decision Tree with green color scheme
plt.figure(figsize=(20,10))
# 使用colormap参数指定颜色映射
plot_tree(dt_regressor, feature_names=feature_columns, filled=True, max_depth=3, fontsize=10, cmap=cmap)
plt.title("满意度影响因素决策树")
plt.show()

灰色关联

灰色关联分析(Grey Relational Analysis, GRA)是灰色系统理论的一个重要部分,由中国学者邓聚龙在20世纪80年代初提出。灰色系统理论主要研究部分信息已知、部分信息未知的系统,而灰色关联分析则是用来分析和处理系统中因素之间的关联程度的一种方法。

基础知识

主要用途

  1. 关联度分析:通过计算和比较序列之间的关联度(相似度),确定各个因素对系统行为的影响程度,识别出主要影响因素和次要影响因素。
  2. 预测与决策支持:在只有少量数据或系统信息不完全的情况下,灰色关联分析可以用来预测系统的未来发展趋势,为决策提供依据。
  3. 模式识别和分类:根据关联度的高低,可以将系统中的因素或对象进行分类,识别出具有相似性质的群体或模式。

分析特点

  • 适用于小样本和不完全信息:与传统的统计分析方法相比,灰色关联分析不需要大量数据,适用于信息不完全和数据量少的情况。
  • 操作简便,易于理解和实施:灰色关联分析的计算过程相对简单,容易实现,计算结果直观易懂。
  • 广泛的应用领域:灰色关联分析在经济分析、社会科学、环境工程、生物医药等多个领域都有应用。

应用示例

假设要分析某一地区经济增长与多个因素(如投资、消费、出口等)之间的关系,可以使用灰色关联分析来确定哪些因素与经济增长的关联度最高,从而为经济政策的制定提供依据。

灰色关联分析通过计算参考数列(如经济增长率)与比较数列(如投资增长率、消费增长率等)之间的关联度,来评估各因素对目标(经济增长)的影响程度。通过比较各因素的关联度大小,可以辨识出对经济增长影响最大的因素。

灰色关联:探究满意度的提升和打造产品的关系

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# 假设 df_gone 是自变量的DataFrame,df_gone['Q26|1'] 是因变量

# 数据准备
columns = ['Q2', 'Q4', 'Q5', 'Q8', 'Q9', 'Q10', 'Q11', 'Q12', 'Q13', 'Q14', 'Q15']

# 数值型列
numerical_columns = ['Q2', 'Q4', 'Q5']

# 类别型列
categorical_columns = [col for col in columns if col not in numerical_columns]

# 分类变量进行独热编码
df_encoded = pd.get_dummies(df_gone[categorical_columns])

# 保留数值型列的原始数据
df_numerical = df_gone[numerical_columns]

# 合并数值型数据和经过独热编码的分类数据
X_encoded = pd.concat([df_numerical, df_encoded], axis=1)

X = X_encoded  # 自变量数据
y = df_gone['Q26|1'].values  # 因变量数据

# 标准化(归一化)处理
X_norm = (X - X.min()) / (X.max() - X.min())
y_norm = (y - y.min()) / (y.max() - y.min())

# 计算关联系数
def grey_relational_coefficient(x, y):
    epsilon = 0.5  # 分辨系数,一般取值为(0,1)
    delta = np.abs(x - y)
    min_delta = np.min(delta)
    max_delta = np.max(delta)
    xi = (min_delta + epsilon * max_delta) / (delta + epsilon * max_delta)
    return xi

# 计算每个自变量与因变量的关联系数
xi_matrix = np.array([grey_relational_coefficient(X_norm.iloc[:, i].values, y_norm) for i in range(X_norm.shape[1])])

# 计算关联度
grey_relational_grades = np.mean(xi_matrix, axis=1)

# 对关联度进行降序排列,并获取排序后的索引
sorted_indices = np.argsort(grey_relational_grades)[::-1]

# 可视化关联度
plt.figure(figsize=(10, 6))
plt.barh(range(len(sorted_indices)), grey_relational_grades[sorted_indices], color='skyblue')
plt.yticks(range(len(sorted_indices)), X_encoded.columns[sorted_indices])
plt.xlabel('Grey Relational Grade')
plt.title('Grey Relational Analysis (GRA) of Variables to Q26')
plt.gca().invert_yaxis()  # 使得y轴按关联度从高到低排列
plt.show()

关联规则

关联规则分析是一种在大数据集中寻找项目间的有趣关系(如频繁模式、关联、相关性)的方法,主要用于事务数据、序列数据等。它最著名的应用是市场篮分析,但也广泛应用于生物信息学、药品配对、推荐系统等领域。

基础知识

作用

  • 发现数据间隐藏的关系:帮助识别数据集中不同项目之间的关联性,即用户行为或特征之间的内在联系。
  • 决策支持:为零售商提供交叉销售和产品捆绑的策略依据,通过理解产品之间的关系来优化库存管理和推广策略。
  • 推荐系统:基于用户的购买历史或行为模式,推荐他们可能感兴趣的商品或服务。

实现方法

  • Apriori算法:最早也是最著名的关联规则挖掘算法,通过逐层搜索频繁项集的方法来发现数据之间的强规则。
  • FP-Growth算法:比Apriori效率更高,不需要产生候选项集,直接构造FP树来挖掘频繁项集。
  • Eclat算法:基于垂直数据格式,使用交集操作来快速得到频繁项集,效率高于Apriori。

关联规则分析:推荐意愿分析

针对27、28进行关联规则分析

Q27|1,Q28|1,Q28|23,Q28|21,Q28|22,Q28|24,Q28|25,Q28|4,Q28|2,Q28|26,Q28|27,Q28|28,Q28|29,Q28|35,Q28|34,

排除Q28|34|open


支持度(Support):支持度可以理解为物品当前流行程度。计算方式是:

支持度 = (包含物品A的记录数量) / (总的记录数量)
用上面的超市记录举例,一共有五个交易,牛奶出现在三个交易中,故而{牛奶}的支持度为3/5。{鸡蛋}的支持度是4/5。牛奶和鸡蛋同时出现的次数是2,故而{牛奶,鸡蛋}的支持度为2/5。

置信度(Confidence):置信度是指如果购买物品A,有较大可能购买物品B。计算方式是这样:

置信度( A -> B) = (包含物品A和B的记录数量) / (包含 A 的记录数量)
举例:我们已经知道,(牛奶,鸡蛋)一起购买的次数是两次,鸡蛋的购买次数是4次。那么Confidence(牛奶->鸡蛋)的计算方式是Confidence(牛奶->鸡蛋)=2 / 4。

提升度(Lift):提升度指当销售一个物品时,另一个物品销售率会增加多少。计算方式是:

提升度( A -> B) = 置信度( A -> B) / (支持度 A)

泡泡图:support, confidence, lift

from mlxtend.frequent_patterns import apriori
from mlxtend.frequent_patterns import association_rules
import matplotlib.pyplot as plt
import pandas as pd

df_apriori = df_gone[['Q28|1','Q28|23','Q28|21','Q28|22','Q28|24','Q28|25','Q28|4','Q28|2','Q28|26','Q28|27','Q28|28','Q28|29','Q28|35','Q28|34']]

df_apriori = df_apriori.rename(columns={
    'Q28|1': '草原风光',
    'Q28|23': '矮杜鹃花花海',
    'Q28|21': '乌蒙滑雪场',
    'Q28|22': '七彩滑道',
    'Q28|24': '玻璃水滑道',
    'Q28|25': '观佛台观赏佛光',
    'Q28|4': '野炊露营',
    'Q28|2': '民俗风情',
    'Q28|26': '蒙古包酒店',
    'Q28|27': '精品民宿',
    'Q28|28': '牛肉馆',
    'Q28|29': '羊汤锅',
    'Q28|35': '旅游商品',
    # (刺梨系列商品、盘县火腿、人民小酒、茶叶、银杏系列商品等)
    'Q28|34': '其他'
})

# 将数据集中的所有值都转换为数值,忽略无法转换的值
df_apriori = df_apriori.apply(pd.to_numeric, errors='coerce')

# 然后再将数据集中的值转换为布尔值
df_encoded = df_apriori.applymap(lambda x: False if x <= 0 else True)

# print(df_encoded)

# 使用apriori找出频繁项集,将支持度阈值提高到0.5
frequent_itemsets = apriori(df_encoded, min_support=0.3, use_colnames=True)

# 使用association_rules找出关联规则,将置信度阈值提高到0.8
rules = association_rules(frequent_itemsets, metric="confidence", min_threshold=0.5)

# 打印关联规则
# print(rules)

# 现在我们绘制泡泡图
# 规则的支持度决定泡泡的大小,提升度决定颜色的深浅
plt.scatter(rules['support'], rules['confidence'], s=rules['support']*1000, c=rules['lift'], cmap='RdYlBu', alpha=0.5)
plt.xlabel('Support')
plt.ylabel('Confidence')
plt.title('Bubble Chart for Association Rules')
plt.colorbar(label='Lift')
plt.show()

变量可视化

# 设置字体为SimHei显示中文
plt.rcParams['font.sans-serif'] = ['SimHei'] 
plt.rcParams['axes.unicode_minus'] = False 

# 创建一个空的矩阵,用于存放支持度和提升度
support_matrix = pd.DataFrame(index=df_encoded.columns, columns=df_encoded.columns)
lift_matrix = pd.DataFrame(index=df_encoded.columns, columns=df_encoded.columns)

# 填充矩阵
for i, rule in rules.iterrows():
    antecedents = next(iter(rule['antecedents']))
    consequents = next(iter(rule['consequents']))
    support_matrix.loc[antecedents, consequents] = rule['support']
    lift_matrix.loc[antecedents, consequents] = rule['lift']

import matplotlib.colors as mcolors

# 绘制泡泡图
fig, ax = plt.subplots(figsize=(14, 9))  # 修改图表宽度
norm = mcolors.Normalize(vmin=lift_matrix.min().min(), vmax=lift_matrix.max().max())  # 创建归一化对象
for i in support_matrix.columns:
    for j in support_matrix.index:
        support = support_matrix.loc[j, i]
        lift = lift_matrix.loc[j, i]
        if pd.notnull(support):
            ax.scatter(i, j, s=support*3500, c=lift, cmap='RdYlBu', norm=norm, alpha=0.7)  # 修改散点图的大小

# 设置图表格式
ax.set_xticks(range(len(support_matrix.columns)))
ax.set_yticks(range(len(support_matrix.index)))
ax.set_xticklabels(support_matrix.columns, rotation=90)
ax.set_yticklabels(support_matrix.index)

# 显示背景网格
ax.grid(True, linestyle='--')

# 添加颜色条
sm = plt.cm.ScalarMappable(cmap='RdYlBu', norm=plt.Normalize(vmin=lift_matrix.min().min(), vmax=lift_matrix.max().max()))
sm._A = []
cbar = plt.colorbar(sm, shrink=0.7)  # 修改颜色条的大小
cbar.set_label('Lift')

# 显示图表
plt.show()

热力图

import seaborn as sns

# 创建一个空的DataFrame,用于存储提升度
lift_matrix = pd.DataFrame(index=df_apriori.columns, columns=df_apriori.columns)

for i in range(rules.shape[0]):
    antecedent = rules.iloc[i]['antecedents']
    consequent = rules.iloc[i]['consequents']
    lift = rules.iloc[i]['lift']
    if len(antecedent) == 1 and len(consequent) == 1:
        antecedent = list(antecedent)[0]
        consequent = list(consequent)[0]
        if isinstance(antecedent, str) and isinstance(consequent, str):
            lift_matrix.loc[antecedent, consequent] = lift

# 将NaN值填充为0
lift_matrix = lift_matrix.fillna(0)

# 使用seaborn的heatmap函数绘制热图
plt.figure(figsize=(10, 8))
sns.heatmap(lift_matrix, cmap='RdYlBu_r', annot=True, fmt=".2f")
plt.title('Lift Matrix')
plt.show()

关联规则分析:旅游资源产品开发

针对Q30,Q31,Q32这三个题项进行分析

Q30|R1,Q30|R2,Q30|R3,Q30|R4,Q30|R5,Q30|R6,Q31|R1,Q31|R2,Q31|R8,Q31|R4,Q31|R7,Q31|R9,Q31|R6,Q31|R5,Q32|R3,Q32|R7,Q32|R4,Q32|R5,Q32|R1,Q32|R2,Q32|R6,Q33|R10,Q33|R12,Q33|R11,Q33|R4,Q33|R5,Q33|R1,Q33|R8,Q33|R7

这些题目都是量表题,关联分析需要多选题,4分及以上算作选择了;然后进行3维的关联规则分析

# 关联规则分析
# 先对把每个问卷的选项转换为布尔值,并把三个问题的选项合并到一起

columns_to_convert = ['Q30|R1', 'Q30|R2', 'Q30|R3', 'Q30|R4', 'Q30|R5', 'Q30|R6', 'Q31|R1', 'Q31|R2', 'Q31|R8', 'Q31|R4', 'Q31|R7', 'Q31|R9', 'Q31|R6', 'Q31|R5', 'Q32|R3', 'Q32|R7', 'Q32|R4', 'Q32|R5', 'Q32|R1', 'Q32|R2', 'Q32|R6']

def convert_score(score):
    return score >= 4

df_product = df_gone[columns_to_convert].map(convert_score)

# 把df_product到处为csv
df_product.to_csv('product.csv', index=False)

df_product.head()

利用关联规则算法Apriori分析和可视化

from mlxtend.frequent_patterns import apriori
from mlxtend.frequent_patterns import association_rules
import matplotlib.pyplot as plt
import pandas as pd

# 设置字体为SimHei显示中文
plt.rcParams['font.sans-serif'] = ['SimHei'] 
plt.rcParams['axes.unicode_minus'] = False 

df_encoded = df_product.rename(columns={
    'Q30|R1': '体育项目',
    'Q30|R2': '网红打卡点',
    'Q30|R3': '趣味项目',
    'Q30|R4': '民族服饰',
    'Q30|R5': '专业旅拍',
    'Q30|R6': '特色民宿',
    'Q31|R1': '盘县火腿',
    'Q31|R2': '刺梨系列产品',
    'Q31|R8': '盘州茶叶',
    'Q31|R4': '核桃乳',
    'Q31|R7': '人民小酒',
    'Q31|R9': '银杏系列商品',
    'Q31|R6': '民族手工制品',
    'Q31|R5': '彝族服饰',
    'Q32|R3': '非遗火腿工艺参观',
    'Q32|R7': '非遗技艺制作地方特色小吃体验',
    'Q32|R4': '非遗剪纸体验',
    'Q32|R5': '古法造纸参观',
    'Q32|R1': '民族刺绣体验',
    'Q32|R2': '民族蜡染体验',
    'Q32|R6': '民族歌舞表演'
})

# 使用apriori找出频繁项集,将支持度阈值提高到0.5
frequent_itemsets = apriori(df_encoded, min_support=0.7, use_colnames=True)

# 使用association_rules找出关联规则,将置信度阈值提高到0.8
rules = association_rules(frequent_itemsets, metric="confidence", min_threshold=0.5)

# 规则的支持度决定泡泡的大小,提升度决定颜色的深浅
plt.scatter(rules['support'], rules['confidence'], s=rules['support']*1000, c=rules['lift'], cmap='viridis', alpha=0.5)
plt.xlabel('支持度')
plt.ylabel('值信度')
plt.title('关联规则气泡图')
plt.colorbar(label='提升度')
plt.show()
import matplotlib.colors as mcolors

# 创建一个空的矩阵,用于存放支持度和提升度
support_matrix = pd.DataFrame(index=df_encoded.columns, columns=df_encoded.columns)
lift_matrix = pd.DataFrame(index=df_encoded.columns, columns=df_encoded.columns)

# 填充矩阵
for i, rule in rules.iterrows():
    antecedents = next(iter(rule['antecedents']))
    consequents = next(iter(rule['consequents']))
    support_matrix.loc[antecedents, consequents] = rule['support']
    lift_matrix.loc[antecedents, consequents] = rule['lift']

# 绘制泡泡图
fig, ax = plt.subplots(figsize=(14, 9))  # 修改图表宽度
norm = mcolors.Normalize(vmin=lift_matrix.min().min(), vmax=lift_matrix.max().max())  # 创建归一化对象
for i in support_matrix.columns:
    for j in support_matrix.index:
        support = support_matrix.loc[j, i]
        lift = lift_matrix.loc[j, i]
        if pd.notnull(support):
            ax.scatter(i, j, s=support*500, c=lift, cmap='viridis', norm=norm, alpha=0.7)  # 修改散点图的大小

# 修改图表的 x 轴和 y 轴标签
ax.set_xticks(range(len(support_matrix.columns)))
ax.set_yticks(range(len(support_matrix.index)))
ax.set_xticklabels(support_matrix.columns, rotation=90)
ax.set_yticklabels(support_matrix.index)

# 显示背景网格
ax.grid(True, linestyle='--')

# 添加颜色条
sm = plt.cm.ScalarMappable(cmap='viridis', norm=plt.Normalize(vmin=lift_matrix.min().min(), vmax=lift_matrix.max().max()))
sm._A = []
cbar = plt.colorbar(sm, shrink=0.7)  # 修改颜色条的大小
cbar.set_label('Lift')

# 显示图表
plt.show()
import matplotlib.pyplot as plt
import pandas as pd
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.colors as mcolors

# 假设 scatter_data 已经正确定义,且包含必要的列

question_names = {
    'Q30|R1': '体育项目',
    'Q30|R2': '网红打卡点',
    'Q30|R3': '趣味项目',
    'Q30|R4': '民族服饰',
    'Q30|R5': '专业旅拍',
    'Q30|R6': '特色民宿',
    'Q31|R1': '盘县火腿',
    'Q31|R2': '刺梨系列产品',
    'Q31|R8': '盘州茶叶',
    'Q31|R4': '核桃乳',
    'Q31|R7': '人民小酒',
    'Q31|R9': '银杏系列商品',
    'Q31|R6': '民族手工制品',
    'Q31|R5': '彝族服饰',
    'Q32|R3': '非遗火腿工艺参观',
    'Q32|R7': '非遗技艺制作地方特色小吃体验',
    'Q32|R4': '非遗剪纸体验',
    'Q32|R5': '古法造纸参观',
    'Q32|R1': '民族刺绣体验',
    'Q32|R2': '民族蜡染体验',
    'Q32|R6': '民族歌舞表演'
}

# 创建映射:从题目名称映射到索引
category_mapping = {name: index for index, name in enumerate(question_names.values())}

# 创建逆映射:从索引映射回题目名称
inverse_mapping = {index: name for name, index in category_mapping.items()}

scatter_data = pd.DataFrame(columns=['Antecedent1', 'Antecedent2', 'Consequent', 'Support', 'Lift'])
for index, row in rules.iterrows():
    antecedents = list(row['antecedents'])
    consequent = next(iter(row['consequents']))
    if len(antecedents) == 2:  # 确保前件有两个元素
        scatter_data.loc[index] = [
            category_mapping[antecedents[0]],
            category_mapping[antecedents[1]],
            category_mapping[consequent],
            row['support'],
            row['lift']
        ]


# 定义颜色映射和规范化实例
cmap = plt.get_cmap('viridis')  # 使用matplotlib内置的颜色映射
norm = mcolors.Normalize(vmin=scatter_data['Lift'].min(), vmax=scatter_data['Lift'].max())  # 根据提升度的最小值和最大值创建规范化实例

# 绘制三维散点图
fig = plt.figure(figsize=(25, 10))  # 调整图表大小
ax = fig.add_subplot(111, projection='3d')

# 散点图绘制
points = ax.scatter(
    scatter_data['Antecedent1'],
    scatter_data['Antecedent2'],
    scatter_data['Consequent'],
    s=scatter_data['Support']*150,  # 泡泡大小基于支持度
    c=[cmap(norm(val)) for val in scatter_data['Lift']],  # 颜色基于提升度
    alpha=0.6
)

# 调整坐标轴标签为题目名称,并调整字体大小和旋转角度
ax.set_xticks(list(category_mapping.values()))
ax.set_xticklabels(list(category_mapping.keys()), rotation=45, ha="right", fontsize=8)  # 调整X轴标签
ax.set_yticks(list(category_mapping.values()))
ax.set_yticklabels(list(category_mapping.keys()), rotation=-45, ha="left", fontsize=8)  # 调整Y轴标签
ax.set_zticks(list(category_mapping.values()))
ax.set_zticklabels(list(category_mapping.keys()), fontsize=8)  # 调整Z轴标签字体大小

# 调整视角
ax.view_init(elev=20, azim=-45)  # 调整视角

# 添加颜色条
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
sm.set_array([])
cbar = fig.colorbar(sm, shrink=0.4, aspect=15, label='提升度')
cbar.ax.tick_params(labelsize=8)  # 调整颜色条刻度的字体大小

plt.show()

生成动态gif

import matplotlib.pyplot as plt
import pandas as pd
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.colors as mcolors
import numpy as np
import imageio  # 用于生成GIF

# 假设 scatter_data 已经正确定义,且包含必要的列

# 绘制三维散点图的函数
def plot_3d(ax, azim):
    ax.clear()  # 清除之前的绘图
    # 散点图绘制
    ax.scatter(
        scatter_data['Antecedent1'],
        scatter_data['Antecedent2'],
        scatter_data['Consequent'],
        s=scatter_data['Support']*150,  # 泡泡大小基于支持度
        c=[cmap(norm(val)) for val in scatter_data['Lift']],  # 颜色基于提升度
        alpha=0.6
    )

    # 设置坐标轴标签
    ax.set_xticks(list(category_mapping.values()))
    ax.set_xticklabels(list(category_mapping.keys()), rotation=45, ha="right", fontsize=8)
    ax.set_yticks(list(category_mapping.values()))
    ax.set_yticklabels(list(category_mapping.keys()), rotation=-45, ha="left", fontsize=8)
    ax.set_zticks(list(category_mapping.values()))
    ax.set_zticklabels(list(category_mapping.keys()), fontsize=8)

    # 调整视角
    ax.view_init(elev=20, azim=azim)  # 旋转视角

    # 由于在函数内部调用 plt.show() 或 plt.savefig() 会阻断循环,我们在这里不调用它们

# 创建GIF的帧
filenames = []
fig = plt.figure(figsize=(25, 10))
ax = fig.add_subplot(111, projection='3d')

for azim in range(0, 90, 2):  # 从-45度旋转到315度,每次增加2度
    plot_3d(ax, azim)  # 绘图
    filename = f'frame_{azim}.png'
    plt.savefig(filename)  # 保存帧
    filenames.append(filename)

# 使用imageio生成GIF
images = []
for filename in filenames:
    images.append(imageio.imread(filename))
imageio.mimsave('rotation.gif', images, fps=20)  # 调整fps(每秒帧数)根据需要

# 清理临时生成的PNG文件
import os
for filename in filenames:
    os.remove(filename)
antecedent_mapping = category_mapping
antecedent_mapping_reverse = inverse_mapping
consequent_mapping = category_mapping
consequent_mapping_reverse = inverse_mapping

q30_options = ['体育项目', '网红打卡点', '趣味项目', '民族服饰', '专业旅拍', '特色民宿']
q31_options = ['盘县火腿', '刺梨系列产品', '盘州茶叶', '核桃乳', '人民小酒', '银杏系列商品', '民族手工制品', '彝族服饰']
q32_options = ['非遗火腿工艺参观', '非遗技艺制作地方特色小吃体验', '非遗剪纸体验', '古法造纸参观', '民族刺绣体验', '民族蜡染体验', '民族歌舞表演']

# 遍历scatter_data
for i, row in scatter_data.iterrows():
    lift = row['Lift']
    # 只输出提升度大于1.45的规则
    if lift > 1.3:
        antecedent1 = antecedent_mapping_reverse.get(row['Antecedent1'], 'Unknown')
        antecedent2 = antecedent_mapping_reverse.get(row['Antecedent2'], 'Unknown')
        consequent = consequent_mapping_reverse.get(row['Consequent'], 'Unknown')
        support = row['Support']

        # 检查前件和后件是否包含q30、q31和q32中的任意一个问题
        if ((antecedent1 in q30_options and antecedent2 in q31_options and consequent in q32_options) or
            (antecedent1 in q30_options and antecedent2 in q32_options and consequent in q31_options) or
            (antecedent1 in q31_options and antecedent2 in q30_options and consequent in q32_options) or
            (antecedent1 in q31_options and antecedent2 in q32_options and consequent in q30_options) or
            (antecedent1 in q32_options and antecedent2 in q30_options and consequent in q31_options) or
            (antecedent1 in q32_options and antecedent2 in q31_options and consequent in q30_options)):
            print(f"规则: {antecedent1}, {antecedent2} => {consequent}, 支持度: {support:.2f}, 提升度: {lift:.2f}")

客户粘性分析

游客画像作为自变量,客户粘性作为因变量进行分析

比较关心的是客户粘性打分为4/5的客户,看他们的游客画像是什么。

  • 第一个思路是,把去过的游客先进行聚类,再把聚类的结果来看哪一类人群的粘性最高;在数据处理的逻辑上思路更好。
  • 第二个思路是,把游客的基本信息不处理,全部丢给随机森林进行处理

客户粘性题号:Q26|1

游客基本信息:columns 游客画像聚类结果:

注:运行此段代码前,先运行“开始聚类”代码

# 查看Q26的打分分布
print("查看Q26的打分分布\n",df_gone['Q26|1'].value_counts())

# 查看总的数据量
print("\n查看总的数据量\n",df_gone.shape)

# 检查游客基本信息的题号,排除开放性题号
print("\n检查游客基本信息的题号,排除开放性题号\n",columns)

# 聚类结果统计
print("\n聚类结果统计\n ",df_gone['k-prototypes cluster'].value_counts())

对df_gone进行聚类

# 思路1   把去过的游客先进行聚类,再把聚类的结果来看哪一类人群的粘性最高
from kmodes.kprototypes import KPrototypes

# 列表中的列名
# columns = ['Q1', 'Q2', 'Q3', 'Q4', 'Q5', 'Q7|1', 'Q7|4', 'Q7|2', 'Q7|3', 'Q7|5', 'Q7|6', 'Q8', 'Q9', 'Q10', 'Q11', 'Q12', 'Q13', 'Q14', 'Q15']
columns = ['Q2', 'Q4', 'Q5', 'Q8', 'Q9', 'Q10', 'Q11', 'Q12', 'Q13', 'Q14', 'Q15']

# 数值型列
numerical_columns = ['Q2', 'Q4', 'Q5']

# 类别型列
categorical_columns = [col for col in columns if col not in numerical_columns]

# 将类别型列的索引转换为数字
categorical_columns_indices = [columns.index(col) for col in categorical_columns]

# 初始化 K-原型聚类器
kproto = KPrototypes(n_clusters=4, init='Cao', verbose=2)

# 对所有游客进行聚类
clusters = kproto.fit_predict(df_gone[columns].values, categorical=categorical_columns_indices)

# 将聚类结果添加到数据集作为新的特征
df_gone['k-prototypes cluster'] = clusters

# 想查看每个簇的中心点
print(kproto.cluster_centroids_)

# 查看每个簇的数量
print(df_gone['k-prototypes cluster'].value_counts())

df_gone.head()

可视化

from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
from umap import UMAP
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

# 数据
X = df_gone[columns].values

# 创建PCA,t-SNE,UMAP对象
pca = PCA(n_components=2)
tsne = TSNE(n_components=2)
umap = UMAP(n_components=2)

# 降维
X_pca = pca.fit_transform(X)
X_tsne = tsne.fit_transform(X)
X_umap = umap.fit_transform(X)

# 创建图形和子图
fig, axs = plt.subplots(3, 2, figsize=(15, 20))

# 绘制二维版本
axs[0, 0].scatter(X_pca[:, 0], X_pca[:, 1], c=clusters, cmap='viridis', alpha=0.5)
axs[0, 0].set_title('PCA 2D')
fig.colorbar(sc, ax=axs[0, 0], shrink=0.6)

axs[1, 0].scatter(X_tsne[:, 0], X_tsne[:, 1], c=clusters, cmap='viridis', alpha=0.5)
axs[1, 0].set_title('t-SNE 2D')
fig.colorbar(sc, ax=axs[1, 0], shrink=0.6)

axs[2, 0].scatter(X_umap[:, 0], X_umap[:, 1], c=clusters, cmap='viridis', alpha=0.5)
axs[2, 0].set_title('UMAP 2D')
fig.colorbar(sc, ax=axs[2, 0], shrink=0.6)


# 创建PCA,t-SNE,UMAP对象
pca = PCA(n_components=3)
tsne = TSNE(n_components=3)
umap = UMAP(n_components=3)

# 降维
X_pca = pca.fit_transform(X)
X_tsne = tsne.fit_transform(X)
X_umap = umap.fit_transform(X)

# 绘制三维版本
axs[0, 1] = fig.add_subplot(3, 2, 2, projection='3d')
axs[0, 1].scatter(X_pca[:, 0], X_pca[:, 1], X_pca[:, 2], c=clusters, cmap='viridis', alpha=0.5)
axs[0, 1].set_title('PCA 3D')
fig.colorbar(sc, ax=axs[0, 1], shrink=0.6, pad=0.2)  # 修改的代码

axs[1, 1] = fig.add_subplot(3, 2, 4, projection='3d')
axs[1, 1].scatter(X_tsne[:, 0], X_tsne[:, 1], X_tsne[:, 2], c=clusters, cmap='viridis', alpha=0.5)
axs[1, 1].set_title('t-SNE 3D')
fig.colorbar(sc, ax=axs[1, 1], shrink=0.6, pad=0.2)  # 修改的代码

axs[2, 1] = fig.add_subplot(3, 2, 6, projection='3d')
axs[2, 1].scatter(X_umap[:, 0], X_umap[:, 1], X_umap[:, 2], c=clusters, cmap='viridis', alpha=0.5)
axs[2, 1].set_title('UMAP 3D')
fig.colorbar(sc, ax=axs[2, 1], shrink=0.6, pad=0.2)  # 修改的代码

plt.tight_layout()
plt.show()

描述性统计

分析df_gone中的’k-prototypes cluster’和’Q26|1’之间的关系

卡方检验:如果p值小于0.05,那么我们可以拒绝零假设,认为’k-prototypes cluster’和’Q26|1’之间存在显著的关系。如果p值大于0.05,那么我们不能拒绝零假设,认为’k-prototypes cluster’和’Q26|1’之间不存在显著的关系。

from scipy.stats import chi2_contingency

# 创建列联表
contingency_table = pd.crosstab(df_gone['k-prototypes cluster'], df_gone['Q26|1'] >= 4)

# 进行卡方检验
chi2, p, dof, expected = chi2_contingency(contingency_table)

print(f"Chi-square value: {chi2}")
print(f"P-value: {p}")
print(f"Degrees of freedom: {dof}")
import seaborn as sns

# 创建新的二元变量
df_gone['Q26|1_high_score'] = (df_gone['Q26|1'] >= 4)

# 绘制计数图
sns.countplot(x='k-prototypes cluster', hue='Q26|1_high_score', data=df_gone)
plt.show()

随机森林模型

自变量是聚类的结果,因变量是Q26的打分(这个不行) 方法二可以做随机森林

我想研究的是哪一类人群在Q26的打分更高,我比较关注打分在大于等于4分的人群是哪一类

from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

# 定义自变量和因变量
X = df_gone[columns]
y = df_gone['Q26|1'].values

# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 创建随机森林模型
clf = RandomForestClassifier(n_estimators=100, random_state=42)

# 训练模型
clf.fit(X_train, y_train)

# 预测测试集
y_pred = clf.predict(X_test)

# 打印分类报告
print(classification_report(y_test, y_pred))
import shap
import numpy as np

# 创建解释器
explainer = shap.TreeExplainer(clf)

# 计算SHAP值
shap_values = explainer.shap_values(X_test)

# 创建类别标签
class_labels = ['Class 1', 'Class 2', 'Class 3', 'Class 4', 'Class 5']

# 设置图形大小
plt.figure(figsize=(40, 10))

# 绘制所有类别的SHAP值概览图(保持原始顺序)
plt.subplot(1, 3, 1)
shap.summary_plot(shap_values, X_test, class_names=class_labels, plot_type="bar", show=False)
plt.title("All Classes Feature Importance")

# 绘制类别4的特征贡献度
plt.subplot(1, 3, 2)
shap.summary_plot(shap_values[3][:, sorted_indices_class_4], X_test.iloc[:, sorted_indices_class_4], plot_type="bar", feature_names=X_test.columns[sorted_indices_class_4], show=False)
plt.title("Class 4 Feature Importance")

# 绘制类别5的特征贡献度
plt.subplot(1, 3, 3)
shap.summary_plot(shap_values[4][:, sorted_indices_class_5], X_test.iloc[:, sorted_indices_class_5], plot_type="bar", feature_names=X_test.columns[sorted_indices_class_5], show=False)
plt.title("Class 5 Feature Importance")

plt.tight_layout()
plt.show()
shap.summary_plot(shap_values, X_test, class_names=class_labels, plot_type="bar", show=False)
plt.title("All Classes Feature Importance")

# mean(|SHAP value|)(average impact on model output magnitude)