资讯动态

Python+SQLite+Matplotlib:构建体质测试数据管理与可视化系统

发布时间:2026/9/19 16:08:59 来源:尧图企业网站定制
简介基于Python的体质测试数据分析及可视化系统完整项目实例面向具备Python基础、熟悉数据分析或Web开发的科研人员、软件开发者和教育、体育、健康管理领域的技术人员。系统以pandas、NumPy完成数据处理Matplotlib、Seaborn实现可视化Flask/Django构建前后端交互覆盖体测数据采集、清洗、标准化、综合评分建模、多维统计分析与可视化展示的完整流程可应用于高校学生体测管理、中小学体质监测、企业员工健康评估及公共卫生政策支持等场景。资源仅含1个docx文档压缩包大小102KB文档从项目背景、目标意义、挑战与解决方案入手梳理数据采集与导入、预处理与清洗、指标标准化与评分模型、分析挖掘、可视化展示等分层架构并给出数据加载、缺失值处理、BMI计算、派生特征生成、综合评分模型构建、分组统计对比及线性回归预测等可直接参考的代码示例同时涉及数据库设计、评分模型配置化与多角色权限管理。已有94人学习适合作为教学案例、毕业设计参考或系统原型进行二次开发与推广。1. 体质测试数据管理与可视化系统核心不只是画图体质测试数据之所以难管不是因为它数据量大而是因为它横跨了“采集—清洗—归档—分析—上报”整条链路。最常见的痛点有三类一是体测数据通常来自多个入口手持设备导出、Excel手工录入、学校教务系统导出的CSV格式五花八门二是同一学生的多次测试记录需要按时间纵向对比而原始表往往是一行一个人、字段固定根本存不下历史版本三是管理端和展示端分离管理老师要的是筛选、修改、补录领导要看的是趋势图和达标率分布一套工具很难同时满足。本文讲的这套基于Python的体质测试数据管理与可视化系统就是围绕这三个痛点展开用SQLite做底层存储用Pandas做清洗和透视用Tkinter搭GUI管理端再用Matplotlib做可视化输出。整个项目不依赖重型框架一台普通办公电脑就能跑起来适合作为课程设计、毕业设计或小型单位的体测数据管理原型。如果你已经能写Python基础语法但还没完整做过一个“数据库界面图表”的项目这篇可以直接照着落地。2. 体质测试系统的数据库设计让数据存得住、查得动2.1 为什么不选MySQL而选SQLite很多人在做数据库设计时第一反应是装MySQL但对体质测试数据这种单机、单用户、低并发的场景SQLite是更务实的选型。SQLite是一个嵌入式关系型数据库它以文件形式存在比如fitness.db不需要单独安装服务端Python标准库sqlite3直接就能操作这对课程设计和毕设演示来说大大降低了环境依赖。更重要的是SQLite支持标准SQL语法包括JOIN、GROUP BY、子查询和事务从SQLite迁移到MySQL的代码成本很低——只需要改连接方式SQL语句几乎不用动。体质测试数据的特点是“写多读少、单条记录变化频繁、需要保留历史版本”。用SQLite的文件型存储还有个好处整个数据库就是一个文件备份时直接复制文件即可不需要mysqldump。对于数据量在几万行以内的体测数据SQLite的查询性能完全够用加了索引后毫秒级返回。2.2 体测数据表的字段设计与三范式权衡体质测试数据管理系统的核心表设计我一般拆成三张表学生基本信息表、体测记录表、体测项目字典表。这是对第三范式的合理让步——不把身高体重写进学生表而是作为一次测试记录存在这样可以支持一个学生多条历史记录。学生表student的字段设计如下字段名类型说明student_idTEXT PRIMARY KEY学号主键nameTEXT NOT NULL姓名genderTEXT CHECK(gender IN (男,女))性别class_nameTEXT班级birth_dateTEXT出生日期ISO格式create_timeTIMESTAMP DEFAULT CURRENT_TIMESTAMP建档时间体测记录表physical_test_record是核心表每次测试一行记录字段名类型说明record_idINTEGER PRIMARY KEY AUTOINCREMENT记录IDstudent_idTEXT NOT NULL REFERENCES student(student_id)学号外键test_dateTEXT NOT NULL测试日期heightREAL身高cmweightREAL体重kgvital_capacityINTEGER肺活量mlrun_50mREAL50米跑秒run_1000mREAL1000米跑秒男生run_800mREAL800米跑秒女生sit_and_reachREAL坐位体前屈cmpull_upINTEGER引体向上次男生sit_upINTEGER仰卧起坐次女生bmiREAL GENERATED ALWAYS AS (weight / ((height/100)*(height/100))) VIRTUALBMI计算列这里用了SQLite的生成列GENERATED COLUMN特性BMI由身高体重自动计算不需要在应用层额外维护避免手动赋值时算错。注意run_1000m和run_800m是互斥字段由性别决定写入哪个在应用层做校验。项目字典表test_item_dict用来记录项目名称、计量单位和达标标准方便以后扩展新测试项目或调整评价标准不必改表结构。创建表的DDL语句如下import sqlite3 conn sqlite3.connect(fitness.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS student ( student_id TEXT PRIMARY KEY, name TEXT NOT NULL, gender TEXT CHECK(gender IN (男, 女)), class_name TEXT, birth_date TEXT, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) cursor.execute( CREATE TABLE IF NOT EXISTS physical_test_record ( record_id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT NOT NULL, test_date TEXT NOT NULL, height REAL, weight REAL, vital_capacity INTEGER, run_50m REAL, run_1000m REAL, run_800m REAL, sit_and_reach REAL, pull_up INTEGER, sit_up INTEGER, bmi REAL GENERATED ALWAYS AS ( ROUND(weight / ((height / 100.0) * (height / 100.0)), 2) ) VIRTUAL, FOREIGN KEY (student_id) REFERENCES student(student_id) ) ) cursor.execute( CREATE INDEX IF NOT EXISTS idx_record_student_date ON physical_test_record(student_id, test_date DESC) ) conn.commit() conn.close()代码的逻辑说明先建立连接再依次创建两张核心表和一条复合索引。索引idx_record_student_date是查询性能的关键——按学号查一个人的全部历史测试记录时SQLite可以借助索引直接定位避免全表扫描。复合索引中student_id在前、test_date在后这符合查询条件中“先过滤学生再按日期排序”的常见模式。如果在实际测试中发现按班级查询多、按个人查询少可以再加一个class_name索引但不要盲目加索引因为每次写入都需要维护索引代价是写入变慢。2.3 数据清洗规则Pandas接管Excel和CSV的脏数据体质测试数据入库前最耗时的环节是清洗。从Excel导出的数据常见问题有四类表头不规则有合并单元格或备注行、性别填写不一致“男”和“male”混用、身高体重出现离谱值身高写成255cm、以及重复记录同一个学生同一天被导入两次。清洗逻辑我一般放在一个独立的data_clean.py模块中用Pandas处理后写入SQLite。清洗的核心思路是分三步第一步规范化列名第二步按业务规则过滤异常值第三步去重并校验唯一性。#### 2.3.1 异常值过滤的参考阈值体测数据的异常值过滤不能只靠“大于某个数就删”要结合项目本身的范围来定。以下是我常用的阈值参考字段合理范围处理方式height120cm ~ 210cm超出则标记人工确认weight30kg ~ 150kg超出则标记vital_capacity800ml ~ 7000ml超出则标记run_50m6.0s ~ 15.0s低于或高于则标记sit_and_reach-5cm ~ 35cm负数到正数范围对于标记为异常的记录不要直接删除而是在导入时写入一个data_validation_log表保留原始数据供人工复核。这样做的原因是体测现场偶尔会出现极端但真实的数据比如超重学生 BMI 确实很高自动删除会造成数据丢失。清洗并入库的示例代码如下import pandas as pd import sqlite3 # 读取excel跳过前两行表头备注 df pd.read_excel(raw_data.xlsx, skiprows2) # 标准化列名映射 column_mapping { 学号: student_id, 姓名: name, 性别: gender, 班级: class_name, 身高: height, 体重: weight, 肺活量: vital_capacity, 50米跑: run_50m, 1000米跑: run_1000m, 800米跑: run_800m, 坐位体前屈: sit_and_reach, 引体向上: pull_up, 仰卧起坐: sit_up, # 测试日期在导入时用业务日期填充 } df df.rename(columnscolumn_mapping) # 性别归一化 df[gender] df[gender].replace({male: 男, female: 女, M: 男, F: 女}) # 异常值过滤保留合理范围内的记录并把异常行写入日志 invalid_mask ( ~df[height].between(120, 210) | ~df[weight].between(30, 150) | ~df[vital_capacity].between(800, 7000) ) invalid_df df[invalid_mask] df df[~invalid_mask] # 去重同一学生同一天只保留一条记录 df df.drop_duplicates(subset[student_id], keeplast) # 连接数据库并写入 conn sqlite3.connect(fitness.db) df.to_sql(physical_test_record, conn, if_existsappend, indexFalse) conn.close()这段代码的关键点在于drop_duplicates的keeplast参数。如果你的数据源是同一个学生当天有多条记录且后来的记录覆盖前面的“保留最后一条”是正确策略。但如果是同一天多次测试取最优成绩你应该改用groupby后取每个学生每个项目的最优值不能用简单的drop_duplicates。我在实际项目中遇到过这个坑直接用keeplast去重结果把学生上午第一次测试的1000米成绩覆盖成了下午补测的成绩但补测可能只是因为上午犯规重跑不代表成绩更好。所以去重策略要和业务对齐而不是机械地去重。3. Python数据管理与查询层格式化、缓存与批处理3.1 数据访问层为什么要单独封装体质测试系统如果直接把SQL语句写在GUI事件回调里刚开始很爽但功能一多就失控。比如“查询某班级最近一次体测达标率”这个需求如果直接在按钮事件里写SQL那么这个SQL片段会被复制到导出Excel、生成报表、界面刷新三个地方以后改一次达标线就要改三个位置。所以我一般会用独立的数据访问模块来统一封装增删改查操作。数据访问层对外暴露的是语义化接口而不是裸SQL。调用方只需要传入参数不关心表结构变化。这是为了做到“界面代码和数据操作分离”——以后把GUI从Tkinter换成PySide6或者Web界面数据层完全不用动。#### 3.1.1 关键方法的设计数据访问层的核心方法包括按条件分页查询测试记录、录入单条记录、批量导入记录、更新记录、按学号查询历史记录、按班级做统计汇总。我用一个DataManager类来组织这些方法它接收数据库路径作为参数内部维护连接和游标。对写操作开启事务失败时回滚避免半条数据落库。import sqlite3 class DataManager: def __init__(self, db_pathfitness.db): self.db_path db_path self.conn sqlite3.connect(self.db_path) self.conn.row_factory sqlite3.Row # 按列名访问 def query_records(self, class_nameNone, student_idNone, start_dateNone, end_dateNone, page1, page_size50): 带筛选和分页的查询 conditions [] params [] if class_name: conditions.append(s.class_name ?) params.append(class_name) if student_id: conditions.append(r.student_id LIKE ?) params.append(f%{student_id}%) # 模糊匹配 if start_date: conditions.append(r.test_date ?) params.append(start_date) if end_date: conditions.append(r.test_date ?) params.append(end_date) where_clause AND .join(conditions) if conditions else 11 offset (page - 1) * page_size sql f SELECT r.*, s.name, s.gender, s.class_name FROM physical_test_record r JOIN student s ON r.student_id s.student_id WHERE {where_clause} ORDER BY r.test_date DESC, s.class_name LIMIT ? OFFSET ? # 注意参数顺序和LIMIT/OFFSET rows self.conn.execute(sql, params [page_size, offset]).fetchall() # 统计总数以便计算总页数 count_sql f SELECT COUNT(*) FROM physical_test_record r JOIN student s ON r.student_id s.student_id WHERE {where_clause} total self.conn.execute(count_sql, params).fetchone()[0] return rows, total def add_record(self, record: dict): 新增一条体测记录含事务 sql INSERT INTO physical_test_record (student_id, test_date, height, weight, vital_capacity, run_50m, run_1000m, run_800m, sit_and_reach, pull_up, sit_up) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) try: with self.conn: self.conn.execute(sql, ( record[student_id], record[test_date], record.get(height), record.get(weight), record.get(vital_capacity), record.get(run_50m), record.get(run_1000m), record.get(run_800m), record.get(sit_and_reach), record.get(pull_up), record.get(sit_up) )) except sqlite3.IntegrityError as e: raise ValueError(f数据入库失败请检查学号是否存在于学生表{e}) def close(self): self.conn.close()这里有几个参数细节值得注意。row_factory sqlite3.Row让查询结果支持像字典那样按列名访问row[gender]而不是只能通过下标这对后续GUI表格绑定数据非常方便。LIMIT ? OFFSET ?中LIMIT接收的是每页条数OFFSET是跳过的条数在动态拼接SQL时要确保参数顺序正确——先拼接查询条件最后把分页参数追加到列表末尾。事务处理的写法with self.conn:是SQLite的上下文管理器特性进入代码块时自动开启事务块中任何异常都会触发回滚正常退出则提交。这个写法比手动conn.commit()更安全不会出现忘记提交导致数据丢失的情况。我还特意在IntegrityError异常中抛出了带提示的ValueError这样GUI界面的提示框可以直接显示“学号不存在”这样的友好信息而不是让用户看到一堆数据库报错堆栈。3.2 批量导入的三种策略与一致性保障体质测试系统的数据录入场景很特别平时增量是一条一条加但开学集中体测时一次要导入几百条记录。批量导入的高效做法是用executemany而不是循环执行execute差距在数据量超过1000条时非常明显。另外还有一个关键策略导入时先检查学生表如果学生信息不存在可以选择自动建档或者跳过并记录日志这取决于业务上的要求。executemany的参考实现def batch_import(self, records: list[dict]): sql INSERT INTO physical_test_record (student_id, test_date, height, weight, vital_capacity, run_50m, run_1000m, run_800m, sit_and_reach, pull_up, sit_up) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) data [ ( rec[student_id], rec[test_date], rec.get(height), rec.get(weight), rec.get(vital_capacity), rec.get(run_50m), rec.get(run_1000m), rec.get(run_800m), rec.get(sit_and_reach), rec.get(pull_up), rec.get(sit_up) ) for rec in records ] with self.conn: self.conn.executemany(sql, data)批量导入时有一点容易被忽略如果列表中有重复数据或者学生ID在学生表中不存在整批导入会抛IntegrityError并回滚全部记录。为了避免“一个好数据拖累整批入库”大家通常会在批量导入前增加一道预校验先查询学生表中存在的学号集合筛除不存在的再用字典或集合检测重复控制导入条数和失败的条数并汇总日志。4. Tkinter GUI与Matplotlib可视化界面怎么写才不糊4.1 体测系统的窗口结构规划体质测试数据管理系统的GUI我通常划分成三个区域顶部是操作栏导入按钮、筛选条件、刷新按钮中间是数据表格Treeview组件底部是图表展示区嵌入Matplotlib画布。窗口的分区比例很关键——表格占大头图表可以放在右侧或者下方用PanedWindow让用户拖拽调整比固定布局更实用。Tkinter作为Python标准库自带的GUI工具包虽然外观朴素但对体质测试管理系统这种内部工具足够用。使用Tkinter有三个好处零额外安装依赖Python安装后自带、事件回调模型简单绑定按钮和回调函数即可、和Matplotlib的FigureCanvasTkAgg适配成熟。唯一的缺点是界面在Windows和Linux下的显示效果有差异字体需要做平台判断。关于GUI布局一个重要的经验是不要在一个窗口里塞太多东西。体质测试数据的筛选条件至少有“班级、学号、日期范围”三个维度如果再加上项目列表和多选界面就会变得拥挤。我的做法是用一个“筛选面板 结果表格 图表”的三段式垂直布局筛选面板默认收起通过按钮展开。这样既保留了功能完整性又不会让主界面看起来像填表页面。4.2 把Matplotlib图表嵌入Tkinter画布可视化是体质测试系统的展示重点但很多人在做嵌入时走了弯路——直接在Tkinter窗口里弹出Matplotlib的独立窗口体验很差。正确的做法是用matplotlib.backends.backend_tkagg.FigureCanvasTkAgg把图表画在Tkinter的容器组件中。下面是最小可运行的嵌入代码import tkinter as tk from tkinter import ttk import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg class FitnessApp(tk.Tk): def __init__(self): super().__init__() self.title(体质测试数据管理与可视化系统) self.geometry(1100x700) # 用PanedWindow让用户调整表格和图表区域比例 self.paned ttk.PanedWindow(self, orienttk.HORIZONTAL) self.paned.pack(filltk.BOTH, expandTrue) # 左侧表格区域 self.table_frame ttk.Frame(self.paned) self.paned.add(self.table_frame, weight3) self._build_table() # 右侧图表区域 self.chart_frame ttk.Frame(self.paned) self.paned.add(self.chart_frame, weight2) self._build_chart() def _build_table(self): # 表格使用Treeview显示学号、姓名、班级、身高、体重、BMI等列 columns (student_id, name, class_name, height, weight, bmi, test_date) self.tree ttk.Treeview(self.table_frame, columnscolumns, showheadings, height20) for col, text in zip(columns, [学号, 姓名, 班级, 身高cm, 体重kg, BMI, 测试日期]): self.tree.heading(col, texttext) self.tree.column(col, width80, anchorcenter) scrollbar ttk.Scrollbar(self.table_frame, orienttk.VERTICAL, commandself.tree.yview) self.tree.configure(yscrollcommandscrollbar.set) self.tree.pack(sidetk.LEFT, filltk.BOTH, expandTrue) scrollbar.pack(sidetk.RIGHT, filltk.Y) def _build_chart(self): # 创建Matplotlib图形和画布 self.fig, self.ax plt.subplots(figsize(5, 4)) self.canvas FigureCanvasTkAgg(self.fig, masterself.chart_frame) self.canvas.get_tk_widget().pack(filltk.BOTH, expandTrue) def refresh_chart(self, data): 根据当前筛选的数据刷新图表 self.ax.clear() self.ax.hist(data[bmi], bins12, edgecolorwhite, alpha0.7, colorsteelblue) self.ax.set_xlabel(BMI) self.ax.set_ylabel(人数) self.ax.set_title(BMI分布直方图) self.canvas.draw_idle()这里有个重要细节数据刷新后一定用self.ax.clear()而不是新建plt.subplots()。因为FigureCanvasTkAgg绑定的是第一次创建时的figure对象如果在你刷新时重新赋值self.fig、self.ax那画布上显示的还是旧的Figure对象图表诡异地不更新。draw_idle()和draw()的区别是前者只在空闲时重绘适合数据频繁更新的场景避免界面卡顿。4.3 交互逻辑筛选联动与权限控制在体测管理界面中数据表格和图表必须联动。用户在下拉列表中选择“计算机2401班”并点击“查询”后表格更新为该班所有记录图表同步更新为该班的BMI分布。这种联动实现的要点在于用同一个查询结果同时驱动两个组件每次查询返回的数据先存入一个self.current_data_df属性DataFrame表格刷新时从这个DataFrame取数填充Treeview图表刷新时直接把这个DataFrame传给绘图函数。用户在管理界面中还应注意到修改操作直观体现是双击某一条记录弹出编辑窗口记录ID做唯一性校验避免编辑时错改他人记录。体质测试数据涉及学生姓名、出生日期等字段在界面层传入用户角色做基础控制非管理角色隐藏删除和批量修改按钮。不需要复杂的权限框架一个if role admin就能满足功能需求。5. 体测数据可视化进阶达标率、趋势对比与异常识别5.1 年级达标率的雷达图实现体测数据不能停留在“画几根柱状图”的层面。体质测试有国家标准如《国家学生体质健康标准》中对各项目的单项评分和综合评价有明确规定及格、良好、优秀的分数线各不相同。可视化的核心价值在于把“每个单项得分情况”和“综合达标率”之间建立直观联系。我会用雷达图展示某个班级或年级在多个测试项目上的平均得分率用柱状图显示各项目的达标率排名。计算单项达标率时需要先知道每个项目的评分标准但不同性别的评分表不同。可以用一个字典来存放分段标准再把标准连到测试数据做判断。因为不同项目单位都不一样——身高体重用BMI、肺活量用毫升、跑步用秒所以做雷达图之前必须把原始数值映射到 0~100 的得分不然十几个量纲不同的项目放在一个坐标系里完全没法看。每项得分率定义参照不同的达标基准映射到百分制再按班级聚合求均值交付显示层。实现时我们可以先把每个人各项目得分算出来再按班级分组平均把结果传给雷达图绘制函数def calculate_scores(df): 按《国家学生体质健康标准》简化版计算各项目得分 scores {} # BMI评分示例大学男生BMI在17.9~23.9之间得100分 bmi_std {男: [(17.9, 23.9, 100), (17.8, 24.0, 80)], 女: [(17.2, 23.9, 100), (17.1, 24.0, 80)]} # 简化实际标准有多档这里只展示思路 df[bmi_score] df.apply( lambda r: 100 if r[gender] 男 and 17.9 r[bmi] 23.9 else 80, axis1) # ... 其他项目score计算 # 按班级聚合 class_scores df.groupby(class_name)[[ bmi_score, vital_capacity_score, run_50m_score, sit_and_reach_score ]].mean().reset_index() return class_scores def plot_radar(class_scores, class_name): plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False data class_scores[class_scores[class_name] class_name].iloc[0] categories [BMI, 肺活量, 50米跑, 坐位体前屈] values data[[bmi_score, vital_capacity_score, run_50m_score, sit_and_reach_score]].values angles [i / float(len(categories)) * 2 * 3.1415926 for i in range(len(categories))] values list(values) [values[0]] # 闭合雷达图 angles angles [angles[0]] fig, ax plt.subplots(figsize(5, 5), subplot_kwdict(polarTrue)) ax.fill(angles, values, colorskyblue, alpha0.4) ax.plot(angles, values, colorroyalblue, linewidth2) ax.set_xticks(angles[:-1]) ax.set_xticklabels(categories) ax.set_ylim(0, 100) ax.set_title(f{class_name} 体测单项平均得分) return fig在绘图之前两行rcParams设置是必须的尤其是axes.unicode_minusFalse。很多人画Matplotlib图时发现坐标轴的负号显示为方块就是忘了设置这一行。另外雷达图的坑在于必须把第一个值重复加到列表末尾才能形成闭合图形否则画出来的图形会缺一条边。5.2 历史趋势对比与异常记录筛查体测系统还包括历史对比功能界面提供“个人纵向对比”和“班级横向对比”两种视图。个人纵向对比是针对同一个学生选择多次测试记录观察其体重、肺活量等指标的变化。对于体重控制、运动改善这类长期跟踪场景这是比单次截图更有说服力的视角。历史趋势图的实现核心是查询“同一学生按test_date排序的全部记录”再对指定字段做折线图。但有个前提条件学生表中的student_id是固定的学生转班或换号的情况要有数据迁移逻辑否则对比时会断裂。我在实际项目中遇到过学号中包含前导零的情况Excel打开时把0012301读成了12301导致关联不上。解决方法是导入时把学号列强制读取为字符串格式Pandas读取Excel时加dtype{student_id: str}。异常记录的筛查用离群点检测。体测数据的异常往往不是单字段越界而是字段组合越界例如“身高170cm、体重30kg”单看身高正常体重也正常但组合成BMI小于10就明显反常。这里的一个小技巧是用各字段的Z-Score计算偏离程度组合判定加白加权的综合异常分数。对学生体测数据使用Z-Score注意样本量不能太小班级人数少于10人时Z-Score的均值容易被极端值拉偏适合用MAD绝对中位差方法折中判断但平时直接用个位数阈值过滤即可。5.3 导出统计报告的完整流程体质测试系统的最后环节往往是出分析报告领导要的是直接从界面点一个按钮就能拿到按班级汇总的表格而不是截图。导出流程我一般直接用Pandas的to_excel借助pd.ExcelWriter实现“多Sheet输出”一个Sheet放原始明细一个Sheet放班级汇总一个Sheet放图表。需要注意中文文件名编码问题Windows环境下直接使用中文文件名时有时会因编码报错导致无法保存解决方法是利用日期构造合理的英文文件名加中文Sheet名。导出报告时还应注意Excel的写入引擎依赖openpyxl库在运行前完成安装。整体导出的封装代码如下def export_report(df_detail, df_summary, figures, output_path): with pd.ExcelWriter(output_path, engineopenpyxl) as writer: df_detail.to_excel(writer, sheet_name体测明细, indexFalse) df_summary.to_excel(writer, sheet_name班级汇总, indexFalse) # 图表以图片形式插入Excel不方便可以把图表存为PNG再插入 for name, fig in figures.items(): fig.savefig(ftemp_{name}.png, dpi150, bbox_inchestight) # 使用openpyxl的Image插入 from openpyxl.drawing.image import Image as XLImage ws writer.book.create_sheet(fchart_{name}) img XLImage(ftemp_{name}.png) ws.add_image(img, A1) print(f报告已导出到{output_path})最后一点经验图表放进Excel时dpi150是屏幕查看和打印兼顾的折中值dpi太大会导致文件体积剧增对体测系统而言完全没有必要追求超高清。bbox_inchestight则可以裁掉图表周围的空白边距让图片更紧凑。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价