基於Apriori關聯規則的電影推薦系統(附python程式碼)

轻松学编程發表於2024-03-31

基於Apriori關聯規則的電影推薦系統

1、效果圖

demo點我跳轉

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

相關文章