關聯式資料函式庫在處理大量資料和複雜查詢時,效能和擴充套件性可能受限。圖資料函式庫則更擅長處理關聯和路徑分析,因此將 Steam 遊戲資料從 MySQL 遷移至圖資料函式庫能提升查詢效率。本文使用 Python 的 igraph 函式庫,示範如何將使用者的遊戲遊玩和購買資料轉換為圖資料結構,並進一步建構遊戲推薦系統。過程中,我們會先為使用者和遊戲建立獨特的 ID,再將這些 ID 與遊玩時數、購買紀錄等資訊一同轉換成圖資料函式庫的節點和邊。最後,利用 igraph 提供的路徑分析功能,結合 Jaccard 相似度等演算法,設計更精準的遊戲推薦系統。

資料轉換與圖資料函式庫建立

在進行資料轉換之前,我們首先需要了解原始資料的結構和內容。根據提供的程式碼,play_datapurchase_data 分別代表了使用者的遊戲遊玩記錄和購買記錄。

步驟1:資料準備與理解

首先,我們需要檢查 play_datapurchase_data 的內容,以瞭解資料的結構。

print(play_data[:10])
purchase_query = 'SELECT id, game_name FROM steam_purchase'
purchase_data = query_mysql(purchase_query, password=PASSWORD)
print(purchase_data[:10])

內容解密:

  • play_data 包含了使用者的ID、玩過的遊戲名稱以及遊玩時間。
  • purchase_data 則包含了使用者的ID和購買的遊戲名稱。

步驟2:生成唯一的使用者和遊戲ID

為了在圖資料函式庫中使用igraph函式庫,我們需要為使用者和遊戲節點分配一個從0開始的整數ID。

users = set([row[0] for row in play_data] + [row[0] for row in purchase_data])
user_ids = {user_id: igraph_id for igraph_id, user_id in enumerate(users)}
print(len(user_ids))

games = set([row[1] for row in play_data] + [row[1] for row in purchase_data])
game_ids = {game_id: igraph_id for igraph_id, game_id in enumerate(games, len(user_ids))}
print(len(game_ids))

內容解密:

  • 使用 enumerate() 方法為使用者和遊戲生成唯一的igraph ID。
  • 使用者ID從0開始,而遊戲ID則從最後一個使用者ID之後開始,以避免ID衝突。

步驟3:驗證生成的ID

為了確認生成的ID正確無誤,我們需要檢查最高使用者ID和最低遊戲ID。

print(sorted(user_ids.values(), reverse=True)[:10])
print(sorted(game_ids.values(), reverse=False)[:10])

all_ids = sorted(list(user_ids.values()) + list(game_ids.values()))
assert all_ids == list(range(len(all_ids)))

內容解密:

  • 透過比較最高使用者ID和最低遊戲ID,確認ID分配的正確性。
  • 使用 assert 陳述式確保所有ID是連續且唯一的。

步驟4:建立圖資料函式庫

現在,我們可以開始建立圖資料函式庫了。首先,需要匯入igraph函式庫並建立一個空的有向圖。

import igraph as ig
g = ig.Graph(directed=True)

內容解密:

  • 使用 ig.Graph(directed=True) 建立一個有向圖。

步驟5:新增節點和節點屬性

接下來,我們需要新增節點及其屬性到圖中。

user_ids = dict(sorted(user_ids.items(), key=lambda item: item[1]))
game_ids = dict(sorted(game_ids.items(), key=lambda item: item[1]))

steam_user_ids = list(user_ids.keys())
steam_game_ids = list(game_ids.keys())

g.add_vertices(len(steam_user_ids) + len(steam_game_ids))
assert len(g.vs) == len(steam_user_ids) + len(steam_game_ids)

all_steam_ids = steam_user_ids + steam_game_ids
node_types = ['user' for _ in steam_user_ids] + ['game' for _ in steam_game_ids]

g.vs['steam_id'] = all_steam_ids
g.vs['type'] = node_types

內容解密:

  • 對使用者和遊戲ID進行排序,以保持與igraph ID的一致性。
  • 新增節點及其屬性(Steam ID和型別)到圖中。

步驟6:新增邊和邊屬性

最後,我們需要新增表示使用者與遊戲之間關係的邊。

purchase_edges = [[user_ids[user], game_ids[purchase]] for user, purchase in purchase_data]
play_edges = [[user_ids[user], game_ids[game], hours] for user, game, hours in play_data]

g.add_edges([(n, m) for n, m, _ in play_edges])
g.es['hours'] = [hours for _, _, hours in play_edges]

g.add_edges(purchase_edges)

edge_type = ['PLAYED' for _ in play_edges] + ['PURCHASED' for _ in purchase_edges]
g.es['edge_type'] = edge_type

內容解密:

  • 生成表示使用者玩過或購買遊戲的邊。
  • 新增邊屬性(遊玩時間和邊型別)到圖中。

透過上述步驟,我們成功地將關聯式資料函式庫中的資料轉換為圖資料函式庫,並使用igraph函式庫進行了表示。這個過程不僅展現了資料轉換的技術細節,也體現了圖資料函式庫在表示複雜關係資料時的優勢。

從關聯式資料函式庫轉換至圖形資料函式庫的資料模型轉換

在前面的章節中,我們使用關聯式資料函式庫 MySQL 來儲存和查詢 Steam 遊戲的購買和遊玩資料。然而,隨著資料量的增長和查詢複雜度的提高,關聯式資料函式庫的效能和可擴充套件性可能會成為瓶頸。為瞭解決這個問題,我們可以將資料模型轉換為圖形資料函式庫,以利用其在路徑查詢和關聯分析方面的優勢。

建立 igraph.Graph 物件

首先,我們需要建立一個 igraph.Graph 物件來儲存我們的資料。我們可以使用 igraph 函式庫提供的 API 來建立節點和邊,並將資料從關聯式資料函式庫匯入圖形資料函式庫。

import igraph as ig

# 建立一個空的圖形
g = ig.Graph(directed=True)

# 新增節點和邊
g.add_vertices(len(steam_users) + len(steam_games))
g.add_edges([(user_id, game_id) for user_id, game_id in steam_purchase.iterrows()])

內容解密:

這段程式碼建立了一個空的圖形 g,並使用 add_vertices 方法新增節點,使用 add_edges 方法新增邊。我們使用 steam_userssteam_games 資料框的長度來決定節點的數量,並使用 steam_purchase 資料框中的資料來建立使用者和遊戲之間的購買關係。

驗證資料匯入正確性

在匯入資料後,我們需要驗證資料是否正確匯入。我們可以使用以下程式碼來檢查特定使用者的購買記錄數量是否正確:

user_id_ex = g.vs.select(steam_id_eq='151603712')[0].index
purchased_ex = g.es.select(_source_eq=user_id_ex, edge_type='PURCHASED')
print(len(list(purchased_ex)))

內容解密:

這段程式碼首先使用 g.vs.select 方法找到特定使用者的節點索引,然後使用 g.es.select 方法找到與該使用者相關的購買記錄邊。最後,列印出購買記錄的數量,以驗證資料是否正確匯入。

使用圖形資料函式庫進行路徑分析

圖形資料函式庫的一個主要優勢是其在路徑查詢和關聯分析方面的能力。我們可以使用 get_all_simple_paths 方法來找到特定使用者可能感興趣的遊戲。

paths = g.get_all_simple_paths(user_id_ex, cutoff=3, mode='all')
print(paths[:10])

內容解密:

這段程式碼使用 get_all_simple_paths 方法找到從特定使用者節點開始的所有簡單路徑,路徑長度不超過 3。我們使用 cutoff 引數來限制路徑長度,使用 mode 引數來指定邊的方向。

遊戲推薦

我們可以使用找到的路徑來進行遊戲推薦。首先,我們需要過濾掉長度小於 4 的路徑,並提取出第四個節點(即遊戲節點)的索引。

rec_game_ids = [path[3] for path in paths if len(path) == 4]
game_names = [g.vs[game_id]['steam_id'] for game_id in rec_game_ids]

內容解密:

這段程式碼使用列表推導式來過濾掉長度小於 4 的路徑,並提取出第四個節點的索引。然後,使用另一個列表推導式來根據遊戲節點的索引找到對應的遊戲名稱。

去除已購買或遊玩的遊戲

為了避免推薦已購買或遊玩的遊戲,我們需要去除這些遊戲。

neighbors = g.neighbors(user_id_ex)
purchased_games = [g.vs[node_id]['steam_id'] for node_id in neighbors]
game_names = [game for game in game_names if game not in purchased_games]

內容解密:

這段程式碼首先使用 neighbors 方法找到特定使用者的鄰居節點(即已購買或遊玩的遊戲)。然後,使用列表推導式來根據鄰居節點的索引找到對應的遊戲名稱。最後,使用另一個列表推導式來去除已購買或遊玩的遊戲。

統計遊戲頻率

最後,我們可以使用 Counter 類別來統計遊戲的頻率。

from collections import Counter
game_frequency = Counter(game_names)
print(game_frequency)

內容解密:

這段程式碼使用 Counter 類別來統計遊戲名稱的頻率,並列印出結果。

從關聯式資料函式庫轉換至圖資料函式庫的資料模型轉換

在前面的章節中,我們使用圖來對資料進行根據路徑的分析。當涉及路徑時,圖是表示和查詢實體之間關係的理想資料模型。我們探討了簡單的方法來向使用者推薦遊戲,但現實世界中的推薦引擎通常使用更複雜的方法。

我們的推薦系統

現在我們已經將資料儲存在 Python 圖中,讓我們進一步設計一個更強健的推薦流程,這通常是使用圖資料完成的。

讓我們扮演解決方案工程師或資料科學家的角色,根據我們的 Steam 資料撰寫一個遊戲推薦系統。推薦系統在導向客戶的應用程式中被大量使用,以向使用者展示他們可能感興趣的產品。產品推薦通常根據行為相似的使用者已經玩過和購買過的遊戲。

通用的 MySQL 到 igraph 方法

這次,我們將撰寫一套可重複使用的通用方法,從 MySQL 表中的列建立 igraph 圖。這些函式將被設計為建立一個異質的、二分的、有向圖,給定一組列名。

首先,我們來撰寫一個主要函式 mysql_to_graph()。該方法需要接受一個 MySQL 表名、該表的來源和目標列名,以及一列邊權重。在本例中,我們將使用 steam_play 表,其中我們的權重是每個遊戲(目標)被使用者(來源)玩過的小時數。我們將按步驟順序排列這些方法,以便更容易實作:

  1. 在我們的新方法中,我們可以使用 f 字串和替換生成具有所需搜尋條件的 SQL 查詢。SELECT 後面的列名和要從中選擇的表現在可以使用 mysql_to_graph 接受的引數動態設定。然後,我們可以使用在「使用 MySQL 查詢 SQL」一節的步驟 4 中定義的方法 query_mysql() 從 MySQL 資料函式庫取得列表的列表:
def mysql_to_graph(table, source, target, weights, password):
    import igraph as ig
    sql_query = f'SELECT {source}, {target}, {weights} FROM {table}'
    data = query_mysql(sql_query, password=PASSWORD)

內容解密:

  • mysql_to_graph 函式接受表名、來源列、目標列、權重列和密碼作為引數。
  • 使用 f 字串動態生成 SQL 查詢,根據提供的列名從指定的表中選擇資料。
  • query_mysql 函式用於執行 SQL 查詢並傳回結果。
  1. 現在我們有了資料,可以使用一系列列表推導式將資料分成來源節點和目標節點。這有助於我們以合理的格式分配 igraph ID。在撰寫主要的 mysql_to_graph() 方法之後,我們將撰寫一個單獨的方法 create_igraph_ids(),以建立 {Steam ID: igraph ID} 對的字典:
source_nodes = sorted(list(set([source for source, _, _ in data])))
target_nodes = sorted(list(set([target for _, target, _ in data])))
source_igraph_ids = create_igraph_ids(source_nodes)
target_igraph_ids = create_igraph_ids(target_nodes, len(source_igraph_ids))

內容解密:

  • 使用列表推導式從資料中提取唯一的來源和目標節點。
  • create_igraph_ids 函式用於為節點分配唯一的 igraph ID。
  1. 然後,我們需要使用建立的字典中的配對生成 igraph 邊緣列表。我們可以使用另一個列表推導式,使用來源和目標作為鍵。最後,在建立圖之前,我們必須從 MySQL 傳回的列表中提取邊權重,以便稍後新增到圖邊緣:
edges = [(source_igraph_ids[source], target_igraph_ids[target]) for source, target, _ in data]
weights = [weight for _, _, weight in data]

內容解密:

  • 使用列表推導式建立邊緣列表,將來源和目標節點對映到它們的 igraph ID。
  • 從資料中提取權重。
  1. 現在,所有資料都以正確的格式新增到 igraph 圖中。首先,我們必須使用 ig.Graph() 建立一個空的有向 Graph() 物件。然後,我們必須使用 g.add_vertices() 新增節點,等於兩個節點列表的長度。透過存取圖的 g.vs 屬性,使用 source_igraph_idstarget_igraph_ids 字典鍵,將內部 ID(在我們的案例中是 Steam ID)新增到節點。再次使用 g.vs,透過建立我們的型別字串列表(來源和目標),新增節點型別,在 Steam 資料的情況下,分別對應於使用者和遊戲。最後,可以從包含來源和目標 igraph ID 的邊緣列表中新增邊緣,並且可以透過存取圖的 g.es 屬性,以列表方式新增邊權重和玩過的小時數。然後傳回我們的圖 g,以便我們可以在單獨的函式中分析它:
g = ig.Graph(directed=True)
g.add_vertices(len(source_nodes + target_nodes))
g.vs['internal_id'] = list(source_igraph_ids.keys()) + list(target_igraph_ids.keys())
g.vs['type'] = ['source' for _ in source_nodes] + ['target' for _ in target_nodes]
g.add_edges(edges)
g.es['weight'] = weights
return g

內容解密:

  • 建立一個有向圖並新增節點,節點數量等於來源和目標節點的總數。
  • 為節點新增內部 ID 和型別屬性。
  • 新增邊緣及其權重。
  1. 在我們定義的前一個 mysql_to_graph 方法中,我們呼叫了單獨的使用者定義函式 create_igraph_ids(),我們需要單獨定義它。我們可以像本章前面那樣,使用字典推導式並利用 Python 的 enumerate() 方法,為每個節點建立 igraph ID。那麼,我們的 create_graph_ids 方法需要的引數只是一個節點名稱列表和 enumerate() 的起始索引:
def create_igraph_ids(nodes, from_index=0):
    igraph_ids = {internal_id: igraph_id for igraph_id, internal_id in enumerate(nodes, from_index)}
    return igraph_ids

內容解密:

  • 使用字典推導式為節點建立唯一的 igraph ID。
  • enumerate() 用於為節點分配連續的 ID。

現在,我們有了一個可重複使用的通用方法,可以從描述任何具有來源、目標和該關係權重的關係的 MySQL 資料建立圖。回到我們的 Steam 使用案例,在我們的圖中,我們有使用者、他們玩過的遊戲以及他們玩這些遊戲的時間長度。這足夠我們提出關於現有使用者可能喜歡購買的遊戲建議。

更複雜的推薦系統

早些時候,在本章中,我們根據使用者是否玩過與指定使用者相同的遊戲以及他們玩過的遊戲,做出了非常簡單的遊戲推薦。然而,這是一個非常簡單的推薦系統示例。使用我們的圖,我們可以實作一個更強健的系統,該系統可以投入生產,接受使用者 ID 作為輸入,並使用更複雜的方法(即 Jaccard 相似度)推薦遊戲。其他相似度方法也可以使用 igraph 應用,例如 Dice 相似度和逆對數加權相似度,這也是比較節點群組的有效方法。選擇 Jaccard 相似度是因為它的簡單性和普遍良好的效能,但您可以自由嘗試不同的相似度方法。