descriptive_stats_view.py:
# -*- coding: UTF-8 -*- #
"""
@filename: descriptive_stats_view.py
@author : Sun S Z
@time : 2025/9/15 14:24
@software: PyCharm
"""
"""
描述性统计视图(MVVM架构中的View层)
负责描述性统计界面的展示和用户交互处理
"""
import matplotlib.pyplot as plt
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QLabel, QHBoxLayout, QComboBox,
QCheckBox, QPushButton, QTableWidget, QSplitter,
QTableWidgetItem, QMessageBox, QHeaderView
)
from PySide6.QtGui import QFont
from PySide6.QtCore import Qt, Signal, Slot
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
class DescriptiveStatsView(QWidget):
"""描述性统计视图类"""
# 定义信号
analyze_requested = Signal(str, list) # (变量名, 选中的统计量)
variable_changed = Signal(str) # 变量选择变化
request_valid_variables = Signal() # 请求有效变量列表
def __init__(self, viewmodel=None, parent=None):
super().__init__(parent)
self.viewmodel = viewmodel # 现在是DescriptiveStatsViewModel实例
self.parent = parent
self.col_type_map = {} # 列类型映射
self.current_figure = None # 保存当前图表引用
self._data = None # 存储视图使用的数据
# 初始化UI
self.init_ui()
# 连接信号与槽
self.connect_signals()
# 初始化数据
self.initialize()
def init_ui(self):
"""初始化UI组件"""
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(15, 15, 15, 15)
main_layout.setSpacing(12)
# 标题
self.title = QLabel("描述性统计分析")
self.title.setFont(QFont("Arial", 18, QFont.Bold))
self.title.setStyleSheet("color: #0ea5e9;")
main_layout.addWidget(self.title)
# 变量选择区域
self.var_widget = QWidget()
self.var_layout = QHBoxLayout(self.var_widget)
# 变量选择标签和下拉框
self.var_label = QLabel("选择变量:")
self.var_combo = QComboBox()
# 统计量选择区域
self.stats_label = QLabel("统计量:")
self.stats_container = QWidget()
self.stats_container_layout = QHBoxLayout(self.stats_container)
# 创建统计量复选框
self.sample_count_check = QCheckBox("样本个数")
self.mean_check = QCheckBox("平均值")
self.median_check = QCheckBox("中位数")
self.mode_check = QCheckBox("众数")
self.std_check = QCheckBox("标准差")
self.min_check = QCheckBox("最小值")
self.max_check = QCheckBox("最大值")
self.range_check = QCheckBox("范围")
self.p10_check = QCheckBox("10%分位数")
self.p25_check = QCheckBox("25%分位数")
self.p75_check = QCheckBox("75%分位数")
self.p90_check = QCheckBox("90%分位数")
# 存储所有统计量复选框
self.stats_checks = [
self.sample_count_check, self.mean_check, self.median_check,
self.mode_check, self.std_check, self.min_check,
self.max_check, self.range_check, self.p10_check,
self.p25_check, self.p75_check, self.p90_check
]
# 添加到布局
for check in self.stats_checks:
check.setChecked(True)
self.stats_container_layout.addWidget(check)
# 分析按钮
self.analyze_btn = QPushButton("执行分析")
self.analyze_btn.setMinimumWidth(100) # 确保按钮可见
self.analyze_btn.setStyleSheet("""
QPushButton {
background-color: #0ea5e9;
color: white;
border-radius: 4px;
padding: 6px;
}
QPushButton:hover {
background-color: #0284c7;
}
""")
# 组装变量选择区域
self.var_layout.addWidget(self.var_label)
self.var_layout.addWidget(self.var_combo)
self.var_layout.addSpacing(20)
self.var_layout.addWidget(self.stats_label)
self.var_layout.addWidget(self.stats_container)
self.var_layout.addStretch()
self.var_layout.addWidget(self.analyze_btn)
main_layout.addWidget(self.var_widget)
# 结果区域
self.result_table = QTableWidget()
self.result_table.setRowCount(1)
self.result_table.setHorizontalHeaderLabels(["统计量名称", "统计值"])
# 图表区域 - 使用Figure而非plt.figure()以避免全局状态问题
self.figure = Figure(figsize=(10, 6))
self.canvas = FigureCanvas(self.figure)
# 分割器
self.splitter = QSplitter(Qt.Vertical)
self.splitter.addWidget(self.result_table)
self.splitter.addWidget(self.canvas)
self.splitter.setSizes([100, 600])
main_layout.addWidget(self.splitter, 1)
def connect_signals(self):
"""连接信号与槽"""
# 确保视图模型存在再连接信号
if not self.viewmodel:
return
# 视图 -> 视图模型
self.analyze_btn.clicked.connect(self.on_analyze_clicked)
self.var_combo.currentIndexChanged.connect(self.on_var_changed)
self.request_valid_variables.connect(self.viewmodel.request_valid_variables)
self.analyze_requested.connect(self.viewmodel.calculate_descriptive_stats)
# 视图模型 -> 视图
self.viewmodel.stats_results_updated.connect(self.update_results)
self.viewmodel.chart_updated.connect(self.update_chart)
self.viewmodel.valid_variables_updated.connect(self.update_variable_list)
self.viewmodel.error_occurred.connect(self.show_error_message)
def initialize(self):
"""初始化视图"""
# 请求有效变量列表
self.request_valid_variables.emit()
def on_analyze_clicked(self):
"""处理分析按钮点击事件 - 仅在点击时触发更新"""
current_var = self.var_combo.currentText()
if current_var and current_var != "无可用变量":
# 收集选中的统计量
selected_stats = [check.text() for check in self.stats_checks if check.isChecked()]
# 发送分析请求信号
self.analyze_requested.emit(current_var, selected_stats)
def on_var_changed(self, index):
"""处理变量选择变化事件 - 只调整统计量可用性,不自动分析"""
current_var = self.var_combo.currentText()
if current_var in self.col_type_map:
col_type = self.col_type_map[current_var]
self.adjust_stats_for_col_type(col_type)
self.variable_changed.emit(current_var)
def adjust_stats_for_col_type(self, col_type):
"""根据列类型调整统计量的可用性"""
if col_type == "datetime":
for check in self.stats_checks:
if check.text() in ["样本个数", "最小值", "最大值", "范围"]:
check.setEnabled(True)
else:
check.setEnabled(False)
else:
for check in self.stats_checks:
check.setEnabled(True)
@Slot(list, dict)
def update_variable_list(self, variables, col_type_map):
"""更新变量下拉列表"""
self.col_type_map = col_type_map
self.var_combo.clear()
if variables:
self.var_combo.addItems(variables)
self.var_combo.setEnabled(True)
self.analyze_btn.setEnabled(True)
else:
self.var_combo.addItem("无可用变量")
self.var_combo.setEnabled(False)
self.analyze_btn.setEnabled(False)
@Slot(dict)
def update_results(self, results):
"""更新结果表格"""
if not results:
return
# 清空表格
self.result_table.clear()
# 设置表格尺寸
self.result_table.setRowCount(1)
self.result_table.setColumnCount(len(results))
self.result_table.setHorizontalHeaderLabels(list(results.keys()))
# 填充数据
for col_idx, (stat_name, value) in enumerate(results.items()):
if isinstance(value, (int, float)):
# 样本个数显示为整数
if stat_name == "样本个数":
value_item = QTableWidgetItem(f"{int(value)}")
else:
value_item = QTableWidgetItem(f"{value:.4f}")
else:
value_item = QTableWidgetItem(str(value))
value_item.setFlags(value_item.flags() & ~Qt.ItemIsEditable)
value_item.setTextAlignment(Qt.AlignCenter)
self.result_table.setItem(0, col_idx, value_item)
# 自动调整列宽
self.result_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
@Slot(object)
def update_chart(self, figure):
"""更新图表显示"""
try:
# 清除现有图表
self.figure.clear()
# 保存新图表引用
self.current_figure = figure
# 复制新图表内容
for ax in figure.axes:
# 创建新的坐标轴
new_ax = self.figure.add_subplot(111)
# 复制数据和设置
if ax.get_title():
new_ax.set_title(ax.get_title())
if ax.get_xlabel():
new_ax.set_xlabel(ax.get_xlabel())
if ax.get_ylabel():
new_ax.set_ylabel(ax.get_ylabel())
# 复制绘图数据
for line in ax.get_lines():
new_ax.plot(line.get_xdata(), line.get_ydata(),
color=line.get_color(), alpha=line.get_alpha())
# 复制直方图数据
for patch in ax.patches:
new_ax.hist(patch.get_xy(), bins=ax.get_bins(),
color=patch.get_facecolor(),
edgecolor=patch.get_edgecolor(),
alpha=patch.get_alpha())
# 应用旋转设置
tick_params = ax.xaxis.get_tick_params()
rotation = tick_params.get('rotation', 0)
if rotation:
new_ax.tick_params(axis='x', rotation=rotation)
self.figure.tight_layout()
self.canvas.draw()
except Exception as e:
self.show_error_message(f"图表更新错误: {str(e)}")
finally:
# 释放原始图表资源
if figure:
plt.close(figure)
@Slot(str)
def show_error_message(self, message):
"""显示错误消息"""
QMessageBox.critical(self, "错误", message)
def set_view_model(self, viewmodel):
"""设置视图模型"""
self.viewmodel = viewmodel
self.connect_signals()
self.initialize()
def set_data(self, data):
"""设置视图数据"""
self._data = data
# 如果已经设置了视图模型,同步数据
if self.viewmodel and hasattr(self.viewmodel.model, 'set_data'):
self.viewmodel.model.set_data(data)
# 重新初始化以刷新界面
self.initialize()
def closeEvent(self, event):
"""关闭窗口时释放图表资源"""
if self.current_figure:
plt.close(self.current_figure)
plt.close(self.figure)
event.accept()
descriptive_stats_viewmodel.py:
# -*- coding: UTF-8 -*- #
"""
@filename: descriptive_stats_viewmodel.py
@author : Sun S Z
@time : 2025/9/15 14:24
@software: PyCharm
"""
"""
描述性统计视图模型(MVVM架构中的ViewModel层)
负责处理描述性统计的业务逻辑,协调视图和模型
"""
from PySide6.QtCore import QObject, Signal, Slot
from models.statistical_analysis_models.descriptive_stats_model import DescriptiveStatsModel
class DescriptiveStatsViewModel(QObject):
"""描述性统计视图模型类"""
# 定义信号
stats_results_updated = Signal(dict) # 传递统计结果
chart_updated = Signal(object) # 传递图表对象
valid_variables_updated = Signal(list, dict) # 传递有效变量和类型映射
error_occurred = Signal(str) # 传递错误信息
def __init__(self, data_model=None, parent=None):
super().__init__(parent)
self.data_model = data_model # 数据模型引用
self.col_type_map = {} # 列类型映射
# 初始化模型
self.model = DescriptiveStatsModel()
# 连接模型信号
self.model.calculation_completed.connect(self._on_calculation_completed)
self.model.chart_generated.connect(self._on_chart_generated)
self.model.error_occurred.connect(self._on_error_occurred)
def has_data(self):
"""检查是否有可用数据"""
return self.data_model is not None and self.data_model.has_data()
@Slot()
def request_valid_variables(self):
"""请求获取有效变量列表"""
if not self.has_data():
self.valid_variables_updated.emit([], {})
return
try:
# 从数据模型获取数据
dataframe = self.data_model.get_data()
valid_cols, col_type_map = self.model.get_valid_variables(dataframe)
self.col_type_map = col_type_map
self.valid_variables_updated.emit(valid_cols, col_type_map)
except Exception as e:
self.error_occurred.emit(f"获取变量列表错误: {str(e)}")
@Slot(str, list)
def calculate_descriptive_stats(self, column, selected_stats):
"""计算描述性统计 - 确保正确处理并转发结果"""
if not self.has_data() or not column:
self.error_occurred.emit("没有可用数据或未选择变量")
return
try:
# 获取数据并计算统计量
dataframe = self.data_model.get_data()
self.model.calculate_stats(dataframe, column, selected_stats)
except Exception as e:
self.error_occurred.emit(f"处理分析请求错误: {str(e)}")
def _on_calculation_completed(self, results):
"""处理计算完成信号"""
self.stats_results_updated.emit(results)
def _on_chart_generated(self, figure):
"""处理图表生成信号"""
self.chart_updated.emit(figure)
def _on_error_occurred(self, message):
"""处理错误信号"""
self.error_occurred.emit(message)
descriptive_stats_model.py:
# -*- coding: UTF-8 -*- #
"""
@filename: descriptive_stats_model.py
@author : Sun S Z
@time : 2025/9/15 14:24
@software: PyCharm
"""
"""
描述性统计模型(MVVM架构中的Model层)
负责执行描述性统计的核心计算和图表生成
"""
import pandas as pd
import matplotlib.pyplot as plt
from PySide6.QtCore import QObject, Signal
# 导入工具类
from models.font_setting import FontSetting
class DescriptiveStatsModel(QObject):
"""描述性统计模型,处理统计计算和图表生成"""
# 定义信号
calculation_completed = Signal(dict) # 传递统计结果
chart_generated = Signal(object) # 传递图表对象
error_occurred = Signal(str) # 传递错误信息
def __init__(self):
super().__init__()
# 确保中文字体正确设置
FontSetting.set_chinese_font()
self._data = None # 存储模型数据
def set_data(self, data):
"""设置模型数据"""
self._data = data.copy() # 复制数据避免外部修改影响
def has_data(self):
"""检查是否有可用数据"""
return self._data is not None and not self._data.empty
def get_data(self):
"""获取模型数据"""
return self._data
def get_valid_variables(self, dataframe):
"""
获取有效变量列表和类型映射
Args:
dataframe: 输入的DataFrame
Returns:
tuple: (有效变量列表, 变量类型映射)
"""
valid_cols = []
col_type_map = {}
if dataframe is None or dataframe.empty:
return valid_cols, col_type_map
for col in dataframe.columns:
# 跳过空列
col_data = dataframe[col].dropna()
if len(col_data) == 0:
continue
# 判断列类型
if col_data.dtype.kind in 'iufc': # 数值型
valid_cols.append(col)
col_type_map[col] = "numeric"
else: # 尝试判断是否为时间型
try:
pd.to_datetime(col_data, errors='raise')
valid_cols.append(col)
col_type_map[col] = "datetime"
except:
continue # 非数值、非时间列
return valid_cols, col_type_map
def calculate_stats(self, dataframe, column, selected_stats):
"""
计算描述性统计量
Args:
dataframe: 输入的DataFrame
column: 要分析的列名
selected_stats: 选中的统计量列表
"""
try:
if dataframe is None or column not in dataframe.columns:
self.error_occurred.emit("无效的数据或列名")
return
# 准备数据
data = dataframe[column].dropna()
if data.empty:
self.error_occurred.emit("所选列没有有效数据")
return
# 确定列类型
col_type = "numeric"
try:
# 尝试转换为时间格式
pd.to_datetime(data, errors='raise')
col_type = "datetime"
except:
# 确保是数值型
data = pd.to_numeric(data, errors='coerce').dropna()
if data.empty:
self.error_occurred.emit("无法将列转换为数值型数据")
return
# 计算统计量
results = {}
sample_count = len(data)
# 处理样本个数
if "样本个数" in selected_stats:
results["样本个数"] = sample_count
# 处理数值型列
if col_type == "numeric":
if "平均值" in selected_stats:
results["平均值"] = round(data.mean(), 4)
if "中位数" in selected_stats:
results["中位数"] = round(data.median(), 4)
if "众数" in selected_stats:
mode_value = data.mode().iloc[0] if not data.mode().empty else None
results["众数"] = round(mode_value, 4) if mode_value is not None else "无"
if "标准差" in selected_stats:
results["标准差"] = round(data.std(), 4)
if "最小值" in selected_stats:
results["最小值"] = round(data.min(), 4)
if "最大值" in selected_stats:
results["最大值"] = round(data.max(), 4)
if "范围" in selected_stats:
results["范围"] = round(data.max() - data.min(), 4)
if "10%分位数" in selected_stats:
results["10%分位数"] = round(data.quantile(0.10), 4)
if "25%分位数" in selected_stats:
results["25%分位数"] = round(data.quantile(0.25), 4)
if "75%分位数" in selected_stats:
results["75%分位数"] = round(data.quantile(0.75), 4)
if "90%分位数" in selected_stats:
results["90%分位数"] = round(data.quantile(0.90), 4)
# 处理时间型列
else:
data = pd.to_datetime(data)
if "最小值" in selected_stats:
results["最小值(最早时间)"] = data.min().strftime("%Y-%m-%d %H:%M:%S")
if "最大值" in selected_stats:
results["最大值(最晚时间)"] = data.max().strftime("%Y-%m-%d %H:%M:%S")
if "范围" in selected_stats:
# 计算时间差(转为小时)
time_diff = (data.max() - data.min()).total_seconds() / 3600
results["范围(小时)"] = round(time_diff, 2)
# 发送计算结果
self.calculation_completed.emit(results)
# 生成图表
self.generate_chart(data, column, col_type)
except Exception as e:
self.error_occurred.emit(f"统计计算错误: {str(e)}")
def generate_chart(self, data, column, col_type):
"""
生成统计图表
Args:
data: 要可视化的数据
column: 列名
col_type: 列类型(numeric/datetime)
"""
try:
# 创建图表,确保使用正确的字体
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]
figure, ax = plt.subplots(figsize=(10, 6))
if col_type == "numeric":
# 数值列:直方图
ax.hist(data, bins=15, color='#0ea5e9', edgecolor='white', alpha=0.7)
ax.set_title(f'"{column}" 数值分布直方图')
ax.set_xlabel('数值')
ax.set_ylabel('频数')
else:
# 时间列:时间序列趋势图
data_sorted = data.sort_values()
ax.plot(data_sorted, range(len(data_sorted)), color='#0ea5e9', alpha=0.8)
ax.set_title(f'"{column}" 时间分布趋势')
ax.set_xlabel('时间')
ax.set_ylabel('累计数量')
ax.tick_params(axis='x', rotation=45)
plt.tight_layout()
self.chart_generated.emit(figure)
except Exception as e:
self.error_occurred.emit(f"图表生成错误: {str(e)}")
统计分析表格为什么一直显示时间列的统计结果?我需要可以显示我选择对应列后点击执行分析按钮后表格显示的是对应列的统计结果。是否descriptive_stats_model.py中的calculate_stats和generate_chart并没有连接到descriptive_stats_view.py中,或者descriptive_stats_view.py中的update_results和update_chart有问题?给出完整修改方案代码,其他不变