农产品推荐系统实战:基于Python+Django的协同过滤实现与部署
2026/9/12 13:55:52 网站建设 项目流程

简介:这是一份基于Python和Django的农产品推荐系统完整源码,主要面向学习Web全栈、爬虫开发与推荐算法的高校学生、毕业设计者及初级工程师。项目围绕农产品电商场景,以“惠农网”为目标数据源,使用Scrapy框架完成数据抓取,再经清洗、规范化处理后存入MySQL及Hadoop分布式文件系统;推荐部分采用协同过滤算法,依据用户历史浏览和购买记录计算相似度,并结合Vue与Element Plus搭建前端页面,完整覆盖数据采集、预处理、算法推荐到用户管理的业务闭环。压缩包共22个文件,含png界面截图、py爬虫脚本、scala与java辅助代码、jpg图片及md说明文档,整体大小约5.05MB,目录结构简洁,便于按模块查阅。目前已有140人学习下载。通过源码不仅能快速搭建一套农产品推荐系统,还可深入理解Hadoop、Spark等大数据组件在真实项目中的整合方式,适合作为课程设计或毕设参考。

1. 农产品推荐系统用 Python 和 Django 落地:先解决「推荐什么」,再谈算法

农产品和标准工业品的推荐逻辑完全不同:用户买手机看参数,买土豆看产地和价格,同一品类换个产地,复购决策就变了。这套基于 Python 和 Django 的农产品推荐系统源码,价值不在算法多炫,而在于把用户行为、商品属性、推荐策略串成一条 Django 能跑的生产流水线。Django 负责数据建模、后台管理和请求分发,推荐部分用协同过滤就能覆盖大多数场景,数据量大了再替换成向量检索也留得住接口。适合正在做课设选型的学生,也适合给生鲜电商 MVP 搭推荐位的开发——拿到任何同主题源码,都可以按这套思路改自己的数据集。

2. 数据模型设计:用 Django ORM 建出推荐系统需要的三张表

2.1 农产品 SKU 的特殊性决定了 item 的粒度

先想清楚推荐什么,再写代码。农产品没有品牌壁垒,SKU 跟季节走,一个「红富士苹果」的规格、价格、库存每个月都在变,如果把每个 SKU 当成独立的 item,评分矩阵立刻稀疏成筛子。常见的做法是把 item 粒度放在「品类 + 品种 + 产地」上,例如「烟台红富士」和「洛川红富士」是两个可比较的实体,不同包装规格只是在价格字段上做乘法。这套设计直接决定了后面相似度计算用的是商品属性向量,而不是对描述文本做分词。

以我搭过的生鲜类项目为例,推荐位要能解释「为什么推这个」,所以商品画像至少要留三个字段:品类、产地、季节标签。这三个字段既参与相似度计算,也能在前端渲染成筛选标签,一套数据两处用。同类项目里,旅游推荐系统推的是线路,item 数量固定且属性稳定,而农产品商品随时令变动,所以模型里必须给「季节/上架状态」留字段,否则换季时推荐列表里全是下架商品。

2.2 模型代码:三张表的最小可用版本

下面这个模型集是这类源码里最常见的骨架。Rating 单独成表而不是在 Product 上挂一个平均分字段,是因为推荐算法需要的是原始行为记录,随时可以重算聚合值,不需要为了展示去维护冗余列。

from django.db import models from django.contrib.auth.models import User class Category(models.Model): name = models.CharField(max_length=50, unique=True, verbose_name="品类") class Product(models.Model): name = models.CharField(max_length=100, verbose_name="商品名") category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="products") origin = models.CharField(max_length=50, verbose_name="产地") price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name="参考价") rating = models.FloatField(default=0.0, verbose_name="综合评分") def __str__(self): return self.name class Rating(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="ratings") product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="ratings") score = models.PositiveSmallIntegerField(default=5, verbose_name="评分") created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ("user", "product") indexes = [ models.Index(fields=["user", "product"]), ]

几个参数值得解释。on_delete=models.CASCADE表示用户或商品删除时评分级联删除,避免孤儿数据;如果电商系统走软删除,这里要改成 PROTECT。unique_together保证同一用户对同一商品只有一条评分,配合 Django 的update_or_create写入,评分数据天然干净。related_nameuser.ratingsproduct.ratings反向查询可用,算法代码里少写一串 filter。最后那个联合索引很重要,协同过滤循环里反复按user_idproduct_id,没有这个索引,数据量到几千条以后查询时间会肉眼可见地涨。

2.3 先造数据:用 admin 和 fixture 把开发环境跑起来

模型建好先别急着写算法,用 Django Admin 灌一批假数据,把链路打通。在admin.py里注册:

from django.contrib import admin from .models import Category, Product, Rating @admin.register(Category) class CategoryAdmin(admin.ModelAdmin): list_display = ("id", "name") @admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display = ("id", "name", "category", "origin", "price", "rating") list_filter = ("category", "origin") search_fields = ("name", "origin") @admin.register(Rating) class RatingAdmin(admin.ModelAdmin): list_display = ("user", "product", "score", "created_at")
python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver

访问/admin录入 5 个用户、20 个商品、每用户 8~10 条评分即可。数据太少时相似度全是零是正常的,后面章节会给冷启动兜底策略。如果不想手点,也可以用 Django fixture:先python manage.py dumpdata shop --indent 2 > shop/fixtures/dev.json,换环境后python manage.py loaddata dev.json一键恢复,多人协作时这套流程比互相传数据库文件省事得多。

2.4 三种推荐策略的选型对照

不同数据规模对应不同实现,这个表可以作为选型依据:

方案输入数据冷启动表现适合的规模实现成本
基于用户的协同过滤评分/收藏行为差,新用户无邻居千级用户以下
基于物品的协同过滤行为 + 商品画像较好,新商品可用属性兜底商品数少、用户多
内容相似度推荐商品属性任意低(但效果糙)

农产品系统商品数量通常远小于用户数,所以实践中我更推荐基于物品的协同过滤:商品相似度矩阵可以定时预计算,线上只做查表。下一章先讲实现起来最快、也最容易理解错的用户协同过滤,再讲物品方案的矩阵构建思路。

3. 协同过滤推荐算法实现:Python 写的相似度计算要注意的边界

3.1 先把 ORM 数据拉成字典矩阵

推荐算法不关心 Django 对象,关心的是一个{user_id: {product_id: score}}的嵌套结构。常见做法是python manage.py startapp recommender建一个独立 app,把算法代码和视图解耦。从 ORM 直接拉模型对象列表会触发 N+1 查询,正确做法是用values_list只取需要的列:

from collections import defaultdict from .models import Rating def build_user_item_matrix(): rows = Rating.objects.values_list("user_id", "product_id", "score") matrix = defaultdict(dict) for uid, pid, score in rows: matrix[uid][pid] = float(score) return matrix

values_list返回的是轻量元组而不是模型对象,内存占用小一个量级。农产品项目的评分表上万行完全没问题,到了百万级再换iterator()分块取或者直接读 CSV 都行,只要这个函数返回结构不变,后面算法层就不用动。

3.2 余弦相似度:注意零向量和共同评分

基于用户的协同过滤,核心是算两个用户评分向量的相似度,再用相似用户对目标用户没买过的商品做加权评分。余弦相似度的实现看起来短,坑都在边界:

import math def cosine_similarity(vec_a, vec_b): common = set(vec_a.keys()) & set(vec_b.keys()) if not common: return 0.0 dot = sum(vec_a[p] * vec_b[p] for p in common) norm_a = math.sqrt(sum(v * v for v in vec_a.values())) norm_b = math.sqrt(sum(v * v for v in vec_b.values())) if norm_a == 0 or norm_b == 0: return 0.0 return dot / (norm_a * norm_b)

三个必看的点。第一,只取common里的商品参与点积,但分母用的是各自全量向量的模,不这么写就会出现两个人都给同一商品打了 5 分、却因为其中一人评分多导致相似度被稀释。第二,norm_anorm_b为零时直接返回 0,否则除零异常会让整个推荐接口 500。第三,评分为 0 的隐式反馈(浏览、点击)不要混进这个函数,隐式反馈只有 0/1 两种值,更适合用 Jaccard 或共现次数:

行为类型数据形式相似度算法是否需要中心化
评分1~5 连续值余弦相似度需要
浏览/点击0/1 布尔Jaccard / 共现不需要

提示:要区分显式评分和隐式反馈,两者可以加权合并,但不要在同一个函数里混算。

3.3 Top-N 推荐:加权聚合与三个常见误用

有了相似度,推荐就是两遍循环:找邻居,聚合邻居的评分。

def recommend_for_user(target_user_id, top_n=6): matrix = build_user_item_matrix() target_vec = matrix.get(target_user_id) if not target_vec: return popular_products(top_n) scores = defaultdict(float) for uid, vec in matrix.items(): if uid == target_user_id: continue sim = cosine_similarity(target_vec, vec) if sim <= 0.0: continue for pid, score in vec.items(): if pid in target_vec: continue scores[pid] += sim * score ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True) return [pid for pid, _ in ranked[:top_n]]

这段代码的效率问题在数据量上去后会暴露:两层循环的复杂度是 O(用户数 × 平均评分商品数)。先加一个简单门槛:sim <= 0直接跳过,避免把负相关用户的商品也聚合进来。第二个常见误用是分数没有中心化,购物多、打高分多的用户会压倒性占优,简单修正办法是把聚合值改成sim * (score - user_mean),让评分围绕零上下波动再做加权。第三个坑在返回阶段,top_n不要超过商品总数的三分之一,农产品类目下用户真正会买的品类就那几个,列表塞太多反而让点击率下降,后面第 5 章会给出具体调法。

3.4 冷启动兜底:新用户返回热销榜

冷启动在农产品场景里特别常见,新注册用户没有任何评分行为。两层兜底:第一层,用户没有行为数据时返回热销榜;第二层,商品没有评分时用品类均值填充。代码里已经有一个popular_products入口:

def popular_products(top_n=6): from .models import Product return list( Product.objects .select_related("category") .order_by("-rating", "-id") .values_list("id", flat=True)[:top_n] )

排序键用-rating再叠加-id,保证评分相同时新上架商品有更高优先级。这个顺序在生鲜场景里符合「上新即送曝光」的运营习惯,而且热销榜本身也是评估协同过滤效果的基准线。

4. Django 视图、模板与缓存:把推荐结果送到模板的完整链路

4.1 推荐视图:套缓存而不是裸算

算法算完只是第一步,线上响应速度才是能用的关键。视图里不直接调用上一章的函数,而是套一层缓存:

from django.shortcuts import render from django.core.cache import cache from .recommender import recommend_for_user from .models import Product def recommendation_view(request, user_id): cache_key = f"rec:{user_id}:6" rec_ids = cache.get(cache_key) if rec_ids is None: rec_ids = recommend_for_user(user_id, top_n=6) cache.set(cache_key, rec_ids, 60 * 30) products = Product.objects.filter(id__in=rec_ids).select_related("category") return render(request, "shop/recommend.html", {"products": products})

缓存键里带用户 ID 和 top_n,是为了让不同推荐位(首页 banner、详情页关联、购物车底部)的缓存互不干扰。TTL 设 30 分钟是因为农产品推荐对时效不敏感,半小时内的结果不会造成体验差异。select_related("category")解决模板里p.category.name的 N+1 查询,这类优化在 Django 里属于必做项。如果做前后端分离,把render换成JsonResponse返回rec_ids对应的商品字典即可,算法层不用改。

4.2 信号机制:评分变化后自动失效缓存

上面缓存有个隐患:用户刚给商品打了 5 分,刷新推荐列表拿到的还是旧数据。标准解法是用 Django 信号:

from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from django.core.cache import cache from .models import Rating @receiver([post_save, post_delete], sender=Rating) def invalidate_recommendation_cache(sender, instance, **kwargs): cache.delete(f"rec:{instance.user_id}:6")

post_delete也要监听,否则删评分后缓存还是旧的。这里的失效粒度是用户级,精确度已经够;如果评分行为很频繁,可以把失效粒度扩大到「该用户所有推荐位」,但没必要为省一个缓存键把代码搞复杂。缓存参数可以按下面的值去设:

配置项建议值原因
key 前缀rec:{user_id}:{top_n}推荐位之间不互相覆盖
TTL1800 秒半小时内结果可接受
失效粒度用户级实现简单,误伤可控

4.3 模板渲染:推荐位的信息密度

推荐位模板要展示的不是评分数字,而是用户做决策需要的字段:产地、价格、品类。信息密度太低,推荐算法再准也会被运营否决。

<div class="rec-grid"> {% for p in products %} <div class="rec-card"> <h3>{{ p.name }}</h3> <p class="meta">{{ p.category.name }} · {{ p.origin }}</p> <p class="price">¥{{ p.price }}</p> <a href="{% url 'product_detail' p.id %}">查看</a> </div> {% empty %} <p>暂无推荐,去热门商品逛逛</p> {% endfor %} </div>

{% empty %}分支不能省。农产品推荐系统最怕后端返回空列表时前端渲染出一片空白,用空分支给运营文案,比抛异常体面,也不会让用户误以为页面坏了。另外注意模板里不要出现p.rating,综合评分只参与排序,不参与用户决策展示。

4.4 推荐接口的访问控制

推荐接口如果被测试脚本或爬虫高频请求,协同过滤的计算压力会直接打到数据库。常见做法是给视图加一个轻量计数:用cache.add(key, 1, 60)做每分钟请求计数,超过阈值直接返回 429。这个中间件逻辑简单,两小时就能写完,却能把开发环境从偶发卡死里救出来,属于这类系统里性价比最高的防护。

5. 推荐算法的离线评估与 Django 部署验收

5.1 用留出法评估推荐质量

推荐系统上线前必须有一个可量化的验收动作。最省事的是留出法:把每个用户的评分按时间排序,最后一条当测试集,其余当训练集,然后算 Precision@K。

def precision_at_k(test_items, rec_ids, k=6): rec = set(rec_ids[:k]) return len(rec & set(test_items)) / k

这个指标的合理基线不是 100%,而是热销榜的命中率。如果协同过滤命中率连热销榜都不如,说明评分数据噪声太大,先回头检查数据质量,而不是继续调参数。

5.2 三个必调参数

评分中心化:把聚合值从sim * score改成sim * (score - user_mean),消除爱打高分用户的系统性偏差,通常能让 Precision@K 提升 2~5 个百分点。相似度阈值:sim小于 0.1 的邻居直接丢弃,减少噪声聚合,代价是覆盖率略降。top_n:生鲜场景 6~10 个比较合适,超过 15 个后点击率会明显下降,因为用户没有耐心翻第二屏。

5.3 部署到 Linux 的验收清单

python manage.py check --deploy python manage.py migrate python manage.py collectstatic --noinput gunicorn mysite.wsgi:application -w 4 -b 127.0.0.1:8000

check --deploy会输出一串生产环境警告,逐一处理后上线。用宝塔面板的话,在站点设置里把运行模式切到 Python,进程填gunicorn mysite.wsgi即可。最后一条验证:用两个评分完全不同的账号打开推荐页,确认返回的商品列表不同;给商品重新打分后,列表在最长 30 分钟窗口内会翻新。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询