资讯详情

基于Flask的居家健身推荐系统:从数据清洗到可解释推荐全链路

📅 2026/10/30 1:03:32 | 华诺云谱 👁 阅读
基于Flask的居家健身推荐系统:从数据清洗到可解释推荐全链路
简介这份资源是一套基于Python Flask框架开发的个性化居家健身推荐系统完整源码面向人工智能、计算机科学与技术等相关专业的学生尤其适合用作毕业设计课题或课程作业。系统采用基于内容的过滤算法结合数据分析与机器学习根据用户的健身目标、体能水平和可用设备生成定制化锻炼方案着力解决居家健身难以坚持、方案不匹配等痛点。压缩包共约2000个文件整体96.4MB其中以1114张jpg图片和874个json数据文件为主另含少量html页面、csv数据集与md说明文档覆盖数据收集、清洗、推荐逻辑与前端界面等模块。目前已有111人学习下载。读者可从中获得一套结构完整、可直接运行的项目方案便于理解推荐算法落地流程、参考目录组织方式并在此基础上二次开发。1. 居家健身推荐系统拆包从 exercises.csv 到 Flask 推荐页的完整链路打开这个压缩包第一眼看到的不是一堆散乱文件而是一条完整的数据流水线exercises.csv是原始锻炼动作库exercises_cleaned.csv是清洗后的版本welcome.html、beginner.html、advanced.html是分层入口页recommendations.html和more_recommendations.html负责展示推荐结果。整套东西跑在 Flask 上推荐逻辑用的是基于内容的过滤算法。说白了它解决的是一个很具体的问题用户在家想练但不知道自己该练什么、能练什么、练多少。系统根据你的健身目标、体能水平和手头设备从动作库里筛出匹配项再按页面分层推给你。适合谁做毕业设计、课程作业的计算机和人工智能方向学生以及想快速搭一个可演示推荐原型的开发者。它不是一个玩具 demo数据清洗、特征匹配、页面路由都有实际代码支撑但也不是生产级系统边界后面会讲清楚。2. 数据层拆解exercises.csv 的字段含义与清洗逻辑2.1 原始数据长什么样exercises.csv是整个系统的燃料。常见做法是每行代表一个锻炼动作字段一般包括动作名称、目标肌群、所需设备、难度等级、动作描述、卡路里消耗估算等。我拿到这类文件的第一件事不是急着写代码而是先用 pandas 把列名、缺失值、重复行扫一遍。因为推荐系统的命门在数据质量字段脏了后面算法再漂亮也是白搭。import pandas as pd # 读取原始动作库 df pd.read_csv(exercises.csv) # 看列名、类型、缺失情况 print(df.columns.tolist()) print(df.dtypes) print(df.isnull().sum()) # 看重复行和唯一值分布 print(重复行数:, df.duplicated().sum()) for col in df.select_dtypes(includeobject).columns: print(f{col} 唯一值数量:, df[col].nunique())这段代码的逻辑很直接先摸清数据轮廓。df.isnull().sum()告诉你哪些字段有空洞比如设备字段缺失推荐时就没法按设备过滤。df.duplicated().sum()查重复动作重复项会让推荐结果里出现两个一模一样的动作用户体验直接翻车。nunique()帮你看分类字段的粒度如果“难度等级”只有两个值那分层推荐的意义就不大。参数上没什么好调的但有一个习惯我建议你养成把df.dtypes的输出存下来后面写推荐逻辑时对照着看避免把数值字段当字符串处理。2.2 清洗成 exercises_cleaned.csv 的关键动作清洗不是简单 dropna 就完事。这个项目里exercises_cleaned.csv的存在说明作者做了标准化。常见做法包括统一设备名称比如“哑铃”和“dumbbell”合并、把难度映射成有序等级beginner/intermediate/advanced、剔除描述为空或过短的行、对目标肌群做多标签拆分。# 统一设备名称大小写和空格 df[equipment] df[equipment].str.strip().str.lower() # 难度映射为有序类别 difficulty_map {beginner: 0, intermediate: 1, advanced: 2} df[difficulty_level] df[difficulty].map(difficulty_map) # 剔除描述过短的动作 df df[df[description].str.len() 10] # 目标肌群多标签拆分假设用逗号分隔 df[muscle_groups] df[muscle_groups].str.split(,) # 去重 df df.drop_duplicates(subset[exercise_name]) # 保存清洗后数据 df.to_csv(exercises_cleaned.csv, indexFalse)逻辑说明str.strip().str.lower()解决的是同一设备多种写法的问题不统一的话用户选“哑铃”可能漏掉标着“Dumbbell”的动作。难度映射成数值后推荐时可以按用户水平做区间过滤而不是字符串精确匹配。描述长度过滤是防脏数据太短的动作说明对用户没价值。多标签拆分是为了后面做基于内容的相似度计算一个动作可能同时练胸和肱三头肌拆成列表才能逐项匹配。去重按动作名称保留第一条。参数方面description长度阈值 10 是我一般会用的经验值你可以根据实际数据分布调整。drop_duplicates的subset参数很关键如果按整行去重可能漏掉同名但描述略有差异的重复项。注意清洗后的文件不要覆盖原始文件保留exercises.csv作为回溯依据。一旦推荐结果异常你可以对比清洗前后差异快速定位是数据问题还是算法问题。3. Flask 推荐引擎基于内容的过滤怎么落到代码里3.1 用户画像与动作特征的向量化基于内容的过滤核心思想是给用户建一个偏好向量给每个动作建一个特征向量然后算相似度。这个项目里用户输入的是健身目标、体能水平、可用设备。动作的特征来自清洗后的字段目标肌群、难度、设备。常见做法是用 one-hot 编码或者 TF-IDF 把文本特征转成数值。设备、难度这种类别字段用 one-hot目标肌群这种多标签字段可以用 MultiLabelBinarizer。from sklearn.preprocessing import MultiLabelBinarizer, OneHotEncoder from sklearn.feature_extraction.text import TfidfVectorizer import numpy as np # 动作特征目标肌群多标签二值化 mlb MultiLabelBinarizer() muscle_matrix mlb.fit_transform(df[muscle_groups]) # 设备 one-hot ohe OneHotEncoder(sparse_outputFalse) equipment_matrix ohe.fit_transform(df[[equipment]]) # 难度作为数值特征直接归一化 difficulty_matrix df[difficulty_level].values.reshape(-1, 1) / 2.0 # 拼接成动作特征矩阵 exercise_features np.hstack([muscle_matrix, equipment_matrix, difficulty_matrix])逻辑说明MultiLabelBinarizer把每个动作的肌群列表转成 0/1 向量比如“胸、三头”变成 [1,0,1,0...]。OneHotEncoder处理设备每个设备一列。难度归一化到 0-1 之间避免数值尺度差异过大影响相似度计算。最后np.hstack横向拼接得到每个动作的完整特征向量。参数上sparse_outputFalse是为了后续直接做矩阵运算数据量不大时没必要用稀疏矩阵。难度除以 2.0 是因为难度等级映射成了 0、1、2归一化后变成 0、0.5、1。3.2 相似度计算与推荐排序用户画像怎么建用户选了目标肌群、设备、难度同样用上面的编码器转成向量然后和动作特征矩阵算余弦相似度。from sklearn.metrics.pairwise import cosine_similarity def build_user_vector(goal_muscles, available_equipment, fitness_level): # 肌群向量 muscle_vec mlb.transform([goal_muscles]) # 设备向量 equip_vec ohe.transform([[available_equipment]]) # 难度向量 diff_vec np.array([[fitness_level / 2.0]]) return np.hstack([muscle_vec, equip_vec, diff_vec]) def recommend(user_vector, top_n10): sim_scores cosine_similarity(user_vector, exercise_features)[0] top_indices sim_scores.argsort()[-top_n:][::-1] return df.iloc[top_indices][[exercise_name, equipment, difficulty]], sim_scores[top_indices] # 示例用户想练胸有哑铃初级水平 user_vec build_user_vector([chest], dumbbell, 0) results, scores recommend(user_vec, top_n5) print(results) print(scores)逻辑说明build_user_vector把用户输入转成和动作特征同维度的向量。cosine_similarity算用户向量和每个动作向量的夹角余弦值值越接近 1 越相似。argsort()[-top_n:][::-1]取相似度最高的前 N 个反转是为了从高到低排。参数上top_n控制推荐数量页面上recommendations.html和more_recommendations.html可能就是分别展示前 5 和前 20。fitness_level传 0、1、2 对应初级、中级、高级。这里有个细节如果用户选的设备在动作库里没有匹配项相似度会普遍偏低推荐结果可能不相关。常见做法是加一个兜底逻辑设备不匹配时降权而不是直接排除。提示余弦相似度对向量长度不敏感适合这种混合了类别和数值特征的场景。但如果你发现推荐结果总是偏向某几个肌群检查一下 one-hot 编码后各维度是否被过度放大。4. 页面路由与分层展示beginner.html 和 advanced.html 怎么接4.1 Flask 路由结构这个项目的页面文件包括welcome.html、beginner.html、advanced.html、recommendations.html、more_recommendations.html。从命名看welcome.html是入口beginner.html和advanced.html是按体能水平分的两个分支推荐结果页是后两个。常见做法是在 Flask 里定义路由每个页面一个视图函数推荐逻辑在提交表单后触发。from flask import Flask, render_template, request, session app Flask(__name__) app.secret_key your_secret_key_here app.route(/) def welcome(): return render_template(welcome.html) app.route(/beginner) def beginner(): return render_template(beginner.html) app.route(/advanced) def advanced(): return render_template(advanced.html) app.route(/recommend, methods[POST]) def recommend_page(): goal request.form.get(goal_muscles, ) equipment request.form.get(equipment, ) level int(request.form.get(fitness_level, 0)) muscles [m.strip() for m in goal.split(,) if m.strip()] user_vec build_user_vector(muscles, equipment, level) results, scores recommend(user_vec, top_n10) session[last_results] results.to_dict(records) return render_template(recommendations.html, exercisesresults.to_dict(records)) app.route(/more) def more_recommendations(): results session.get(last_results, []) return render_template(more_recommendations.html, exercisesresults)逻辑说明welcome是首页beginner和advanced是分层入口用户从这两个页面填表单提交到/recommend。request.form.get拿表单字段split(,)处理多选肌群。推荐结果存到 session 里/more路由从 session 取避免重复计算。secret_key是 session 必须的随便设一个字符串就行但生产环境要换成随机值。参数上top_n10对应recommendations.html展示的数量more_recommendations.html可以展示全部或更多。session存储适合这种轻量场景但注意 session 数据存在客户端 cookie 里数据量大时会有性能问题。4.2 模板变量与前端渲染recommendations.html里用 Jinja2 循环渲染结果。常见写法是!-- recommendations.html 片段 -- ul {% for ex in exercises %} li strong{{ ex.exercise_name }}/strong - 设备: {{ ex.equipment }} - 难度: {{ ex.difficulty }} /li {% endfor %} /ul逻辑说明{% for %}遍历后端传来的exercises列表{{ }}输出字段值。字段名要和后端to_dict(records)的键一致否则页面空白。这里容易踩的坑是 DataFrame 列名带空格或特殊字符Jinja2 里访问会报错清洗阶段就要把列名规范成下划线命名。注意如果页面显示空白但后端没报错先检查render_template传的变量名和模板里的是否一致。这是最常见的翻车点血泪经验。5. 避坑与排查推荐系统跑不起来时先看这几处5.1 现象页面报 500日志显示 KeyError原因模板里引用了不存在的字段或者后端to_dict的键和模板变量对不上。比如 DataFrame 列名是exercise_name模板里写了ex.name。解决在视图函数里打印results.columns.tolist()和模板里的变量逐个核对。养成习惯后端返回前先print(results.head())确认字段名和数据类型。5.2 现象推荐结果全是同一个动作或相似度全为 0原因特征矩阵构建时维度不匹配或者用户向量和动作向量拼接顺序不一致。常见于 one-hot 编码器在训练集和用户输入上分别 fit导致列顺序不同。解决编码器只在动作库上 fit 一次用户输入用同一个编码器的transform。检查exercise_features.shape和user_vector.shape是否一致。如果不一致说明拼接逻辑有问题。5.3 现象中文动作名称显示乱码原因CSV 文件编码不是 UTF-8或者 Flask 返回时没设置正确的 content-type。解决读取 CSV 时指定encodingutf-8如果报错就试gbk。Flask 默认用 UTF-8但模板文件本身要保存为 UTF-8 无 BOM 格式。Windows 下用记事本另存为时容易带 BOM导致页面开头多出空白行。5.4 现象session 数据丢失/more 页面空白原因Flask 的 secret_key 没设或每次重启变化导致 session cookie 失效。或者用户浏览器禁用了 cookie。解决设置固定的app.secret_key不要用随机生成。如果数据量大改用服务端存储或直接重新计算推荐结果不依赖 session。5.5 现象清洗后的 CSV 行数比原始少很多原因去重和缺失值过滤叠加把大量数据删掉了。常见于设备字段缺失率高或者描述字段大量为空。解决先统计各字段缺失率缺失超过 50% 的字段考虑不用而不是直接删行。去重前先看重复项是否真的完全一样有时候同名动作针对不同难度不应该去重。6. 进阶技巧把推荐结果做成可解释的分层输出这套系统跑通之后最值得花时间的地方不是换更复杂的算法而是让推荐结果可解释。用户看到“推荐这个动作”和“推荐这个动作因为练胸、只需哑铃、适合初级”是两种体验。我的做法是在推荐函数里同时返回匹配原因。def recommend_with_reason(user_vector, user_muscles, user_equipment, user_level, top_n10): sim_scores cosine_similarity(user_vector, exercise_features)[0] top_indices sim_scores.argsort()[-top_n:][::-1] results [] for idx in top_indices: row df.iloc[idx] reasons [] # 肌群匹配 matched set(row[muscle_groups]) set(user_muscles) if matched: reasons.append(f针对{,.join(matched)}) # 设备匹配 if row[equipment] user_equipment: reasons.append(f只需{user_equipment}) # 难度匹配 if abs(row[difficulty_level] - user_level) 1: reasons.append(难度适合) results.append({ name: row[exercise_name], reason: .join(reasons) if reasons else 综合推荐 }) return results逻辑说明在原有相似度排序基础上逐条检查肌群交集、设备相等、难度差值。set交集算匹配肌群abs差值判断难度是否接近。reasons列表拼成一句话没有匹配项时给“综合推荐”兜底。参数上难度差值阈值设为 1意味着初级用户也能看到中级动作但不推高级。这个阈值可以调设 0 就是严格匹配设 2 就是全放开。我一般会设 1给用户一点进阶空间。表格对比一下三种输出方式输出方式用户理解成本开发成本适用场景只给动作名高不知道为什么推这个低内部测试动作名相似度分数中分数不直观低调试阶段动作名匹配原因低一眼看懂中正式演示还有一个技巧把more_recommendations.html做成“换一批”功能每次从相似度排名 11-20 里取而不是重复展示前 10。这样用户点“更多”时有新鲜感也避免了 session 数据重复渲染。从那以后我每次做完推荐系统都会强制走一遍“可解释性检查”随便选一个用户画像把推荐结果和匹配原因打印出来人工看一遍是否合理。如果推荐结果连自己都说服不了那肯定有问题。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑