学习日志02 ETF 基础数据可视化分析与简易管理系统
从头开始了,现在有数据的变动还有要用jupyter,这个文学编程的确很好,虽然我们老师有点push有点严格,但觉得好好学确实能收获不少知识的!!!是的!
已经搭建了miniconda关联的jupyter notebook
1 我发现jupyter是不可以关闭conda终端运行的
对哒,Jupyter Notebook 是依赖终端(或 Anaconda Prompt)启动的本地服务,终端窗口不能直接关闭,否则 Jupyter 会断开连接~ 给你说下原因和正确关闭方式:
一、为啥不能关终端?
- Jupyter 启动后,终端其实是在运行一个 “本地服务器”(监听
localhost:8888
端口),负责处理 Notebook 的代码运行、文件保存等请求。 - 关掉终端,这个服务器就会停止,浏览器里的 Jupyter 页面会报错(比如无法运行代码、加载文件),甚至直接无法访问。
二、正确关闭流程
- 先关浏览器里的 Jupyter 标签页:把所有 Jupyter 相关的浏览器标签页关掉。
- 再关终端:回到启动 Jupyter 的终端(或 Anaconda Prompt),按
Ctrl + C
(Windows/Linux)或Command + C
(Mac),等终端显示 “Shutting down server...” 后,再关闭终端窗口。
三、意外关了终端咋办?
如果不小心关了终端,重新打开 Anaconda Prompt → 激活环境(conda activate py313
)→ 重新运行 jupyter notebook
即可,之前的 Notebook 文件不会丢,重新打开继续写就行~
简单说,终端是 Jupyter 的 “幕后管家”,用完按正确流程关,就不会出问题啦,安心写代码吧~
--------------要写一点技术文档,这些是昨天忘记发的,今天好好学习,努力把这个ui跑起来-----------
2 基本框架
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from ipywidgets import (AppLayout, Dropdown, Button, Output, VBox, HBox, Label, Layout, SelectMultiple,IntSlider, FloatSlider, Checkbox)
from IPython.display import display, clear_output
import numpy as np
import os
from datetime import datetime# 设置中文字体和样式
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC", "sans-serif"]
plt.rcParams["axes.unicode_minus"] = False
sns.set_style("whitegrid")
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = Falseclass EnhancedExcelVisualizer:def __init__(self, file_path1, file_path2):"""初始化增强版Excel数据可视化应用"""self.file_path1 = file_path1self.file_path2 = file_path2self.current_figure = None # 添加当前图表引用# 尝试加载文件try:self.excel_file1 = pd.ExcelFile(file_path1)self.excel_file2 = pd.ExcelFile(file_path2)self.sheet_names1 = self.excel_file1.sheet_namesself.sheet_names2 = self.excel_file2.sheet_namesexcept Exception as e:print(f"文件加载错误: {str(e)}")return# 当前状态变量self.current_data = Noneself.current_sheet = Noneself.current_file = Noneself.current_chart_type = 'line'self.figsize = (10, 6)self.color_palette = 'viridis'# 创建UI组件self.create_ui_components()# 显示UIself.display_ui()# 初始化self.update_sheet_selector()def create_ui_components(self):"""创建所有UI组件"""# 文件选择器file1_name = os.path.basename(self.file_path1)file2_name = os.path.basename(self.file_path2)self.file_selector = Dropdown(options=[f'文件1 ({file1_name})', f'文件2 ({file2_name})'],value=f'文件1 ({file1_name})',description='选择文件:',layout=Layout(width='100%'))# 工作表选择器self.sheet_selector = Dropdown(options=[],description='选择工作表:',layout=Layout(width='100%'))# 数据列选择器 (支持多选Y轴)self.x_axis_selector = Dropdown(options=[],description='X轴数据列:',layout=Layout(width='100%'))self.y_axis_selector = SelectMultiple(options=[],description='Y轴数据列:',layout=Layout(width='100%'))# 图表类型选择器self.chart_type_selector = Dropdown(options=['折线图', '柱状图', '饼图', '面积图', '散点图', '箱线图', '热力图'],value='折线图',description='图表类型:',layout=Layout(width='100%'))# 图表样式控制self.color_palette_selector = Dropdown(options=['viridis', 'plasma', 'inferno', 'magma', 'cividis', 'Pastel1', 'Pastel2', 'Paired', 'Accent', 'Dark2'],value='viridis',description='配色方案:',layout=Layout(width='100%'))self.figsize_slider = IntSlider(min=5, max=20, value=10, step=1,description='图表大小:',layout=Layout(width='100%'))self.rotation_slider = IntSlider(min=0, max=90, value=45, step=5,description='标签旋转:',layout=Layout(width='100%'))self.transparency_slider = FloatSlider(min=0.1, max=1.0, value=0.8, step=0.1,description='透明度:',layout=Layout(width='100%'))self.grid_checkbox = Checkbox(value=True,description='显示网格',layout=Layout(width='100%'))# 操作按钮self.refresh_button = Button(description='刷新图表',button_style='primary',icon='refresh',layout=Layout(width='100%'))self.export_button = Button(description='导出图表',button_style='info',icon='save',layout=Layout(width='100%'))# 输出区域self.data_preview_output = Output(layout=Layout(height='auto', max_height='300px', overflow='auto'))self.chart_output = Output(layout=Layout(height='500px', border='1px solid #e0e0e0'))self.stats_output = Output(layout=Layout(height='auto', max_height='300px', overflow='auto'))self.log_output = Output(layout=Layout(height='auto', max_height='200px', overflow='auto'))# 绑定事件self.file_selector.observe(self.on_file_change, names='value')self.sheet_selector.observe(self.on_sheet_change, names='value')self.chart_type_selector.observe(self.on_chart_type_change, names='value')self.refresh_button.on_click(self.on_refresh_click)self.export_button.on_click(self.on_export_click)# 样式控制绑定controls = [self.color_palette_selector, self.figsize_slider, self.rotation_slider, self.transparency_slider,self.grid_checkbox]for control in controls:control.observe(self.on_style_change, names='value')def update_sheet_selector(self):"""更新工作表选择器选项"""selected_file = self.file_selector.value.split(' ')[0]if selected_file == '文件1':self.sheet_selector.options = self.sheet_names1else:self.sheet_selector.options = self.sheet_names2if self.sheet_selector.options:self.sheet_selector.value = self.sheet_selector.options[0]def update_column_selectors(self):"""更新数据列选择器选项"""selected_file = self.file_selector.value.split(' ')[0]selected_sheet = self.sheet_selector.valueif not selected_sheet:self.x_axis_selector.options = []self.y_axis_selector.options = []returntry:if selected_file == '文件1':self.current_data = self.excel_file1.parse(selected_sheet)else:self.current_data = self.excel_file2.parse(selected_sheet)except Exception as e:with self.log_output:print(f"读取工作表数据时出错: {str(e)}")returnself.current_sheet = selected_sheetself.current_file = selected_file# 更新列选择器column_names = list(self.current_data.columns)self.x_axis_selector.options = column_namesself.y_axis_selector.options = column_names# 自动选择默认列if len(column_names) >= 2:self.x_axis_selector.value = column_names[0]self.y_axis_selector.value = (column_names[1],) if len(column_names) > 1 else ()# 更新数据预览和统计信息self.update_data_preview()self.update_stats()def update_data_preview(self):"""更新数据预览"""with self.data_preview_output:clear_output()if self.current_data is not None and not self.current_data.empty:print(f"数据预览 ({self.current_data.shape[0]} 行 × {self.current_data.shape[1]} 列):")display(self.current_data.head(10).style.background_gradient(cmap='Blues').set_properties(**{'text-align': 'left'}))else:print("没有数据可供预览")def update_stats(self):"""更新数据统计信息"""with self.stats_output:clear_output()if self.current_data is not None and not self.current_data.empty:print("数据统计信息:")# 对数值列设置2位小数,对其他列保持原样stats_df = self.current_data.describe(include='all')display(stats_df.style.background_gradient(cmap='Greens').format("{:.2f}", subset=stats_df.select_dtypes(include='number').columns).set_properties(**{'text-align': 'left'}))else:print("没有数据可供统计")def on_file_change(self, change):"""文件选择变化事件处理"""with self.log_output:print(f"已选择文件: {change.new}")self.update_sheet_selector()def on_sheet_change(self, change):"""工作表选择变化事件处理"""with self.log_output:print(f"已选择工作表: {change.new}")self.update_column_selectors()def on_chart_type_change(self, change):"""图表类型选择变化事件处理"""with self.log_output:print(f"已选择图表类型: {change.new}")self.refresh_chart()def on_style_change(self, change):"""样式设置变化事件处理"""with self.log_output:print(f"样式设置已更新: {change.owner.description} = {change.new}")self.refresh_chart()def on_refresh_click(self, b):"""刷新按钮点击事件处理"""with self.log_output:print("正在刷新图表...")self.refresh_chart()def on_export_click(self, b):"""导出按钮点击事件处理"""with self.log_output:print("正在尝试导出图表...")self.export_chart()def refresh_chart(self):"""刷新图表显示"""x_axis = self.x_axis_selector.valuey_axes = self.y_axis_selector.valueif not x_axis or not y_axes or self.current_data is None:with self.chart_output:clear_output()print('请选择有效的X轴和Y轴数据列')returnwith self.chart_output:clear_output()plt.close('all') # 清除之前的图形try:# 数据预处理filtered_data = self.current_data.dropna(subset=[x_axis] + list(y_axes)).copy()if filtered_data.empty:print('所选列中没有有效数据')return# 设置图表大小self.figsize = (self.figsize_slider.value, self.figsize_slider.value*0.6)self.current_figure = plt.figure(figsize=self.figsize)# 设置颜色sns.set_palette(self.color_palette_selector.value)# 图表类型处理chart_type = self.chart_type_selector.valueif chart_type == '折线图':self._draw_line_plot(filtered_data, x_axis, y_axes)elif chart_type == '柱状图':self._draw_bar_plot(filtered_data, x_axis, y_axes)elif chart_type == '饼图':self._draw_pie_plot(filtered_data, x_axis, y_axes)elif chart_type == '面积图':self._draw_area_plot(filtered_data, x_axis, y_axes)elif chart_type == '散点图':self._draw_scatter_plot(filtered_data, x_axis, y_axes)elif chart_type == '箱线图':self._draw_box_plot(filtered_data, x_axis, y_axes)elif chart_type == '热力图':self._draw_heatmap(filtered_data, x_axis, y_axes)# 通用设置plt.grid(self.grid_checkbox.value)plt.tight_layout()plt.show()except Exception as e:with self.log_output:print(f"绘制图表时出错: {str(e)}")import tracebacktraceback.print_exc()def _draw_line_plot(self, data, x_axis, y_axes):"""绘制折线图"""# 尝试转换日期数据try:data[x_axis] = pd.to_datetime(data[x_axis])data = data.sort_values(by=x_axis)date_flag = Trueexcept:date_flag = Falsefor y_axis in y_axes:if pd.api.types.is_numeric_dtype(data[y_axis]):sns.lineplot(x=x_axis, y=y_axis, data=data, alpha=self.transparency_slider.value,label=y_axis)plt.title(f'折线图: {", ".join(y_axes)} vs {x_axis}')plt.xlabel(x_axis)plt.ylabel('值')if date_flag:plt.gcf().autofmt_xdate()if len(y_axes) > 1:plt.legend()def _draw_bar_plot(self, data, x_axis, y_axes):"""绘制柱状图"""# 限制显示数量if len(data) > 20:data = data.iloc[:20]with self.log_output:print('注意: 只显示前20条数据以避免图表拥挤')bar_width = 0.8 / len(y_axes)x = np.arange(len(data[x_axis]))for i, y_axis in enumerate(y_axes):if pd.api.types.is_numeric_dtype(data[y_axis]):plt.bar(x + i*bar_width, data[y_axis], width=bar_width,alpha=self.transparency_slider.value,label=y_axis)plt.title(f'柱状图: {", ".join(y_axes)} vs {x_axis}')plt.xlabel(x_axis)plt.ylabel('值')plt.xticks(x + bar_width*(len(y_axes)-1)/2, data[x_axis].astype(str),rotation=self.rotation_slider.value)if len(y_axes) > 1:plt.legend()def _draw_pie_plot(self, data, x_axis, y_axes):"""绘制饼图"""if len(y_axes) > 1:with self.log_output:print('饼图只支持单个Y轴,将使用第一个Y轴')y_axis = y_axes[0]if not pd.api.types.is_numeric_dtype(data[y_axis]):with self.log_output:print('饼图需要数值型Y轴数据')return# 限制类别数量if len(data) > 10:data = data.nlargest(10, y_axis)with self.log_output:print('注意: 只显示前10个类别以避免图表拥挤')plt.pie(data[y_axis], labels=data[x_axis].astype(str),autopct='%1.1f%%',startangle=90,colors=sns.color_palette(self.color_palette_selector.value),wedgeprops={'alpha': self.transparency_slider.value})plt.title(f'饼图: {y_axis} 分布')def _draw_area_plot(self, data, x_axis, y_axes):"""绘制面积图"""# 尝试转换日期数据try:data[x_axis] = pd.to_datetime(data[x_axis])data = data.sort_values(by=x_axis)date_flag = Trueexcept:date_flag = Falsefor y_axis in y_axes:if pd.api.types.is_numeric_dtype(data[y_axis]):plt.fill_between(data[x_axis], data[y_axis], alpha=self.transparency_slider.value,label=y_axis)plt.title(f'面积图: {", ".join(y_axes)} vs {x_axis}')plt.xlabel(x_axis)plt.ylabel('值')if date_flag:plt.gcf().autofmt_xdate()if len(y_axes) > 1:plt.legend()def _draw_scatter_plot(self, data, x_axis, y_axes):"""绘制散点图"""if len(y_axes) != 1:with self.log_output:print('散点图需要且仅需要一个Y轴')returny_axis = y_axes[0]if pd.api.types.is_numeric_dtype(data[x_axis]) and pd.api.types.is_numeric_dtype(data[y_axis]):sns.scatterplot(x=x_axis, y=y_axis, data=data,alpha=self.transparency_slider.value)plt.title(f'散点图: {y_axis} vs {x_axis}')plt.xlabel(x_axis)plt.ylabel(y_axis)else:with self.log_output:print('散点图需要数值型X轴和Y轴数据')def _draw_box_plot(self, data, x_axis, y_axes):"""绘制箱线图"""if len(y_axes) != 1:with self.log_output:print('箱线图需要且仅需要一个Y轴')returny_axis = y_axes[0]if pd.api.types.is_numeric_dtype(data[y_axis]):sns.boxplot(x=x_axis, y=y_axis, data=data)plt.title(f'箱线图: {y_axis} 按 {x_axis} 分布')plt.xlabel(x_axis)plt.ylabel(y_axis)plt.xticks(rotation=self.rotation_slider.value)else:with self.log_output:print('箱线图需要数值型Y轴数据')def _draw_heatmap(self, data, x_axis, y_axes):"""绘制热力图"""# 选择数值列numeric_cols = [col for col in data.select_dtypes(include='number').columns if col in y_axes or col == x_axis]if len(numeric_cols) < 2:with self.log_output:print('热力图需要至少两个数值列')return# 创建相关矩阵corr_matrix = data[numeric_cols].corr()sns.heatmap(corr_matrix, annot=True, cmap=self.color_palette_selector.value,fmt=".2f",linewidths=.5)plt.title('热力图: 变量相关性')def export_chart(self):"""导出图表"""if not hasattr(self, 'current_figure') or self.current_figure is None:with self.log_output:print('没有可导出的图表,请先生成图表')returntimestamp = datetime.now().strftime("%Y%m%d_%H%M%S")filename = f"chart_export_{timestamp}.png"try:self.current_figure.savefig(filename, dpi=300, bbox_inches='tight')with self.log_output:print(f'图表已成功导出为: {filename}')print(f'文件保存在: {os.path.abspath(filename)}')except Exception as e:with self.log_output:print(f'导出图表时出错: {str(e)}')import tracebacktraceback.print_exc()def display_ui(self):"""显示应用界面"""# 控制面板control_panel = VBox([self.file_selector,self.sheet_selector,self.x_axis_selector,self.y_axis_selector,self.chart_type_selector,self.color_palette_selector,self.figsize_slider,self.rotation_slider,self.transparency_slider,self.grid_checkbox,HBox([self.refresh_button, self.export_button])], layout=Layout(width='30%', padding='10px'))# 输出面板output_panel = VBox([Label('📊 数据预览'),self.data_preview_output,Label('📈 数据统计'),self.stats_output,Label('✨ 数据可视化'),self.chart_output,Label('📝 日志信息'),self.log_output], layout=Layout(width='70%', padding='10px'))# 整体布局app_layout = HBox([control_panel, output_panel])# 显示应用display(app_layout)# 使用示例
if __name__ == "__main__":# 修改为您的Excel文件路径file_path1 = 'G:\\PyCharm\\Python_project\\daily_chart.xlsx'file_path2 = 'G:\\PyCharm\\Python_project\\daily_market_data.xlsx'# 创建并显示应用app = EnhancedExcelVisualizer(file_path1, file_path2)
一、整体架构概述
该工具是一个基于 Python 的交互式 Excel 数据可视化应用,采用面向对象设计,通过EnhancedExcelVisualizer
类封装所有功能。整体架构可分为以下几个核心部分:
- 文件与数据处理模块:负责 Excel 文件加载、工作表解析和数据预处理
- UI 交互模块:通过 ipywidgets 创建交互式控件(下拉框、滑块等)
- 可视化模块:基于 matplotlib 和 seaborn 实现多种图表类型
- 事件处理模块:响应控件交互事件并更新图表
- 辅助功能模块:包括数据预览、统计信息展示和图表导出
二、核心功能模块分析
1. 初始化与文件加载模块
-
__init__
方法:- 接收两个 Excel 文件路径作为输入
- 尝试加载文件并获取工作表名称
- 初始化 UI 组件和状态变量(当前数据、图表类型等)
- 调用
update_sheet_selector
和display_ui
完成初始设置
-
关键技术点:
- 使用
pd.ExcelFile
处理 Excel 文件,支持多工作表读取 - 异常处理机制确保文件加载失败时的友好提示
- 状态变量(如
current_data
)用于跟踪当前操作的数据
- 使用
2. UI 组件创建模块
-
create_ui_components
方法:- 文件与工作表选择:通过
Dropdown
控件选择文件和工作表 - 数据列选择:
Dropdown
(X 轴)和SelectMultiple
(Y 轴)支持多列选择 - 图表配置:包括图表类型、配色方案、尺寸、标签旋转等控件
- 操作按钮:刷新和导出图表的功能按钮
- 输出区域:使用
Output
组件分隔数据预览、图表和日志
- 文件与工作表选择:通过
-
交互绑定:
- 通过
observe
和on_click
方法绑定控件事件处理函数 - 实现控件值变化时自动触发对应功能(如文件选择变化时更新工作表列表)
- 通过
3. 数据处理与更新模块
-
update_sheet_selector
方法:- 根据当前选择的文件(文件 1 或文件 2)更新工作表下拉框选项
-
update_column_selectors
方法:- 解析当前选择的工作表数据
- 更新 X 轴和 Y 轴列选择器的选项
- 自动选择默认列(如第一列作为 X 轴,第二列作为 Y 轴)
- 调用
update_data_preview
和update_stats
展示数据预览和统计信息
-
update_data_preview
与update_stats
方法:- 使用
DataFrame.head()
展示数据前 10 行 - 通过
DataFrame.describe()
生成数据统计信息 - 结合
style
属性实现表格样式美化(背景渐变、对齐方式等)
- 使用
4. 图表绘制模块
-
refresh_chart
方法:- 核心图表更新逻辑,根据当前选择的 X 轴、Y 轴和图表类型绘制图表
- 数据预处理:过滤空值、限制数据量(如柱状图前 20 条)
- 调用不同图表类型的私有方法(如
_draw_line_plot
) - 通用图表设置:网格、布局、颜色等
-
图表类型私有方法:
- 折线图(
_draw_line_plot
):支持日期数据自动格式化,多 Y 轴时显示图例 - 柱状图(
_draw_bar_plot
):处理多 Y 轴时的分组显示,限制数据量避免拥挤 - 饼图(
_draw_pie_plot
):仅支持单 Y 轴,显示百分比,限制类别数量 - 面积图(
_draw_area_plot
):类似折线图,但填充区域 - 散点图(
_draw_scatter_plot
):要求数值型 X 和 Y 轴 - 箱线图(
_draw_box_plot
):展示数据分布,支持分类 X 轴 - 热力图(
_draw_heatmap
):计算数值列相关性矩阵并可视化
- 折线图(
5. 辅助功能模块
-
事件处理方法:
on_file_change
、on_sheet_change
等方法响应控件值变化on_refresh_click
和on_export_click
处理按钮点击事件
-
图表导出(
export_chart
方法):- 使用
Figure.savefig
保存当前图表为 PNG 文件 - 文件名包含时间戳避免重复,显示保存路径
- 使用
三、关键技术亮点
-
交互式控件设计:
- 结合
Dropdown
、SelectMultiple
、Slider
等控件实现灵活的参数调整 - 实时响应控件变化并更新图表,提升用户体验
- 结合
-
多图表类型支持:
- 涵盖 7 种常见图表类型,满足不同数据可视化需求
- 针对每种图表类型进行数据适配(如日期格式处理、数值型检查)
-
数据预处理与异常处理:
- 自动过滤空值、限制数据量,避免图表显示异常
- 完整的异常捕获机制(文件加载、图表绘制等环节),并通过日志输出错误信息
-
样式与交互优化:
- 中文字体配置确保图表标签正常显示
- 图表样式统一设置(配色方案、网格线、透明度等)
- 数据预览和统计信息的表格美化,提升可读性
四、模块间协作流程
- 初始化阶段:
- 加载文件 → 创建 UI 组件 → 显示界面 → 更新工作表选择器
- 用户交互阶段:
- 用户选择文件 / 工作表 →
on_file_change
/on_sheet_change
触发 → 更新列选择器 → 显示数据预览和统计 - 用户选择图表类型 / 参数 →
on_chart_type_change
/on_style_change
触发 →refresh_chart
绘制图表
- 用户选择文件 / 工作表 →
- 图表更新阶段:
- 根据图表类型调用对应绘制方法 → 处理数据格式 → 生成图表 → 显示在输出区域
- 导出阶段:
- 点击导出按钮 →
export_chart
保存当前图表 → 日志提示导出结果
- 点击导出按钮 →
五、总结与扩展方向
该工具通过模块化设计实现了 Excel 数据的交互式可视化,适合数据分析人员快速探索数据。若需扩展,可考虑:
- 支持更多文件格式(如 CSV、SQL 数据库)
- 添加数据筛选和转换功能(如分组、聚合)
- 集成机器学习模型进行数据预测可视化
- 优化大数据量下的性能(如分页加载、抽样显示)
通过各模块的协同工作,该工具实现了从数据加载到可视化的完整流程,兼具功能性和易用性。
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from ipywidgets import (AppLayout, Dropdown, Button, Output, VBox, HBox, Label, Layout, SelectMultiple,IntSlider, FloatSlider, Checkbox)
from IPython.display import display, clear_output
import numpy as np
import os
from datetime import datetime# 设置中文字体和样式
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC", "sans-serif"]
plt.rcParams["axes.unicode_minus"] = False
sns.set_style("whitegrid")
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = Falseclass EnhancedExcelVisualizer:def __init__(self, file_path1, file_path2):"""初始化增强版Excel数据可视化应用"""self.file_path1 = file_path1self.file_path2 = file_path2self.current_figure = None # 添加当前图表引用# 尝试加载文件try:self.excel_file1 = pd.ExcelFile(file_path1)self.excel_file2 = pd.ExcelFile(file_path2)self.sheet_names1 = self.excel_file1.sheet_namesself.sheet_names2 = self.excel_file2.sheet_namesexcept Exception as e:print(f"文件加载错误: {str(e)}")return# 当前状态变量self.current_data = Noneself.current_sheet = Noneself.current_file = Noneself.current_chart_type = 'line'self.figsize = (10, 6)self.color_palette = 'viridis'# 创建UI组件self.create_ui_components()# 显示UIself.display_ui()# 初始化self.update_sheet_selector()def create_ui_components(self):"""创建所有UI组件"""# 文件选择器file1_name = os.path.basename(self.file_path1)file2_name = os.path.basename(self.file_path2)self.file_selector = Dropdown(options=[f'文件1 ({file1_name})', f'文件2 ({file2_name})'],value=f'文件1 ({file1_name})',description='选择文件:',layout=Layout(width='100%'))# 工作表选择器self.sheet_selector = Dropdown(options=[],description='选择工作表:',layout=Layout(width='100%'))# 数据列选择器 (支持多选Y轴)self.x_axis_selector = Dropdown(options=[],description='X轴数据列:',layout=Layout(width='100%'))self.y_axis_selector = SelectMultiple(options=[],description='Y轴数据列:',layout=Layout(width='100%'))# 图表类型选择器self.chart_type_selector = Dropdown(options=['折线图', '柱状图', '饼图', '面积图', '散点图', '箱线图', '热力图'],value='折线图',description='图表类型:',layout=Layout(width='100%'))# 图表样式控制self.color_palette_selector = Dropdown(options=['viridis', 'plasma', 'inferno', 'magma', 'cividis', 'Pastel1', 'Pastel2', 'Paired', 'Accent', 'Dark2'],value='viridis',description='配色方案:',layout=Layout(width='100%'))self.figsize_slider = IntSlider(min=5, max=20, value=10, step=1,description='图表大小:',layout=Layout(width='100%'))self.rotation_slider = IntSlider(min=0, max=90, value=45, step=5,description='标签旋转:',layout=Layout(width='100%'))self.transparency_slider = FloatSlider(min=0.1, max=1.0, value=0.8, step=0.1,description='透明度:',layout=Layout(width='100%'))self.grid_checkbox = Checkbox(value=True,description='显示网格',layout=Layout(width='100%'))# 操作按钮self.refresh_button = Button(description='刷新图表',button_style='primary',icon='refresh',layout=Layout(width='100%'))self.export_button = Button(description='导出图表',button_style='info',icon='save',layout=Layout(width='100%'))# 输出区域self.data_preview_output = Output(layout=Layout(height='auto', max_height='300px', overflow='auto'))self.chart_output = Output(layout=Layout(height='500px', border='1px solid #e0e0e0'))self.stats_output = Output(layout=Layout(height='auto', max_height='300px', overflow='auto'))self.log_output = Output(layout=Layout(height='auto', max_height='200px', overflow='auto'))# 绑定事件self.file_selector.observe(self.on_file_change, names='value')self.sheet_selector.observe(self.on_sheet_change, names='value')self.chart_type_selector.observe(self.on_chart_type_change, names='value')self.refresh_button.on_click(self.on_refresh_click)self.export_button.on_click(self.on_export_click)# 样式控制绑定 - 修改为使用refresh_chart而非on_style_changecontrols = [self.color_palette_selector, self.figsize_slider, self.rotation_slider, self.transparency_slider,self.grid_checkbox]for control in controls:control.observe(lambda change: self.refresh_chart(), names='value')def update_sheet_selector(self):"""更新工作表选择器选项"""selected_file = self.file_selector.value.split(' ')[0]if selected_file == '文件1':self.sheet_selector.options = self.sheet_names1else:self.sheet_selector.options = self.sheet_names2if self.sheet_selector.options:self.sheet_selector.value = self.sheet_selector.options[0]def update_column_selectors(self):"""更新数据列选择器选项"""selected_file = self.file_selector.value.split(' ')[0]selected_sheet = self.sheet_selector.valueif not selected_sheet:self.x_axis_selector.options = []self.y_axis_selector.options = []returntry:if selected_file == '文件1':self.current_data = self.excel_file1.parse(selected_sheet)else:self.current_data = self.excel_file2.parse(selected_sheet)except Exception as e:with self.log_output:print(f"读取工作表数据时出错: {str(e)}")returnself.current_sheet = selected_sheetself.current_file = selected_file# 更新列选择器column_names = list(self.current_data.columns)self.x_axis_selector.options = column_namesself.y_axis_selector.options = column_names# 自动选择默认列if len(column_names) >= 2:self.x_axis_selector.value = column_names[0]self.y_axis_selector.value = (column_names[1],) if len(column_names) > 1 else ()# 更新数据预览和统计信息self.update_data_preview()self.update_stats()# 首次加载时自动刷新图表self.refresh_chart()def update_data_preview(self):"""更新数据预览"""with self.data_preview_output:clear_output()if self.current_data is not None and not self.current_data.empty:print(f"数据预览 ({self.current_data.shape[0]} 行 × {self.current_data.shape[1]} 列):")display(self.current_data.head(10).style.background_gradient(cmap='Blues').set_properties(**{'text-align': 'left'}))else:print("没有数据可供预览")def update_stats(self):"""更新数据统计信息"""with self.stats_output:clear_output()if self.current_data is not None and not self.current_data.empty:print("数据统计信息:")# 对数值列设置2位小数,对其他列保持原样stats_df = self.current_data.describe(include='all')display(stats_df.style.background_gradient(cmap='Greens').format("{:.2f}", subset=stats_df.select_dtypes(include='number').columns).set_properties(**{'text-align': 'left'}))else:print("没有数据可供统计")def on_file_change(self, change):"""文件选择变化事件处理"""with self.log_output:print(f"已选择文件: {change.new}")self.update_sheet_selector()def on_sheet_change(self, change):"""工作表选择变化事件处理"""with self.log_output:print(f"已选择工作表: {change.new}")self.update_column_selectors()def on_chart_type_change(self, change):"""图表类型选择变化事件处理"""with self.log_output:print(f"已选择图表类型: {change.new}")self.current_chart_type = change.newself.refresh_chart()def on_refresh_click(self, b):"""刷新按钮点击事件处理"""with self.log_output:print("正在刷新图表...")self.refresh_chart()def on_export_click(self, b):"""导出按钮点击事件处理"""with self.log_output:print("正在尝试导出图表...")self.export_chart()def refresh_chart(self):"""刷新图表显示"""x_axis = self.x_axis_selector.valuey_axes = self.y_axis_selector.valueif not x_axis or not y_axes or self.current_data is None:with self.chart_output:clear_output()print('请选择有效的X轴和Y轴数据列')returnwith self.chart_output:clear_output()plt.close('all') # 清除之前的图形try:# 数据预处理filtered_data = self.current_data.dropna(subset=[x_axis] + list(y_axes)).copy()if filtered_data.empty:print('所选列中没有有效数据')return# 设置图表大小self.figsize = (self.figsize_slider.value, self.figsize_slider.value*0.6)self.current_figure = plt.figure(figsize=self.figsize)# 设置颜色sns.set_palette(self.color_palette_selector.value)# 图表类型处理chart_type = self.chart_type_selector.valueif chart_type == '折线图':self._draw_line_plot(filtered_data, x_axis, y_axes)elif chart_type == '柱状图':self._draw_bar_plot(filtered_data, x_axis, y_axes)elif chart_type == '饼图':self._draw_pie_plot(filtered_data, x_axis, y_axes)elif chart_type == '面积图':self._draw_area_plot(filtered_data, x_axis, y_axes)elif chart_type == '散点图':self._draw_scatter_plot(filtered_data, x_axis, y_axes)elif chart_type == '箱线图':self._draw_box_plot(filtered_data, x_axis, y_axes)elif chart_type == '热力图':self._draw_heatmap(filtered_data, x_axis, y_axes)# 通用设置plt.grid(self.grid_checkbox.value)plt.tight_layout()plt.show()except Exception as e:with self.log_output:print(f"绘制图表时出错: {str(e)}")import tracebacktraceback.print_exc()def _draw_line_plot(self, data, x_axis, y_axes):"""绘制折线图"""# 尝试转换日期数据date_flag = Falsetry:# 尝试常见的日期格式date_formats = ['%Y-%m-%d', '%m/%d/%Y', '%Y/%m/%d', '%d-%m-%Y', '%d/%m/%Y']for fmt in date_formats:try:data[x_axis] = pd.to_datetime(data[x_axis], format=fmt)breakexcept ValueError:continueelse:# 如果所有格式都不匹配,使用默认方式data[x_axis] = pd.to_datetime(data[x_axis])data = data.sort_values(by=x_axis)date_flag = Trueexcept Exception as e:with self.log_output:print(f"日期转换失败: {str(e)}")for y_axis in y_axes:if pd.api.types.is_numeric_dtype(data[y_axis]):sns.lineplot(x=x_axis, y=y_axis, data=data, alpha=self.transparency_slider.value,label=y_axis)plt.title(f'折线图: {", ".join(y_axes)} vs {x_axis}')plt.xlabel(x_axis)plt.ylabel('值')if date_flag:plt.gcf().autofmt_xdate()if len(y_axes) > 1:plt.legend()def _draw_bar_plot(self, data, x_axis, y_axes):"""绘制柱状图"""# 限制显示数量if len(data) > 20:data = data.iloc[:20]with self.log_output:print('注意: 只显示前20条数据以避免图表拥挤')bar_width = 0.8 / len(y_axes)x = np.arange(len(data[x_axis]))for i, y_axis in enumerate(y_axes):if pd.api.types.is_numeric_dtype(data[y_axis]):plt.bar(x + i*bar_width, data[y_axis], width=bar_width,alpha=self.transparency_slider.value,label=y_axis)plt.title(f'柱状图: {", ".join(y_axes)} vs {x_axis}')plt.xlabel(x_axis)plt.ylabel('值')plt.xticks(x + bar_width*(len(y_axes)-1)/2, data[x_axis].astype(str),rotation=self.rotation_slider.value)if len(y_axes) > 1:plt.legend()def _draw_pie_plot(self, data, x_axis, y_axes):"""绘制饼图"""if len(y_axes) > 1:with self.log_output:print('饼图只支持单个Y轴,将使用第一个Y轴')y_axis = y_axes[0]if not pd.api.types.is_numeric_dtype(data[y_axis]):with self.log_output:print('饼图需要数值型Y轴数据')return# 按Y轴值排序并限制类别数量sorted_data = data.sort_values(by=y_axis, ascending=False)if len(sorted_data) > 10:top_data = sorted_data.iloc[:9]other_sum = sorted_data.iloc[9:][y_axis].sum()other_row = pd.DataFrame({x_axis: ['其他'], y_axis: [other_sum]})plot_data = pd.concat([top_data, other_row])else:plot_data = sorted_dataplt.pie(plot_data[y_axis], labels=plot_data[x_axis].astype(str),autopct='%1.1f%%',startangle=90,colors=sns.color_palette(self.color_palette_selector.value),wedgeprops={'alpha': self.transparency_slider.value})plt.title(f'饼图: {y_axis} 分布')def _draw_area_plot(self, data, x_axis, y_axes):"""绘制面积图"""# 尝试转换日期数据try:data[x_axis] = pd.to_datetime(data[x_axis])data = data.sort_values(by=x_axis)date_flag = Trueexcept:date_flag = Falsefor y_axis in y_axes:if pd.api.types.is_numeric_dtype(data[y_axis]):plt.fill_between(data[x_axis], data[y_axis], alpha=self.transparency_slider.value,label=y_axis)plt.title(f'面积图: {", ".join(y_axes)} vs {x_axis}')plt.xlabel(x_axis)plt.ylabel('值')if date_flag:plt.gcf().autofmt_xdate()if len(y_axes) > 1:plt.legend()def _draw_scatter_plot(self, data, x_axis, y_axes):"""绘制散点图"""if len(y_axes) != 1:with self.log_output:print('散点图需要且仅需要一个Y轴')returny_axis = y_axes[0]if pd.api.types.is_numeric_dtype(data[x_axis]) and pd.api.types.is_numeric_dtype(data[y_axis]):sns.scatterplot(x=x_axis, y=y_axis, data=data,alpha=self.transparency_slider.value)plt.title(f'散点图: {y_axis} vs {x_axis}')plt.xlabel(x_axis)plt.ylabel(y_axis)else:with self.log_output:print('散点图需要数值型X轴和Y轴数据')def _draw_box_plot(self, data, x_axis, y_axes):"""绘制箱线图"""if len(y_axes) != 1:with self.log_output:print('箱线图需要且仅需要一个Y轴')returny_axis = y_axes[0]if pd.api.types.is_numeric_dtype(data[y_axis]):# 如果X轴是数值型,将其转换为类别以便更好地展示if pd.api.types.is_numeric_dtype(data[x_axis]):data['x_category'] = pd.qcut(data[x_axis], 5, duplicates='drop')sns.boxplot(x='x_category', y=y_axis, data=data)plt.xlabel(f'{x_axis} (分箱)')else:sns.boxplot(x=x_axis, y=y_axis, data=data)plt.xlabel(x_axis)plt.title(f'箱线图: {y_axis} 按 {x_axis} 分布')plt.ylabel(y_axis)plt.xticks(rotation=self.rotation_slider.value)else:with self.log_output:print('箱线图需要数值型Y轴数据')def _draw_heatmap(self, data, x_axis, y_axes):"""绘制热力图"""# 选择数值列numeric_cols = [col for col in data.select_dtypes(include='number').columns if col in y_axes or col == x_axis]if len(numeric_cols) < 2:with self.log_output:print('热力图需要至少两个数值列')return# 创建相关矩阵corr_matrix = data[numeric_cols].corr()sns.heatmap(corr_matrix, annot=True, cmap=self.color_palette_selector.value,fmt=".2f",linewidths=.5)plt.title('热力图: 变量相关性')def export_chart(self):"""导出图表"""if not hasattr(self, 'current_figure') or self.current_figure is None:with self.log_output:print('没有可导出的图表,请先生成图表')returntimestamp = datetime.now().strftime("%Y%m%d_%H%M%S")filename = f"chart_export_{timestamp}.png"try:self.current_figure.savefig(filename, dpi=300, bbox_inches='tight')with self.log_output:print(f'图表已成功导出为: {filename}')print(f'文件保存在: {os.path.abspath(filename)}')except Exception as e:with self.log_output:print(f'导出图表时出错: {str(e)}')import tracebacktraceback.print_exc()def display_ui(self):"""显示应用界面"""# 控制面板control_panel = VBox([self.file_selector,self.sheet_selector,self.x_axis_selector,self.y_axis_selector,self.chart_type_selector,self.color_palette_selector,self.figsize_slider,self.rotation_slider,self.transparency_slider,self.grid_checkbox,HBox([self.refresh_button, self.export_button])], layout=Layout(width='30%', padding='10px'))# 输出面板output_panel = VBox([Label('📊 数据预览'),self.data_preview_output,Label('📈 数据统计'),self.stats_output,Label('✨ 数据可视化'),self.chart_output,Label('📝 日志信息'),self.log_output], layout=Layout(width='70%', padding='10px'))# 整体布局app_layout = HBox([control_panel, output_panel])# 显示应用display(app_layout)# 使用示例
if __name__ == "__main__":# 修改为您的Excel文件路径file_path1 = 'G:\\PyCharm\\Python_project\\daily_chart.xlsx'file_path2 = 'G:\\PyCharm\\Python_project\\daily_market_data.xlsx'# 创建并显示应用app = EnhancedExcelVisualizer(file_path1, file_path2)
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from ipywidgets import (AppLayout, Dropdown, Button, Output, VBox, HBox, Label, Layout, SelectMultiple,IntSlider, FloatSlider, Checkbox)
from IPython.display import display, clear_output
import numpy as np
import os
from datetime import datetime# 设置中文字体和样式
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC", "sans-serif"]
plt.rcParams["axes.unicode_minus"] = False
sns.set_style("whitegrid")
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = Falseclass EnhancedExcelVisualizer:def __init__(self, file_path1, file_path2):"""初始化增强版Excel数据可视化应用"""self.file_path1 = file_path1self.file_path2 = file_path2self.current_figure = None # 添加当前图表引用# 尝试加载文件try:self.excel_file1 = pd.ExcelFile(file_path1)self.excel_file2 = pd.ExcelFile(file_path2)self.sheet_names1 = self.excel_file1.sheet_namesself.sheet_names2 = self.excel_file2.sheet_namesexcept Exception as e:print(f"文件加载错误: {str(e)}")return# 当前状态变量self.current_data = Noneself.current_sheet = Noneself.current_file = Noneself.current_chart_type = 'line'self.figsize = (10, 6)self.color_palette = 'viridis'# 创建UI组件self.create_ui_components()# 显示UIself.display_ui()# 初始化self.update_sheet_selector()def create_ui_components(self):"""创建所有UI组件"""# 文件选择器file1_name = os.path.basename(self.file_path1)file2_name = os.path.basename(self.file_path2)self.file_selector = Dropdown(options=[f'文件1 ({file1_name})', f'文件2 ({file2_name})'],value=f'文件1 ({file1_name})',description='选择文件:',layout=Layout(width='100%'))# 工作表选择器self.sheet_selector = Dropdown(options=[],description='选择工作表:',layout=Layout(width='100%'))# 数据列选择器 (支持多选Y轴)self.x_axis_selector = Dropdown(options=[],description='X轴数据列:',layout=Layout(width='100%'))self.y_axis_selector = SelectMultiple(options=[],description='Y轴数据列:',layout=Layout(width='100%'))# 图表类型选择器self.chart_type_selector = Dropdown(options=['折线图', '柱状图', '饼图', '面积图', '散点图', '箱线图', '热力图'],value='折线图',description='图表类型:',layout=Layout(width='100%'))# 图表样式控制self.color_palette_selector = Dropdown(options=['viridis', 'plasma', 'inferno', 'magma', 'cividis', 'Pastel1', 'Pastel2', 'Paired', 'Accent', 'Dark2'],value='viridis',description='配色方案:',layout=Layout(width='100%'))self.figsize_slider = IntSlider(min=5, max=20, value=10, step=1,description='图表大小:',layout=Layout(width='100%'))self.rotation_slider = IntSlider(min=0, max=90, value=45, step=5,description='标签旋转:',layout=Layout(width='100%'))self.transparency_slider = FloatSlider(min=0.1, max=1.0, value=0.8, step=0.1,description='透明度:',layout=Layout(width='100%'))self.grid_checkbox = Checkbox(value=True,description='显示网格',layout=Layout(width='100%'))# 操作按钮self.refresh_button = Button(description='刷新图表',button_style='primary',icon='refresh',layout=Layout(width='100%'))self.export_button = Button(description='导出图表',button_style='info',icon='save',layout=Layout(width='100%'))# 输出区域self.data_preview_output = Output(layout=Layout(height='auto', max_height='300px', overflow='auto'))self.chart_output = Output(layout=Layout(height='500px', border='1px solid #e0e0e0'))self.stats_output = Output(layout=Layout(height='auto', max_height='300px', overflow='auto'))self.log_output = Output(layout=Layout(height='auto', max_height='200px', overflow='auto'))# 绑定事件self.file_selector.observe(self.on_file_change, names='value')self.sheet_selector.observe(self.on_sheet_change, names='value')self.chart_type_selector.observe(self.on_chart_type_change, names='value')self.refresh_button.on_click(self.on_refresh_click)self.export_button.on_click(self.on_export_click)# 样式控制绑定 - 修改为使用refresh_chart而非on_style_changecontrols = [self.color_palette_selector, self.figsize_slider, self.rotation_slider, self.transparency_slider,self.grid_checkbox]for control in controls:control.observe(lambda change: self.refresh_chart(), names='value')def update_sheet_selector(self):"""更新工作表选择器选项"""selected_file = self.file_selector.value.split(' ')[0]if selected_file == '文件1':self.sheet_selector.options = self.sheet_names1else:self.sheet_selector.options = self.sheet_names2if self.sheet_selector.options:self.sheet_selector.value = self.sheet_selector.options[0]def update_column_selectors(self):"""更新数据列选择器选项"""selected_file = self.file_selector.value.split(' ')[0]selected_sheet = self.sheet_selector.valueif not selected_sheet:self.x_axis_selector.options = []self.y_axis_selector.options = []returntry:if selected_file == '文件1':self.current_data = self.excel_file1.parse(selected_sheet)else:self.current_data = self.excel_file2.parse(selected_sheet)except Exception as e:with self.log_output:print(f"读取工作表数据时出错: {str(e)}")returnself.current_sheet = selected_sheetself.current_file = selected_file# 更新列选择器column_names = list(self.current_data.columns)self.x_axis_selector.options = column_namesself.y_axis_selector.options = column_names# 自动选择默认列if len(column_names) >= 2:self.x_axis_selector.value = column_names[0]self.y_axis_selector.value = (column_names[1],) if len(column_names) > 1 else ()# 更新数据预览和统计信息self.update_data_preview()self.update_stats()# 首次加载时自动刷新图表self.refresh_chart()def update_data_preview(self):"""更新数据预览"""with self.data_preview_output:clear_output()if self.current_data is not None and not self.current_data.empty:print(f"数据预览 ({self.current_data.shape[0]} 行 × {self.current_data.shape[1]} 列):")display(self.current_data.head(10).style.background_gradient(cmap='Blues').set_properties(**{'text-align': 'left'}))else:print("没有数据可供预览")def update_stats(self):"""更新数据统计信息"""with self.stats_output:clear_output()if self.current_data is not None and not self.current_data.empty:print("数据统计信息:")# 对数值列设置2位小数,对其他列保持原样stats_df = self.current_data.describe(include='all')display(stats_df.style.background_gradient(cmap='Greens').format("{:.2f}", subset=stats_df.select_dtypes(include='number').columns).set_properties(**{'text-align': 'left'}))else:print("没有数据可供统计")def on_file_change(self, change):"""文件选择变化事件处理"""with self.log_output:print(f"已选择文件: {change.new}")self.update_sheet_selector()def on_sheet_change(self, change):"""工作表选择变化事件处理"""with self.log_output:print(f"已选择工作表: {change.new}")self.update_column_selectors()def on_chart_type_change(self, change):"""图表类型选择变化事件处理"""with self.log_output:print(f"已选择图表类型: {change.new}")self.current_chart_type = change.newself.refresh_chart()def on_refresh_click(self, b):"""刷新按钮点击事件处理"""with self.log_output:print("正在刷新图表...")self.refresh_chart()def on_export_click(self, b):"""导出按钮点击事件处理"""with self.log_output:print("正在尝试导出图表...")self.export_chart()def refresh_chart(self):"""刷新图表显示"""x_axis = self.x_axis_selector.valuey_axes = self.y_axis_selector.valueif not x_axis or not y_axes or self.current_data is None:with self.chart_output:clear_output()print('请选择有效的X轴和Y轴数据列')returnwith self.chart_output:clear_output()plt.close('all') # 清除之前的图形try:# 数据预处理filtered_data = self.current_data.dropna(subset=[x_axis] + list(y_axes)).copy()# 添加这行来检查过滤后的数据形状print(f"过滤后数据形状: {filtered_data.shape}")if filtered_data.empty:print('所选列中没有有效数据')return# 设置图表大小self.figsize = (self.figsize_slider.value, self.figsize_slider.value*0.6)self.current_figure = plt.figure(figsize=self.figsize)# 设置颜色sns.set_palette(self.color_palette_selector.value)# 图表类型处理chart_type = self.chart_type_selector.valueif chart_type == '折线图':self._draw_line_plot(filtered_data, x_axis, y_axes)elif chart_type == '柱状图':self._draw_bar_plot(filtered_data, x_axis, y_axes)elif chart_type == '饼图':self._draw_pie_plot(filtered_data, x_axis, y_axes)elif chart_type == '面积图':self._draw_area_plot(filtered_data, x_axis, y_axes)elif chart_type == '散点图':self._draw_scatter_plot(filtered_data, x_axis, y_axes)elif chart_type == '箱线图':self._draw_box_plot(filtered_data, x_axis, y_axes)elif chart_type == '热力图':self._draw_heatmap(filtered_data, x_axis, y_axes)# 通用设置plt.grid(self.grid_checkbox.value)plt.tight_layout()plt.show()except Exception as e:with self.log_output:print(f"绘制图表时出错: {str(e)}")import tracebacktraceback.print_exc()def _draw_line_plot(self, data, x_axis, y_axes):"""绘制折线图"""# 尝试转换日期数据date_flag = Falsetry:# 尝试常见的日期格式date_formats = ['%Y-%m-%d', '%m/%d/%Y', '%Y/%m/%d', '%d-%m-%Y', '%d/%m/%Y']for fmt in date_formats:try:data[x_axis] = pd.to_datetime(data[x_axis], format=fmt)breakexcept ValueError:continueelse:# 如果所有格式都不匹配,使用默认方式data[x_axis] = pd.to_datetime(data[x_axis])data = data.sort_values(by=x_axis)date_flag = Trueexcept Exception as e:with self.log_output:print(f"日期转换失败: {str(e)}")for y_axis in y_axes:if pd.api.types.is_numeric_dtype(data[y_axis]):sns.lineplot(x=x_axis, y=y_axis, data=data, alpha=self.transparency_slider.value,label=y_axis)plt.title(f'折线图: {", ".join(y_axes)} vs {x_axis}')plt.xlabel(x_axis)plt.ylabel('值')if date_flag:plt.gcf().autofmt_xdate()if len(y_axes) > 1:plt.legend()def _draw_bar_plot(self, data, x_axis, y_axes):"""绘制柱状图"""# 限制显示数量if len(data) > 20:data = data.iloc[:20]with self.log_output:print('注意: 只显示前20条数据以避免图表拥挤')bar_width = 0.8 / len(y_axes)x = np.arange(len(data[x_axis]))for i, y_axis in enumerate(y_axes):if pd.api.types.is_numeric_dtype(data[y_axis]):plt.bar(x + i*bar_width, data[y_axis], width=bar_width,alpha=self.transparency_slider.value,label=y_axis)plt.title(f'柱状图: {", ".join(y_axes)} vs {x_axis}')plt.xlabel(x_axis)plt.ylabel('值')plt.xticks(x + bar_width*(len(y_axes)-1)/2, data[x_axis].astype(str),rotation=self.rotation_slider.value)if len(y_axes) > 1:plt.legend()def _draw_pie_plot(self, data, x_axis, y_axes):"""绘制饼图"""if len(y_axes) > 1:with self.log_output:print('饼图只支持单个Y轴,将使用第一个Y轴')y_axis = y_axes[0]if not pd.api.types.is_numeric_dtype(data[y_axis]):with self.log_output:print('饼图需要数值型Y轴数据')return# 按Y轴值排序并限制类别数量sorted_data = data.sort_values(by=y_axis, ascending=False)if len(sorted_data) > 10:top_data = sorted_data.iloc[:9]other_sum = sorted_data.iloc[9:][y_axis].sum()other_row = pd.DataFrame({x_axis: ['其他'], y_axis: [other_sum]})plot_data = pd.concat([top_data, other_row])else:plot_data = sorted_dataplt.pie(plot_data[y_axis], labels=plot_data[x_axis].astype(str),autopct='%1.1f%%',startangle=90,colors=sns.color_palette(self.color_palette_selector.value),wedgeprops={'alpha': self.transparency_slider.value})plt.title(f'饼图: {y_axis} 分布')def _draw_area_plot(self, data, x_axis, y_axes):"""绘制面积图"""# 尝试转换日期数据try:data[x_axis] = pd.to_datetime(data[x_axis])data = data.sort_values(by=x_axis)date_flag = Trueexcept:date_flag = Falsefor y_axis in y_axes:if pd.api.types.is_numeric_dtype(data[y_axis]):plt.fill_between(data[x_axis], data[y_axis], alpha=self.transparency_slider.value,label=y_axis)plt.title(f'面积图: {", ".join(y_axes)} vs {x_axis}')plt.xlabel(x_axis)plt.ylabel('值')if date_flag:plt.gcf().autofmt_xdate()if len(y_axes) > 1:plt.legend()def _draw_scatter_plot(self, data, x_axis, y_axes):"""绘制散点图"""if len(y_axes) != 1:with self.log_output:print('散点图需要且仅需要一个Y轴')returny_axis = y_axes[0]if pd.api.types.is_numeric_dtype(data[x_axis]) and pd.api.types.is_numeric_dtype(data[y_axis]):sns.scatterplot(x=x_axis, y=y_axis, data=data,alpha=self.transparency_slider.value)plt.title(f'散点图: {y_axis} vs {x_axis}')plt.xlabel(x_axis)plt.ylabel(y_axis)else:with self.log_output:print('散点图需要数值型X轴和Y轴数据')def _draw_box_plot(self, data, x_axis, y_axes):"""绘制箱线图"""if len(y_axes) != 1:with self.log_output:print('箱线图需要且仅需要一个Y轴')returny_axis = y_axes[0]if pd.api.types.is_numeric_dtype(data[y_axis]):# 如果X轴是数值型,将其转换为类别以便更好地展示if pd.api.types.is_numeric_dtype(data[x_axis]):data['x_category'] = pd.qcut(data[x_axis], 5, duplicates='drop')sns.boxplot(x='x_category', y=y_axis, data=data)plt.xlabel(f'{x_axis} (分箱)')else:sns.boxplot(x=x_axis, y=y_axis, data=data)plt.xlabel(x_axis)plt.title(f'箱线图: {y_axis} 按 {x_axis} 分布')plt.ylabel(y_axis)plt.xticks(rotation=self.rotation_slider.value)else:with self.log_output:print('箱线图需要数值型Y轴数据')def _draw_heatmap(self, data, x_axis, y_axes):"""绘制热力图"""# 选择数值列numeric_cols = [col for col in data.select_dtypes(include='number').columns if col in y_axes or col == x_axis]if len(numeric_cols) < 2:with self.log_output:print('热力图需要至少两个数值列')return# 创建相关矩阵corr_matrix = data[numeric_cols].corr()sns.heatmap(corr_matrix, annot=True, cmap=self.color_palette_selector.value,fmt=".2f",linewidths=.5)plt.title('热力图: 变量相关性')def export_chart(self):"""导出图表"""if not hasattr(self, 'current_figure') or self.current_figure is None:with self.log_output:print('没有可导出的图表,请先生成图表')returntimestamp = datetime.now().strftime("%Y%m%d_%H%M%S")filename = f"chart_export_{timestamp}.png"try:self.current_figure.savefig(filename, dpi=300, bbox_inches='tight')with self.log_output:print(f'图表已成功导出为: {filename}')print(f'文件保存在: {os.path.abspath(filename)}')except Exception as e:with self.log_output:print(f'导出图表时出错: {str(e)}')import tracebacktraceback.print_exc()def display_ui(self):"""显示应用界面"""# 控制面板control_panel = VBox([self.file_selector,self.sheet_selector,self.x_axis_selector,self.y_axis_selector,self.chart_type_selector,self.color_palette_selector,self.figsize_slider,self.rotation_slider,self.transparency_slider,self.grid_checkbox,HBox([self.refresh_button, self.export_button])], layout=Layout(width='30%', padding='10px'))# 输出面板output_panel = VBox([Label('📊 数据预览'),self.data_preview_output,Label('📈 数据统计'),self.stats_output,Label('✨ 数据可视化'),self.chart_output,Label('📝 日志信息'),self.log_output], layout=Layout(width='70%', padding='10px'))# 整体布局app_layout = HBox([control_panel, output_panel])# 显示应用display(app_layout)# 使用示例
if __name__ == "__main__":# 修改为您的Excel文件路径file_path1 = 'G:\\PyCharm\\Python_project\\daily_chart.xlsx'file_path2 = 'G:\\PyCharm\\Python_project\\daily_market_data.xlsx'# 创建并显示应用app = EnhancedExcelVisualizer(file_path1, file_path2)
# 必须在 Jupyter Notebook 的第一行添加以下魔术指令
%matplotlib widgetimport pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from ipywidgets import (AppLayout, Dropdown, Button, Output, VBox, HBox, Label, Layout, SelectMultiple,IntSlider, FloatSlider, Checkbox)
from IPython.display import display, clear_output
import numpy as np
import os
from datetime import datetime# 设置中文字体和样式
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC", "sans-serif"]
plt.rcParams["axes.unicode_minus"] = False
sns.set_style("whitegrid")
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = Falseclass EnhancedExcelVisualizer:def __init__(self, file_path1, file_path2):"""初始化增强版Excel数据可视化应用"""self.file_path1 = file_path1self.file_path2 = file_path2self.current_figure = None # 添加当前图表引用# 尝试加载文件try:self.excel_file1 = pd.ExcelFile(file_path1)self.excel_file2 = pd.ExcelFile(file_path2)self.sheet_names1 = self.excel_file1.sheet_namesself.sheet_names2 = self.excel_file2.sheet_namesexcept Exception as e:print(f"文件加载错误: {str(e)}")return# 当前状态变量self.current_data = Noneself.current_sheet = Noneself.current_file = Noneself.current_chart_type = 'line'self.figsize = (10, 6)self.color_palette = 'viridis'# 创建UI组件self.create_ui_components()# 显示UIself.display_ui()# 初始化self.update_sheet_selector()def create_ui_components(self):"""创建所有UI组件"""# 文件选择器file1_name = os.path.basename(self.file_path1)file2_name = os.path.basename(self.file_path2)self.file_selector = Dropdown(options=[f'文件1 ({file1_name})', f'文件2 ({file2_name})'],value=f'文件1 ({file1_name})',description='选择文件:',layout=Layout(width='100%'))# 工作表选择器self.sheet_selector = Dropdown(options=[],description='选择工作表:',layout=Layout(width='100%'))# 数据列选择器 (支持多选Y轴)self.x_axis_selector = Dropdown(options=[],description='X轴数据列:',layout=Layout(width='100%'))self.y_axis_selector = SelectMultiple(options=[],description='Y轴数据列:',layout=Layout(width='100%'))# 图表类型选择器self.chart_type_selector = Dropdown(options=['折线图', '柱状图', '饼图', '面积图', '散点图', '箱线图', '热力图'],value='折线图',description='图表类型:',layout=Layout(width='100%'))# 图表样式控制self.color_palette_selector = Dropdown(options=['viridis', 'plasma', 'inferno', 'magma', 'cividis', 'Pastel1', 'Pastel2', 'Paired', 'Accent', 'Dark2'],value='viridis',description='配色方案:',layout=Layout(width='100%'))self.figsize_slider = IntSlider(min=5, max=20, value=10, step=1,description='图表大小:',layout=Layout(width='100%'))self.rotation_slider = IntSlider(min=0, max=90, value=45, step=5,description='标签旋转:',layout=Layout(width='100%'))self.transparency_slider = FloatSlider(min=0.1, max=1.0, value=0.8, step=0.1,description='透明度:',layout=Layout(width='100%'))self.grid_checkbox = Checkbox(value=True,description='显示网格',layout=Layout(width='100%'))# 操作按钮self.refresh_button = Button(description='刷新图表',button_style='primary',icon='refresh',layout=Layout(width='100%'))self.export_button = Button(description='导出图表',button_style='info',icon='save',layout=Layout(width='100%'))# 输出区域self.data_preview_output = Output(layout=Layout(height='auto', max_height='300px', overflow='auto'))self.chart_output = Output(layout=Layout(height='500px', border='1px solid #e0e0e0'))self.stats_output = Output(layout=Layout(height='auto', max_height='300px', overflow='auto'))self.log_output = Output(layout=Layout(height='auto', max_height='200px', overflow='auto'))# 绑定事件self.file_selector.observe(self.on_file_change, names='value')self.sheet_selector.observe(self.on_sheet_change, names='value')self.chart_type_selector.observe(self.on_chart_type_change, names='value')self.refresh_button.on_click(self.on_refresh_click)self.export_button.on_click(self.on_export_click)# 样式控制绑定 - 修改为使用refresh_chart而非on_style_changecontrols = [self.color_palette_selector, self.figsize_slider, self.rotation_slider, self.transparency_slider,self.grid_checkbox]for control in controls:control.observe(lambda change: self.refresh_chart(), names='value')def update_sheet_selector(self):"""更新工作表选择器选项"""selected_file = self.file_selector.value.split(' ')[0]if selected_file == '文件1':self.sheet_selector.options = self.sheet_names1else:self.sheet_selector.options = self.sheet_names2if self.sheet_selector.options:self.sheet_selector.value = self.sheet_selector.options[0]def update_column_selectors(self):"""更新数据列选择器选项"""selected_file = self.file_selector.value.split(' ')[0]selected_sheet = self.sheet_selector.valueif not selected_sheet:self.x_axis_selector.options = []self.y_axis_selector.options = []returntry:if selected_file == '文件1':self.current_data = self.excel_file1.parse(selected_sheet)else:self.current_data = self.excel_file2.parse(selected_sheet)except Exception as e:with self.log_output:print(f"读取工作表数据时出错: {str(e)}")returnself.current_sheet = selected_sheetself.current_file = selected_file# 更新列选择器column_names = list(self.current_data.columns)self.x_axis_selector.options = column_namesself.y_axis_selector.options = column_names# 自动选择默认列if len(column_names) >= 2:self.x_axis_selector.value = column_names[0]self.y_axis_selector.value = (column_names[1],) if len(column_names) > 1 else ()# 更新数据预览和统计信息self.update_data_preview()self.update_stats()# 首次加载时自动刷新图表self.refresh_chart()def update_data_preview(self):"""更新数据预览"""with self.data_preview_output:clear_output()if self.current_data is not None and not self.current_data.empty:print(f"数据预览 ({self.current_data.shape[0]} 行 × {self.current_data.shape[1]} 列):")display(self.current_data.head(10).style.background_gradient(cmap='Blues').set_properties(**{'text-align': 'left'}))else:print("没有数据可供预览")def update_stats(self):"""更新数据统计信息"""with self.stats_output:clear_output()if self.current_data is not None and not self.current_data.empty:print("数据统计信息:")# 对数值列设置2位小数,对其他列保持原样stats_df = self.current_data.describe(include='all')display(stats_df.style.background_gradient(cmap='Greens').format("{:.2f}", subset=stats_df.select_dtypes(include='number').columns).set_properties(**{'text-align': 'left'}))else:print("没有数据可供统计")def on_file_change(self, change):"""文件选择变化事件处理"""with self.log_output:print(f"已选择文件: {change.new}")self.update_sheet_selector()def on_sheet_change(self, change):"""工作表选择变化事件处理"""with self.log_output:print(f"已选择工作表: {change.new}")self.update_column_selectors()def on_chart_type_change(self, change):"""图表类型选择变化事件处理"""with self.log_output:print(f"已选择图表类型: {change.new}")self.current_chart_type = change.newself.refresh_chart()def on_refresh_click(self, b):"""刷新按钮点击事件处理"""with self.log_output:print("正在刷新图表...")self.refresh_chart()def on_export_click(self, b):"""导出按钮点击事件处理"""with self.log_output:print("正在尝试导出图表...")self.export_chart()def refresh_chart(self):"""刷新图表显示"""x_axis = self.x_axis_selector.valuey_axes = self.y_axis_selector.valueif not x_axis or not y_axes or self.current_data is None:with self.chart_output:clear_output()print('请选择有效的X轴和Y轴数据列')returnwith self.chart_output:clear_output()plt.close('all') # 清除之前的图形try:# 数据预处理filtered_data = self.current_data.dropna(subset=[x_axis] + list(y_axes)).copy()# 添加这行来检查过滤后的数据形状print(f"过滤后数据形状: {filtered_data.shape}")if filtered_data.empty:print('所选列中没有有效数据')return# 设置图表大小self.figsize = (self.figsize_slider.value, self.figsize_slider.value*0.6)self.current_figure = plt.figure(figsize=self.figsize)# 设置颜色sns.set_palette(self.color_palette_selector.value)# 图表类型处理chart_type = self.chart_type_selector.valueif chart_type == '折线图':self._draw_line_plot(filtered_data, x_axis, y_axes)elif chart_type == '柱状图':self._draw_bar_plot(filtered_data, x_axis, y_axes)elif chart_type == '饼图':self._draw_pie_plot(filtered_data, x_axis, y_axes)elif chart_type == '面积图':self._draw_area_plot(filtered_data, x_axis, y_axes)elif chart_type == '散点图':self._draw_scatter_plot(filtered_data, x_axis, y_axes)elif chart_type == '箱线图':self._draw_box_plot(filtered_data, x_axis, y_axes)elif chart_type == '热力图':self._draw_heatmap(filtered_data, x_axis, y_axes)# 通用设置plt.grid(self.grid_checkbox.value)plt.tight_layout()plt.show()except Exception as e:with self.log_output:print(f"绘制图表时出错: {str(e)}")import tracebacktraceback.print_exc()def _draw_line_plot(self, data, x_axis, y_axes):"""绘制折线图"""# 尝试转换日期数据date_flag = Falsetry:# 尝试常见的日期格式date_formats = ['%Y-%m-%d', '%m/%d/%Y', '%Y/%m/%d', '%d-%m-%Y', '%d/%m/%Y']for fmt in date_formats:try:data[x_axis] = pd.to_datetime(data[x_axis], format=fmt)breakexcept ValueError:continueelse:# 如果所有格式都不匹配,使用默认方式data[x_axis] = pd.to_datetime(data[x_axis])data = data.sort_values(by=x_axis)date_flag = Trueexcept Exception as e:with self.log_output:print(f"日期转换失败: {str(e)}")for y_axis in y_axes:if pd.api.types.is_numeric_dtype(data[y_axis]):sns.lineplot(x=x_axis, y=y_axis, data=data, alpha=self.transparency_slider.value,label=y_axis)plt.title(f'折线图: {", ".join(y_axes)} vs {x_axis}')plt.xlabel(x_axis)plt.ylabel('值')if date_flag:plt.gcf().autofmt_xdate()if len(y_axes) > 1:plt.legend()def _draw_bar_plot(self, data, x_axis, y_axes):# 打印Y轴数据类型print(f"Y轴数据类型: {data[y_axes].dtypes}")# 限制显示数量if len(data) > 20:data = data.iloc[:20]with self.log_output:print('注意: 只显示前20条数据以避免图表拥挤')bar_width = 0.8 / len(y_axes)x = np.arange(len(data[x_axis]))for i, y_axis in enumerate(y_axes):if pd.api.types.is_numeric_dtype(data[y_axis]):plt.bar(x + i*bar_width, data[y_axis], width=bar_width,alpha=self.transparency_slider.value,label=y_axis)plt.title(f'柱状图: {", ".join(y_axes)} vs {x_axis}')plt.xlabel(x_axis)plt.ylabel('值')plt.xticks(x + bar_width*(len(y_axes)-1)/2, data[x_axis].astype(str),rotation=self.rotation_slider.value)if len(y_axes) > 1:plt.legend()def _draw_pie_plot(self, data, x_axis, y_axes):"""绘制饼图"""if len(y_axes) > 1:with self.log_output:print('饼图只支持单个Y轴,将使用第一个Y轴')y_axis = y_axes[0]if not pd.api.types.is_numeric_dtype(data[y_axis]):with self.log_output:print('饼图需要数值型Y轴数据')return# 按Y轴值排序并限制类别数量sorted_data = data.sort_values(by=y_axis, ascending=False)if len(sorted_data) > 10:top_data = sorted_data.iloc[:9]other_sum = sorted_data.iloc[9:][y_axis].sum()other_row = pd.DataFrame({x_axis: ['其他'], y_axis: [other_sum]})plot_data = pd.concat([top_data, other_row])else:plot_data = sorted_dataplt.pie(plot_data[y_axis], labels=plot_data[x_axis].astype(str),autopct='%1.1f%%',startangle=90,colors=sns.color_palette(self.color_palette_selector.value),wedgeprops={'alpha': self.transparency_slider.value})plt.title(f'饼图: {y_axis} 分布')def _draw_area_plot(self, data, x_axis, y_axes):"""绘制面积图"""# 尝试转换日期数据try:data[x_axis] = pd.to_datetime(data[x_axis])data = data.sort_values(by=x_axis)date_flag = Trueexcept:date_flag = Falsefor y_axis in y_axes:if pd.api.types.is_numeric_dtype(data[y_axis]):plt.fill_between(data[x_axis], data[y_axis], alpha=self.transparency_slider.value,label=y_axis)plt.title(f'面积图: {", ".join(y_axes)} vs {x_axis}')plt.xlabel(x_axis)plt.ylabel('值')if date_flag:plt.gcf().autofmt_xdate()if len(y_axes) > 1:plt.legend()def _draw_scatter_plot(self, data, x_axis, y_axes):"""绘制散点图"""if len(y_axes) != 1:with self.log_output:print('散点图需要且仅需要一个Y轴')returny_axis = y_axes[0]if pd.api.types.is_numeric_dtype(data[x_axis]) and pd.api.types.is_numeric_dtype(data[y_axis]):sns.scatterplot(x=x_axis, y=y_axis, data=data,alpha=self.transparency_slider.value)plt.title(f'散点图: {y_axis} vs {x_axis}')plt.xlabel(x_axis)plt.ylabel(y_axis)else:with self.log_output:print('散点图需要数值型X轴和Y轴数据')def _draw_box_plot(self, data, x_axis, y_axes):"""绘制箱线图"""if len(y_axes) != 1:with self.log_output:print('箱线图需要且仅需要一个Y轴')returny_axis = y_axes[0]if pd.api.types.is_numeric_dtype(data[y_axis]):# 如果X轴是数值型,将其转换为类别以便更好地展示if pd.api.types.is_numeric_dtype(data[x_axis]):data['x_category'] = pd.qcut(data[x_axis], 5, duplicates='drop')sns.boxplot(x='x_category', y=y_axis, data=data)plt.xlabel(f'{x_axis} (分箱)')else:sns.boxplot(x=x_axis, y=y_axis, data=data)plt.xlabel(x_axis)plt.title(f'箱线图: {y_axis} 按 {x_axis} 分布')plt.ylabel(y_axis)plt.xticks(rotation=self.rotation_slider.value)else:with self.log_output:print('箱线图需要数值型Y轴数据')def _draw_heatmap(self, data, x_axis, y_axes):"""绘制热力图"""# 选择数值列numeric_cols = [col for col in data.select_dtypes(include='number').columns if col in y_axes or col == x_axis]if len(numeric_cols) < 2:with self.log_output:print('热力图需要至少两个数值列')return# 创建相关矩阵corr_matrix = data[numeric_cols].corr()sns.heatmap(corr_matrix, annot=True, cmap=self.color_palette_selector.value,fmt=".2f",linewidths=.5)plt.title('热力图: 变量相关性')def export_chart(self):"""导出图表"""if not hasattr(self, 'current_figure') or self.current_figure is None:with self.log_output:print('没有可导出的图表,请先生成图表')returntimestamp = datetime.now().strftime("%Y%m%d_%H%M%S")filename = f"chart_export_{timestamp}.png"try:self.current_figure.savefig(filename, dpi=300, bbox_inches='tight')with self.log_output:print(f'图表已成功导出为: {filename}')print(f'文件保存在: {os.path.abspath(filename)}')except Exception as e:with self.log_output:print(f'导出图表时出错: {str(e)}')import tracebacktraceback.print_exc()def display_ui(self):"""显示应用界面"""# 控制面板control_panel = VBox([self.file_selector,self.sheet_selector,self.x_axis_selector,self.y_axis_selector,self.chart_type_selector,self.color_palette_selector,self.figsize_slider,self.rotation_slider,self.transparency_slider,self.grid_checkbox,HBox([self.refresh_button, self.export_button])], layout=Layout(width='30%', padding='10px'))# 输出面板output_panel = VBox([Label('📊 数据预览'),self.data_preview_output,Label('📈 数据统计'),self.stats_output,Label('✨ 数据可视化'),self.chart_output,Label('📝 日志信息'),self.log_output], layout=Layout(width='70%', padding='10px'))# 整体布局app_layout = HBox([control_panel, output_panel])# 显示应用display(app_layout)# 使用示例
if __name__ == "__main__":# 修改为您的Excel文件路径file_path1 = 'G:\\PyCharm\\Python_project\\daily_chart.xlsx'file_path2 = 'G:\\PyCharm\\Python_project\\daily_market_data.xlsx'# 创建并显示应用app = EnhancedExcelVisualizer(file_path1, file_path2)
很诡异。。。