房源推荐专家系统拆解:模糊推理与POI数据实践
2026/9/15 13:11:23 网站建设 项目流程

简介:面向人工智能课程项目与毕业设计场景,这份复旦大学2022年房源推荐专家系统项目包提供了从源码到文档的完整参考。项目围绕房源推荐任务,涵盖Python推荐算法实现、地图可视化、POI数据处理等关键模块,适合高年级本科生或研究生在课程学习、毕业设计或竞赛准备中对照实践。压缩包共39个文件,以17个Python源码文件为核心,辅以TeX设计文档、Markdown说明、CSV数据集、JSON配置及多张效果展示图片,整体约28.71MB,目录结构清晰,便于按模块研读。内容包含系统核心代码、可运行资源、项目设计报告与使用说明,可直接用于课程报告撰写、系统复现或毕业设计功能扩展;资源内还提供可视化效果图片与POI数据,便于理解算法效果与数据来源。目前已有66人学习下载,对于希望快速搭建同类房源推荐系统的开发者具有切实参考价值。

1. 一份能直接在本地跑的AI课设:房源推荐专家系统的骨架

拿到“复旦大学2022年人工智能课程项目,房源推荐专家系统.zip”这个包时,我第一反应是看看它到底能不能不用改代码直接run起来。解压后,里面是标准的Django工程:manage.py、dwelling应用、fuzzy.py、process.py、score.py,还有上海市POI数据压缩包和两份CSV。它不是那种只丢给你一个train.py就完事的玩具,而是一个完整的“专家系统+Web展示”闭环——用户在前端输入预算、面积、通勤偏好,系统用模糊推理和POI空间计算给出一份排序房源列表。这套源码的可读性不错,模块边界也很清楚,适合人工智能课程设计、毕业设计参考,也适合想搞懂“专家系统在生产环境怎么写”的人。下面我按推理引擎、数据处理、Web集成三块拆开讲,最后给出调参时的实际经验。

2. 专家系统推理核心:模糊规则与评分矩阵设计

这期拆解的核心是fuzzy.py和score.py。很多做毕业设计的同学习惯直接堆if-else,但这个项目用一条可配置的“规则链”把房源推荐变成了可解释的专家系统。先看整体结构:专家系统由事实库、规则库、推理机三部分组成。事实库是estates.csv,规则库存放在subtype.json,推理机则由fuzzy.py实现。这样设计的好处是,切换业务场景时只需要修改JSON,不用改代码。

2.1 事实库与规则库的分离

先看事实库。estates.csv每行是一条房源,字段包括房型、面积、总价、朝向、所在区域、经纬度等。process.py处理后的fuzzy_estates.csv会额外带上到最近地铁站、到商业中心等派生指标。为什么需要这份派生数据?因为直接拿原始地址做推理没有可比较性,必须先把POI距离计算成数值型特征。

2.2 subtype.json定义模糊集合

subtype.json是规则库,它定义了每个特征列的模糊区间。下面是一个典型结构:

{ "area": { "labels": ["small", "medium", "large"], "small": [0, 30, 60], "medium": [40, 70, 100], "large": [80, 120, 200] }, "distance_to_subway": { "labels": ["near", "mid", "far"], "near": [0, 0.5, 1.0], "mid": [0.8, 1.5, 2.5], "far": [2.0, 3.0, 5.0] } }

这里每个模糊集合用三个数字表示三角形隶属函数的左边界、顶点、右边界。例如“small”面积区间[0, 30, 60],表示30平米时隶属度为1.0,小于0或大于60则为0。注意“distance_to_subway”使用的是公里数,所以JSON里是0.5、1.0这样的浮点数。labels数组不是必须的,但你可以在代码里用它遍历所有标签。

2.3 fuzzy.py的隶属度计算

fuzzy.py的主要任务是把数值映射为隶属度。常见的实现方式是:

def triangular_membership(x, a, b, c): """三角形隶属函数,a为左边界,b为顶点,c为右边界""" if x <= a or x >= c: return 0.0 if x <= b: return (x - a) / (b - a) return (c - x) / (c - b) def get_fuzzy_vector(row, feature_cfg): result = {} for feature, func_cfg in feature_cfg.items(): val = float(row[feature]) memberships = {} for label in func_cfg["labels"]: a, b, c = func_cfg[label] memberships[label] = round(triangular_membership(val, a, b, c), 4) result[feature] = memberships return result

参数说明:row是房源字典,feature_cfg是subtype.json中某个字段的配置;返回值是每个标签下的隶属度。比如面积51平米,对“medium”的隶属度为0.6333。之后推理机再根据规则库中的“如果面积小且距离近,则推荐度高”等规则加权,而不是简单判断面积是否小于阈值。这里没有复杂的去模糊化,因为最终评分是各规则贡献值累加,不需要输出一个连续控制量,直接按总分排序即可。

2.4 score.py的多因子加权评分

score.py把隶属度结果映射为最终的推荐分。权重表需要反复调试,下面是一份可用的初始配置:

评分因子子项权重
居住舒适度面积、朝向0.3
交通便利度到地铁站距离0.3
生活配套商圈、餐饮、医疗POI密度0.25
性价比每平米单价与区域均价对比0.15

注意权重的含义是“归一化后贡献的比重”,总分超过1时score.py会做归一化。实际调用中,代码会把feature_cfg和权重合并在一起,逐条生成推荐分并排序返回。这里可以给你一个参考:在复旦大学邯郸校区附近的房源测试时,交通便利度的权重从0.2调到0.4,排序结果会有明显变化,原因是校区周边本身商业配套密集,区分度主要来自地铁距离。

在推理链里,还有一个容易被忽略的步骤:规则没有写死,而是用字典驱动。score.py内部维护一个rules字典,key是模糊标签组合,value是贡献分数。例如:

rules = { ("area", "medium"): 0.4, ("distance_to_subway", "near"): 0.5, ("price_per_m2", "low"): 0.6, }

这样做的好处是可以把业务人员的经验直接翻译成配置文件,课程答辩时也能讲清楚“哪条规则起了主要作用”。

2.5 为什么不用机器学习而用专家系统

课程设计的时间通常只有两三个月,机器学习方案会卡在标注数据、特征工程和模型解释性上。专家系统的优势在于规则透明,评委问起“为什么推荐这套房”可以直接回溯到某条规则。缺点是需要人工维护规则,这也是项目把规则外置到JSON、把POI数据单独挂出来的原因。你在做毕设时,只要把subtype.json里的区间改成自己城市的数据,系统就能复用。另外,毕业生要小心不要踩“拿着机器学习模型去比专家系统效果”的坑,两者评价指标完全不同,专家系统比的是覆盖面和可解释性。

3. 上海市POI数据清洗与房源画像构建

这章处理的是另一个重头戏:上海市POI数据。没有POI,专家系统就只剩下面积和价格两个维度,推荐结果会很单调。POI是Point of Interest的缩写,指地图上的兴趣点,比如餐厅、地铁站、医院。项目里这部分由process.py和map.py负责,产出是fuzzy_estates.csv。

3.1 原始POI数据的结构

拿到“上海市POI数据.7z”后,解压出来通常是JSON或CSV,字段里面有经度、纬度、名称、大类、小类。因为POI原始数据经常有坐标系不一致的问题——上海市本地一些数据用GCJ-02,互联网地图用BD-09——所以process.py里要先做坐标转换。我们项目实测时,发现个别POI文件还是火星坐标,直接和房源坐标混用会导致距离算出几十公里的夸张结果。

3.2 process.py的清洗流程

process.py一般会做三件事:去重、换坐标、过滤类别。下面是一个参考实现:

import pandas as pd def clean_poi(raw_path, out_path): data = pd.read_json(raw_path, lines=True) # 去掉缺少经纬度的记录 data = data.dropna(subset=["lon", "lat"]) # 将GCJ-02坐标统一转成标准经纬度 data[["lon", "lat"]] = data.apply( lambda r: gcj02_to_wgs84(r["lon"], r["lat"]), axis=1 ) # 提取需要的类别 data = data[data["type"].isin(["餐饮", "购物", "医疗", "教育"])] data.to_csv(out_path, index=False)

说明:gcj02_to_wgs84是常见的火星坐标转WGS84函数,这里不展开,项目里有实现。注意转换会引入亚米级误差,但房源推荐场景完全够用。清洗的关键是去重:同一POI可能出现在多个源文件里,连续两次抓取的记录会产生重复条目。你可以用经纬度+名称做联合去重。

提示:如果使用macOS或Linux直接读源码包里的POI文件,很容易遇到编码异常。这些数据可能来自Windows环境下抓取,读取时最好显式指定encoding="gbk"encoding="utf-8",具体看文件第一个非注释行的格式。

3.3 空间关联:为每套房计算POI密度

把房源点位和POI点关联,常见做法是计算固定半径内的POI数量。代码示意:

def count_pois_within_radius(estate_lon, estate_lat, poi_df, radius_km=2): # 使用Haversine公式快速过滤 res = poi_df.apply( lambda r: haversine(estate_lon, estate_lat, r["lon"], r["lat"]) <= radius_km, axis=1 ) return res.sum()

注意这种逐行计算复杂度高,如果房源数量超过几千条,建议使用KDTree或Geohash批量处理。原项目考虑到正常课程数据量不大,直接用了DataFrame的apply,实际性能也够。但答辩时会有人问“数据量大怎么优化”,你可以答:先按Geohash格网做预聚合,再在格网内计算精确距离。这里还有一个很容易踩的坑:Haversine公式的半径参数传的是米还是公里,subtype.json里写的是公里,所以radius_km=2是正确的。如果你直接拿米来传,距离从2米变2000米,推荐结果完全乱套。

3.4 将POI类别映射为推荐分

不同类别的POI对房源价值的影响不一样,不是数量越多越好。项目里通常用一张映射表:

POI大类推荐逻辑评分倾向
地铁站距离越近越好,超过3公里忽略极高
商场/购物2公里内数量越多越好
餐饮1公里内密度高加分,过多则噪音大
学校按学区房逻辑加权,具体看用户需求自定义
医院1公里内有即可,不追求密集

这张表可以在score.py里维护成字典,也可以写成独立的poi_weights.json。原项目的subtype.json只存了特征区间,POI权重多半是在score.py里写死的。我建议你将来复用的时候把这些也抽出来,方便换城市。

3.5 map.py:把结果打到地图上

map.py负责生成房源分布图,用folium或pyecharts常见。它读取fuzzy_estates.csv,按得分给房源标注颜色,输出一个可交互的HTML地图。这是课程展示中加分最快的部分。实际操作时,注意POI数据解压密码和中文路径问题,很多课程包会设置编码是GBK,如果你直接在macOS上跑会报UnicodeDecodeError,最好在process.py开头强制指定encoding参数为utf-8或gbk。

整个数据链路的运行顺序是:先解压上海市POI数据,然后运行process.py生成fuzzy_estates.csv,再运行map.py生成可视化,最后才启动Django服务。如果你发现网页里基本没有带“推荐理由”的房源,多半是fuzzy_estates.csv没有生成成功,或者CSV的列名和subtype.json配置的字段名对不上。检查一下CSV头:有没有areadistance_to_subwayprice_per_m2这三个关键列。

4. Django Web层:从一个manage.py命令到网页推荐结果

前面的推理和数据清洗都在本地脚本里完成,真正面向用户的是Django部分。很多课程项目把Django当成“套模板”的工具,但这个项目中manage.py、dwelling应用和html webpage是串在同一闭环里的。

4.1 Django项目结构

Django工程入口是manage.py,应用目录叫dwelling。整个请求链路:浏览器访问路由 → views.py调用推理引擎 → 返回渲染后的HTML。urls.py定义了两类路由:首页和结果页。通常首页是个表单,结果页是一个列表或地图页。如果你打开项目发现只有views.py没有urls.py,那可能使用了Django 1.x写法,需要手动加到根路由里。

4.2 视图函数如何调用推理引擎

views.py里的推荐视图长这样:

# dwelling/views.py from django.shortcuts import render from . import fuzzy, score def recommend(request): if request.method == "POST": params = { "max_price": request.POST.get("max_price"), "min_area": request.POST.get("min_area"), "work_lon": request.POST.get("work_lon"), "work_lat": request.POST.get("work_lat"), } estates = fuzzy.load_fuzzy_estates() ranked = fuzzy.infer(estates, params) return render(request, "result.html", {"ranked": ranked}) return render(request, "index.html")

这里fuzzy.infer内部会按参数过滤后计算得分,返回按推荐分降序的列表。注意参数从POST里取出来是字符串,必须转成float,否则后面做数值比较时会走字符串比较,出现“10000”小于“9999”这种诡异结果。建议在视图里做一层数据清洗:

try: max_price = float(request.POST.get("max_price")) except (TypeError, ValueError): max_price = 10_000_000

4.3 表单向路由渲染的动态逻辑

HTML页面里,表单的action指向/recommend,method是post。result.html可以用Django模板语言循环渲染排名,同时把评分理由一起显示出来,方便用户理解推荐逻辑。

{% for item in ranked %} <div class="card"> <h3>{{ item.name }}</h3> <span>推荐分:{{ item.score }}</span> <p>{{ item.reason }}</p> </div> {% empty %} <p>没有找到合适的房源,请调整筛选条件。</p> {% endfor %}

推荐分和reason都由score.py在排名后按规则生成。reason是可解释性的关键,比如“面积适中但离地铁站超过1.5公里,整体推荐度一般”。这个理由字符串一定要生成,答辩时评委很看重这个。

4.4 运行时的三个常见坑

提示:本地调试时,先把ALLOWED_HOSTS设为['*'],否则访问不了。

第一,settings.py里的ALLOWED_HOSTS如果为空,部署到服务器上会报DisallowedHost,本地调试可以用['*']。第二,静态文件路径:HTML里的CSS和JS最好用Django的{% static 'css/style.css' %},不要写死相对路径。第三,数据库:项目看起来没有用到models.py,数据都来自CSV,所以默认的sqlite3可以不用迁移。如果跑python manage.py migrate会创建一堆无关表,不迁移也不影响推荐功能。你可以直接python manage.py runserver启动,然后在浏览器里打开localhost:8000看效果。

5. 验证推荐结果与模糊参数调优技巧

5.1 用report里的PDF交叉验证

项目里有report/pic目录和main.tex,说明这套课设还配了LaTeX报告。报告里的图一般是房源分布地图、POI热力图和评分柱状图。拿到这些图之后,你可以反过来验证代码逻辑。比如跑通Django后在浏览器里提交一个“预算800万、面积100平、工作地点在人民广场”的查询,如果结果里推荐的房源全部落在松江或者青浦,那说明评分权重里的距离因子权重太低了,或者POI密度计算中没有把工作地点算进去。

具体做法是:复制一个真实房源,手动计算它的距离,再和score.py的输出对比。误差超过5%就要检查坐标转换函数,因为GCJ-02和BD-09混用会导致经纬度偏移数百米,距离计算自然就错了。

5.2 调整subtype.json的区间别动顶点

很多人调模糊集合时喜欢把区间范围拉大,但这会导致中点附近房源区分度降低。正确的方法是保持左边界和右边界不动,只移动顶点。比如把"medium": [40, 70, 100]改成[40, 65, 100],则65平米以上就进入“大”区间,适合用来表达“偏好大面积”的业务场景。改完JSON后,不要清空fuzzy_estates.csv,score.py会自动读取新配置。

5.3 用日志排查哪条规则在起作用

在score.py里临时加一条print或logging,输出每个房源的模糊向量和rules命中情况。例如:

logging.debug(f"area membership: {membership['area']}, hit rule: {hit_rules}")

这样你可以看到某个推荐结果到底是因为“价格低”还是“离地铁近”被顶上来的。很多毕业设计只给最终排名,答辩被追问“为什么第一名的价格比第二名高”就答不上来,提前把日志逻辑写进去就能从容应对。

5.4 最后一点验收技巧

把POI数据压缩包单独留在项目根目录,但提醒使用者先解压再运行process.py。不要直接在代码里用7z路径,Django会认不出来。验证时,用python manage.py runserver启动,打开首页提交查询,对比浏览器结果和执行process.py生成的CSV,两者应该完全一致。如果不一致,优先检查是否同时有两份estates.csv——项目根目录一份,dwelling目录一份,有时候Django读取的是当前工作目录下的文件,而脚本读取的是工程根目录下的文件,配置不对就会读到旧版本。

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

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

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

立即咨询