# -*- coding: utf-8 -*-
"""
Created on Fri Aug 15 14:34:51 2025
@author: fly03
"""
import os
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
from sklearn.metrics import silhouette_score, calinski_harabasz_score, davies_bouldin_score
from sklearn.impute import SimpleImputer
import warnings
warnings.filterwarnings('ignore')
# 设置中文字体和样式
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
sns.set_style("whitegrid")
#读取多个含有相同字段的Excel文件
def readMulFile(inputfilename=[]):
megred_data=pd.DataFrame()
for file in inputfilename:
data=pd.read_excel(file)
megred_data=pd.concat([megred_data,data],ignore_index=True)
return megred_data
#是否贫困预处理,用0和1表示
def processPinKun(data):
data['是否贫困'] = data['是否贫困'].apply(lambda x: 1 if x == '是' else 0)
return data
#统计每列缺失值的情况
def countMissingValue(data):
return data.isnull().sum()
#统计交易金额正负情况,分别返回为负数和非负数的记录数
def countMoneyNegative(data):
amount_distribution=(data['交易金额']<0).value_counts()
return amount_distribution.get(True),amount_distribution.get(False)
#删除交易金额为负数的记录
def deleteMoneyNegative(data):
data_cleaned = data[(data[['交易金额']] >= 0).all(axis=1)]
return data_cleaned
#按交易地点分类,分为普通餐饮类、特色餐饮类、生活服务类和休闲娱乐类
def classify(data):
# 定义分类条件
#含水果店、水吧、超市为生活服务类
condition1 = (
data['交易地点'].str.contains('水果店|水吧|超市', na=False, regex=True)
)
#含雪、果茶、水果捞、甜品、糖葫芦、咖啡、蛋糕、面包为休闲娱乐类
condition2 = (
data['交易地点'].str.contains('雪|果茶|水果捞|甜品|糖葫芦|咖啡|蛋糕|面包', na=False, regex=True)
)
# 创建分类列并设置默认值
data['分类'] = '餐饮类' # 默认分类为特色餐饮
#满足条件1则为生活服务类
data.loc[condition1,'分类'] = '生活服务类'
#满足条件2为休闲娱乐类
data.loc[condition2,'分类'] = '休闲娱乐类'
return data
#数据时间预处理
def preprocess_time(df):
"""数据预处理和时间特征提取"""
# 转换日期和时间
df['交易日期'] = pd.to_datetime(df['交易日期'])
df['交易时间'] = pd.to_datetime(df['交易时间'], format='%H:%M:%S').dt.time
df['交易日期时间'] = df.apply(lambda x: datetime.combine(x['交易日期'], x['交易时间']), axis=1)
# 提取时间特征
df['月份'] = df['交易日期'].dt.month
df['周几'] = df['交易日期'].dt.dayofweek # 0-6, 周一到周日
df['是否周末'] = df['周几'].isin([5, 6]).astype(int)
# 提取时间段
def get_time_period(time_obj):
hour = time_obj.hour
if 6 <= hour < 9:
return '早餐时段'
elif 11 <= hour < 14:
return '午餐时段'
elif 17 <= hour < 20:
return '晚餐时段'
elif 20 <= hour < 24:
return '夜宵时段'
else:
return '其他时段'
df['时间段'] = df['交易时间'].apply(get_time_period)
return df
#按月统计贫困生和非贫困生在不同交易地点的交易次数和占比,交易点的分类的字段名为‘分类’
def countMonthNum(data):
data['年月']=data['交易日期'].dt.to_period('M')
pivot_result = data.pivot_table(
index=['年月','分类'],
columns='是否贫困',
values='学号',
aggfunc='count',
fill_value=0
).reset_index()
pivot_result.columns = ['年月','交易地点类别','非贫困生交易次数','贫困生交易次数']
pivot_result['非贫困生月总次数']=pivot_result.groupby('年月')['非贫困生交易次数'].transform('sum')
pivot_result['非贫困生每月交易地点占比']=(pivot_result['非贫困生交易次数']/pivot_result['非贫困生月总次数']*100).round(2)
pivot_result['贫困生月总次数']=pivot_result.groupby('年月')['贫困生交易次数'].transform('sum')
pivot_result['贫困生每月交易地点占比']=(pivot_result['贫困生交易次数']/pivot_result['贫困生月总次数']*100).round(2)
#按年月和交易地点排序
pivot_result=pivot_result.sort_values(['年月','交易地点类别'],ascending=[True,False])
result=pivot_result[['年月','交易地点类别','非贫困生每月交易地点占比','贫困生每月交易地点占比']]
result['差异']=pivot_result['非贫困生每月交易地点占比']-pivot_result['贫困生每月交易地点占比']
return result
#按交易地点统计贫困生和非贫困生的消费总额占比,按分类列处理交易地点
def countMoney(data):
data['年月']=data['交易日期'].dt.to_period('M')
pivot_result = data.pivot_table(
index=['年月','分类'],
columns='是否贫困',
values='交易金额',
aggfunc=['sum'],
fill_value=0
).reset_index()
pivot_result.columns = ['年月','交易地点类别','非贫困生总消费额','贫困生总消费额']
pivot_result['非贫困生月总消费额']=pivot_result.groupby('年月')['非贫困生总消费额'].transform('sum')
pivot_result['非贫困生月总消费额占比']=(pivot_result['非贫困生总消费额']/pivot_result['非贫困生月总消费额']*100).round(2)
pivot_result['贫困生月总消费额']=pivot_result.groupby('年月')['贫困生总消费额'].transform('sum')
pivot_result['贫困生月总消费额占比']=(pivot_result['贫困生总消费额']/pivot_result['贫困生月总消费额']*100).round(2)
#按年月和交易地点排序
pivot_result=pivot_result.sort_values(['年月','交易地点类别'],ascending=[True,False])
result=pivot_result[['年月','交易地点类别','非贫困生月总消费额占比','贫困生月总消费额占比']]
result['差异']=pivot_result['非贫困生月总消费额占比']-pivot_result['贫困生月总消费额占比']
return result
#统计消费月均值
def monthMean(data):
data['年月']=data['交易日期'].dt.to_period('M')
data['年月']=data['交易日期'].dt.to_period('M')
pivot_result = data.pivot_table(
index='年月',
columns='是否贫困',
values='交易金额',
aggfunc=['mean'],
fill_value=0
).reset_index()
pivot_result.columns = ['年月','非贫困生月均消费额','贫困生月均消费额']
pivot_result['差异']=pivot_result['非贫困生月均消费额']-pivot_result['贫困生月均消费额']
pivot_result=pivot_result.round(2)
return pivot_result
#分析周一到周日每天的交易金额平均值
def week(df):
"""分析时间模式"""
results = []
#1 周内消费分析
pivot_result = df.pivot_table(
index='周几',
columns='是否贫困',
values='交易金额',
aggfunc=['mean'],
fill_value=0
).reset_index()
pivot_result.columns = ['周几','非贫困生均消费额','贫困生均消费额']
pivot_result['差异']=pivot_result['非贫困生均消费额']-pivot_result['贫困生均消费额']
pivot_result=pivot_result.round(2)
return pivot_result
#按时间段分析均消费额
def time(df):
"""分析时间模式"""
results = []
#1 周内消费分析
pivot_result = df.pivot_table(
index='时间段',
columns='是否贫困',
values='交易金额',
aggfunc=['mean'],
fill_value=0
).reset_index()
pivot_result.columns = ['时间段','非贫困生均消费额','贫困生均消费额']
pivot_result['差异']=pivot_result['非贫困生均消费额']-pivot_result['贫困生均消费额']
pivot_result=pivot_result.round(2)
return pivot_result
def pinKunFenxi(df):
grouped = df.groupby('学号')
# 初始化特征 DataFrame
features = pd.DataFrame(index=grouped.groups.keys())
features.index.name = '学号'
# 1. 消费金额相关特征
features['总消费金额'] = grouped['交易金额'].sum()
features['消费次数'] = grouped.size()
features['平均消费金额'] = grouped['交易金额'].mean()
features['消费金额标准差'] = grouped['交易金额'].std()
features['消费金额变异系数'] = features['平均消费金额'] / (features['消费金额标准差'] + 1e-5)
features['最大消费金额'] = grouped['交易金额'].max()
features['最小消费金额'] = grouped['交易金额'].min()
features['消费金额中位数'] = grouped['交易金额'].median()
# 2. 消费时间相关特征
time_period_counts = df.groupby(['学号', '时间段']).size().unstack(fill_value=0)
time_period_counts.columns = [f'{col}_消费次数' for col in time_period_counts.columns]
features = features.join(time_period_counts)
# 周末消费
weekend_stats = df.groupby('学号')['是否周末'].mean()
features['周末消费比例'] = weekend_stats
# 3. 消费地点相关特征
location_counts = df.groupby('学号')['分类'].nunique()
features['消费地点类别数量'] = location_counts
# 计算主要消费地点比例
main_locations = df.groupby(['学号', '分类']).size().groupby('学号').max() / features['消费次数']
features['主要地点类别消费比例'] = main_locations
# 对交易地点进行编码并计算熵(衡量消费地点的分散程度)
from scipy.stats import entropy
def calculate_entropy(series):
value_counts = series.value_counts(normalize=True)
return entropy(value_counts)
location_entropy = df.groupby('学号')['分类'].apply(calculate_entropy)
features['消费地点熵'] = location_entropy.fillna(0)
# 计算食堂消费比例
cafeteria_mask = df['分类'].str.contains('餐饮')
cafeteria_ratio = df[cafeteria_mask].groupby('学号')['交易金额'].sum() / features['总消费金额']
features['餐饮类消费比例'] = cafeteria_ratio.fillna(0)
# 计算生活服务类消费比例
cafeteria_mask = df['分类'].str.contains('生活')
cafeteria_ratio = df[cafeteria_mask].groupby('学号')['交易金额'].sum() / features['总消费金额']
features['生活服务类消费比例'] = cafeteria_ratio.fillna(0)
# 计算休闲娱乐类消费比例
cafeteria_mask = df['分类'].str.contains('休闲')
cafeteria_ratio = df[cafeteria_mask].groupby('学号')['交易金额'].sum() / features['总消费金额']
features['休闲娱乐类消费比例'] = cafeteria_ratio.fillna(0)
# 处理缺失值
features = features.fillna(0)
# 处理无穷大值(由于除以0导致)
features = features.replace([np.inf, -np.inf], 0)
return features
# 3. 确定最佳K值
def find_optimal_k(X_scaled, max_k=10):
"""
使用肘部法则和轮廓系数确定最佳K值
"""
# 计算不同K值的SSE(平方误差和)
sse = []
silhouette_scores = []
k_range = range(2, max_k+1)
for k in k_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
kmeans.fit(X_scaled)
sse.append(kmeans.inertia_)
# 计算轮廓系数(仅当k>1时)
if k > 1:
silhouette_scores.append(silhouette_score(X_scaled, kmeans.labels_))
else:
silhouette_scores.append(0)
# 绘制肘部法则图
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.plot(k_range, sse, 'bo-')
plt.xlabel('K值')
plt.ylabel('SSE')
plt.title('肘部法则')
# 绘制轮廓系数图
plt.subplot(1, 2, 2)
plt.plot(k_range[1:], silhouette_scores[1:], 'ro-')
plt.xlabel('K值')
plt.ylabel('轮廓系数')
plt.title('轮廓系数法')
plt.tight_layout()
plt.show()
# 找到轮廓系数最大的K值
best_k = np.argmax(silhouette_scores[1:]) + 2 # +2因为从K=2开始
print(f"最佳K值(基于轮廓系数): {best_k}")
print(f"对应的轮廓系数: {silhouette_scores[best_k-2]:.4f}")
return best_k
# 4. K-Means聚类分析
def perform_kmeans_clustering(features, n_clusters=3):
"""
使用K-Means算法对学生进行聚类
"""
# 检查并处理缺失值
print(f"处理前数据形状: {features.shape}")
print(f"缺失值统计:\n{features.isnull().sum()}")
# 删除包含NaN的行
features_clean = features.dropna()
print(f"删除缺失值后数据形状: {features_clean.shape}")
if len(features_clean) == 0:
print("错误: 删除缺失值后数据为空!")
return None, None, None, None, None
# 标准化特征
scaler = StandardScaler()
X_scaled = scaler.fit_transform(features_clean)
# 使用PCA进行降维可视化
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)
# 执行K-Means聚类
kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
clusters = kmeans.fit_predict(X_scaled)
# 评估聚类效果
silhouette = silhouette_score(X_scaled, clusters)
calinski = calinski_harabasz_score(X_scaled, clusters)
davies = davies_bouldin_score(X_scaled, clusters)
print(f"K-Means聚类评估指标:")
print(f"轮廓系数: {silhouette:.4f} (越大越好,范围[-1,1])")
print(f"Calinski-Harabasz指数: {calinski:.4f} (越大越好)")
print(f"Davies-Bouldin指数: {davies:.4f} (越小越好)")
# 可视化聚类结果
plt.figure(figsize=(10, 6))
scatter = plt.scatter(X_pca[:, 0], X_pca[:, 1], c=clusters, cmap='viridis', alpha=0.6)
plt.colorbar(scatter, label='聚类标签')
plt.xlabel('主成分1')
plt.ylabel('主成分2')
plt.title(f'K-Means聚类结果 (K={n_clusters})')
plt.show()
return clusters, kmeans, scaler, X_scaled, features_clean
# 5. 解释聚类结果并分配贫困等级
def interpret_clusters_and_assign_poverty_level(features, clusters, kmeans_model):
"""
分析聚类结果并为每个簇分配贫困等级
"""
# 将聚类标签添加到特征数据中
features_with_clusters = features.copy()
features_with_clusters['cluster'] = clusters
# 计算每个聚类的特征均值
cluster_means = features_with_clusters.groupby('cluster').mean()
# 打印每个聚类的特征统计
print("各聚类的特征均值:")
print(cluster_means[['总消费金额', '平均消费金额', '餐饮类消费比例', '消费地点熵', '周末消费比例']])
# 根据总消费金额排序聚类,消费最低的为最贫困
poverty_mapping = {}
sorted_clusters = cluster_means.sort_values('总消费金额').index
# 分配贫困等级 - 固定为3个等级
# 0: '特别贫困', 1: '一般贫困', 2: '相对不贫困'
for i, cluster_id in enumerate(sorted_clusters):
poverty_mapping[cluster_id] = i
features_with_clusters['贫困等级'] = features_with_clusters['cluster'].map(poverty_mapping)
# 固定使用3个贫困等级标签(针对贫困生内部的细分)
label_mapping = {0: '特别贫困', 1: '一般贫困', 2: '相对不贫困'}
# 如果聚类数量不等于3,需要调整映射
n_clusters = len(cluster_means)
if n_clusters != 3:
print(f"警告: 聚类数量为{n_clusters},但需要3个贫困等级。将尝试重新分配...")
# 根据聚类数量重新分配标签
if n_clusters < 3:
# 如果聚类数量少于3,重复使用某些标签
labels = ['特别贫困', '一般贫困', '相对不贫困']
label_mapping = {i: labels[min(i, 2)] for i in range(n_clusters)}
else:
# 如果聚类数量多于3,使用前缀+编号
label_mapping = {i: f'贫困等级{i+1}' for i in range(n_clusters)}
features_with_clusters['贫困等级标签'] = features_with_clusters['贫困等级'].map(label_mapping)
# 显示贫困等级分布
poverty_distribution = features_with_clusters['贫困等级标签'].value_counts()
print("\n贫困等级分布:")
for level, count in poverty_distribution.items():
percentage = count / len(features_with_clusters) * 100
print(f"{level}: {count} 名学生 ({percentage:.1f}%)")
# 可视化贫困等级与关键特征的关系
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# 总消费金额 vs 食堂消费比例
scatter = axes[0, 0].scatter(
features_with_clusters['总消费金额'],
features_with_clusters['餐饮类消费比例'],
c=features_with_clusters['贫困等级'],
cmap='viridis'
)
axes[0, 0].set_xlabel('总消费金额')
axes[0, 0].set_ylabel('餐饮消费比例')
plt.colorbar(scatter, ax=axes[0, 0], label='贫困等级')
axes[0, 0].set_title('总消费金额 vs 餐饮消费比例')
# 平均消费金额 vs 消费地点熵
scatter = axes[0, 1].scatter(
features_with_clusters['平均消费金额'],
features_with_clusters['消费地点熵'],
c=features_with_clusters['贫困等级'],
cmap='viridis'
)
axes[0, 1].set_xlabel('平均消费金额')
axes[0, 1].set_ylabel('消费地点熵')
plt.colorbar(scatter, ax=axes[0, 1], label='贫困等级')
axes[0, 1].set_title('平均消费金额 vs 消费地点熵')
# 贫困等级与总消费金额的箱线图
sns.boxplot(x='贫困等级标签', y='总消费金额', data=features_with_clusters, ax=axes[1, 0])
axes[1, 0].set_title('各贫困等级的总消费金额分布')
axes[1, 0].tick_params(axis='x', rotation=45)
# 贫困等级与食堂消费比例的箱线图
sns.boxplot(x='贫困等级标签', y='餐饮类消费比例', data=features_with_clusters, ax=axes[1, 1])
axes[1, 1].set_title('各贫困等级的餐饮消费比例分布')
axes[1, 1].tick_params(axis='x', rotation=45)
plt.tight_layout()
plt.show()
# 打印每个贫困等级的典型特征
print("\n各贫困等级的典型特征:")
poverty_means = features_with_clusters.groupby('贫困等级标签').mean()
for level in poverty_means.index:
row = poverty_means.loc[level]
print(f"\n{level}学生:")
print(f" 平均总消费金额: {row['总消费金额']:.2f}元")
print(f" 平均单次消费金额: {row['平均消费金额']:.2f}元")
print(f" 餐饮消费比例: {row['餐饮类消费比例']:.2%}")
print(f" 消费地点多样性(熵): {row['消费地点熵']:.3f}")
return features_with_clusters
def main():
inputfilename=[]
filenum=int(input('请输入文件的个数:\n'))
for i in range(filenum):
fname=input(f'请输入第{i+1}个文件名:\n')
if os.path.exists(fname):
inputfilename.append(fname)
else:
print('输入有误,该文件不存在!')
print("文件输入完毕!")
print("是否贫困数据标准化化预处理中......")
data=readMulFile(inputfilename)
if data.empty:
print("数据为空!")
return
#数据不为空,则进行是否贫困数据预处理
data=processPinKun(data)
#输出每列缺失值情况
print(f'每列缺失值情况如下:\n{countMissingValue(data)}')
#输出交易金额负数和非负数记录数情况
yes,no=countMoneyNegative(data)
print('交易金额正负数情况如下:')
print(f'交易金额为负数的记录共{yes}条,非负数的记录共{no}条')
#删除交易金额为负数的记录
print('交易金额为负的记录正在删除中......')
data=deleteMoneyNegative(data)
print('交易金额为负的记录删除完成')
#筛选贫困生数据
print('筛选贫困生数据中......')
poverty_data = data[data['是否贫困'] == 1].copy()
print(f"贫困生数据记录数: {len(poverty_data)}")
print(f"贫困生人数: {poverty_data['学号'].nunique()}")
if len(poverty_data) == 0:
print("错误: 没有找到贫困生数据!")
return
#按交易地点进行分类
print('按交易地点进行分类中......')
poverty_data=classify(poverty_data)
poverty_data=preprocess_time(poverty_data)
# 生成特征
print('生成贫困生消费特征中......')
features = pinKunFenxi(poverty_data)
# 检查特征数据
print(f"特征数据形状: {features.shape}")
print(f"特征数据缺失值统计:\n{features.isnull().sum()}")
# 如果有缺失值,进行处理
if features.isnull().sum().sum() > 0:
print("处理缺失值...")
features = features.fillna(0)
features = features.replace([np.inf, -np.inf], 0)
# 删除仍有缺失值的行
features_clean = features.dropna()
print(f"清理后特征数据形状: {features_clean.shape}")
if len(features_clean) == 0:
print("错误: 清理后数据为空!")
return
# 标准化特征
scaler = StandardScaler()
X_scaled = scaler.fit_transform(features_clean)
# 强制使用3个聚类,因为需要3个贫困等级
optimal_k = 3
print(f"\n使用K={optimal_k}进行K-Means聚类(固定为3个贫困等级)...")
result = perform_kmeans_clustering(features_clean, n_clusters=optimal_k)
if result[0] is not None:
clusters, kmeans_model, scaler, X_scaled, features_clean = result
print("\n解释聚类结果并分配贫困等级...")
features_with_clusters = interpret_clusters_and_assign_poverty_level(features_clean, clusters, kmeans_model)
# 保存结果
result_df = features_with_clusters[['贫困等级', '贫困等级标签']].copy()
result_df.to_csv('贫困生内部贫困等级划分结果.csv')
print("\n结果已保存到 '贫困生内部贫困等级划分结果.csv'")
# 显示部分学生的贫困等级
print("\n部分贫困生的贫困等级划分结果:")
sample_results = result_df.sample(min(10, len(result_df)))
for idx, row in sample_results.iterrows():
print(f"学号: {idx}, 贫困等级: {row['贫困等级标签']}")
# 显示贫困等级统计
print("\n贫困等级统计:")
poverty_stats = result_df['贫困等级标签'].value_counts().sort_index()
for level, count in poverty_stats.items():
percentage = count / len(result_df) * 100
print(f"{level}: {count}人 ({percentage:.1f}%)")
# 保存详细的贫困生分析结果
detailed_results = features_with_clusters.copy()
detailed_results.to_csv('贫困生详细分析结果.csv')
print("详细分析结果已保存到 '贫困生详细分析结果.csv'")
else:
print("聚类分析失败!")
if __name__=='__main__':
main()