基于Apriori关联规则的电影推荐系统
1、效果图
2、算法原理
Apriori算法是一种用于挖掘关联规则的频繁项集算法,它采用逐层搜索的迭代方法来发现数据库中项集之间的关系并形成规则。
其核心思想是利用Apriori性质来压缩搜索空间,即如果一个项集是非频繁的,那么它的所有父集也是非频繁的,反之亦然。
Apriori算法的过程包括连接和剪枝两个主要步骤。在连接步骤中,算法会生成候选项集,这些候选项集是由前一次迭代发现的频繁项集通过连接操作产生的。在剪枝步骤中,算法会去除那些支持度低于用户定义的最小支持度的项集。
根据生成的关联规则实现给用户推送精准的物品推荐。
3、案例说明
3.1、评分表
现有电影评分表如下:
用户 | 评分的电影 |
---|---|
1 | 肖申克的救赎、霸王别姬、茶馆 |
2 | 霸王别姬、阿甘正传 |
3 | 肖申克的救赎、霸王别姬、阿甘正传 |
4 | 肖申克的救赎、美丽人生 |
5 | 霸王别姬、美丽人生 |
6 | 霸王别姬、美丽人生 |
7 | 肖申克的救赎、美丽人生 |
8 | 肖申克的救赎、霸王别姬、美丽人生、茶馆 |
9 | 肖申克的救赎、霸王别姬、美丽人生 |
根据表中的信息,评分的电影有['肖申克的救赎','霸王别姬','美丽人生','阿甘正传','茶馆']五部,总共有9个用户进行评分。
注意: 用户评分的电影组合越多,那么关联评分的可能性就越高。
这就需要根据经验,人为设置一个最小的评分组合出现次数。
因为上面的评分用户数量太少,所以就设置为2,就是只要用户评过分的电影出现2部以上,就认为他有关联其他电影评分的可能性存在。
因此需要统计用户评过分的每部电影出现的次数。
因为有5部电影,因此一个用户评分组合包含的电影数量的可能性为 1-5,需要逐级分析。
3.2、只评分1部电影的组合
这里把用户只评分1部电影作为一个组合,是因为2部电影的评分组合是基于1部电影评分的组合产生的。
统计电影评分中的组合,可以得到下表:
评分的电影组合 | 出现次数 |
---|---|
肖申克的救赎 | 6 |
霸王别姬 | 7 |
美丽人生 | 6 |
阿甘正传 | 2 |
茶馆 | 2 |
从表中可以看出,5部电影评分的用户数量,都超过了设置的2次,就把这些电影评分组合称作频繁电影评分组合;因此,上表中的所有电影组合都是频繁评分的;就会得到下面这个频繁电影评分组合表:
评分的电影组合 | 出现次数 |
---|---|
肖申克的救赎 | 6 |
霸王别姬 | 7 |
美丽人生 | 6 |
阿甘正传 | 2 |
茶馆 | 2 |
虽然上述两个表看起来没有任何变化,但是实际上进行了一个对比:出现次数与设置的最小出现次数2进行了对比,只是都合格了。
3.3、只评分2部电影的组合
接着对用户给2部电影评分形成的组合进行统计(顺序不同不重复计数),可以得到下表:
评分的电影组合 | 出现次数 |
---|---|
肖申克的救赎、霸王别姬 | 4 |
肖申克的救赎、美丽人生 | 4 |
肖申克的救赎、阿甘正传 | 1 |
肖申克的救赎、茶馆 | 2 |
霸王别姬、美丽人生 | 4 |
霸王别姬、阿甘正传 | 2 |
霸王别姬、茶馆 | 2 |
美丽人生、阿甘正传 | 0 |
美丽人生、茶馆 | 1 |
阿甘正传、茶馆 | 0 |
根据第一次的操作顺序,对每个评分组合与最小出现的次数2进行比较。保留出现次数不小于2的组合,得到下表:
评分的电影组合 | 出现次数 |
---|---|
肖申克的救赎、霸王别姬 | 4 |
肖申克的救赎、美丽人生 | 4 |
肖申克的救赎、茶馆 | 2 |
霸王别姬、美丽人生 | 4 |
霸王别姬、阿甘正传 | 2 |
霸王别姬、茶馆 | 2 |
上面两个表的情况就清晰的反映了,一些评分组合的出现次数太少,意味着关联评分的可能性已经很小了,保留下来的就是有关联评分可能性的。
3.4、只评分3部电影的组合
注意,如果用户只评分2部电影是关联性已经很小的组合时,那么只评分3部电影的组合可能性就会更小。因此不需要统计包含该2部电影的评分3部电影的组合。
同样的,对3部电影评分组合进行统计:
评分的电影组合 | 出现次数 |
---|---|
肖申克的救赎、霸王别姬、美丽人生 | 2 |
肖申克的救赎、霸王别姬、茶馆 | 2 |
肖申克的救赎、美丽人生、茶馆 | 0 |
霸王别姬、美丽人生、阿甘正传 | 0 |
霸王别姬、美丽人生、茶馆 | 1 |
霸王别姬、阿甘正传、茶馆 | 0 |
会发现有些组合没有出现在表中,比如{肖申克的救赎、霸王别姬、阿甘正传},原因就是刚刚说明的,因为{肖申克的救赎、阿甘正传}的出现次数小于2,因此就将评分包含{肖申克的救赎、阿甘正传}的3部电影组合排除掉了;
然后,继续统计满足最小出现次数不小于2的组合,得到下表:
评分的电影组合 | 出现次数 |
---|---|
肖申克的救赎、霸王别姬、美丽人生 | 2 |
肖申克的救赎、霸王别姬、茶馆 | 2 |
发现,还有2个评分组合满足我们的最小计数。
3.5、只评分4部电影的组合
根据上面的原则,4个产品组合的表格如下:
评分的电影组合 | 出现次数 |
---|---|
肖申克的救赎、霸王别姬、美丽人生、茶馆 | 1 |
发现组合的出现次数小于2,因此排除,就会得到下表:
评分的电影组合 | 出现次数 |
---|---|
无 | 无 |
意味着对电影的评分组合分类已经到头了,已经得到了不同个数的评分组合出现的次数。但是这还不够,因为需要进一步考虑如何根据这些组合推荐电影。
注意: 既然已经得到了电影评分组合M,那么根据用户A已经评分的电影O,推荐有该电影的评分组合M里其他没有评过分的电影就可以了。
原因如下: 如果想让用户在对【肖申克的救赎】这部电影评分的情况下,完成{肖申克的救赎、霸王别姬}的评分组合;那么评分【霸王别姬】的可能性是建立在评分组合{肖申克的救赎}出现6次的基础上,而不是{肖申克的救赎、霸王别姬}出现4次的基础上。
因此,需要对每种关联评分的可能性进行计算,直观地观察每种关联评分的可能性的大小。
3.6、找出强关联评分的规则
什么是规则?规则就是我评分了一部电影的情况下,再去评分另一部电影;注意,这里的评分是有顺序的;先【肖申克的救赎】后【霸王别姬】和先【霸王别姬】后【肖申克的救赎】是不同的规则。
我们这里以{肖申克的救赎、霸王别姬、茶馆}举例,该组合总共出现2次,可能出现的关联评分情况如下:
已评分电影组合 | 已评分电影组合次数 | 推荐电影 | 期望的电影组合 | 可能性 |
---|---|---|---|---|
肖申克的救赎、霸王别姬 | 4 | 茶馆 | 肖申克的救赎、霸王别姬、茶馆(2次) | 2/4=0.5 |
肖申克的救赎、茶馆 | 2 | 霸王别姬 | 肖申克的救赎、霸王别姬、茶馆(2次) | 2/2=1 |
霸王别姬、茶馆 | 2 | 肖申克的救赎 | 肖申克的救赎、霸王别姬、茶馆(2次) | 2/2=1 |
肖申克的救赎 | 6 | 霸王别姬、茶馆 | 肖申克的救赎、霸王别姬、茶馆(2次) | 2/6=0.33 |
霸王别姬 | 7 | 肖申克的救赎、茶馆 | 肖申克的救赎、霸王别姬、茶馆(2次) | 2/7=0.29 |
茶馆 | 2 | 肖申克的救赎、霸王别姬 | 肖申克的救赎、霸王别姬、茶馆(2次) | 2/2=1 |
上表中,最终希望用户达成的评分组合是{肖申克的救赎、霸王别姬、茶馆},因此根据用户已评分的组合情况,对应推荐未评分的电影组合,可能性计算的方式就是最终期望电影组合出现的次数除以已评分电影组合出现的次数,也可以理解为最终期望组合占用户已评分组合的比例。
可以看到,可能性有高有低;实际分析中,不可能出现100%评分的情况,因此就要根据经验或者要求设计一个最低的可能性,比如至少可能性大于70%,我们才有意愿去把电影组合推荐给用户,这就会得到最终的关联规则表:
已评分电影组合 | 已评分电影组合次数 | 推荐电影 | 期望的电影组合 | 可能性 |
---|---|---|---|---|
肖申克的救赎、茶馆 | 2 | 霸王别姬 | 肖申克的救赎、霸王别姬、茶馆(2次) | 2/2=1 |
霸王别姬、茶馆 | 2 | 肖申克的救赎 | 肖申克的救赎、霸王别姬、茶馆(2次) | 2/2=1 |
茶馆 | 2 | 肖申克的救赎、霸王别姬 | 肖申克的救赎、霸王别姬、茶馆(2次) | 2/2=1 |
上表的意思就是,当发现有用户满足上述已评分电影组合,就把该电影组合推荐给用户,用户评分的可能性是100%。
比如用户对【茶馆】进行了评分,那么就推荐【肖申克的救赎、霸王别姬】,用户对它们评分的可能性为100%。
4、Apriori 算法
4.1、名词解释
现有一数据集合有 N 条记录,关注其中元素组合 X、Y:
规则:根据组合 X 推出组合 Y
支持度:
在 N 条记录中,X 和 Y 被同时满足的记录数有 n 个,那么支持度就是 n/N;
支持度计数就是 n;
在进行分析时,需要人为设置最小支持度为 m/N ,其中 m 就是最小支持度计数;
不满足最小支持度的元素组合就会被剔除;
在10个用户中,X 和 Y 同时被评分的用户数至少有 3 个,那么支持度就是 3/10,支持度计数就是 3;
进行分析时,设置最小支持度为 m/N,那最小支持度计数就是 m,不满足的产品组合就会被剔除;
置信度:
在满足组合 X 的情况下,满足组合 Y 的概率;
也可以说是同时满足 X 和 Y 的记录数在只满足 X 的记录数的比率;
项集:
一个组合中包含几个元素,就叫做几项集。如:组合 X 为 {x1} 就叫做1项集;组合 Y 为 {Y1、Y2} 就叫做2项集;
频繁项集:
某个评分组合出现的频率大于或等于设置的支持度,那么这个评分组合就是频繁项集;
频繁项集的非空子集必须是频繁项集;
频繁项集中含有 i 个元素,就叫做频繁 i 项集。
5、算法流程
6、主体代码
# -*- coding: utf-8 -*-
"""
@contact: 微信 1257309054
@file: apriori_recommend.py
@time: 2024/3/31 0:06
@author: LDC
使用Apriori关联规则实现一个频繁项集推荐算法
"""
import math
import time
def get_item_set(data):
'''
获取项的字典
:param data: 数据集
:return: 项的字典
'''
item_set = set()
for d in data:
item_set = item_set | set(d)
return item_set
def apriori(item_set, data, min_support=0.20):
'''
获取频繁项集
:param item_set: 项的字典
:param data: 数据集
:param min_support: 最小支持度,默认为0.20
:return: None
'''
# 初始化存储非频繁项集的列表
infrequent_list = []
# 初始化存储频繁项集的列表
frequent_list = []
# 初始化存储频繁项集的支持度的列表
frequent_support_list = []
# 遍历获取 n-项集
for n in range(1, len(item_set) + 1):
c = []
supports = []
if len(frequent_list) == 0:
# 计算 1-项集
for item in item_set:
items = {
item}
support = calc_support(data, items)
# 如果支持度大于等于最小支持度就为频繁项集
if support >= min_support:
c.append(items)
supports.append(support)
else:
infrequent_list.append(items)
else:
# 计算 n-项集,n > 1
for last_items in frequent_list[-1]:
for item in item_set:
if item > list(last_items)[-1]:
items = last_items.copy()
items.add(item)
# 如果items的子集没有非频繁项集才计算支持度
if is_infrequent(infrequent_list, items) is False:
support = calc_support(data, items)
# 如果支持度大于等于最小支持度就为频繁项集
if support >= min_support:
c.append(items)
supports.append(support)
else:
infrequent_list.append(items)
frequent_list.append(c)
frequent_support_list.append(supports)
print(f"{n}-项集: {c} , 支持度分别为: {supports}")
return infrequent_list, frequent_list, frequent_support_list
def is_infrequent(infrequent_list, items):
'''
判断是否属于非频繁项集的超集
:param infrequent_list: 非频繁项集列表
:param items: 项集
:return: 是否属于非频繁项集的超集
'''
for infrequent in infrequent_list:
if infrequent.issubset(items):
return True
return False
def calc_support(data, items):
'''
计算 support
:param data: 数据集
:param items: 项集
:return: 计算好的支持度
'''
cnt = 0
for d in data:
if items.issubset(d):
cnt += 1
return round(cnt / len(data), 2)
def generate_rules(frequent_list, data, min_confidence=0.60):
'''
根据频繁项集和最小置信度生成规则
:param frequent_list: 存储频繁项集的列表
:param data: 数据集
:param min_confidence: 最小置信度
:return: 规则
'''
rule_key_set = set()
rules = []
for frequent in frequent_list:
for items in frequent:
if len(items) > 1:
for n in range(1, math.ceil(len(items) / 2) + 1):
front_set_list = get_all_combine(list(items), n)
for front_set in front_set_list:
back_set = items - front_set
confidence = calc_confidence(front_set, items, data)
if confidence >= min_confidence:
rule = (front_set, back_set, confidence)
key = f'{front_set} ==> {back_set} , confidence: {confidence}'
if key not in rule_key_set:
rule_key_set.add(key)
rules.append(rule)
print(f"规则{len(rules)}: {key}")
return rules
def get_all_combine(data_set, length):
'''
在指定数据集种获取指定长度的所有组合
:param data_set: 数据集
:param length: 指定的长度
:return: 所有符合约束的组合
'''
def dfs(cur_index, cur_arr):
if cur_index < len(data_set):
cur_arr.append(data_set[cur_index])
if len(cur_arr) == length:
combine_list.append(set(cur_arr))
else:
for index in range(cur_index + 1, len(data_set)):
dfs(index, cur_arr.copy())
combine_list = []
for start_index in range(len(data_set)):
dfs(start_index, [])
return combine_list
def calc_confidence(front_set, total_set, data):
'''
计算规则 X==>Y 的置信度
:param front_set: X
:param total_set: X ∪ Y
:param data: 数据集
:return: 返回规则 X==>Y 的置信度
'''
front_cnt = 0
total_cnt = 0
for d in data:
if front_set.issubset(d):
front_cnt += 1
if total_set.issubset(d):
total_cnt += 1
return round(total_cnt / front_cnt, 2)
if __name__ == '__main__':
# recommend_by_apriori(1)
# 记录开始时间
s = time.time()
# 数据集
data = [['肖申克的救赎', '霸王别姬', '茶馆'],
['霸王别姬', '阿甘正传'],
['肖申克的救赎', '霸王别姬', '阿甘正传'],
['肖申克的救赎', '美丽人生'],
['霸王别姬', '美丽人生'],
['霸王别姬', '美丽人生'],
['肖申克的救赎', '美丽人生'],
['肖申克的救赎', '霸王别姬', '美丽人生', '茶馆'],
['肖申克的救赎', '霸王别姬', '美丽人生'],
]
# 获取项的字典
item_set = get_item_set(data)
print("项的字典:", item_set)
# 根据 Apriori算法 获取 n-频繁项集
infrequent_list, frequent_list, frequent_support_list = apriori(item_set, data, min_support=0.20)
# 生成规则
rule_set = generate_rules(frequent_list, data, min_confidence=0.60)
print('rule_set', rule_set)
# 推荐
user_data = {
'茶馆'} # 用户列表
recommend_id = [] # 推荐列表
for rule in rule_set:
# 置信度要大于0.7
if rule[-1] < 0.7:
continue
# 用户数据与规则有交集,则添加到推荐列表
if user_data & rule[0]:
recommend_id += list(rule[1])
recommend_id = list(set(recommend_id))
print('推荐recommend_id', recommend_id)
# 输出总用时
print("总用时:", (time.time() - s), "s")
输出:
项的字典: {'霸王别姬', '肖申克的救赎', '阿甘正传', '茶馆', '美丽人生'}
1-项集: [{'霸王别姬'}, {'肖申克的救赎'}, {'阿甘正传'}, {'茶馆'}, {'美丽人生'}] , 支持度分别为: [0.78, 0.67, 0.22, 0.22, 0.67]
2-项集: [{'霸王别姬', '肖申克的救赎'}, {'茶馆', '肖申克的救赎'}, {'霸王别姬', '阿甘正传'}, {'霸王别姬', '茶馆'}, {'霸王别姬', '美丽人生'}, {'肖申克的救赎', '美丽人生'}] , 支持度分别为: [0.44, 0.22, 0.22, 0.22, 0.44, 0.44]
3-项集: [{'霸王别姬', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'茶馆', '肖申克的救赎'}, {'霸王别姬', '阿甘正传'}, {'霸王别姬', '茶馆'}, {'霸王别姬', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'肖申克的救赎', '美丽人生'}] , 支持度分别为: [0.44, 0.22, 0.22, 0.22, 0.22, 0.22, 0.44, 0.22, 0.22, 0.44]
4-项集: [{'霸王别姬', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'茶馆', '肖申克的救赎'}, {'霸王别姬', '阿甘正传'}, {'霸王别姬', '茶馆'}, {'霸王别姬', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'肖申克的救赎', '美丽人生'}] , 支持度分别为: [0.44, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.44, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.44]
5-项集: [{'霸王别姬', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'霸王别姬', '茶馆', '肖申克的救赎'}, {'茶馆', '肖申克的救赎'}, {'霸王别姬', '阿甘正传'}, {'霸王别姬', '茶馆'}, {'霸王别姬', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'霸王别姬', '肖申克的救赎', '美丽人生'}, {'肖申克的救赎', '美丽人生'}] , 支持度分别为: [0.44, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.44, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.22, 0.44]
规则1: {'肖申克的救赎'} ==> {'霸王别姬'} , confidence: 0.67
规则2: {'茶馆'} ==> {'肖申克的救赎'} , confidence: 1.0
规则3: {'阿甘正传'} ==> {'霸王别姬'} , confidence: 1.0
规则4: {'茶馆'} ==> {'霸王别姬'} , confidence: 1.0
规则5: {'美丽人生'} ==> {'霸王别姬'} , confidence: 0.67
规则6: {'肖申克的救赎'} ==> {'美丽人生'} , confidence: 0.67
规则7: {'美丽人生'} ==> {'肖申克的救赎'} , confidence: 0.67
规则8: {'茶馆'} ==> {'霸王别姬', '肖申克的救赎'} , confidence: 1.0
规则9: {'霸王别姬', '茶馆'} ==> {'肖申克的救赎'} , confidence: 1.0
规则10: {'茶馆', '肖申克的救赎'} ==> {'霸王别姬'} , confidence: 1.0
rule_set [({'肖申克的救赎'}, {'霸王别姬'}, 0.67), ({'茶馆'}, {'肖申克的救赎'}, 1.0), ({'阿甘正传'}, {'霸王别姬'}, 1.0), ({'茶馆'}, {'霸王别姬'}, 1.0), ({'美丽人生'}, {'霸王别姬'}, 0.67), ({'肖申克的救赎'}, {'美丽人生'}, 0.67), ({'美丽人生'}, {'肖申克的救赎'}, 0.67), ({'茶馆'}, {'霸王别姬', '肖申克的救赎'}, 1.0), ({'霸王别姬', '茶馆'}, {'肖申克的救赎'}, 1.0), ({'茶馆', '肖申克的救赎'}, {'霸王别姬'}, 1.0)]
推荐recommend_id ['霸王别姬', '肖申克的救赎']
总用时: 0.004998922348022461 s