图模式教程
本教程涵盖 GraphFrames 的图模式查找功能。我们将使用 Apache Spark 和 GraphFrames 的图模式查找功能,在代表 Stack Exchange 站点的属性图上执行模式匹配。我们将从 互联网档案馆的 Stack Exchange 数据转储下载 stats.meta 归档文件,使用 PySpark 构建属性图,然后通过结合图查询和关系查询来挖掘属性图网络模式。
什么是图元与网络模体?
图元是大型图中的小型连通子图。网络模体是复杂网络中反复出现的模式,其出现频率显著高于随机网络。它们是复杂网络的构建模块,可用于理解网络的结构和功能。网络模体可用于识别生物网络中的功能模块、检测社交网络中的异常、检测金融网络中的洗钱和恐怖主义融资,以及预测复杂系统的行为。
我们将使用Stack Exchange数据挖掘网络模体。Stack Exchange网络是一个由用户、帖子、投票、徽章和标签组成的复杂网络。我们将使用GraphFrames从Stack Exchange数据转储构建属性图,然后使用GraphFrames的模体发现功能在图中查找网络模体。您将看到如何结合图查询和关系查询来发现图中的复杂模式。
下载 stats.meta 的 Stack Exchange 数据转储
Python教程包含一个位于graphframes stackexchange的CLI实用程序,用于从互联网档案馆下载任意站点的Stack Exchange数据转储。该命令将子域名作为参数,下载对应的7zip压缩包并将其解压至python/graphframes/tutorials/data文件夹。
Usage: graphframes [OPTIONS] COMMAND [ARGS]...
GraphFrames CLI: a collection of commands for graphframes.
Options:
--help Show this message and exit.
Commands:
stackexchange Download Stack Exchange archive for a given SUBDOMAIN.
使用 graphframes stackexchange stats.meta 下载 stats.meta.stackexchange.com 的 Stack Exchange 数据转储。
$ graphframes stackexchange stats.meta
Downloading archive from <https://archive.org/download/stackexchange/stats.meta.stackexchange.com.7z>
Downloading [####################################] 100%
Download complete: python/graphframes/tutorials/data/stats.meta.stackexchange.com.7z
Extracting archive...
Extraction complete: stats.meta.stackexchange.com
构建图
我们将使用PySpark在脚本中基于Stack Exchange数据转储构建一个属性图。数据以单个XML文件形式提供,因此我们使用spark-xml(自Spark 4.0起已内置)来加载数据,提取相关字段并构建图的节点和边。由于某些原因Spark XML会占用大量内存,我们需要将驱动器和执行器内存至少增加到4GB。
$ spark-submit --packages com.databricks:spark-xml_2.12:0.18.0 --driver-memory 4g --executor-memory 4g python/graphframes/tutorials/stackexchange.py
该脚本将在 python/graphframes/tutorials/data 文件夹中输出图的节点和边。我们现在可以使用 GraphFrames 加载图并执行模式查找。
模式发现
我们将使用 GraphFrames 在 Stack Exchange 属性图中查找模式。该脚本 演示了如何加载图、定义各种模式并在图中查找该模式的所有实例。
注意:我使用术语 node 与 vertex 可互换,edge 与 link 或 relationship 可互换。API 是 graphframes.GraphFrame.vertices 和 graphframes.GraphFrame.edges,但某些文档中写的是 relationships。我们需要为 g.vertices 添加别名 g.nodes,并为 g.edges 添加别名 g.relationships 和 g.links。
要快速运行该脚本,请使用以下命令:
spark-submit --packages graphframes:graphframes:0.8.3-spark3.5-s_2.12 python/graphframes/tutorials/motif.py
让我们逐行了解它的功能。该脚本首先导入必要的模块,并定义一些用于可视化g.find()返回路径的实用函数。请注意,如果您给python/graphframes/tutorials/download.py CLI指定不同的子域名,您需要更改STACKEXCHANGE_SITE变量。
import pyspark.sql.functions as F
from graphframes import GraphFrame
from pyspark import SparkContext
from pyspark.sql import DataFrame, SparkSession
# Initialize a SparkSession
spark: SparkSession = (
SparkSession.builder.appName("Stack Overflow Motif Analysis")
# Lets the Id:(Stack Overflow int) and id:(GraphFrames ULID) coexist
.config("spark.sql.caseSensitive", True)
.getOrCreate()
)
sc: SparkContext = spark.sparkContext
sc.setCheckpointDir("/tmp/graphframes-checkpoints")
# Change me if you download a different stackexchange site
STACKEXCHANGE_SITE = "stats.meta.stackexchange.com"
BASE_PATH = f"python/graphframes/tutorials/data/{STACKEXCHANGE_SITE}"
从data文件夹加载图的节点和边,并统计节点和边的类型。我们对节点和边进行重新分区,以便为我们的模式搜索提供并行性。GraphFrames 倾向于将节点/顶点和边/关系进行缓存。
#
# Load the nodes and edges from disk, repartition, checkpoint [plan got long for some reason] and cache.
#
# We created these in stackexchange.py from Stack Exchange data dump XML files
NODES_PATH: str = f"{BASE_PATH}/Nodes.parquet"
nodes_df: DataFrame = spark.read.parquet(NODES_PATH)
# Repartition the nodes to give our motif searches parallelism
nodes_df = nodes_df.repartition(50).checkpoint().cache()
# We created these in stackexchange.py from Stack Exchange data dump XML files
EDGES_PATH: str = f"{BASE_PATH}/Edges.parquet"
edges_df: DataFrame = spark.read.parquet(EDGES_PATH)
# Repartition the edges to give our motif searches parallelism
edges_df = edges_df.repartition(50).checkpoint().cache()
查看我们必须处理的节点类型:
# What kind of nodes we do we have to work with?
node_counts = (
nodes_df
.select("id", F.col("Type").alias("Node Type"))
.groupBy("Node Type")
.count()
.orderBy(F.col("count").desc())
# Add a comma formatted column for display
.withColumn("count", F.format_number(F.col("count"), 0))
)
node_counts.show()
+---------+------+
|Node Type| count|
+---------+------+
| Badge|43,029|
| Vote|42,593|
| User|37,709|
| Answer| 2,978|
| Question| 2,025|
|PostLinks| 1,274|
| Tag| 143|
+---------+------+
查看我们需要处理的边类型:
# What kind of edges do we have to work with?
edge_counts = (
edges_df
.select("src", "dst", F.col("relationship").alias("Edge Type"))
.groupBy("Edge Type")
.count()
.orderBy(F.col("count").desc())
# Add a comma formatted column for display
.withColumn("count", F.format_number(F.col("count"), 0))
)
edge_counts.show()
+----------+------+
| Edge Type| count|
+----------+------+
| Earns|43,029|
| CastFor|40,701|
| Tags| 4,427|
| Answers| 2,978|
| Posts| 2,767|
| Asks| 1,934|
| Links| 1,180|
|Duplicates| 88|
+----------+------+
组合节点类型
注意:您无需运行本节中的代码,这些代码仅供参考。我们之前加载的数据已经准备就绪可供使用。 请直接跳转到 创建 GraphFrames 并运行后续步骤 :)
目前,GraphFrames 存在一个限制:目前仅支持单一节点类型和边类型。 由于仅提供单一节点类型,我们的 GraphFrame 节点中包含多个字段。我通过将所有类型的属性合并到单一节点类中,将不同类型的节点统一为单一类型。我为每种节点类型创建了一个 Type 字段,然后将所有字段合并到全局的 nodes_df DataFrame 中。这个 Type 列随后可在关系型 数据框 操作中用于区分节点类型。
这一限制是一个应在未来修复的烦恼,届时将能够在 GraphFrame 中拥有多种节点类型。实际上这对生产力影响不大,但意味着当您执行 DataFrame.select 操作时,必须为每个节点 Type 选择特定列,否则执行 DataFrame.show() 时数据框的宽度会过大而难以阅读。
以下是在 python/graphframes/tutorials/stackexchange.py 中实现的方法。
#
# Form the nodes from the UNION of posts, users, votes and their combined schemas
#
all_cols: List[Tuple[str, T.StructField]] = list(
set(
list(zip(posts_df.columns, posts_df.schema))
+ list(zip(post_links_df.columns, post_links_df.schema))
+ list(zip(comments_df.columns, comments_df.schema))
+ list(zip(users_df.columns, users_df.schema))
+ list(zip(votes_df.columns, votes_df.schema))
+ list(zip(tags_df.columns, tags_df.schema))
+ list(zip(badges_df.columns, badges_df.schema))
)
)
all_column_names: List[str] = sorted([x[0] for x in all_cols])
def add_missing_columns(df: DataFrame, all_cols: List[Tuple[str, T.StructField]]) -> DataFrame:
"""Add any missing columns from any DataFrame among several we want to merge."""
for col_name, schema_field in all_cols:
if col_name not in df.columns:
df = df.withColumn(col_name, F.lit(None).cast(schema_field.dataType))
return df
# Now apply this function to each of your DataFrames to get a consistent schema
posts_df = add_missing_columns(posts_df, all_cols).select(all_column_names)
post_links_df = add_missing_columns(post_links_df, all_cols).select(all_column_names)
users_df = add_missing_columns(users_df, all_cols).select(all_column_names)
votes_df = add_missing_columns(votes_df, all_cols).select(all_column_names)
tags_df = add_missing_columns(tags_df, all_cols).select(all_column_names)
badges_df = add_missing_columns(badges_df, all_cols).select(all_column_names)
assert (
set(posts_df.columns)
== set(post_links_df.columns)
== set(users_df.columns)
== set(votes_df.columns)
== set(all_column_names)
== set(tags_df.columns)
== set(badges_df.columns)
)
创建 GraphFrames
现在我们使用 nodes_df 和 edges_df DataFrames 创建一个 @pydoc(graphframes.GraphFrame)。我们将使用此对象在图中查找模式。
回到我们的主题 :) 现在是时候创建我们的 graphframes.GraphFrame 对象了。它拥有许多强大的API,包括用于在图中查找主题的 方法。
g = GraphFrame(nodes_df, edges_df)
g.vertices.show(10)
print(f"Node columns: {g.vertices.columns}")
g.edges.sample(0.0001).show(10)
GraphFrame 对象已创建,节点列和边已显示。
# Node DataFrame is too wide to display here... because it has this many columns.
Node columns: ['id', 'AboutMe', 'AcceptedAnswerId', 'AccountId', 'AnswerCount', 'Body', 'Class', 'ClosedDate', 'CommentCount', 'CommunityOwnedDate', 'ContentLicense', 'Count', 'CreationDate', 'Date', 'DisplayName', 'DownVotes', 'ExcerptPostId', 'FavoriteCount', 'Id', 'IsModeratorOnly', 'IsRequired', 'LastAccessDate', 'LastActivityDate', 'LastEditDate', 'LastEditorDisplayName', 'LastEditorUserId', 'LinkTypeId', 'Location', 'Name', 'OwnerDisplayName', 'OwnerUserId', 'ParentId', 'PostId', 'PostTypeId', 'RelatedPostId', 'Reputation', 'Score', 'TagBased', 'TagName', 'Tags', 'Text', 'Title', 'Type', 'UpVotes', 'UserDisplayName', 'UserId', 'ViewCount', 'Views', 'VoteType', 'VoteTypeId', 'WebsiteUrl', 'WikiPostId', 'degree']
# Edge DataFrame is simpler
+--------------------+--------------------+------------+
| src| dst|relationship|
+--------------------+--------------------+------------+
|b0d39443-5b0b-42e...|3e84a1ed-1c20-413...| Answers|
|4c781826-3112-4b2...|07665936-d759-4f6...| Earns|
|a11d77a7-da09-4b0...|a9ea6e7d-7cc1-408...| CastFor|
|bd42f75a-b3ee-4d0...|fe216e41-1ae0-4c0...| Earns|
|4dd3c6be-b103-4ab...|2aa18136-59a7-498...| Earns|
|13540451-5823-417...|37966108-de38-4aa...| CastFor|
|f60ed1aa-5361-4ab...|1c5352c1-d084-47c...| Earns|
|9cb948f8-c7d5-40d...|71bc77c4-dfe7-47e...| Earns|
|03980309-e97e-402...|d0b4c366-c8d0-458...| Asks|
|b3736001-b654-419...|14920c81-232b-479...| Earns|
+--------------------+--------------------+------------+
only showing top 10 rows
验证 GraphFrames
让我们验证我们的GraphFrame对象中的所有边是否具有有效ID - 在知识图谱构建的ETL过程中常见错误,会出现指向无效位置的边。GraphFrames会尝试自我验证,但有时可能会接受无效边。
# Sanity test that all edges have valid ids
edge_count = g.edges.count()
valid_edge_count = (
g.edges.join(g.vertices, on=g.edges.src == g.vertices.id)
.select("src", "dst", "relationship")
.join(g.vertices, on=g.edges.dst == g.vertices.id)
.count()
)
# Just up and die if we have edges that point to non-existent nodes
assert (
edge_count == valid_edge_count
), f"Edge count {edge_count} != valid edge count {valid_edge_count}"
print(f"Edge count: {edge_count:,} == Valid edge count: {valid_edge_count:,}")
Edge count: 97,104 == Valid edge count: 97,104
结构模式
让我们寻找一个简单的模式:一个有向三角形。我们将在图中找到所有有向三角形的实例。@pydoc(graphframes.GraphFrame.find) 方法接受一个字符串作为参数,该参数指定了模式的结构,每次一条边,使用与 Cypher 相同的语法,边之间用分号分隔。对于三角形模式,可以表示为:(a)-[e]->(b); (b)-[e2]->(c); (c)-[e3]->(a)。边标签是可选的,这是一个有效的图查询:(a)-[]->(b)。
g.find() 方法返回一个包含模式中每个节点和边标签字段的 DataFrame。要进一步表达您感兴趣的模式,您现在可以使用关系型 DataFrame 操作来筛选、分组和聚合结果。这使得 GraphFrames 中的网络模式发现非常强大,这种类型的属性图模式最初在 graphframes 论文中定义。
关于图查询语言的完整描述请参阅GraphFrames 用户指南。让我们来看一个示例:有向三角形。我们将在图中查找所有有向三角形的实例。
# G4: Continuous Triangles
paths = g.find("(a)-[e1]->(b); (b)-[e2]->(c); (c)-[e3]->(a)")
# Show the first path
paths.show(3)
结果路径包含 find() 中每个步骤的字段;每个字段包含我们节点或边的所有属性。
+------------+------------+------------+------------+------------+------------+
| a| e1| b| e2| c| e3|
+------------+------------+------------+------------+------------+------------+
|{57198c52...|{57198c52...|{7fd044f5...|{7fd044f5...|{695b549b...|{695b549b...|
|{8f534b7c...|{8f534b7c...|{e65038cf...|{e65038cf...|{d5ea2a3d...|{d5ea2a3d...|
|{695b549b...|{695b549b...|{57198c52...|{57198c52...|{7fd044f5...|{7fd044f5...|
+------------+------------+------------+------------+------------+------------+
only showing top 3 rows
这可能看起来令人不知所措,因此在实践中您将使用DataFrame.select()(路径只是一个pyspark.sql.DataFrame)来选择感兴趣的属性。
聚合路径能够表达强大的语义。让我们统计图中每个节点和边类型的这个三角形主题的路径类型数量。
graphlet_type_df = paths.select(
F.col("a.Type").alias("A_Type"),
F.col("e1.relationship").alias("(a)-[e1]->(b)"),
F.col("b.Type").alias("B_Type"),
F.col("e2.relationship").alias("(b)-[e2]->(c)"),
F.col("c.Type").alias("C_Type"),
F.col("e3.relationship").alias("(c)-[e3]->(a)"),
)
graphlet_count_df = (
graphlet_type_df.groupby(
"A_Type", "(a)-[e1]->(b)", "B_Type", "(b)-[e2]->(c)", "C_Type", "(c)-[e3]->(a)"
)
.count()
.orderBy(F.col("count").desc())
# Add a comma formatted column for display
.withColumn("count", F.format_number(F.col("count"), 0))
)
graphlet_count_df.show()
结果显示图中唯一的连续三角形是39个问题链接循环。基于纯拓扑结构的简单模式匹配可用于知识图谱上的探索性数据分析,就像您可能在关系数据库中对表运行GROUP BY / COUNT查询以开始理解其内容一样。
+--------+-------------+--------+-------------+--------+-------------+-----+
| A_Type|(a)-[e1]->(b)| B_Type|(b)-[e2]->(c)| C_Type|(c)-[e3]->(a)|count|
+--------+-------------+--------+-------------+--------+-------------+-----+
|Question| Links|Question| Links|Question| Links| 24|
|Question| Duplicates|Question| Links|Question| Links| 4|
|Question| Links|Question| Links|Question| Duplicates| 4|
|Question| Links|Question| Duplicates|Question| Links| 4|
|Question| Duplicates|Question| Links|Question| Duplicates| 1|
|Question| Duplicates|Question| Duplicates|Question| Links| 1|
|Question| Links|Question| Duplicates|Question| Duplicates| 1|
+--------+-------------+--------+-------------+--------+-------------+-----+
让我们尝试一个不同的三角形,一个发散三角形。每次可视化一个3边模式的代码都是相同的。
# G5: Divergent Triangles
paths = g.find("(a)-[e1]->(b); (a)-[e2]->(c); (c)-[e3]->(b)")
graphlet_type_df = paths.select(
F.col("a.Type").alias("A_Type"),
F.col("e1.relationship").alias("(a)-[e1]->(b)"),
F.col("b.Type").alias("B_Type"),
F.col("e2.relationship").alias("(a)-[e2]->(c)"),
F.col("c.Type").alias("C_Type"),
F.col("e3.relationship").alias("(c)-[e3]->(b)"),
)
graphlet_count_df = (
graphlet_type_df.groupby(
"A_Type", "(a)-[e1]->(b)", "B_Type", "(a)-[e2]->(c)", "C_Type", "(c)-[e3]->(b)"
)
.count()
.orderBy(F.col("count").desc())
# Add a comma formatted column for display
.withColumn("count", F.format_number(F.col("count"), 0))
)
graphlet_count_df.show()
结果是图中按类型统计的发散三角形数量。
+--------+-------------+--------+-------------+--------+-------------+-----+
| A_Type|(a)-[e1]->(b)| B_Type|(a)-[e2]->(c)| C_Type|(c)-[e3]->(b)|count|
+--------+-------------+--------+-------------+--------+-------------+-----+
| Tag| Tags|Question| Tags|Question| Links|1,775|
| User| Asks|Question| Posts| Answer| Answers| 274|
|Question| Links|Question| Links|Question| Links| 236|
| Tag| Tags|Question| Tags|Question| Duplicates| 140|
| User| Asks|Question| Asks|Question| Links| 103|
|Question| Links|Question| Links|Question| Duplicates| 14|
|Question| Duplicates|Question| Links|Question| Links| 13|
|Question| Links|Question| Duplicates|Question| Links| 12|
| User| Asks|Question| Asks|Question| Duplicates| 8|
|Question| Duplicates|Question| Links|Question| Duplicates| 8|
|Question| Duplicates|Question| Duplicates|Question| Links| 7|
|Question| Duplicates|Question| Duplicates|Question| Duplicates| 2|
|Question| Links|Question| Duplicates|Question| Duplicates| 1|
+--------+-------------+--------+-------------+--------+-------------+-----+
(Tag)-[Tags]->(Question B); (Tag)-[Tags]->(Question C); (Question C)-[Links]->(Question B),或“一个标签被用于一个问题,该标签也被用于另一个问题,并且这两个问题相互链接。”共享标签的问题经常相互链接,这是合理的。(User)-[Asks]->(Question B); (User)-[Posts]->(Answer C); (Answer C)-[Answers]->(Question B),或“用户回答自己的问题”。- 一个相互关联问题的三角关系。
(Tag)-[Tags]->(Question B); (Tag)-[Tags]->(Question C); (Question B)-[Duplicates]->(Question C),或“一个标签出现在一对重复答案上。”- 用户提出关联问题。
属性图模式
简单的模式查找可用于探索知识图谱。也可以使用领域知识来定义和匹配已知模式,然后探索新的变体模式。这可用于应用并扩展关于知识图谱的领域知识。这是非常强大的功能!
除了按节点和边类型计数外,我们还能利用路径属性做更多分析。通过路径中节点和边的属性进行筛选、分组和聚合,可以构建属性图模式。这类复杂模式最早在描述本项目的论文中被定义(当时未正式命名):GraphFrames: An Integrated API for Mixing Graph and Relational Queries, Dave et al. 2016。它们是图查询与关系型查询的结合体,能帮助我们在图中发现复杂模式。
随着模式规模增大,它们会变得更加有趣。根据图数据的大小,Spark集群通常最多能处理五节点模式。本例中我将限制使用四路径模式,因为您可能没有用于学习的Spark集群。请记住这里讨论的是路径——通过聚合操作,一个模式可能覆盖数千个节点!
首先让我们表达我们要寻找的图模式的结构逻辑。让我们尝试 G22 - 一个三角形,其中第四个节点指向入度为2的节点。该模式为 (a)-[e1]->(b); (a)-[e2]->(c); (c)-[e3]->(b); (d)-[e4]->(b)。
从视觉上看,这种模式如下所示:
具有四个节点的最简单模式是一个3路径有向图元G30。让我们看看聚合如何使这个模式比我们最初猜测的更强大。
# G17: A directed 3-path is a surprisingly diverse graphlet
paths = g.find("(a)-[e1]->(b); (b)-[e2]->(c); (d)-[e3]->(c)")
让我们统计图中这条路径按类型划分的实例数量。分享一个来之不易的技巧:使用其模式为边设置别名。这样即使当C指向A或B而非D时,也能更轻松地读取结果。
# Visualize the four-path by counting instances of paths by node / edge type
graphlet_type_df = paths.select(
F.col("a.Type").alias("A_Type"),
F.col("e1.relationship").alias("(a)-[e1]->(b)"),
F.col("b.Type").alias("B_Type"),
F.col("e2.relationship").alias("(b)-[e2]->(c)"),
F.col("c.Type").alias("C_Type"),
F.col("e3.relationship").alias("(d)-[e3]->(c)"),
F.col("d.Type").alias("D_Type"),
)
graphlet_count_df = (
graphlet_type_df.groupby(
"A_Type",
"(a)-[e1]->(b)",
"B_Type",
"(b)-[e2]->(c)",
"C_Type",
"(d)-[e3]->(c)",
"D_Type",
)
.count()
.orderBy(F.col("count").desc())
# Add a comma formatted column for display
.withColumn("count", F.format_number(F.col("count"), 0))
)
graphlet_count_df.show()
+--------+-------------+--------+-------------+--------+-------------+--------+-------+
| A_Type|(a)-[e1]->(b)| B_Type|(b)-[e2]->(c)| C_Type|(d)-[e3]->(c)| D_Type| count|
+--------+-------------+--------+-------------+--------+-------------+--------+-------+
| Vote| CastFor| Answer| Answers|Question| CastFor| Vote|445,707|
| Vote| CastFor|Question| Links|Question| CastFor| Vote|300,017|
| Vote| CastFor| Answer| Answers|Question| Answers| Answer|117,981|
| Vote| CastFor|Question| Links|Question| Links|Question| 73,227|
| Vote| CastFor| Answer| Answers|Question| Tags| Tag| 64,510|
| Tag| Tags|Question| Links|Question| CastFor| Vote| 62,203|
| Answer| Answers|Question| Links|Question| CastFor| Vote| 56,119|
| Vote| CastFor|Question| Links|Question| Answers| Answer| 55,938|
| Vote| CastFor| Answer| Answers|Question| Links|Question| 55,139|
| Vote| CastFor|Question| Links|Question| Tags| Tag| 38,633|
| User| Posts| Answer| Answers|Question| CastFor| Vote| 37,655|
|Question| Links|Question| Links|Question| CastFor| Vote| 33,747|
| Vote| CastFor| Answer| Answers|Question| Asks| User| 23,234|
| User| Asks|Question| Links|Question| CastFor| Vote| 22,243|
| Tag| Tags|Question| Links|Question| Links|Question| 17,266|
| Answer| Answers|Question| Links|Question| Links|Question| 16,362|
| Answer| Answers|Question| Links|Question| Answers| Answer| 14,013|
| Vote| CastFor|Question| Links|Question| Asks| User| 13,252|
| Tag| Tags|Question| Links|Question| Answers| Answer| 12,920|
| User| Posts| Answer| Answers|Question| Answers| Answer| 12,105|
+--------+-------------+--------+-------------+--------+-------------+--------+-------+
only showing top 20 rows
图中有多少个这样的模式?让我们来统计它们。
graphlet_count_df.count()
104
104 - 我们通过这个模式找到了网络主题的宝藏!让我们按照模式的连续元素进行排序,以便逻辑上对它们进行分组。
graphlet_count_df.orderBy([
"A_Type",
"(a)-[e1]->(b)",
"B_Type",
"(b)-[e2]->(c)",
"C_Type",
"(d)-[e3]->(c)",
"D_Type",
], ascending=False).show(104)
+--------+-------------+--------+-------------+--------+-------------+--------+-------+
| A_Type|(a)-[e1]->(b)| B_Type|(b)-[e2]->(c)| C_Type|(d)-[e3]->(c)| D_Type| count|
+--------+-------------+--------+-------------+--------+-------------+--------+-------+
| Vote| CastFor|Question| Links|Question| Tags| Tag| 38,633|
| Vote| CastFor|Question| Links|Question| Links|Question| 73,227|
| Vote| CastFor|Question| Links|Question| Duplicates|Question| 3,337|
| Vote| CastFor|Question| Links|Question| CastFor| Vote|300,017|
| Vote| CastFor|Question| Links|Question| Asks| User| 13,252|
| Vote| CastFor|Question| Links|Question| Answers| Answer| 55,938|
| Vote| CastFor|Question| Links| Answer| Posts| User| 18|
| Vote| CastFor|Question| Links| Answer| Links|Question| 18|
| Vote| CastFor|Question| Links| Answer| CastFor| Vote| 36|
| Vote| CastFor|Question| Duplicates|Question| Tags| Tag| 1,292|
| Vote| CastFor|Question| Duplicates|Question| Links|Question| 1,556|
| Vote| CastFor|Question| Duplicates|Question| Duplicates|Question| 693|
| Vote| CastFor|Question| Duplicates|Question| CastFor| Vote| 8,205|
| Vote| CastFor|Question| Duplicates|Question| Asks| User| 418|
| Vote| CastFor|Question| Duplicates|Question| Answers| Answer| 2,423|
| Vote| CastFor| Answer| Answers|Question| Tags| Tag| 64,510|
| Vote| CastFor| Answer| Answers|Question| Links|Question| 55,139|
| Vote| CastFor| Answer| Answers|Question| Duplicates|Question| 3,941|
| Vote| CastFor| Answer| Answers|Question| CastFor| Vote|445,707|
| Vote| CastFor| Answer| Answers|Question| Asks| User| 23,234|
| Vote| CastFor| Answer| Answers|Question| Answers| Answer|117,981|
| User| Posts| Answer| Answers|Question| Tags| Tag| 7,164|
| User| Posts| Answer| Answers|Question| Links|Question| 4,494|
| User| Posts| Answer| Answers|Question| Duplicates|Question| 378|
| User| Posts| Answer| Answers|Question| CastFor| Vote| 37,655|
| User| Posts| Answer| Answers|Question| Asks| User| 2,614|
| User| Posts| Answer| Answers|Question| Answers| Answer| 12,105|
| User| Asks|Question| Links|Question| Tags| Tag| 3,169|
| User| Asks|Question| Links|Question| Links|Question| 6,119|
| User| Asks|Question| Links|Question| Duplicates|Question| 331|
| User| Asks|Question| Links|Question| CastFor| Vote| 22,243|
| User| Asks|Question| Links|Question| Asks| User| 1,064|
| User| Asks|Question| Links|Question| Answers| Answer| 4,599|
| User| Asks|Question| Links| Answer| Posts| User| 1|
| User| Asks|Question| Links| Answer| Links|Question| 1|
| User| Asks|Question| Links| Answer| CastFor| Vote| 2|
| User| Asks|Question| Duplicates|Question| Tags| Tag| 264|
| User| Asks|Question| Duplicates|Question| Links|Question| 338|
| User| Asks|Question| Duplicates|Question| Duplicates|Question| 134|
| User| Asks|Question| Duplicates|Question| CastFor| Vote| 1,528|
| User| Asks|Question| Duplicates|Question| Asks| User| 86|
| User| Asks|Question| Duplicates|Question| Answers| Answer| 374|
| Tag| Tags|Question| Links|Question| Tags| Tag| 9,332|
| Tag| Tags|Question| Links|Question| Links|Question| 17,266|
| Tag| Tags|Question| Links|Question| Duplicates|Question| 931|
| Tag| Tags|Question| Links|Question| CastFor| Vote| 62,203|
| Tag| Tags|Question| Links|Question| Asks| User| 3,037|
| Tag| Tags|Question| Links|Question| Answers| Answer| 12,920|
| Tag| Tags|Question| Links| Answer| Posts| User| 8|
| Tag| Tags|Question| Links| Answer| Links|Question| 8|
| Tag| Tags|Question| Links| Answer| CastFor| Vote| 16|
| Tag| Tags|Question| Duplicates|Question| Tags| Tag| 666|
| Tag| Tags|Question| Duplicates|Question| Links|Question| 828|
| Tag| Tags|Question| Duplicates|Question| Duplicates|Question| 341|
| Tag| Tags|Question| Duplicates|Question| CastFor| Vote| 3,715|
| Tag| Tags|Question| Duplicates|Question| Asks| User| 215|
| Tag| Tags|Question| Duplicates|Question| Answers| Answer| 965|
|Question| Links|Question| Links|Question| Tags| Tag| 5,220|
|Question| Links|Question| Links|Question| Links|Question| 10,140|
|Question| Links|Question| Links|Question| Duplicates|Question| 387|
|Question| Links|Question| Links|Question| CastFor| Vote| 33,747|
|Question| Links|Question| Links|Question| Asks| User| 1,740|
|Question| Links|Question| Links|Question| Answers| Answer| 7,330|
|Question| Links|Question| Links| Answer| Posts| User| 2|
|Question| Links|Question| Links| Answer| Links|Question| 2|
|Question| Links|Question| Links| Answer| CastFor| Vote| 4|
|Question| Links|Question| Duplicates|Question| Tags| Tag| 102|
|Question| Links|Question| Duplicates|Question| Links|Question| 163|
|Question| Links|Question| Duplicates|Question| Duplicates|Question| 85|
|Question| Links|Question| Duplicates|Question| CastFor| Vote| 611|
|Question| Links|Question| Duplicates|Question| Asks| User| 45|
|Question| Links|Question| Duplicates|Question| Answers| Answer| 308|
|Question| Links| Answer| Answers|Question| Tags| Tag| 4|
|Question| Links| Answer| Answers|Question| Links|Question| 4|
|Question| Links| Answer| Answers|Question| CastFor| Vote| 10|
|Question| Links| Answer| Answers|Question| Asks| User| 2|
|Question| Links| Answer| Answers|Question| Answers| Answer| 17|
|Question| Duplicates|Question| Links|Question| Tags| Tag| 328|
|Question| Duplicates|Question| Links|Question| Links|Question| 511|
|Question| Duplicates|Question| Links|Question| Duplicates|Question| 38|
|Question| Duplicates|Question| Links|Question| CastFor| Vote| 2,019|
|Question| Duplicates|Question| Links|Question| Asks| User| 125|
|Question| Duplicates|Question| Links|Question| Answers| Answer| 559|
|Question| Duplicates|Question| Duplicates|Question| Tags| Tag| 19|
|Question| Duplicates|Question| Duplicates|Question| Links|Question| 20|
|Question| Duplicates|Question| Duplicates|Question| Duplicates|Question| 17|
|Question| Duplicates|Question| Duplicates|Question| CastFor| Vote| 98|
|Question| Duplicates|Question| Duplicates|Question| Asks| User| 9|
|Question| Duplicates|Question| Duplicates|Question| Answers| Answer| 67|
| Answer| Answers|Question| Links|Question| Tags| Tag| 8,187|
| Answer| Answers|Question| Links|Question| Links|Question| 16,362|
| Answer| Answers|Question| Links|Question| Duplicates|Question| 811|
| Answer| Answers|Question| Links|Question| CastFor| Vote| 56,119|
| Answer| Answers|Question| Links|Question| Asks| User| 2,758|
| Answer| Answers|Question| Links|Question| Answers| Answer| 14,013|
| Answer| Answers|Question| Links| Answer| Posts| User| 2|
| Answer| Answers|Question| Links| Answer| Links|Question| 2|
| Answer| Answers|Question| Links| Answer| CastFor| Vote| 4|
| Answer| Answers|Question| Duplicates|Question| Tags| Tag| 224|
| Answer| Answers|Question| Duplicates|Question| Links|Question| 316|
| Answer| Answers|Question| Duplicates|Question| Duplicates|Question| 198|
| Answer| Answers|Question| Duplicates|Question| CastFor| Vote| 1,330|
| Answer| Answers|Question| Duplicates|Question| Asks| User| 110|
| Answer| Answers|Question| Duplicates|Question| Answers| Answer| 1,174|
+--------+-------------+--------+-------------+--------+-------------+--------+-------+
第四行引起了我的注意 - 关联问题的投票匹配数达到300,017条:(Vote A)-[CastFor]->(Question B); (Question B)-[Links]->(Question C); (Vote D)-[CastFor]->(Question C)。这为比较关联问题的受欢迎程度提供了方法!让我们计算关联问题之间的相关性。
# A user answers an answer that answers a question that links to an answer.
linked_vote_paths = paths.filter(
(F.col("a.Type") == "Vote") &
(F.col("e1.relationship") == "CastFor") &
(F.col("b.Type") == "Question") &
(F.col("e2.relationship") == "Links") &
(F.col("c.Type") == "Question") &
(F.col("e3.relationship") == "CastFor") &
(F.col("d.Type") == "Vote")
)
# Sanity check the count - it should match the table above
linked_vote_paths.count()
300017
我们首先使用聚合来计算每个链接问题两端的总投票数。要获取问题B的计数,获取不同的3路径,按其ID分组并统计投票数。
b_vote_counts = linked_vote_paths.select("a", "b").distinct().groupBy("b").count()
c_vote_counts = linked_vote_paths.select("c", "d").distinct().groupBy("c").count()
现在将计数与链接进行连接,以获取每对关联问题的总票数。然后运行 pyspark.sql.DataFrame.stats.corr() 来获取关联问题票数之间的相关性。我们将使用 Vote.VoteTypeId 来确保仅统计正面投票。
linked_vote_counts = (
linked_vote_paths
.filter((F.col("a.VoteTypeId") == 2) & (F.col("d.VoteTypeId") == 2))
.select("b", "c")
.join(b_vote_counts, on="b", how="inner")
.withColumnRenamed("count", "b_count")
.join(c_vote_counts, on="c", how="inner")
.withColumnRenamed("count", "c_count")
)
linked_vote_counts.stat.corr("b_count", "c_count")
0.4287709940689788
我们得出结论,关联问题之间的投票数存在中等程度的相关性。这是合理的。请注意这仅是第四行,还有更多模式有待检验和考量。
这只是您可以使用的其中一种聚合类型 - 但希望它能说明属性、聚合及其他关系运算符如何将简单的模式匹配转化为探索知识图谱的强大工具。
结论
在本教程中,我们学习了使用GraphFrames在属性图中查找网络模体。我们了解了如何结合图查询和关系查询来发现图中的复杂模式。我们还看到了如何利用路径中节点和边的属性进行过滤、分组和聚合结果,以形成复杂的属性图模体。GraphFrames中的模体发现是一项强大技术,可用于探索和理解复杂网络。网络模体是复杂网络的基本构建单元。