import pandas as pd
import numpy as np
from sklearn.preprocessing import MinMaxScaler
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import dash
from dash import dcc, html, Input, Output, State
import dash_bootstrap_components as dbc
import os
from datetime import datetime
# 1. 数据预处理类
class 二课数据预处理:
def __init__(self, 文件列表):
self.文件列表 = 文件列表
self.合并数据 = None
self.日志 = [] # 用于记录处理日志
def 记录日志(self, 消息):
"""记录处理日志"""
时间戳 = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.日志.append(f"[{时间戳}] {消息}")
print(f"[{时间戳}] {消息}")
def 数据清洗(self):
"""清洗并合并所有Excel文件数据"""
self.记录日志("开始数据清洗流程")
dfs = []
for 文件 in self.文件列表:
try:
# 检查文件是否存在
if not os.path.exists(文件):
self.记录日志(f"文件不存在: {文件}")
continue
# 检查文件扩展名
if not 文件.lower().endswith(('.xls', '.xlsx')):
self.记录日志(f"文件格式不支持: {文件}")
continue
# 尝试读取Excel文件
try:
excel_data = pd.ExcelFile(文件)
if '学分明细' not in excel_data.sheet_names:
self.记录日志(f"文件 {文件} 中未找到'学分明细'工作表")
continue
df = pd.read_excel(文件, sheet_name='学分明细')
# 检查数据是否为空
if df.empty:
self.记录日志(f"文件 {文件} 的'学分明细'工作表为空")
continue
except Exception as e:
self.记录日志(f"读取文件 {文件} 时出错: {str(e)}")
continue
# 从文件名提取年级
年级 = 0
try:
年级_match = ''.join(filter(str.isdigit, os.path.basename(文件).split('明细')[-1]))
if 年级_match:
年级 = int(年级_match)
except:
self.记录日志(f"无法从文件名 {文件} 提取年级信息,默认设为0")
df['年级'] = 年级
dfs.append(df)
self.记录日志(f"成功处理文件: {文件} (记录数: {len(df)})")
except Exception as e:
self.记录日志(f"处理文件 {文件} 时发生意外错误: {str(e)}")
continue
if not dfs:
self.记录日志("错误: 没有有效数据可处理")
raise ValueError("没有有效数据可处理 - 请检查: 1) 文件是否存在 2) 文件格式是否正确 3) 是否包含'学分明细'工作表")
# 合并数据
self.合并数据 = pd.concat(dfs, ignore_index=True)
self.记录日志(f"成功合并数据,总记录数: {len(self.合并数据)}")
# 数据清洗
columns_to_drop = ['单位', '活动分类', '数量', '获取原因', '发送人',
'参与时长(分)', '参与开始时间', '参与结束时间', '获取时间']
# 检查是否有要删除的列
existing_columns = [col for col in columns_to_drop if col in self.合并数据.columns]
self.合并数据 = self.合并数据[self.合并数据['是否有效'] == '是'].drop(columns=existing_columns, errors='ignore')
self.记录日志(f"数据清洗完成,剩余记录数: {len(self.合并数据)}")
return self.合并数据
def 构建评价模型(self):
"""构建二课积分评价模型"""
self.记录日志("开始构建评价模型")
if self.合并数据 is None:
self.数据清洗()
# 检查必要列是否存在
required_columns = ['院系', '年级', '班级', '学号', '姓名', '学分类型', '转换后学分']
missing_columns = [col for col in required_columns if col not in self.合并数据.columns]
if missing_columns:
self.记录日志(f"错误: 缺少必要列 {missing_columns}")
raise ValueError(f"数据中缺少必要列: {missing_columns}")
# 按学分类型分组计算
try:
pivot_df = self.合并数据.groupby(['院系', '年级', '班级', '学号', '姓名', '学分类型'])['转换后学分'].sum().unstack(fill_value=0).reset_index()
except Exception as e:
self.记录日志(f"分组计算失败: {str(e)}")
raise
# 重命名列
pivot_df.rename(columns={
'创新创业': '创新创业转换后学分',
'综合素养': '综合素养转换后学分',
'社会实践与劳动教育': '社会实践与劳动教育转换后学分'
}, inplace=True)
# 检查是否有缺失的学分类型
for col in ['创新创业转换后学分', '综合素养转换后学分', '社会实践与劳动教育转换后学分']:
if col not in pivot_df.columns:
pivot_df[col] = 0
self.记录日志(f"警告: 缺少学分类型 {col},已设为0")
# 计算原始得分
pivot_df['二课原始得分'] = pivot_df[['创新创业转换后学分', '综合素养转换后学分', '社会实践与劳动教育转换后学分']].sum(axis=1)
# 标准化处理
try:
scaler = MinMaxScaler()
标准化得分 = scaler.fit_transform(pivot_df[['创新创业转换后学分', '综合素养转换后学分', '社会实践与劳动教育转换后学分']])
pivot_df[['创新创业标准化', '综合素养标准化', '社会实践标准化']] = 标准化得分
except Exception as e:
self.记录日志(f"标准化处理失败: {str(e)}")
raise
# 计算综合得分(可调整权重)
权重 = {
'创新创业标准化': 0.4,
'综合素养标准化': 0.3,
'社会实践标准化': 0.3
}
pivot_df['二课综合得分'] = (pivot_df['创新创业标准化'] * 权重['创新创业标准化'] +
pivot_df['综合素养标准化'] * 权重['综合素养标准化'] +
pivot_df['社会实践标准化'] * 权重['社会实践标准化'])
# 按综合得分排名
pivot_df['综合排名'] = pivot_df.groupby(['院系', '年级'])['二课综合得分'].rank(ascending=False, method='min')
pivot_df['原始排名'] = pivot_df.groupby(['院系', '年级'])['二课原始得分'].rank(ascending=False, method='min')
# 保存处理结果
self.处理结果 = pivot_df.round(2)
self.记录日志(f"评价模型构建完成,共处理 {len(self.处理结果)} 条记录")
return self.处理结果
# 2. 可视化分析类
class 二课可视化分析:
def __init__(self, 处理后的数据):
self.df = 处理后的数据
self.颜色映射 = {
'2022': '#1f77b4',
'2023': '#ff7f0e',
'2024': '#2ca02c'
}
def 生成雷达图(self, 学生列表=None, 年级=None):
if 学生列表 is None and 年级 is not None:
学生列表 = self.df[self.df['年级'] == 年级].sort_values('二课综合得分', ascending=False).head(5)['姓名'].tolist()
if not 学生列表:
学生列表 = self.df.sort_values('二课综合得分', ascending=False).head(5)['姓名'].tolist()
筛选数据 = self.df[self.df['姓名'].isin(学生列表)]
fig = go.Figure()
for _, row in 筛选数据.iterrows():
fig.add_trace(go.Scatterpolar(
r=[row['创新创业标准化'], row['综合素养标准化'], row['社会实践标准化']],
theta=['创新创业', '综合素养', '社会实践'],
fill='toself',
name=f"{row['姓名']} (综合得分: {row['二课综合得分']:.2f})"
))
fig.update_layout(
polar=dict(
radialaxis=dict(
visible=True,
range=[0, 1]
)),
showlegend=True,
title='学生二课积分雷达图对比'
)
return fig
def 生成折线图(self, 院系=None, 年级=None):
if 院系 and 年级:
筛选数据 = self.df[(self.df['院系'] == 院系) & (self.df['年级'] == 年级)]
elif 院系:
筛选数据 = self.df[self.df['院系'] == 院系]
elif 年级:
筛选数据 = self.df[self.df['年级'] == 年级]
else:
筛选数据 = self.df
# 按班级和学号排序
筛选数据 = 筛选数据.sort_values(['班级', '学号'])
fig = go.Figure()
# 添加综合得分线
fig.add_trace(go.Scatter(
x=筛选数据['姓名'],
y=筛选数据['二课综合得分'],
mode='lines+markers',
name='综合得分',
line=dict(color='red', width=2)
))
# 添加各维度得分柱状图
fig.add_trace(go.Bar(
x=筛选数据['姓名'],
y=筛选数据['创新创业转换后学分'],
name='创新创业',
marker_color='#3498db'
))
fig.add_trace(go.Bar(
x=筛选数据['姓名'],
y=筛选数据['综合素养转换后学分'],
name='综合素养',
marker_color='#2ecc71'
))
fig.add_trace(go.Bar(
x=筛选数据['姓名'],
y=筛选数据['社会实践与劳动教育转换后学分'],
name='社会实践',
marker_color='#f39c12'
))
fig.update_layout(
barmode='group',
title='学生二课积分分布',
xaxis_title='学生姓名',
yaxis_title='积分',
xaxis_tickangle=-45
)
return fig
def 生成热力图(self):
# 计算相关系数
corr_matrix = self.df[['创新创业转换后学分', '综合素养转换后学分', '社会实践与劳动教育转换后学分']].corr()
fig = go.Figure(data=go.Heatmap(
z=corr_matrix.values,
x=corr_matrix.columns,
y=corr_matrix.columns,
colorscale='Viridis',
zmin=-1,
zmax=1,
hoverongaps=False,
colorbar=dict(title='相关系数')
)
fig.update_layout(
title='二课积分维度相关性分析'
)
return fig
def 生成年级对比图(self):
年级数据 = self.df.groupby('年级').agg({
'创新创业转换后学分': 'mean',
'综合素养转换后学分': 'mean',
'社会实践与劳动教育转换后学分': 'mean'
}).reset_index()
fig = make_subplots(rows=1, cols=2, specs=[[{'type': 'bar'}, {'type': 'pie'}]])
# 柱状图
for col in ['创新创业转换后学分', '综合素养转换后学分', '社会实践与劳动教育转换后学分']:
fig.add_trace(
go.Bar(
x=年级数据['年级'].astype(str),
y=年级数据[col],
name=col.split('转换后')[0]
),
row=1, col=1
)
# 饼图
fig.add_trace(
go.Pie(
labels=['创新创业', '综合素养', '社会实践'],
values=年级数据.mean()[['创新创业转换后学分', '综合素养转换后学分', '社会实践与劳动教育转换后学分']],
hole=0.3
),
row=1, col=2
)
fig.update_layout(
title_text='各年级二课积分对比',
showlegend=True
)
return fig
# 3. 交互式大屏应用
def 创建交互式大屏(处理后的数据):
# 初始化可视化分析类
可视化分析 = 二课可视化分析(处理后的数据)
# 初始化Dash应用
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
# 获取筛选选项
院系列表 = sorted(处理后的数据['院系'].unique().tolist())
年级列表 = sorted(处理后的数据['年级'].unique().tolist())
班级列表 = sorted(处理后的数据['班级'].unique().tolist())
app.layout = dbc.Container([
dbc.Row([
dbc.Col(html.H1("学生二课积分分析大屏", className="text-center mb-4"), width=12)
]),
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader("筛选条件"),
dbc.CardBody([
dbc.Row([
dbc.Col([
dcc.Dropdown(
id='院系筛选',
options=[{'label': x, 'value': x} for x in 院系列表],
placeholder="选择院系"
)
], width=6),
dbc.Col([
dcc.Dropdown(
id='年级筛选',
options=[{'label': str(x), 'value': x} for x in 年级列表],
placeholder="选择年级"
)
], width=6)
]),
dbc.Row([
dbc.Col([
dcc.Dropdown(
id='班级筛选',
options=[{'label': x, 'value': x} for x in 班级列表],
placeholder="选择班级",
multi=True
)
], width=12)
], className="mt-2"),
dbc.Row([
dbc.Col([
dcc.Dropdown(
id='学生筛选',
placeholder="选择学生(可多选)",
multi=True
)
], width=12)
], className="mt-2"),
dbc.Row([
dbc.Col([
dbc.Button("应用筛选", id="筛选按钮", color="primary", className="w-100")
], width=12)
], className="mt-2"),
dbc.Row([
dbc.Col([
html.Div(id='状态提示', className="text-muted small")
], width=12)
], className="mt-2")
])
])
], width=3),
dbc.Col([
dbc.Card([
dbc.CardHeader("二课积分统计概览"),
dbc.CardBody([
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader("平均综合得分"),
dbc.CardBody([
html.H4(id='平均综合得分', className="card-title text-center")
])
], className="h-100")
], width=4),
dbc.Col([
dbc.Card([
dbc.CardHeader("最高综合得分"),
dbc.CardBody([
html.H4(id='最高综合得分', className="card-title text-center")
])
], className="h-100")
], width=4),
dbc.Col([
dbc.Card([
dbc.CardHeader("参与学生数"),
dbc.CardBody([
html.H4(id='学生总数', className="card-title text-center")
])
], className="h-100")
], width=4)
]),
dbc.Row([
dbc.Col([
dcc.Graph(id='年级对比图')
], width=12)
], className="mt-3")
])
], className="h-100")
], width=9)
], className="mb-4"),
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader("学生积分雷达图"),
dbc.CardBody([
dcc.Graph(id='雷达图')
])
])
], width=6),
dbc.Col([
dbc.Card([
dbc.CardHeader("维度相关性分析"),
dbc.CardBody([
dcc.Graph(id='热力图')
])
])
], width=6)
], className="mb-4"),
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader("学生积分详细分布"),
dbc.CardBody([
dcc.Graph(id='折线图')
])
])
], width=12)
]),
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader("数据明细"),
dbc.CardBody([
html.Div(id='数据表')
])
])
], width=12)
], className="mb-4")
], fluid=True)
# 回调函数
@app.callback(
Output('班级筛选', 'options'),
Input('院系筛选', 'value'),
Input('年级筛选', 'value'),
prevent_initial_call=True
)
def 更新班级选项(selected_院系, selected_年级):
if not selected_院系 and not selected_年级:
return dash.no_update
筛选数据 = 处理后的数据.copy()
if selected_院系:
筛选数据 = 筛选数据[筛选数据['院系'] == selected_院系]
if selected_年级:
筛选数据 = 筛选数据[筛选数据['年级'] == selected_年级]
可选班级 = sorted(筛选数据['班级'].unique().tolist())
return [{'label': x, 'value': x} for x in 可选班级]
@app.callback(
Output('学生筛选', 'options'),
Input('院系筛选', 'value'),
Input('年级筛选', 'value'),
Input('班级筛选', 'value'),
prevent_initial_call=True
)
def 更新学生选项(selected_院系, selected_年级, selected_班级):
if not selected_院系 and not selected_年级 and not selected_班级:
return dash.no_update
筛选数据 = 处理后的数据.copy()
if selected_院系:
筛选数据 = 筛选数据[筛选数据['院系'] == selected_院系]
if selected_年级:
筛选数据 = 筛选数据[筛选数据['年级'] == selected_年级]
if selected_班级:
筛选数据 = 筛选数据[筛选数据['班级'].isin(selected_班级)]
可选学生 = sorted(筛选数据['姓名'].unique().tolist())
return [{'label': x, 'value': x} for x in 可选学生]
@app.callback(
[Output('雷达图', 'figure'),
Output('折线图', 'figure'),
Output('热力图', 'figure'),
Output('年级对比图', 'figure'),
Output('平均综合得分', 'children'),
Output('最高综合得分', 'children'),
Output('学生总数', 'children'),
Output('数据表', 'children'),
Output('状态提示', 'children')],
Input('筛选按钮', 'n_clicks'),
[State('院系筛选', 'value'),
State('年级筛选', 'value'),
State('班级筛选', 'value'),
State('学生筛选', 'value')],
prevent_initial_call=True
)
def 更新所有图表(n_clicks, selected_院系, selected_年级, selected_班级, selected_学生):
筛选数据 = 处理后的数据.copy()
状态消息 = ""
# 应用筛选条件
if selected_院系:
筛选数据 = 筛选数据[筛选数据['院系'] == selected_院系]
状态消息 += f"院系: {selected_院系} | "
if selected_年级:
筛选数据 = 筛选数据[筛选数据['年级'] == selected_年级]
状态消息 += f"年级: {selected_年级} | "
if selected_班级:
筛选数据 = 筛选数据[筛选数据['班级'].isin(selected_班级)]
状态消息 += f"班级: {', '.join(selected_班级)} | "
if selected_学生:
筛选数据 = 筛选数据[筛选数据['姓名'].isin(selected_学生)]
状态消息 += f"学生: {len(selected_学生)}人"
# 生成图表
try:
雷达图 = 可视化分析.生成雷达图(selected_学生, selected_年级) if selected_学生 else 可视化分析.生成雷达图(年级=selected_年级)
折线图 = 可视化分析.生成折线图(selected_院系, selected_年级)
热力图 = 可视化分析.生成热力图()
年级对比图 = 可视化分析.生成年级对比图()
# 计算统计指标
平均综合得分 = f"{筛选数据['二课综合得分'].mean():.2f}"
最高综合得分 = f"{筛选数据['二课综合得分'].max():.2f}"
学生总数 = f"{len(筛选数据)}人"
# 生成数据表
数据表 = dash.dash_table.DataTable(
columns=[{"name": i, "id": i} for i in 筛选数据.columns],
data=筛选数据.to_dict('records'),
page_size=10,
style_table={'overflowX': 'auto'},
style_cell={
'textAlign': 'left',
'padding': '5px'
},
style_header={
'backgroundColor': 'rgb(230, 230, 230)',
'fontWeight': 'bold'
}
)
return 雷达图, 折线图, 热力图, 年级对比图, 平均综合得分, 最高综合得分, 学生总数, 数据表, 状态消息 or "未应用筛选条件"
except Exception as e:
print(f"生成图表时出错: {str(e)}")
return go.Figure(), go.Figure(), go.Figure(), go.Figure(), "N/A", "N/A", "0", html.Div("数据加载失败"), f"错误: {str(e)}"
return app
# 主程序
def 主程序():
print("=== 学生二课积分分析系统 ===")
# 获取当前脚本所在目录
base_dir = os.path.dirname(os.path.abspath(__file__)) if '__file__' in globals() else os.getcwd()
print(f"当前工作目录: {base_dir}")
# 定义文件列表
文件列表 = [
os.path.join(base_dir, '学分明细2022级.xlsx'),
os.path.join(base_dir, '学分明细2023级.xlsx'),
os.path.join(base_dir, '学分明细2024级.xlsx')
]
# 检查文件是否存在
print("\n检查数据文件:")
有效文件 = []
for 文件 in 文件列表:
if os.path.exists(文件):
print(f"✓ {文件}")
有效文件.append(文件)
else:
print(f"× {文件} (文件不存在)")
if not 有效文件:
print("\n错误: 没有找到任何有效数据文件!")
print("请确保以下文件存在于当前目录:")
for 文件 in 文件列表:
print(f"- {os.path.basename(文件)}")
exit(1)
# 1. 数据预处理
print("\n开始数据预处理...")
数据处理器 = 二课数据预处理(有效文件)
try:
处理后的数据 = 数据处理器.构建评价模型()
print(f"\n数据处理完成,共 {len(处理后的数据)} 条记录")
except Exception as e:
print(f"\n数据处理失败: {str(e)}")
print("\n处理日志:")
for 日志 in 数据处理器.日志:
print(日志)
exit(1)
# 2. 创建交互式大屏
print("\n创建交互式大屏...")
app = 创建交互式大屏(处理后的数据)
# 3. 运行应用
print("\n启动Dash应用...")
print(f"请在浏览器中访问: http://localhost:8050")
app.run_server(debug=True, port=8050)
if __name__ == "__main__":
主程序() 以上代码出现File "C:\Users\w10\AppData\Local\Temp\ipykernel_49596\3947897257.py", line 278
fig.update_layout(
^
SyntaxError: invalid syntax