本文介紹如何運用 Neo4j 和 igraph 處理圖資料,並以旅行規劃案例示範。首先,我們從 Neo4j 擷取城市節點和路徑關係資料,轉換成 igraph 圖形物件,方便後續計算。接著,使用 igraph 內建的 Dijkstra 演算法,計算出起點到終點的最短路徑,並列印出路徑上的城市名稱。為了更精確地計算距離,我們使用 geopy 函式庫計算地理距離,並將其新增為圖形的邊屬性。最後,我們示範如何將計算結果寫入 CSV 檔案,再匯入 Neo4j 資料函式庫,方便後續查詢和分析。
使用圖資料函式庫最佳化旅行規劃
在前面的章節中,我們已經成功地從Neo4j資料函式庫中提取了城市之間的旅行路徑。現在,我們將利用Python和igraph函式庫來建立一個旅行時間圖,以便進一步分析和最佳化旅行規劃。
步驟1:處理節點資料
首先,我們需要處理從Neo4j查詢結果中提取的節點資料。由於節點可能出現在多條路徑中,因此我們需要去除重複的節點。為此,我們可以使用一個臨時的字典來儲存節點,其中鍵是節點的ID。
node_attributes = {node['node_id']: node for node in nodes}.values()
內容解密:
這段程式碼建立了一個字典,其中鍵是節點的ID,值是節點本身。然後,使用.values()方法將字典的值轉換回一個列表,去除重複的節點。
步驟2:提取節點屬性和建立igraph ID對映
接下來,我們需要提取節點的屬性和建立igraph ID對映。
node_ids = [node['node_id'] for node in node_attributes]
names = [node['name'] for node in node_attributes]
populations = [node['population'] for node in node_attributes]
latitudes = [node['latitude'] for node in node_attributes]
longitudes = [node['longitude'] for node in node_attributes]
igraph_ids = {node['node_id']: i for i, node in enumerate(node_attributes)}
內容解密:
這段程式碼提取了節點的各種屬性,並建立了一個字典igraph_ids,將原始的節點ID對映到igraph中的整數ID。
步驟3:建立邊緣列表和邊緣屬性
現在,我們需要建立邊緣列表和邊緣屬性。
travel_time_paths = [path['rels'] for path in result]
edge_list = []
edge_attributes = []
for path, times in zip(paths, travel_time_paths):
clean_path = [node['node_id'] for node in path if node != 'AIR_TRAVEL']
travel_times = [rel['travel_time'] for rel in times]
for n, time in zip(zip(clean_path, clean_path[1:]), travel_times):
edge_list.append([igraph_ids[n[0]], igraph_ids[n[1]]])
edge_attributes.append(time)
內容解密:
這段程式碼首先提取了路徑中的邊緣屬性,然後使用迴圈建立了邊緣列表和邊緣屬性。其中,zip函式用於將相鄰的節點配對,並將對應的旅行時間新增到邊緣屬性中。
步驟4:建立igraph圖
最後,我們可以使用igraph函式庫建立一個有向圖,並新增節點和邊緣。
import igraph
g = igraph.Graph(directed=True)
g.add_vertices(len(node_ids))
g.vs['node_id'] = node_ids
g.vs['name'] = names
g.vs['population'] = populations
g.vs['latitude'] = latitudes
g.vs['longitude'] = longitudes
g.add_edges(edge_list)
g.es['travel_time'] = edge_attributes
內容解密:
這段程式碼建立了一個有向圖,並增加了節點和邊緣。其中,add_vertices方法用於新增節點,add_edges方法用於新增邊緣,vs和es屬性用於設定節點和邊緣的屬性。
驗證結果
為了驗證結果,我們可以列印出圖中的邊緣,並在Neo4j Browser中執行Cypher查詢來確認結果。
print([[g.vs[edge.source]['node_id'], g.vs[edge.target]['node_id'], edge['travel_time']] for edge in g.es])
內容解密:
這段程式碼列印出了圖中的邊緣,包括起始節點ID、目標節點ID和旅行時間。
MATCH (n:City {node_id:294})-[:AIR_TRAVEL]->(m:City {node_id:401})
RETURN n, m
內容解密:
這段Cypher查詢用於在Neo4j Browser中驗證結果,查詢特定節點之間的路徑。
使用Python和Cypher最佳化旅行路線
在前面的章節中,我們已經建立了一個使用igraph函式庫的圖形,並計算了從聖地牙哥到聖約翰的最短旅行時間。現在,我們將進一步探討如何使用Python和Cypher來最佳化旅行路線。
使用igraph計算最短路徑
首先,我們使用igraph的get_shortest_paths()方法來計算從聖地牙哥到聖約翰的最短路徑。我們指定了weights引數為travel_time,以便使用旅行時間作為邊權重。
source = g.vs.select(name_eq='San Diego, CA')
target = g.vs.select(name_eq='St. Johns, NL')
shortest_path = g.get_shortest_paths(source[0], target[0], weights='travel_time')
內容解密:
g.vs.select(name_eq='San Diego, CA')用於選擇名稱為「San Diego, CA」的節點作為起點。g.get_shortest_paths()方法用於計算最短路徑,weights='travel_time'表示使用travel_time作為權重。source[0]和target[0]分別代表起點和終點的索引。
接下來,我們可以取得路徑中每個城市的名稱,並檢視最優路徑:
shortest_path = [g.vs[node]['name'] for node in shortest_path[0]]
print(shortest_path)
內容解密:
g.vs[node]['name']用於取得節點對應的城市名稱。shortest_path[0]表示最短路徑的第一條路徑(因為get_shortest_paths()傳回的是一個列表)。
計算每段路程的距離
為了取得每段路程的距離,我們可以使用output='epath'引數來取得邊的ID,然後存取這些邊的travel_time屬性。
short_path_rels = g.get_shortest_paths(source[0], target[0], weights='travel_time', output='epath')
short_path_distances = [g.es[edge]['travel_time'] for edge in short_path_rels[0]]
print(short_path_distances)
shortest_travel_time = sum(short_path_distances)
print(shortest_travel_time)
內容解密:
output='epath'用於取得最短路徑中的邊ID。g.es[edge]['travel_time']用於取得每條邊的旅行時間。sum(short_path_distances)用於計算總旅行時間。
根據物理距離計算最短路徑
如果我們想要根據物理距離來計算最短路徑,可以先提取邊、緯度和經度,然後使用geopy函式庫來計算距離。
edges = [[g.vs[edge.source]['node_id'], g.vs[edge.target]['node_id']] for edge in g.es]
latitudes = {node['node_id']: node['latitude'] for node in g.vs}
longitudes = {node['node_id']: node['longitude'] for node in g.vs}
from geopy.distance import geodesic
def find_distances(edges, latitudes, longitudes):
distances = []
for n, m in edges:
loc_1 = (latitudes[n], longitudes[n])
loc_2 = (latitudes[m], longitudes[m])
distance = geodesic(loc_1, loc_2).km
distances.append(int(distance))
return distances
distances = find_distances(edges, latitudes, longitudes)
g.es['distance'] = distances
內容解密:
geodesic(loc_1, loc_2).km用於計算兩個坐標點之間的距離(公里)。g.es['distance'] = distances用於將計算出的距離指定給圖中的邊。
然後,我們可以使用與之前相同的過程來計算根據物理距離的最短路徑。
將距離資料匯入Neo4j
為了將物理距離資料匯入Neo4j,我們首先需要提取邊和節點的資料,然後計算距離,最後將結果寫入CSV檔案並匯入Neo4j。
edges_cypher = 'MATCH (n)-[:AIR_TRAVEL]->(m) RETURN n.node_id, m.node_id'
nodes_cypher = 'MATCH (n) RETURN n.node_id, n.latitude, n.longitude'
connection = Neo4jConnect('bolt://localhost:7687', 'admin', 'testpython')
edges = connection.query(edges_cypher).data()
lat_longs = connection.query(nodes_cypher).data()
connection.close()
edges = [[edge['n.node_id'], edge['m.node_id']] for edge in edges]
latitudes = {node['n.node_id']: node['n.latitude'] for node in lat_longs}
longitudes = {node['n.node_id']: node['n.longitude'] for node in lat_longs}
distances = find_distances(edges, latitudes, longitudes)
distances = list(zip(edges, distances))
import csv
with open('chapter05/data/distances.csv', 'w', newline='') as c:
writer = csv.writer(c)
for edge in distances:
writer.writerow(edge)
內容解密:
MATCH (n)-[:AIR_TRAVEL]->(m) RETURN n.node_id, m.node_id用於提取圖中所有邊的起點和終點ID。MATCH (n) RETURN n.node_id, n.latitude, n.longitude用於提取所有節點的ID、緯度和經度。
最後,使用Cypher將CSV檔案中的距離資料匯入Neo4j:
LOAD CSV from 'file:///distances.csv' as row
MATCH (n:City {node_id: toInteger(row[0])})
MATCH (m:City {node_id: toInteger(row[1])})
MATCH (n)-[r:AIR_TRAVEL]->(m)
SET r.distance = toInteger(row[2])
內容解密:
LOAD CSV用於從CSV檔案中讀取資料。MATCH (n:City {node_id: toInteger(row[0])})等陳述式用於根據CSV檔案中的資料匹配相應的節點和邊。SET r.distance = toInteger(row[2])用於將距離資料更新到相應的邊上。
管道開發
在前一章中,我們建立了一個靜態的Neo4j圖形,查詢和分析了它,並更新了其功能。這種型別的解決方案可能會在生產環境中使用,在那裡它可能會被用來為應用程式提供結果。
然而,這種型別的過程中仍有一些元素我們尚未涵蓋。在生產系統中,節點和邊可能定期讀取和寫入圖形資料函式庫,通常以小批次進行。複雜的處理可能在Neo4j之外進行,使用Python或其他語言,然後再將資料匯入。在第6章“管道開發”中,我們將研究一個複雜的圖形資料管道的例子,並探討如何使其可靠和高效。
摘要
我們在本章中涵蓋了很多內容,現在您應該對記憶體內資料函式庫(如Neo4j)的強大功能有了很好的理解。我們首先研究瞭如何設定Neo4j例項,並快速啟動和執行。
接著,我們研究瞭如何使用Cypher查詢語言對Neo4j資料函式庫執行查詢。對於一些讀者來說,這將是第一次接觸Cypher,我們向您介紹了這種語言與其他查詢語言(如SQL)的相似之處。為了更好地理解如何使用Cypher,Neo4j網站(https://neo4j.com/developer/cypher/)是一個很好的資源。
我們接著研究瞭如何使用Cypher和Python的cypher套件將資料儲存在Neo4j圖形資料函式庫中,並開始與圖形資料函式庫互動。這最終導致我們探索了一個推薦最佳旅行路線的使用案例,並分析了兩個點之間的最佳路徑。在這裡,我們介紹了Dijkstra演算法(https://isaaccomputerscience.org/concepts/dsa_search_dijkstra?examBoard=all&stage=all),並用它來計算兩個點之間的最短路徑。這模擬了一個管道和日常應用程式(如Google Maps或Uber的路由系統)。
管道開發
本章將涉及作為一名進階的圖形資料科學家,直接參與構建生產級別的架構。在這裡,我們將傳授我們作為圖形實踐者多年來積累的所有經驗。
本章的使用案例將是開發一個可以用於觀察客戶購買習慣的架構,其最終目標是構建一個可以在新(未見過的)資料新增到圖形時使用的推薦系統。這將非常類別似於串流媒體服務,在那裡,您將獲得產品推薦,而不是“您可能會喜歡這部電影”的推薦。
技術需求
我們將使用Jupyter筆記本執行我們的程式設計練習,這需要python>=3.8.0,以及以下需要使用pip install命令在環境中安裝的套件:
- neo4j==5.5.0
- Faker==17.0.0
對於本章,您還需要安裝Neo4j Desktop。如果尚未安裝Neo4j,請參閱第5章“使用圖形資料函式庫”的技術需求部分,因為需要它來跟隨本章中的教程。
圖形管道開發
在上一章中,我們學習瞭如何將Python與Neo4j資料函式庫介面。我們利用Neo4j的長期圖形儲存解決方案,建立了一個更真實、類別似生產環境的系統,可以根據多個引數查詢以找到航空旅行路線。
我們透過編寫大量資料到圖形資料函式庫來設定它。在設定資料函式庫時,我們知道我們希望它包含哪些資料,並編寫了Cypher查詢以將資料批次匯入。所產生的圖形的重點是向前端網頁應用程式等提供讀取查詢結果。這些讀取查詢僅在我們對資料提出問題時才對資料函式庫執行。
然而,實際上,這些圖形資料函式庫系統通常作為應用程式的後端,頻繁且自動地向圖形資料函式庫傳送查詢。這些查詢可能不僅由直接使用者行為驅動,還可以執行以被動收集和計算資訊。
導向零售的圖形資料函式庫
在本文中,我們將設計一個後端系統,以處理由網頁應用程式或行動應用程式產生的請求。在一個具體例子中,我們將探討為零售商提供產品推薦給客戶的方法所需的步驟。我們將構建後端資料函式庫,使其能夠處理大量小型的讀取和寫入查詢,透過模擬使用者和使用者互動為線上零售商服務。與之前一樣,我們將使用Neo4j和Python來實作這一點,充分利用兩者的優勢。
# 以下是一個範例程式碼,用於建立Neo4j圖形資料函式庫
from neo4j import GraphDatabase
# 連線到Neo4j資料函式庫
driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password"))
# 建立一個新的圖形資料函式庫
with driver.session() as session:
session.run("CREATE (n:Product {name: 'Product A'})")
session.run("CREATE (n:Product {name: 'Product B'})")
session.run("CREATE (n:Customer {name: 'Customer 1'})")
session.run("CREATE (n:Customer {name: 'Customer 2'})")
# 建立產品之間的關係
session.run("MATCH (p1:Product {name: 'Product A'}), (p2:Product {name: 'Product B'}) CREATE (p1)-[:SIMILAR_TO {similarity: 0.8}]->(p2)")
# 建立客戶與產品之間的關係
session.run("MATCH (c:Customer {name: 'Customer 1'}), (p:Product {name: 'Product A'}) CREATE (c)-[:PURCHASED]->(p)")
session.run("MATCH (c:Customer {name: 'Customer 2'}), (p:Product {name: 'Product B'}) CREATE (c)-[:PURCHASED]->(p)")
# 關閉與Neo4j資料函式庫的連線
driver.close()
內容解密:
此範例程式碼展示瞭如何使用Python連線到Neo4j資料函式庫,並建立一個新的圖形資料函式庫。它首先建立了兩個產品節點和兩個客戶節點,然後建立了產品之間的相似關係,以及客戶與產品之間的購買關係。這種關係可以用於後續的產品推薦。
- 連線到Neo4j資料函式庫:使用
GraphDatabase.driver方法連線到本地的Neo4j資料函式庫,指定了連線URL和驗證資訊。 - 建立新的圖形資料函式庫:使用Cypher查詢語言建立新的節點和關係。首先建立了兩個產品節點和兩個客戶節點。
- 建立產品之間的關係:使用Cypher查詢語言建立產品之間的相似關係,指定了相似度為0.8。
- 建立客戶與產品之間的關係:使用Cypher查詢語言建立客戶與產品之間的購買關係,將客戶節點與產品節點連線起來。
- 關閉與Neo4j資料函式庫的連線:使用
driver.close()方法關閉與Neo4j資料函式庫的連線。
這個範例展示瞭如何使用Python和Neo4j建立一個簡單的圖形資料函式庫,並為後續的產品推薦做好準備。