图形神经网络:综述
图形神经网络导论
https://mapr.com/blog/driving-insights-network-graphs/
在过去的十年中,我们已经看到神经网络可以在图像和文本等结构化数据中表现得非常好。大多数流行的模型,如卷积网络、递归、自动编码器,对于具有表格格式(如矩阵或向量)的数据工作得非常好。
但是非结构化数据呢?图形数据呢?有没有一种模式可以向他们高效学习?大概你从题目就猜到了。答案是图形神经网络。
图形神经网络早在 2005 年就被引入(像所有其他好主意一样),但它们在最近 5 年才开始流行。gnn 能够模拟图中节点之间的关系,并生成其数字表示。
GNNs 的重要性非常显著,因为有如此多的真实世界数据可以用图形来表示。社交网络、化合物、地图、交通系统等等。
因此,让我们找出 GNNs 背后的基本原理以及它们工作的原因。
让我们首先定义我们的基本问题:我们想要将一个给定的图映射到一个标签,这个标签可以是一个数值、一个类或者其他任何东西。换句话说:
F(图)=嵌入
我们想找到函数 f,例如,想象每一个图是一个化合物或一个分子,标签是这个分子被用来生产某种药物的可能性。如果我们有办法从每张图中提取标签,我们基本上找到了一种方法来预测哪些分子更有可能用于药物。很酷,对吧?
我们该怎么做?我们已经知道了一种可以(在某种程度上)用于图形的神经网络。如果你考虑一下,递归神经网络可以在一种特殊类型的图上操作。链式图(基本上是一条线的图)。时间序列实际上是链式图,其中每个时间戳是一个节点,后面跟着下一个时间戳。
因此,事实上,我们可以建立一个网络,其中每个图节点都是一个循环单元(LSTM 或其他东西),节点的信息是一个嵌入,将通过链进行传输(像消息一样)。因为单元都是循环的,所以当嵌入穿过图时,信息不会丢失。这是我们熟悉的递归神经网络。与语言翻译和其他自然语言处理应用程序中使用的方法完全相同。
当然,我们可以将这个想法扩展到适当的图,我们得到这个:
这张幻灯片摘自微软研究院关于 GNNs 的精彩演讲。让我们来看看这里发生了什么。
每个橙色三角形曾经是图形节点,现在被一个循环单元取代。包络表示将穿过图的节点的嵌入。每个图的边也被一个神经网络代替,以捕捉边的信息(它的权重)。
现在是学习的部分。在单个时间步骤中,每个节点从其所有邻居提取嵌入,计算它们的总和,并将它们与其嵌入一起传递给递归单元,这将产生新的嵌入。
这个新的嵌入包含节点的信息加上所有邻居的信息。在下一个时间步中,它还将包含其二阶邻居的信息。诸如此类。这个过程一直持续到每个节点都知道图中的所有其他节点。每个嵌入现在都有来自所有其他节点的信息。
最后一步是收集所有的嵌入并添加它们,这将为我们提供整个图的单个嵌入。
T3【https://tkipf.github.io/graph-convolutional-networks/】T5
就是这样。我们做到了。我们设法在一次嵌入中捕获整个图形。这种嵌入现在可以用在一些其他模型中,以执行一些分类、预测、聚类等操作。让你的想象力漫游。
如果你想试验图形神经网络,我会帮你搞定:
我个人会选择第三种,因为它有更好的文档,但这是你的选择。
给你。鉴于 gnn 能够有效地建模图形结构,我猜测它们将在深度学习的未来发挥重要作用。通过将 GNN 加入他们的军械库,人工智能研究人员现在可以将一种全新类型的数据加入到他们的模型和架构中。老实说,天空才是极限。
太棒了。
如果你有兴趣阅读更多关于人工智能和机器学习的帖子,请不要不看我们的博客就离开
原载于 2020 年 2 月 1 日【https://theaisummer.com】。
图形处理:一个没有明确胜利者的问题
意见
你知道最流行的图形处理解决方案吗?没有吗?别担心。现在还没有。
我们都依赖互联网来寻找技术问题的潜在解决方案。例如,对于大数据问题,在谷歌五分钟后,你会发现 Spark 可能会帮助你。即使你不知道什么是火花,你也会碰到这个名字。TensorFlow 在搜索深度学习解决方案时也会出现类似的情况,Kubernetes 用于云,Docker 用于容器… 似乎计算机科学中的每个流行词都有一个平台/框架/库。但是,尝试寻找一个图形处理解决方案。你会发现没有明确的胜利者。我觉得这很令人惊讶。
2015 年,我和我在 Inria 的同事发表了一篇文章,提出了一种中间件,可以激励开发人员提供一个通用框架来实现分布式图形处理解决方案。我们有一种强烈的感觉,没有一个一致的建议来加速大规模图形处理解决方案的开发。如果我们考虑到 Graph500 基准测试在使用图形时存在一些计算密集型问题,这就令人惊讶了。脸书和 Twitter 诞生后社交网络的爆炸吸引了研究界的注意,并提出了计算和可扩展性方面的新问题。此外,使用图形作为底层数据结构存在大量问题。图形用于欺诈检测、博弈论和大量与数据相关的问题。
有一个庞大的图库生态系统。仅举几个可用的我有经验的,我们有: GraphTool , SNAP , igraph , NetworkX , BGL , network for R 等。此外,我们还有其他更受欢迎的平台的扩展模块,如 Spark 的 GraphX、graphs 的 Hadoop 扩展、Neo4j,这是一个数据库,而不是库,尽管它可以用于图形操作。
然后,我们进入一个更模糊的世界,在某些方面声称是最有效的解决方案。研究机构和/或大学已经开展了大量的工作。然而,我不建议任何开发人员采用这些解决方案。纯粹。为什么?因为它们很难维护,没有记录,最终会消失。有一些臭名昭著的例子,如谷歌的 Pregel,它启发了 Apache Giraph,但从未公开发表过(如果我错了,请纠正我)。GraphLab 代码是可用的,慢慢消失,直到成为 Turi 的一部分。
如果您是一名开发人员,并且需要包括一些图形处理,请考虑以下几点:
- 你想使用的编程语言可能会减少你的选择。
- 您是在寻找特定的算法,还是想要探索和/或实现自己的解决方案?第一种方法在某种程度上简化了搜索,因为您可以寻找性能最佳的算法实现。如果你想做自己的探索,看看下一点。
- 你的图表有多大?这是极其重要的。我上面提到的大多数库在处理数百万个顶点和边时都会有问题。如果你的图形不适合内存,那就更糟了。
不幸的是,在写这篇文章的时候,在图形处理解决方案的世界里,我们还没有一个明确的胜利者。公司使用的大多数解决方案都是从零开始量身定制的。开发人员一次又一次地编写相同的算法,导致了现有实现的混乱。我坚信有一个新的范例的空间(和需要),可以帮助开发人员建模分布式图算法,简化开发和可维护性,同时提高性能。类似于 TensorFlow 对神经网络所做的事情,通过抽象公共片段并将社区的努力集中到一个公共解决方案中。
感谢阅读。
图形编程
命令式编程的特点是迭代嵌套。一个函数嵌入了对其他函数的调用。我所说的嵌入是指这些调用在函数体中,因此函数和它的组成部分之间有紧密的耦合。这有许多缺点,通常通过函数组合来解决。
构图就像把两个东西连在一起。没有一个事物是在另一个事物之上的、嵌入的或优越的,它们只是相互补充。因此,所涉及的功能彼此之间是不相干的,这很好,因为你可以混合和匹配,分别测试和调试它们。
大多数组合实用程序或库会生成一个新的函数或对象,表示其组成部分的组合,但不再可能引用这些组成部分。你得到了一个新的不可穿透的盒子(再次嵌套)。
在典型的构图中,你可以继续在这个盒子里或盒子外构图,只要构图是在边上完成的,而不是单个的成分。然而,如果你有非一元函数,你需要开始使用 currying,事情可能会变得复杂。
作为人类,我们不会这样思考或说话,我们可以任意引用概念,以非一元的方式自由写作。
这项工作提出了图编程的概念,这是一种通过以非封闭方式连接函数对来构建函数图的编程方法,旨在取代典型的函数组合。
先决条件
我将使用 python 来说明我的例子。我也将使用函数式编程风格。
你应该熟悉compose_left
、pipe
和curry
才能跟上。
如果您不是,下面是前两者的可行实现:
def compose_left(*funcs):
def composition(input):
for f in funcs:
input = f(input)
return input
return compositiondef pipe(input, funcs*):
return compose_left(*funcs)(input)
至于curry
,由于它的实现有点复杂,您可以使用这个使用示例来了解它的功能:
@curry
def add(x, y):
return x + y# I can give a single argument, getting back a function that
# expects the remaining argument.
add_5 = add(5)
add_5(7) # Will give 12.
非线性程序
考虑以下情况,其中您有以下管道:
foo = compose_left(x_1, x_2, ..., x_n)
foo
是一些函数的组合。太好了。
现在想象一下x_n
得到两个参数而不是一个的情况。所以我们不能再单独使用这个组合,我们需要使用 currying 和一个额外的def
,就像这样:
@curry
def x_n(arg1, arg2):
...def make_foo(head_input, side_input):
return pipe(
head_input,
x_1,
x_2,
...,
x_n(side_input)
)
Currying 很棒,因为我们不需要复杂的x_n
来表达它将在代码中的两个不同位置获得它的两个依赖项。side_input
将作为arg2
进入x_n
。
这里仍然有一些渗漏。我们不必要地将剩余的合成代码暴露给side_input
。如果我有很多依赖项,并且我想在不同的时间给它们,这会变得非常混乱。
考虑这样的情况,其中x_n
是两个独立管道的成员,每个管道给它不同的输入。
我们可以试着这样编程(这显然是行不通的):
def baz(arg1, arg2):
...# incoming argument on position 2
foo = compose_left(x_1, x_2, ..., baz)# incoming argument on position 2
bar = compose_left(y_1, y_2, ..., baz)
调用foo
或bar
会给我们一个熟悉的例外,即baz
期望两个参数,但只得到一个。
我们可以用一些命令式的风格来克服这个问题:
def everything(head_arg1, head_arg2):
return baz(
pipe(head_arg1, ...),
pipe(head_arg2, ...),
)
虽然这适用于简单的情况,但不适用于一般情况。如果我在不同的阶段连接了几个函数,那么我将不得不放弃组合符号,并以命令式风格结束,或者简单地拥有一个大函数,其中所有的东西都暴露给所有的东西。
复杂的依赖结构(作者图片)
想象一下,必须编写单个签名来支持上述结构。那就必须是d3
、d1
、p1
、d2
的结合。函数体本身会全局暴露变量,理解依赖结构会非常困难。
作为人类,如何表达是很明显的。我们只是画出来。让我们回到上面这个简单的例子。
当你能画的时候不需要涂抹(作者的图片
我们也可以“编码”它,例如这是我用来生成上面图像的代码:
digraph G {
x_1 -> x_2 -> baz
y_1 -> y_z -> baz
}
所以我编程的时候应该也能做到。
编写者
“图”这个词本身就来源于素描或绘画,这才是真正正确的意义。我们希望能够画出程序,因为我们对它们的心理模型是非线性结构。
*编程这个词本身来自更早的希腊语“prographein”。我们其实都只是程序员。
https://github.com/hyroai/computation-graph用 python 实现了第一个图形编程框架。
它允许使用用普通 python 编写的纯函数,并显式声明它们的依赖关系,而不需要它们知道彼此。这类似于任何功能组合框架。除此之外,cg 还有其他一些技巧。
非线性合成
第一个技巧是将合成推广到非一元函数,将管道推广到有向无环图(Dag)。依赖不仅仅是“线”,所以我们应该能够,例如,将两条线连接成一个二元函数。我只需要说出我正在编写的关键字就可以完成这个例子。
一个例子:
everything = to_callable([
*compose_left(x1, ..., x_n-1),
*compose_left(x_n-1, baz, keyword="arg1"),
*compose_left(y1, ..., y_n-1),
*compose_left(y_n-1, baz, keyword="arg2"),
])...
# usage:
everything(input_to_x_chain, input_to_y_chain)
每个组合本质上都是边的集合,带有一些语法糖,允许您轻松地创建子结构。
记忆
纯函数和组合很容易处理。它们易于测试和推理,并且不会暴露在令人讨厌的状态中。但是现实仍然需要有记忆的程序,或者运行几次并保留一些上下文的程序,所以我们需要在纯函数中引入状态或记忆。流行的 UI 框架 React 已经使用钩子解决了这个问题(例如useState
)。我们希望对我们使用的函数的纯度有更严格的要求,不要在函数中引入任何神奇的调用。换句话说,我们希望在函数的参数中完整地描述输入,并将输出完整地定义为函数的输出。
我们首先实现了一种特殊的节点,称为缩减器。reducer 返回两个元素,一个输出和它的内存,计算图框架将在下一次“转向”时在一个名为state
(类似于 python 中的self
)的特殊参数中为函数提供内存。
然后我们意识到,这种模式可以用更简单的方式实现,不需要任何特殊的关键字参数或构造。我们意识到记忆是对未来的依赖。这里的未来指的是下一次运行计算图。因此,我们改变了实现,使之具有我们称之为“未来边缘”的东西。未来边缘描述时间轴上的函数组合。所以一个函数可以被组合(即有一个边)到它自己或者其他函数上(甚至是它应该依赖的函数!),而没有真正产生循环。这是因为当前回合所需的值来自前一回合。
当我们引入这个概念时,我们讨论的不再是 Dag,而是一般的图,这些图可能有“圈”(只要这个圈至少有一条未来的边可以打破它)。
请注意,当我们引入内存时,整个计算图的签名变成了两个元素,其中一个是输出,另一个是状态,当我们再次运行它时,我们确保给它前一轮的状态。因此当缩小时,整个计算图具有缩减函数的特征。
记录
日志记录是编程中的一个经典问题。我们写一些代码来做一些事情,然后我们想观察它并记录它在做什么。作为例子,考虑流水线compose_left(x, y, z_1, ..., z_n)
。
并且说我们想看y
的输出。所以我们可能会这样做:
def y(some_input):
...
return output, log
但是唉,z_1
期望得到y
的输出。这意味着我们将不得不复杂化z_i
来处理一对元素。我们想要避免它。对于图形编程来说,这很容易做到。简单地将y
连接成两个面:
(
*compose_left(x, y, z_1, ..., z_n),
*compose_left(y, logger),
*compose_left(logger, final_output, key="log"),
*compose_left(z_n, final_output, key="real_output"),
)
异步编程
python 中的 Async 真的很难搞对。同步函数不能调用异步函数,所以至少可以说混合使用这两种函数是很重要的。此外,你必须仔细选择何时在两行中使用await
,以及何时使用类似asyncio.gather
的东西。但是这个信息,什么需要首先发生,已经在你的程序中了。比如下面这个例子效率不高。
async def my_async_program():
x = await async_f()
y = await async_g()
return sync_h(x=x, y=y)
这是因为我们可以将x
上的await
与y
并行。但是为什么我们作为人类应该注意到它,而不是去推断它呢?
如果我们画出来,就很明显了。
(作者图片)
因此,计算图形库允许您混合异步和同步功能,甚至使用拓扑排序为您推断哪些功能可以并行运行。这将被写成如下。
(
*compose_left(async_f, sync_h, key="x"),
*compose_left(async_g, sync_h, key="y"),
)
含糊
另一个很酷的特性是,你实际上可以在一个函数中组合多个选项,使得图形有点像一个不确定的模型。这意味着输入可以来自任一边。为了使结果可预测,这些边具有优先级,因此计算图将首先尝试优先级更高的边,并且只有当计算路径不成功(即引发特定异常)时,图运行器才会尝试不同的路径。
(
make_edge(f1, g, key="some_kwarg", priority=0),
make_edge(f2, g, key="some_kwarg", priority=1),
)
在上面的代码中,g
可能从f1
获得输入,或者如果不成功,从f2
获得输入。注意,我们在这里使用较低级别的 API,但这与compose_left
非常相似。
TODO:循环
计算图尚不支持循环。当需要时,它们也可能被实现为一个特殊的边缘。这仍处于早期规划阶段,欢迎对此(或 PRs)提出意见。
小注意:计算图和单子
上面讨论的一些问题传统上是由函数式编程语言中的单子解决的。因为我自己没有使用单子的经验,所以我选择让读者来决定图形编程和单子之间的关系,以后可能会再次讨论这一部分。
图形查询搜索(第 3 部分)
使用 TigerGragh 云编写 GSQL 查询
议程
- 在图表中建模发布数据
- 什么是图形查询
- 编写图形查询
- 结论
1.在图表中建模发布数据
这是探索数据提取和建模系列文章的第 3 部分。如果到目前为止您已经了解了这个系列,欢迎回来!。如果你是新来的,我会简单介绍一下我们到目前为止所做的工作。在第一部分中,我们探索了在与新冠肺炎相关的生物医学文献上使用自然语言处理和实体抽取。在第 2 部分的中,我们学习了如何使用 TigerGraph Cloud 获取数据并建模。查看这些文章,深入了解我们到目前为止所做的一切。
现在,在第 3 部分中,我们将研究如何编写图形搜索查询来轻松分析图形中的数据。你需要一个完整的图形来编写查询。你可以按照第二部分中的步骤从头创建一个图,或者你可以将我们创建的图导入到 TigerGraph Cloud 中。该图表以及我们使用的所有数据可以在这里找到。
为了导入图表,按照这些步骤创建一个空白溶液。在您的解决方案主页上,单击导入现有解决方案。
在主页上导入解决方案
不幸的是,您仍然需要手动映射和加载数据。但是,在议程项目 4 和 5 下的第 2 部分中,我将带您了解如何做到这一点。
2.什么是图查询?
在我们开始编写查询之前,我们可能应该了解它们是什么。图形查询本质上是搜索图形并执行某些操作的命令。查询可以用来查找某些顶点或边,进行计算,甚至更新图形。由于图形也有可视化表示,所有这些也可以通过 UI 来完成,如 TigerGraph Cloud 提供的 UI。但是,当处理大量数据或试图创建微调的图形搜索时,使用可视化界面是非常低效的。因此,我们可以编写查询来快速遍历一个图,并提取或插入我们想要的任何数据。
3.查询结构
GSQL 提供了许多不同的查询方法。我们将专注于搜索。图搜索的核心是一个叫做 SELECT 语句的东西。顾名思义,select 语句用于选择一组顶点或边。 SELECT 语句带有几个参数来缩小搜索范围。
来自子句的指定了你选择的边或顶点的类型。
WHERE 子句允许您声明顶点或边的特定条件。
ACCUM 和 POST-ACCUM 子句让您处理 累加器 ,它们是特殊的 GSQL 变量,在您搜索时收集信息(信息可以是数字、顶点集或边集等。).
有子句,类似于 WHERE 子句,让你提供附加条件;但是,这些将在前面的条款之后应用。
通过 ORDER BY 子句,您可以根据某个属性值对聚集的边或顶点进行排序。
最后, LIMIT 子句限制了搜索结果的数量。
您可以在 TigerGraph 文档页面上找到所有这些细节,以及其他参数和查询方法。
3.编写图形查询
几乎任何你能想到的对图的搜索都可以用 SELECT 语句及其相应的子句来处理。为了证明这一事实,让我们练习编写一些查询。
以下所有的疑问都可以在我的 GitHub 页面找到。
这些查询按照从最简单到最复杂的顺序排列。
具有给定许可证的出版物
**目标:**找到属于给定许可类型的所有出版物。
代码:
CREATE QUERY LicensePub(String l) FOR GRAPH MyGraph {/* Finds all publications with a given license type
Sample Inputs: cc0, cc-by, green-oa, cc-by-nc, no-cc */Seed = {LICENSE.*};Pubs = SELECT p FROM Seed:s-(PUB_HAS_LICENSE:e)-PUBLICATION:p WHERE s.id == l;PRINT Pubs[Pubs.id] AS Publications;
}
解释:我们来分解一下我们的代码在做什么。我们要选择连接到特定许可证顶点的所有发布顶点。因此,我们从所有的许可证顶点遍历到所有的发布顶点,条件是许可证 id 是我们指定的(即 cc0、no-cc 等)。).然后,我们只打印我们的结果。在我们的打印声明中有两件事需要注意。
- 如果我们简单地写
PRINT Pubs
,我们的输出将打印出版物及其所有相关数据(标题、摘要等)。因此,为了过滤输出数据,我们可以使用括号指定我们想要的属性。在我们的例子中,我们只通过写PRINT Pubs[Pubs.id]
打印出 id。 - 将用作语句纯粹是装饰性的,它只是改变了打印出的结果列表的名称。这在您提取要在其他上下文中使用的数据时很有用,但对于编写查询来说不是必需的。
现在,让我们保存并安装我们的代码。当我们运行它时,我们得到一个类似这样的输入框:
运行许可证查询后的界面
例如,我输入“cc0”作为许可证代码。当我单击“运行查询”时,我会看到如下所示的图像:
运行许可查询后产生的发布顶点
这显示了拥有我们指定的许可证的每个发布顶点。但是,这种观点相当混乱。我们可以点击左侧的 < … > 图标来查看 JSON 输出。JSON 输出应该是这样的。
许可证查询的 JSON 输出
这个看起来干净多了!我们还可以看到打印报表调整的效果。结果列表的名称是“Publications”,打印的唯一顶点属性是 id。
对于下面的查询,我将只显示 JSON 输出。
大多数出版物的作者
**目标:**找到发表文章次数最多的作者。
代码:
CREATE QUERY AuthorMostPubs() FOR GRAPH MyGraph {/* This query finds the author with the most publications
Change the limit to see top 'x' authors */SumAccum<INT> @pubNum;Seed={AUTHOR.*};Author = SELECT a FROM Seed:a-()-:t ACCUM a.@pubNum +=1 ORDER BY a.@pubNum DESC LIMIT 1;
PRINT Author;
}
解释:我们从选择作者顶点开始。注意这里的 SELECT 语句看起来有所不同。这是因为我们没有指定边或目标顶点。因为我们知道作者顶点只连接到出版物顶点,所以我们可以使用这种“懒惰”语法来避免指定边名和目标顶点。
我们也第一次看到累加器(在这里参考文档)。在这种情况下,我们使用一个名为 pubNum 的本地累加器。一个局部累加器作为每个顶点的唯一变量,一个 SumAccum 是一种存储累加和的累加器。
那么这个累加器是怎么工作的呢?当我们从每个作者顶点遍历到其连接的出版物顶点时,我们添加到我们的累加器中。因此,在 ACCUM 子句中,累加器将连接数(也是发布数)作为变量存储在每个 AUTHOR 顶点中。
下一步使用的是 ORDER BY 子句。我们用它来按照累加器值的降序排列结果作者。因此,拥有最多出版物的作者将会在列表的顶端
最后,我们使用 LIMIT 子句将输出限制为 1 个作者(列表中的第一个作者)。
当我们运行该函数时,我们的输出如下所示:
作者查询的输出
注意作者 id 是“南”。这是出版物没有作者时使用的 id 。所以,我们可以看到 2437 篇文章没有列出作者。这是很好的信息,但不完全是我们想要的。若要查看更多结果,请更改限制。作为一个例子,我将把限制改为 5。
作者搜索的 JSON 输出,限制 5
现在我们可以看到出版最多的作者有 173 本出版物(哇,太多了!)
出版物最多的期刊
我们可以运行一个类似于作者搜索的查询,但是要搜索发表文章最多的期刊。
目标:找到发表文章最多的期刊
代码:
CREATE QUERY JournalMostPubs() FOR GRAPH MyGraph {/* This query finds the journal with the most publications Change the limit to find the top 'x' journals */SumAccum<INT> @pubNum;Seed = {PUBLICATION.*};Pubs = SELECT t FROM Seed:s-(PUB_HAS_JOURNAL) -:t
ACCUM t.@pubNum +=1 ORDER BY t.@pubNum DESC LIMIT 1;PRINT Pubs[Pubs.id, Pubs.@pubNum] AS Most_Published_Journal;
}
**解释:**代码本质上和以前一样,但是我们改为搜索期刊而不是作者。我们还使用前面描述的打印过滤器来使我们的输出更好。
日记帐查询的 JSON 输出
我们看到有 205 篇出版物的“Arch Virol”是我们搜索的顶级期刊。
对某一类类型引用最多的出版物
**目标:**给定一个类别类型,找出该类别中医学术语最多的出版物。
示例类别类型:DNA、疾病、癌症、有机体、分类单元。
完整的名单可以在这里找到。
代码:
CREATE QUERY ClassPub(String c, Int k) FOR GRAPH MyGraph{
/* This query finds the top articles related to a class
Sample Input: CANCER, 5 */SumAccum<INT> @entNum;Seed = {CLASS.*};Ents = SELECT e
FROM Seed:s-(ENTITY_HAS_CLASS)-ENTITY:e
WHERE s.id == c;Pubs = SELECT p FROM Ents:e -(PUB_HAS_ENTITY)-PUBLICATION:p
ACCUM p.@entNum += 1 ORDER BY p.@entNum DESC LIMIT k;PRINT Pubs[Pubs.id, Pubs.@entNum];
}
解释:这是我们第一次看到两跳搜索。第一跳选择与给定类别类型相关的所有实体或关键字,第二跳查找包含这些实体的所有出版物,并对它们进行排名。作为一个例子,让我们给我们的查询分类为癌症,并选择顶部的 5 。我们的输出看起来像这样。
类搜索的 JSON 输出
我们现在可以看到哪些出版物对癌症的引用最多,以及每个出版物的引用数量。
类似出版物
**目标:**给定一篇论文,根据它们共享的关键词找到相似的论文。
我们将使用 Jaccard 相似度来确定两篇论文的相关程度。这种算法本质上是计算两篇论文在关键词总数上的共同关键词数。你可以在这里阅读更多关于算法的内容。你可以在 TigerGraph GitHub 上看到这个公式的一个例子,以及许多其他很酷的图形公式。
代码:
CREATE QUERY SimilarEnt(STRING doi, INT top) FOR GRAPH MyGraph {/* Use Jaccard Similarity to find top similar articles of a given article based on the key medical terms used
Sample Input: 10.1186/1471-2164-7-117, 5 */SumAccum<INT> @intersection_size, @@set_sizeA, @set_sizeB;SumAccum<FLOAT> @similarity;VERTEX check;Seed = {PUBLICATION.*};Start = SELECT p FROM Seed:p WHERE p.id == doi ACCUM check = p,
@@set_sizeA+=p.outdegree("PUB_HAS_ENTITY");Subjects = SELECT t FROM Start:s-(PUB_HAS_ENTITY)-:t;Others = SELECT t FROM Subjects:s -(PUB_HAS_ENTITY) - :t WHERE t!= check ACCUM t.@intersection_size +=1,
t.@set_sizeB = t.outdegree("PUB_HAS_ENTITY")
POST-ACCUM t.@similarity = t.@intersection_size
*1.0/(@@set_sizeA+t.@set_sizeB-
t.@intersection_size) ORDER BY t.@similarity DESC LIMIT top;PRINT Start[Start.id] AS SOURCE_PUBLICATION;
PRINT Others[Others.@similarity] AS SIMILAR_PUBLICATIONS;
}
**解释:**我们首先创建 4 个累加器。每一个都代表 Jaccard 公式中使用的一个值。对于我们的第一个 SELECT 语句,我们选择与输入 doi 匹配的发布,并将连接到该顶点的所有边收集到一个累加器中。对于第二条语句,我们选择该发布中的所有实体。对于我们的第三个语句,我们找到了具有我们刚刚收集的任意数量的实体的所有出版物,并找到了交集大小(与原始论文相同的实体的数量)。最后,我们计算 Jaccard 索引,并将相似度最高的出版物排在输出列表的顶部。
让我们看一个例子。我用了doi= 10.1186/s 40413–016–0096–1 和 top =5。
相似性查询的 JSON 输出
我们可以看到我们的起始出版物以及前 5 个类似的出版物,每个出版物都有各自的相似性得分。
4.结论
如果你跟随这篇文章,我为你鼓掌!这份材料并不容易,学习一门像 GSQL 这样的新语言可能会很棘手。我希望这个 GSQL 查询的演练对您有所启发。我强烈推荐阅读我的其他论文,以获得我们今天所讨论的所有内容的更好的背景。如果您想了解更多的查询算法和结构,请查阅 GSQL 的 TigerGraph 文档。如果你正在寻找更多的内容,请继续关注!我将很快发布第 4 部分。在这个系列的最后一部分,我将介绍如何使用我们的图形数据库和查询来输出信息,我们可以使用 Plotly 的 UI 平台 Dash 来直观地表示这些信息。这样,如果您遵循了所有 4 个部分,您将完成一个完整的端到端应用程序!
如果你喜欢这篇文章,一定要看看我的其他文章,并关注我的更多内容!
资源
- https://towards data science . com/using-scispacy-for-named-entity-recognition-785389 e 7918d
- https://towards data science . com/linking-documents-in-a-semantic-graph-732 ab 511 a01e
- https://gofile.io/d/fyijVS
- https://www.youtube.com/watch?v=JARd9ULRP_I
- https://docs . tiger graph . com/dev/gsql-ref/query/accumulators
- https://docs . tiger graph . com/dev/gsql-ref/query/select-statement # select-statement-data-flow
- https://github.com/akash-kaul/GSQL-Query-Searches.git
- https://allenai.github.io/scispacy/
- https://github . com/tiger graph/gsql-graph-algorithms/blob/master/algorithms/schema-free/JAC card _ nbor _ AP _ JSON . gsql
- https://docs.tigergraph.com/
- https://plotly.com/dash/
图论|网格上的 BFS 最短路径问题
图论简化版
大家好,欢迎回到我关于图论的全新系列的另一个帖子,名为 图论:Go 英雄 。如果你打算开始学习或者想要快速复习,我毫无疑问地推荐完整系列。我们将看到如何使用 广度优先搜索 ( BFS )来解决最短路径问题。我之前已经在 BFS做过另一个帖子。所以,让我们深入研究一下。
我希望你知道什么是 广度优先搜索()以及它是如何工作的,因为我们会大量使用 BFS 的概念。
设置场景
图论中的许多问题可以用网格来表示,因为有趣的是网格是一种 隐式图的形式。 我们可以通过在网格内搜索来确定我们当前位置的邻居。在网格中寻找最短路径的一类问题是解决迷宫,如下所示。
作者照片
另一个例子是通过障碍物(如树木、河流、岩石等)到达一个位置。
网格上的图论
解决图问题的一种常见方法是首先将结构转换成一些表示格式,如邻接矩阵或列表。基本上,这些是存储图中邻域信息的数据结构。让我们来看一个更直观的版本。
假设我们有一个假想的图形。
作者照片
不,这不是图表。请看图 1,但这正是我要说的。假设图 1 中的每个单元格的左、右、下和上都有邻居。为了更清楚起见,单元格 0 有两个邻居,1 和 2。同样,小区 4 也有两个邻居 2 和 3。我们可以将这些单元格视为图中的顶点,其中 行列 将是顶点的总数。图 2 是代表我们假想图的 邻接表 ,现在你可以把它和第一张图联系起来了吧?最后一张图描绘了同一图的 邻接矩阵 。邻接矩阵的每个单元(I,j)都用 1 填充,其中节点 I 和 j 之间有一条边。我们在这里只使用 1 和 0,因为我们没有关于从顶点 I 到 j 的开销的信息。如果我们有,我们也可以使用这些信息。*
一旦我们有了一个图的邻接表/矩阵表示,我们就可以在它上面运行多个图算法来解决不同的用例,比如找到最短路径和连接的组件。
地牢问题
这可能是我们在许多面试和编程竞赛中遇到的一个问题陈述,如下所示。
假设你被困在 2D 的地牢里,你必须找到最简单的出路。等等,我们也有一些障碍。地牢是由单位立方体组成的,里面可能会也可能不会填满石头。向东、向西、向南或向北移动只需要一分钟。你不能斜着走,因为迷宫里布满了坚硬的岩石。
作者照片
地下城的大小为 R x C,其中 R 是行数,C 是列数。我们必须从“S”室开始,在“E”室有出口。数字( # )符号描述了路线中的路障和时段()。)显示一条开放的路线。**
作者照片
在给定的设置中,一个解决方案可以在上面的绿色路线中绘制。我们的方法是从单元格开始做 BFS,直到我们找到单元格的出口******
作者照片
如果您还记得,我们使用了一个队列来存储图形中稍后要访问的点。我们这里也用同样的。我们从单元格(0,0)开始,并将其添加到我们的队列中。一旦它被访问,我们将被访问单元的所有邻居添加到队列中。单元格(0,0)有两个邻居,(0,1)和(1,0)。随着我们的访问,队列变得越来越大,并不断地向队列中添加更多的邻居。当我们满足退出条件时,我们停止这个过程,即我们访问退出单元 E (4,3)。然后我们可以通过回溯重新生成从出口到起点的路径。
替代状态表示
到目前为止,我们一直使用单个队列来跟踪要访问的下一个节点,比如说一个 (i,j) 对。但是这不是最好的方法,因为它需要在队列中来回打包和解包。相反,让我们尝试另一种更好的方法,它可以很好地处理更高维的数据,并且具有更低的复杂度。
另一种方法是对每个维度使用单独的队列,因此在 3D 网格中,每个维度都有一个队列。
作者照片
一旦我们将一些潜在的信息放入队列,x、y 和 z 将进入各自的队列。同样,dequeue 一次检索一组 (x,y,z) 值。
伪代码解决地牢问题
**# Global variables, I intentionally leave the values as **...** because # I don't have any meaningful values yet. But it won't affect how
# you understand the problem, I promise.R, C = ...
m = ...
sr, sc = ...
rq, cq = ...# Variables used to keep track of total number of steps to be taken
move_count = 0
nodes_left_in_layer = 0
nodes_in_next_layer = 1# Variable to see whether we already reached at the end or not
reached_end = false# Matrix to keep track of visited cells.
visited = ...# North, South, East and West direction vectors
dr = [-1, +1, 0, 0]
dc = [0, 0, +1, -1]**
我们从初始化一些全局变量开始。 R 和 C 分别代表地牢的行数和列数。变量 m 是大小为 R x C 的输入字符矩阵。我们在变量 sr 和 sc 中存储我们的 BFS 的起点的地方存储初始行和列值。我们使用两个单独的队列 rq 和 cr 来存储要访问的下一个节点各自的行和列索引。此外,我们使用几个变量来跟踪到达终点的总步骤。nodes _ left _ in _ layer显示了在我们进行下一步操作之前有多少节点必须出列的计数,而nodes _ in _ next _ layer跟踪我们在 BFS 扩展中添加了多少节点,以便我们可以相应地更新nodes _ left _ in _ layer*****。*** 变量 到达 _ 结束 存储我们是否已经到达退出单元格 。 变量 访问过的 是一个大小为 R x C 的矩阵,用来标记访问过的单元格,因为我们不想再访问同一个单元格。变量 博士 和 dc 需要一些解释,我会很快覆盖。****
作者照片
假设我们在 红色 单元格(I,j)。我们假设行索引只能在行之间移动,而列索引可以在列之间移动。因此,唯一可能的行操作是,要么我们可以通过从 I 中减去 1 来向北移动,要么通过将 I 加 1 到来向南移动。同样,通过对列索引加 1 或减 1,即 j ,我们被限制向东或向西移动。我们使用不同的方向值组合在地牢中移动,这就是为什么之前把它定义为变量。我想你明白了。**
我们还没有解决这个问题。我们只定义了几个重要的变量。核心思想即将问世。
**function solve():
rq.enqueue(sr)
cq.enqueue(sc)
visited[sr][sc] = true
while rq.size() > 0:
r = rq.dequeue()
c = cq.dequeue()
if m[r][c] == 'E':
reached_end = true
break
explore_neighbors(r, c)
nodes_left_in_layer --
if nodes_left_in_layer == 0:
nodes_left_in_layer = nodes_in_next_layer
nodes_in_next_layer = 0
move_count ++
if reached_end == true:
return move_count
return -1function explore_neighbors(r, c):
for(i=0; i<4: i++):
rr = r + dr[i]
cc = c + dc[i]
if rr < 0 or cc < 0:
continue
if rr >= R or cc >= C:
continue
if visited[r][c] == true:
continue
if m[r][c] == '#':
continue
rq.enqueue(rr)
rc.enqueue(cc)
visited[r][c] = true
nodes_in_next_layer ++**
这里我定义了两个函数分别是 solve() 和 explore_neighbors()。 我们从开始 BFS 过程的初始(I,j)位置开始排队,并将该单元标记为已访问。
然后,我们反复执行以下步骤,直到 rq 或 cq 变空。
- 将 rq 和 cq 中的每个元素出队。
- 我们检查当前位置是否是出口,如果是,我们退出循环。
- 如果当前位置不是出口点,那么我们必须通过调用explore _ neighbors()函数来探索它的邻居。
- 在explore _ neighbors()函数中,我们迭代地寻找所有可能的位置并检查几个条件。我认为条件是不言自明的。
- 前两个条件检查我们是否在网格之外。这意味着,我们不能超过最小或最大的行数和列数。
- 然后,我们检查当前位置之前是否已经被访问过。如果是真的,我们就不用再去参观了。
- 此外,我们必须确保当前位置没有被阻止,所有被阻止的单元格都标有 #。
5.我们将当前单元格的值排队,并将其标记为已访问。(别忘了,我们是在explore _ neighbors()函数调用里面)。这里发生的事情就像,我们试着移动到所有可能的位置,比如北、东、南、西。然后我们迭代地探索它的邻居。就是这样。
6.最后我们更新nodes _ in _ next _ layer的值,离开。****
我们将再次回到 solve() 函数。
7.我们更新了几个参数来记录到目前为止我们走了多少步。
8.一旦我们找到出口,我们就出去。
TADAAA!!!
整个想法和算法相对来说超级简单,即使伪代码看起来很吓人。
我们开始研究迷宫是如何工作的,以及如何将同样的问题移植到一个更科学的问题中。我们看到了如何使用网格和邻接表来表示问题。我们了解了什么是地牢问题,以及如何使用 BFS 解决它。我的想法是展示我们如何使用 BFS 来解决网格上的最短路径问题。差不多就这些了。
在下一个帖子中,我们将有一个 介绍树算法 。在那之前,再见。
图论|广度优先搜索
简化的图论
欢迎大家回来。今天我们讨论的是广度优先搜索(BFS)——一种图探索算法。我们在之前的帖子中讨论了关于深度优先搜索。如果你不知道什么是图,或者想快速复习一下核心概念,我绝对推荐你看看我全新的关于图论的系列这里。
概观
广度优先搜索 或简称为 BFS 是我们用来探索图的边和顶点的基本算法,它在许多现实世界的应用中起着关键作用。它运行的复杂度为O(V+E)其中 O、V、E 分别对应 大 O 、 顶点 和 边 。这种机制是许多应用程序的主要构件。我们可以通过实际遍历图的方式来区分 BFS 和 DFS。顾名思义,BFS 访问广度先于深度。这是把他们分开的基本事实。BFS 可以用于一个目的:在无向图中寻找最短路径*。*
基本 BFS
BFS 算法从某个任意节点开始,并在移动到下一个深度之前访问其所有邻居。简而言之,BFS 是分层工作的。
作者照片
如果我们从节点 1 开始 BFS,它将首先访问它的邻居,即 2、3 和 4。一旦我们完成了节点 1,我们最终会移动到下一个节点。在我们的例子中,节点 2、3 和 4 没有任何邻居,因此我们移动到下一个未访问的节点,即节点 6。节点 6 的直接邻居是 5、7 和 8。与前一个例子一样,节点 5 和 7 没有任何邻居,因此我们必须使用节点 8。游戏在这里发生了变化,节点 8 有两个邻居未被访问,所以我们访问它们并标记为已访问。这是一个非常基本的例子,但是相信我,这种图表只存在于纸上。
为了跟踪下一个要访问的节点,我们将顺序保存在一个队列中。我将用一个更复杂的例子来解释它,这样我们会对它有更好的理解。
作者照片
我们从节点 0 开始,将其添加到队列中,如下所示。
作者照片
现在,我们必须检查是否有任何邻居未被访问,是的。0 的直接邻居是 9、7 和 11。首先,我们将 0 标记为已访问,并将 0 的所有邻居添加到队列中。
作者照片
如上所示,队列中有三个新的未访问节点。下一个要访问的节点是 9。因此,我们将其标记为已访问,并将所有直接邻居添加到队列中,如下所示。
作者照片
下一个要访问的节点是 7,所以我们考虑到它,并将其所有邻居添加到队列中。节点 7 有三个邻居,即 3、6 和 11。如果我们看一下队列,11 已经在那里了,所以我们省略它,把其余的添加到队列中,如下所示。
作者照片
我们重复这个过程,直到队列中的所有元素都被访问。这是 BFS 的整体工作流程。
使用队列
BFS 算法使用队列来跟踪下一个要访问的节点。到达一个节点后,我们将所有邻居节点添加到同一个队列中,以便以后访问。
作者照片
队列数据结构的工作方式与真实世界的队列完全一样,对象被添加到队列的后面。首先进入队列的对象具有最高优先级。这意味着添加到队列中的第一个元素将得到服务,并首先离开队列。有两种与队列相关的基本操作,即在队列的后面插入一个元素和从前面移除一个元素,前者称为,后者称为*。***
伪码
*****#** global variables **n = number_of_nodes_in_the_graph
g = adjacency_list
visited = [false] * n
q = Queue()
q.enqueue(initial node)
while q is not empty
{
x = q.dequeue();
if x is not visited:
{
visited[x] = true
neighbors = g[x]
for y in neighbors:
if y is not visited:
q.enqueue(y)
}
}*****
我们将节点总数保存在一个名为 n 的变量中。我们将有一个 邻接表——一个用于在内存中存储图形的结构——它包括每个节点及其对应的相邻连接。这是节点到边列表的映射。我们用一个 list visited 来检查一个特定的节点是否被访问过,它用 n 个 false 值初始化,因为我们还没有访问过任何节点。正如我们前面讨论的,我们使用队列 q 来跟踪要访问的节点。我们向队列中插入或 入队 一个任意节点,以启动 BFS 过程。我们移除或 出队 队列中存在的元素,我们必须确保当前节点尚未被访问。如果否,我们将当前节点的已访问状态更改为真。最后,我们将把所有直接邻居排入我们使用的队列。这个过程将被重复,直到队列变空,即当所有节点都被访问时。
BFS 还能做什么
- 对等网络
- 社交网站
- 路径寻找
- 碎片帐集
- 网络广播
这是一个关于广度优先搜索的简介。希望大家觉得有帮助。
在下一篇文章中,我们会看到如何使用 BFS 从图中找到最短路径。谢谢你的时间和努力。
图论|树的中心
图论简化版
欢迎大家回来。我们现在在这个叫做 图论:Go Hero 系列的第 9 个帖子。去看看以前文章的索引页。我尽量每个周末更新。让我们看看如何找到树的中心。
介绍
寻找树的中心是一个需要了解的简便算法,因为我们经常在其他算法的子例程中看到它,而且当我们 为树 求根时,这也是一个选择根节点的有用方法。
作者照片
要记住的一点是,树可以有一个以上的中心,但不能超过两个。
计算中心
请注意,中心始终是树中每条最长路径的中间顶点或中间两个顶点。
作者照片
例如,上图中橙色的路径是最长的路径,红色节点被认为是其中的中心。如果我们重复这个过程,选择另一条可能的长路径,中心将保持不变。
另一种寻找中心的方法是像剥洋葱一样反复挑选每个叶子节点。
所以,我们从郊区开始,逐渐在中心结束。
作者照片
在我们的例子中,中间有一条水平线的节点是叶子。如果我们计算这些节点的 度 ,
节点的度是它所连接的节点的数量。
肯定会是 1。因为所有这些叶节点都恰好连接到一个节点。当我们修剪树叶时,所有其他节点也会受到影响,即节点的度会开始降低。
作者照片
正如我们在上面的图像中看到的,我们的图形将开始反复松开它的叶子,直到我们找到它的中心。记住, 我们可能会找到一个或多个中心 。
伪码
现在让我们看一些伪代码。
function **treeCenters**(g):
n = g.numberOfNodes()
degree = [0] * n
leaves = []
for(i=0; i<n; i++):
if degree[i] == 0 or degree[i] == 1:
leaves.add(i)
degree[i] = 0
count = leaves.size()
while count < n:
new_leaves = []
for node in leaves:
for neighbor in g[node]:
degree[neighbor] = degree[neighbor] - 1
if degree[neighbor] == 1:
new_leaves.add(neighbor)
count += new_leaves.size()
leaves = new_leaves
函数 的参数g***tree centers()***是一个无向图。变量 n 表示我们的树中节点的数量。我们定义了两个数组,degree 和 leaves。前者的大小为 n 并存储树中每个节点的度,后者保留最近一层的叶节点。然后我们进入一个循环,我们计算图中每个节点的度,我们也检查我们是否考虑一个单节点树或者它是一个叶子节点。如果这两个条件中的任何一个为真,我们将条目添加到 leaves 数组中,并将其在 degrees 数组中的值标记为 0——因为我们不必再次访问该节点。变量 count 将记录到目前为止我们已经处理的节点数。然后,我们不断地检查树叶并修剪它们。最后,我们会得到树的中心。
这很简单,对吧?
我们将在下一篇文章中讨论 识别同构树 。敬请期待。
图论|深度优先搜索
图论简化版
这是我系列文章的第三篇,图论:Go 英雄。我强烈推荐查看以前帖子的索引。
在图论中,深度优先搜索 ( DFS )是一种重要的算法,在一些包含图的应用中起着至关重要的作用。
概观
DFS 是我们可以用来探索图的节点和边的最基本的算法。这是一种遍历算法。关于 DFS 的首要事实是它的工程简单性和可理解性。DFS 运行的时间复杂度为 O(V + E) ,其中 O 代表大 O , V 代表顶点, E 代表边。不过,它的一致性可以用于其他不同的应用,如检测桥梁和关节点,计算连接的组件和估计连通性。
基本 DFS
顾名思义,DFS 算法访问 深度 ,而不管当前节点或边之后是哪个节点或边,如果我们到达了一个死胡同——从那里我们不能再前进——我们 回溯 到未被访问的节点,并递归地继续这个过程*,直到我们访问了所有可能的节点和边。我们必须确保我们的 不会再 访问同一个节点*。**
作者照片
对于给定的图形,有多种遍历选项可用。我在这里展示的是从节点 0 开始,然后像 1,2,3,…,9 这样继续下去。实际路径应该是 0-1-2-1-3-1-4-5-6-7-6-5-8-5-4-9。注意,我们再次回溯到几个节点,找出新的未访问路径。
伪码
**# global variables
**n = number_of_nodes_in_the_graph
g = adjacency_list
visited = [false] * n****function dfs(at):
if visited[at]:
return
visited[at] = true** **neighbors = g[at]
for node in neighbors:
dfs(node)**# start DFS at node 0
**start_node = 0
dfs(start_node)****
我们开始定义图中节点的总数(【n】)。我们将有一个 邻接表——一个用于在内存中存储图形的结构——它包括每个节点及其对应的相邻连接。这是节点到边列表的映射。我们定义一个大小为 n 的列表来表示该节点是否已经被访问过。在初始化时,列表将包含所有的 假 值,因为我们还没有访问任何节点。当我们访问每个节点时,相应的位置就会被替换为 真 。已经定义了一个名为 dfs() 的函数,并在伪代码末尾调用了该函数。我们将第一个要访问的节点设置为 0。当函数 dfs() 被调用时,它检查该节点是否已经被访问过,如果没有,我们将用标志着成功访问的 true 替换此时为 false 的 visited[node]。然后,我们检索刚刚访问过的节点的邻居,并对每个邻居递归地调用 dfs() 函数。
连接的组件
让我们讨论一下 DFS 的一个主要用例,即在图中查找连接的组件。无向图的连通分量是节点的最大集合,使得每对节点通过路径连接。
作者照片
有时一个图可能被分成多个部分。连通分量形成了图顶点集合的一个 划分 ,意味着连通分量是非空的,它们是成对不相交的,并且连通分量的并集形成了所有顶点的集合。没有关联边的顶点也是一个独立的组件。从图中识别组件的一种方法是给它们着色。因此,每个组件都有与之相关的独特颜色。给组件着色对人类很有吸引力,但相关的问题是——如何给机器着色,例如计算机。在机器的情况下,我们可以用相似的 id(可能是一些整数)来标记组件的每个节点。就像配色方案一样,每个组件都拥有一个唯一的 id。
我们来看看上面描述的标注是怎么做的。为了方便起见,我从上面的图表中提取了一组组件,如下所示。
作者照片
首先,我们必须从 0 到 n 对每个节点进行显式编号,其中 n 是节点的最大数量。其思想是在每个尚未访问的节点上启动 DFS,并将所有可到达的节点标记为同一组件的一部分。如果我们从节点 0 开始,它本身已经是一个组件,因为它没有传入或传出连接,我们用整数 1 标记它。现在,如果我们选择节点 6 作为下一个节点,我们按照 6 - 14 - 16 - 14 - 5 - 11 的顺序进行 DFS,并将该组件的所有节点标记为 2。我们对图中的每个其他组件重复相同的过程。最终的结果看起来会更加丰富多彩,如下图所示。
作者照片
伪码
**# global variables
**n = number_of_nodes_in_the_graph
g = adjacency_list
count = 0
component = []
visited = [false] * n****function find_components()
for(i=0; i<n: i++):
if !visited[i]:
count ++
dfs(i)
return count****function dfs(at):
visited[at] = true
component[at] = count
for next in g[at]:
if !visited[next]:
dfs(next)****
我们首先定义了一些全局变量来存储列表中节点的数量,邻接表,一个存储组件数量的计数器,一个跟踪组件标签的列表和一个查看节点是否已经被访问过的列表。
我定义了两个名为*find _ components()*和 dfs() 的函数。第一个函数循环遍历我们拥有的每个节点,并确保它被访问。如果没有,计数器将增加 1,以标记新组件的存在,并调用 dfs() 函数对组件进行深度优先搜索。
dfs() 函数采用一个参数,即每个节点的 id 作为自变量。函数 dfs() 做的第一件也是最重要的事情是,它标记当前访问的节点,并用计数器更新组件列表。计数器值表示组件的标签。同一组件中的所有节点将具有相同的标签。然后,该函数在邻接表中搜索任何可用的邻居,并递归调用相同的函数。
DFS 还能做什么
我们可以将 DFS 算法扩展到:
- 计算最小生成树。
- 检测和查找图中的循环。
- 确定二分图。
- 寻找强连接的组件。
- 对节点进行排序。
- 找到桥梁和关节点。
这就是深度优先搜索。当然,下一个是关于广度优先搜索的细节。我绝对应该感谢你的时间,感谢你的努力。
图论|树的介绍
图论简化版
大家都怎么了?这是我的全新系列 图论的最新补充:Go Hero 在这里我们深入讨论图和相关算法。查看一下,快速浏览一下。这里我们将简单介绍一下树,它是一种图形。所以,我们开始吧。
什么是树?
作者照片
我们上面有两张图表,你能找出不一样的吗?
正确,第四个脱颖而出。但是为什么呢?
作者照片
因为树是没有圈的无向图。要记住的关键是树里面不允许有循环。你能找到一个打破规则的,对吗?干得好。
然而,还有另一种简单的方法,我们可以用它来判断给定的图是否是树。所有的树都有 **N - 1 条边,**其中 N 是节点的数量。
作者照片
我们的图表中有三个符合规则,对吗?但最后一个没有。
野外的树木
让我们来看几个我们遇到树的应用的场合。
- 文件结构
计算机文件系统包含目录、子目录和文件,它本质上是一棵树。
作者照片
- 公司层级
公司层级是指公司内部根据权力、地位和工作职能对个人进行的安排和组织。它划分了权力和责任,根据员工、部门、分部和其他管理人员在层级中的位置指定他们的领导。一个完整的公司应该有一个树状结构。
- 评估数学表达式
Tress 可用于将数学表达式和源代码分解成抽象格式,以便以正式的方式对它们进行评估。下面是一个表达式及其相应的树表示。
(a * b) + (c - d)
作者照片
- Web 文档对象模型(DOM)
我们访问的每个网页都是由某些标签组成的,如、
、等。DOM 是一种将 XML 或 HTML 文档视为树结构的接口,其中每个节点都是表示文档一部分的对象。下面给出了一个这样的树。
安东尼通过 pixabay 拍摄的照片
树木还有更多应用,
- 决策树
- 家谱
- 分类学
- 图论树
- 文本分析树
- 社会等级
- 概率树
存储无向树
我们应该从给树中的每个节点分配编号开始,从 0 到 n - 1 ,其中 n 是节点总数。
作者照片
存储这棵树最简单的方法是使用一个边列表,其中列表中的每一对表示两个节点之间的一条边。对于上面的树表示,相应的边列表将是,
[(0,2),(2,3),(2,4),(2,1),(1,5),(5,6),(5,7),(7,8)]
迭代边对非常快速和容易,但不会存储任何邻域信息。由于这个缺点,我们宁愿使用另一种叫做**邻接表的表示法。**这是一张从一个节点到其邻居的地图。上面给定的相同的树可以转换成如下的邻接表,
【0】→【2】
【1】→【2,5】
【2】→【0,1,3,4】
【3】→【2】
【4】→【2】
【5】→【1,6,7】
【6】→【5】
【7】→【5,8】
【8】→【7】
除了这两种方法,我们还有另外一个解决这个问题的方法。我们可以使用邻接矩阵来显示两个节点之间是否存在边。
作者照片
如果存在从节点 i 到jelse 0 的边,我们用 1 填充邻接矩阵的【T50(I,j)】单元。例如,如果在节点 5 和 7 之间存在边,那么(5,7)将是 1。
实际上,将树作为邻接矩阵很麻烦,因为大多数节点之间可能有边,也可能没有边,所以大多数单元会很稀疏,足以完全不占用内存。
有根的树
有根树会有一个特殊的节点被指定为根节点,因此命名为有根树。
图片 bu 作者
这里,橙色的节点被指定为根。任一边缘都可能靠近根部或远离根部。每棵有根的树都只有一个根。一些递归算法使用有根树来跟踪执行流程。
二叉树
我们永远不能离开这个会议没有提到二叉树。我们称之为二叉树或简称为 B 树是因为树中的每个节点总共最多有 2 个子节点。
作者照片
在现实世界中,B 树很少存在。
二叉查找树
我们称一棵树为二叉查找树当且仅当它满足 BST 不变量,BST 不变量定义为,对于每个节点 x,左子树中的值严格小于 x 的值,右子树中的值大于 x 的值。二叉查找树在搜索问题时非常有用。
作者照片
上面的实现禁止我们的树有重复的条目。所以树中的每个节点都会感知到一个独特的元素。
存储有根的树
作者照片
有根树最自然地以自顶向下的方式递归定义。实际上,我们总是维护一个指向根节点的指针,这样我们就可以访问树及其内容。此外,每个节点都可以访问其子节点。
作者照片
这里,橙色的节点是当前引用节点,所有绿色的节点是子节点。注意,子节点是叶子,它们没有任何特定的子节点。
如果我们的树是一棵二叉树,我们可以将它存储在一个扁平数组中。在这种表示中,每个节点都有一个基于它在树中的位置而分配的索引位置。
作者照片
我们从值为 9 的根节点开始,它存储在索引 0 中。接下来,我们有一个值为 8 的节点,它在索引 1 中,依此类推。如果你仔细观察数组,你可以看到一些位置用 null、 why 填充,因为我们在树中没有任何特定位置的值。根节点将总是在索引 0 中,并且相对于位置 I 访问当前节点 I 的子节点。
例如,假设我是当前节点的索引,那么
左侧节点: 2 * i + 1
右节点: 2 * i + 2
反过来,一个节点的父节点应该是,
楼层((i - 1)/2)
结论
今天我们发现了什么是树和它的不同种类。这篇文章的核心思想是让你了解数据结构和它在现实世界中的应用。我们将在本系列的下一篇文章中讨论更多的算法。在那之前,敬请期待。
图论|同构树
图论简化版
大家好。这是我关于图论的博客系列的第十篇文章,名为 图论:英雄 。今天,我们将深入探讨树中的同构。我强烈建议你阅读我最近发表的关于图论的文章,这些文章更多的是从计算机科学的角度出发。所以,让我们开始吧。
定义同构
同态是一个非常普遍的概念,出现在数学的几个领域。这个词来源于 希腊语 iso ,意为“ 等于 ,”而 morphosis ,意为“ 形成 ”或“ 形成 ”它代表了两个相似系统之间的相互关系。维基百科 对它是什么以及它在数学中如何有用做了很好的解释。简而言之,它是两个对象之间的映射。例如,让我们举一个同构的例子。假设我们有两组数字。如果两个有限集有相同数量的元素,那么它们就是同构的。更详细地说,如果你能写下一个函数,将一个集合中的每个元素分配给另一个集合中的一个唯一元素,那么两个(现在不一定是有限的)集合是同构的,这样函数就不会“遗漏”任何一个集合中的元素。
图同构
问两个图 G1 和 G2 是否同构的问题是问它们是否结构相同。**
作者图片
我们可以看到上面的两张图。尽管图 G1 和 G2 的标签不同,可以被看作是不同的。但是,在结构上,它们是相同的图形。因此,反过来,存在一个同构,我们称之为图,同构图。如果我们打开第二个图,重新标记为相同的,我们将得到两个相似的图。
我们也可以用更严格的方式定义图同构的概念,因为说两个图在结构上是相同的并没有很好的定义。如果我们把一个图想象成顶点 V 和边 E 的集合,那么对于图 G1 和 G2,我们将分别有两个集合【G1(V1,E1)*** 和 G2(V2,E2) 。我们称这两个图同构,如果在 V1 和 V2 之间存在一个双射,使得对于 G1 中的所有对,通过对所有边的节点应用函数 φ (phi)形成有效边的顶点导致出现在 G2 中的边。通俗地说,***
为了发生同构,需要有一个函数φ,它可以将 G1 中的所有节点/边映射到 G2,反之亦然。
判定两个图是否同构,不仅人眼显而易见,对计算机来说也是一个难题。
作者图片
图同构问题是否为 NP 完全 还是一个未解决的问题。然而,许多多项式时间同构算法存在 fir 图子类,如树。
如果我让你找出同构的树,你能做到吗?是的,对吗?让我们试一试。
作者图片
如果你的答案是否定的,那你就对了。因为,它们在结构上是不一样的。
作者图片
这个怎么样?
答案是肯定的。因为存在一个标签映射
6 - 0
1 - 1
0 - 2
2 - 3
5 - 4
3 - 5
4 - 6
有几种非常快速的 概率性 (通常是 哈希 或 启发式 基础)算法来识别同构树。这些方法往往很快,但也容易出错,因为在有限的整数空间中会发生哈希冲突。
我们今天要学习的方法涉及到 将 树序列化为 unicode 编码 。这种唯一的编码只是代表一棵树的唯一字符串,如果另一棵树有相同的编码,那么它们就是同构的。
我们可以直接序列化一个没有根的树,但是实际上序列化一个有根的树在代码上更容易。然而,如果我们要对两棵树 T1 和 T2 进行根化以检查它们是否同构,需要注意的一点是,在序列化/编码树之前,要确保在两棵树中选择相同的根节点。
我们可以用来帮助自己的一个技巧是在两棵树之间找到一个公共节点。找到树的中心也会做同样的事情。
编码树
首先,我们取两棵树 T1 和 T2,它们可能同构,也可能不同构。
作者图片
我们必须找到树的中心。
作者图片
我们要做的下一件事是给一棵树生根。
作者图片
现在我们必须为每棵树生成编码,并比较序列化的树是否相等。
作者图片
树编码只是一系列左’(‘和右’)'括号。然而,我们也可以把它们看作 1 和 0。同样的上述编码可以转换成建议的方法,
作者图片
生成树编码
****【AHU】(Aho, Hopcroft , Ullman )算法是一种将树表示为唯一字符串的巧妙的序列化技术。与许多树同构不变量和启发法不同,AHU 能够捕捉树的 度谱 和结构的 完整历史 ,确保检查树同构的确定性方法。
让我们仔细看看。
作者图片
我们首先要给树的每个叶子节点分配一个空括号( () ),如下所示。
作者图片
现在我们必须向上移动(到叶子的父节点)并将叶子的括号组合在一起,并将其分配给父节点。当我们组合树叶的括号时,我们也应该将结果放在另一对括号中。例如,假设我们有一棵树,它有一个单亲和两片叶子。所以我们把()分配给树叶。当我们向父节点移动时,我们像()()一样将叶子的括号组合起来,并像(()())一样将其放在另一对括号中,并将其分配给父节点。这个过程不断重复,直到我们到达根节点。
作者图片
如果你仔细按照上面的类比,我们会在树的末尾有这样的编码。
不要忘记在组合括号之前对其进行排序。排序是按字典顺序进行的。
编码摘要
总结一下我们为 AUH 所做的:
- 叶节点分配有()
- 每当我们向上移动时,组合、排序并包装括号。
- 在处理完一个节点的所有子节点之前,我们无法处理它。
如果两棵树的表示相同,那么这两棵树就是同构的。
抓住你了。我希望这篇简介能对你未来的项目有所帮助。感谢你的时间和耐心。谢谢你。
图论|为树生根
图论简化版
今天,我们要看看如何给一棵树扎根。这是我正在进行的系列 图论:围棋英雄 的第 8 个帖子。你一定要查看索引页面,深入研究图表和相关问题。我通常会在每个周末尝试这个系列的新帖子。我们来看看生根是怎么做的。
如果我们想处理有根的树,这是我们需要的最基本的转换之一。为一棵树生根的动机是,它通常可以帮助添加一个结构并简化问题。一棵有根的树可以把一棵无向树转换成一棵更容易操作的有向树。从概念上讲,给一棵树生根就像通过一个特定的节点捡起树,让所有的边都指向下。
作者照片
我们可以通过使用树的任何节点来确定树的根,但是,在选择节点时要小心,因为不是所有的节点都不能生成平衡的树。所以我们需要有所选择。
在某些情况下,拥有一条返回父节点的路由总是一个好主意,这样我们就可以往回走。我在下面用红线说明了到父节点的路由。
作者照片
让我们来看看如何给一棵树扎根。
生根溶液
用一个 深度优先搜索 ( DFS )很容易地完成一棵树的寻根。我已经创建了一个动画版本的结果 DFS 如下。你肯定会理解的,肯定。
作者创建的 GIF
简而言之,这就是扎根于一棵树。
伪码
class Treenode:
int id;
Treenode parent;
Treenode [] children;function rootTree(g, rootId = 0):
root = Treenode(rootId, null, [])
return buildTree(g, root, null)function buildTree(g, node, parent):
for child in g[node.id]:
if parent != null and childId == parent.id:
continue
child = Treenode(childId, node, [])
node.children.add(child)
buildTree(g, child, node)
return node
我们定义了一个名为 *Treenode 的类。*树中的每个节点都有一个唯一的 id,这就是我们存储在 id 占位符中的内容。正如我们前面所讨论的,保存父节点始终是一个最佳实践,因为这将有助于我们返回。此外,我们保存了一些对当前节点的子节点的引用。
然后我们定义一个名为 rootTree 的函数,它接受两个参数——一个图和节点的 id。图 g 将被表示为具有无向边的邻接表。 rootTree 方法的第一行创建了一个 Treenode 对象,带有给定的 rootId 、父引用和子对象列表。 rootTree 函数调用另一个名为 buildTree 的函数,带有参数 graph g、根节点和对父节点的引用。
buildTree 方法采用我们刚刚谈到的三个参数。当我们进入函数时,我们最终会进入一个遍历当前节点所有子节点的 for 循环。我们知道边是无向的,所以我们绝对需要管理添加指向同一个节点的有向边的情况。如果不满足上述条件,我们确定我们手里有一个确诊的孩子。然后我们为 Treenode 类创建一个对象,并将该子节点添加到当前节点的子节点列表中。之后,它使用新创建的节点将 DFS 更多地放入树中。当我们访问节点的所有邻居时,我们返回当前节点。
所以,这就是我们如何扎根一棵树。我们将在接下来的帖子中讨论 树中心 。让我们一起继续学习。
干杯,全体。
Python 中探索性数据分析的图形化方法
调查新加坡、美国和中国的人口、教育和收入方面的性别平等
探索性数据分析(EDA)是每个数据科学或数据分析问题中最重要的方面之一。它让我们更好地理解我们的数据,并有可能揭示对我们来说不那么明显的隐藏见解。我在 Medium 上写的第一篇文章也是关于在 R 中执行 EDA 的,你可以在这里查看。本帖将更多关注 Python 中使用 matplotlib、回归线甚至运动图的图形化 EDA!
资料组
我们在本文中使用的数据集可以从 Gapminder 获得,并深入到人口、教育中的性别平等和收入。
人口数据包含 1800 年至 2018 年间按世界各国分组的估计常住人口的年度数据。
教育中的性别平等数据包含 1970 年至 2015 年期间 25 至 34 岁年龄段在校女生与男生比例的年度数据,其中包括不同国家的小学、中学和大学教育
收入数据包含 1800 年至 2018 年期间,根据全球不同国家的购买力差异(以国际美元计)进行调整后的人均年收入数据。
EDA 人口
让我们首先绘制一段时间内的人口数据,主要集中在新加坡、美国和中国这三个国家。我们将使用matplotlib
库在同一个图上绘制 3 个不同的折线图。
import pandas as pd
import matplotlib.pylab as plt
%matplotlib inline# read in data
population = pd.read_csv('./population.csv')# plot for the 3 countries
plt.plot(population.Year,population.Singapore,label="Singapore")
plt.plot(population.Year,population.China,label="China")
plt.plot(population.Year,population["United States"],label="United States")# add legends, labels and title
plt.legend(loc='best')
plt.xlabel('Year')
plt.ylabel('Population')
plt.title('Population Growth over time')
plt.show()
按作者分类的图像-新加坡、中国和美国的 Python matplotlib 人口输出
如图所示,新加坡、中国和美国这三个国家的人口值随着时间的推移而增加,尽管新加坡并不明显,因为轴是以十亿为单位,而新加坡的人口只有几百万。
现在,让我们尝试使用linregress
拟合新加坡人口数据的线性回归线,并绘制线性拟合图。我们甚至可以尝试预测 2020 年和 2100 年的新加坡人口。
from scipy.stats import linregress
# set up regression line
slope, intercept, r_value, p_value, std_err = linregress(population.Year,population.Singapore)
line = [slope*xi + intercept for xi in population.Year]# plot the regression line and the linear fit
plt.plot(population.Year,line,'r-', linewidth=3,label='Linear Regression Line')
plt.scatter(population.Year, population.Singapore,label='Population of Singapore')
plt.legend(loc='best')
plt.xlabel('Year')
plt.ylabel('Population')
plt.title('Population Growth of Singapore over time')
plt.show()# Calculate correlation coefficient to see how well is the linear fit
print("The correlation coefficient is " + str(r_value))
## Use the linear fit to predict the resident population in Singapore in 2020 and 2100.
# Using equation y=mx + c, i.e. population=slope*year + intercept
print("The predicted population in Singapore in 2020 will be " + str((slope*2020)+intercept))
print("The predicted population in Singapore in 2100 will be " + str((slope*2100)+intercept))
按作者分类的图像-新加坡人口线性回归线的 Python matplotlib 输出
从图中我们可以看出,线性拟合似乎不太适合新加坡的人口,尽管我们的相关系数接近 1。对人口的预测也很不错,因为 2020 年新加坡的现有人口约为 560 万,远远高于预测的 340 万。
注意,19 世纪 50 年代以前的人口是负数,这是绝对不可能的。因为新加坡是 1965 年建立的,所以让我们过滤一下,只使用 1965 年以后的数据。
from scipy.stats import linregress
# set up regression line
slope, intercept, r_value, p_value, std_err = linregress(population.Year[population.Year>=1965],population.Singapore[population.Year>=1965])
line = [slope*xi + intercept for xi in population.Year[population.Year>=1965]]plt.plot(population.Year[population.Year>=1965],line,'r-', linewidth=3,label='Linear Regression Line')
plt.scatter(population.Year[population.Year>=1965], population.Singapore[population.Year>=1965],label='Singapore')
plt.legend(loc='best')
plt.xlabel('Year')
plt.ylabel('Population')
plt.title('Population Growth of Singapore from 1965 onwards')
plt.show()# Calculate correlation coefficient to see how well is the linear fit
print("The correlation coefficient is " + str(r_value))
## Use the linear fit to predict the resident population in Singapore in 2020 and 2100.
# Using equation y=mx + c, i.e. population=slope*year + intercept
print("The predicted population in Singapore in 2020 will be " + str((slope*2020)+intercept))
print("The predicted population in Singapore in 2100 will be " + str((slope*2100)+intercept))
按作者分类的图像-1965 年以来新加坡人口线性回归线的 Python matplotlib 输出
如图所示,这条线性回归线与相关系数非常吻合。此外,预测的 2020 年人口正好是新加坡目前的人口,让我们希望 2100 年的人口不是真的,因为我们知道新加坡的土地面积相当小。
EDA 关于教育中的性别平等
转到第二个数据集,让我们试着绘制一段时间内新加坡、中国和美国学校的性别比例(女性对男性)图。我们还可以看看新加坡的最高和最低性别比例百分比。
# reading in data
gender_equality = pd.read_csv('./GenderEquality.csv')
# plot the graphs
plt.plot(gender_equality.Year,gender_equality.Singapore,label="Singapore")
plt.plot(gender_equality.Year,gender_equality.China,label="China")
plt.plot(gender_equality.Year,gender_equality["United States"],label="United States")# set up legends, labels and title
plt.legend(loc='best')
plt.xlabel('Year')
plt.ylabel('Gender Ratio of Female to Male in school')
plt.title('Gender Ratio of Female to Male in school over time')
plt.show()# What are the maximum and minimum values for gender ratio in Singapore over the time period?
print("The maximum value is: " + str(max(gender_equality.Singapore)) + " and the minimum is "
+ str(min(gender_equality.Singapore)))
按作者分类的图片-一段时间内新加坡、中国和美国的性别比例的 Python matplotlib 输出
从上面的产出可以看出,性别比率总体上是随着时间的推移而增加的。随着时间的推移,中国和新加坡的性别比例呈线性增长。就美国而言,在性别比率再次上升之前,有一段时间处于停滞状态。新加坡的最低性别比率为 79.5,最高性别比率为 98.9,这是意料之中的,因为过去新加坡的教育对男性比对女性重要得多。
让我们画出新加坡性别比例的线性回归线。
# plot the regression line
slope, intercept, r_value, p_value, std_err = linregress(gender_equality.Year,gender_equality["Singapore"])
line = [slope*xi + intercept for xi in gender_equality.Year]plt.plot(gender_equality.Year,line,'r-', linewidth=3,label='Linear Regression Line')
plt.plot(gender_equality.Year, gender_equality["Singapore"],label='Singapore')
plt.legend(loc='best')
plt.xlabel('Year')
plt.ylabel('Gender Ratio of Female to Male in school')
plt.title('Gender Ratio of Female to Male in school for Singapore over time')
plt.show()
print("The correlation coefficient is " + str(r_value))
按作者分类的图像-新加坡性别比例线性回归线的 Python matplotlib 输出
相关系数表明这是一个很好的匹配,性别比例在未来可能达到 100%。这是有可能的,因为在新加坡,教育不再是一种特权,男性和女性在接受正规教育方面有平等的机会。
收入 EDA
最后让我们转向收入数据,绘制新加坡、美国和中国的收入随时间变化的曲线图。
# read in data
income = pd.read_csv('./Income.csv')
# plot the graphs
plt.plot(income.Year,income.Australia,label="Singapore")
plt.plot(income.Year,income.China,label="China")
plt.plot(income.Year,income["United States"],label="United States")
# set up legends, labels, title
plt.legend(loc='best')
plt.xlabel('Year')
plt.ylabel('Income per person')
plt.title('Income per person over time')
plt.show()
按作者分类的图片-新加坡、中国和美国一段时间内的收入 Python matplotlib 输出
令人惊讶的是,新加坡的人均收入与美国相当,都在中国之上。
动作图——可视化随时间变化的关系
现在,让我们试着构建一个运动图,来可视化所有三个因素人口、性别比例和收入随时间的变化关系。为了用 Python 构建运动图,我们需要motionchart
库。
在此之前,我们需要将所有三个数据集合并成一个单一的,以方便绘制我们的运动图表。可以使用常见的 pandas 命令来完成合并。
# Convert columns into rows for each data set based on country and population/gender ratio/income
population=pd.melt(population,id_vars=['Year'],var_name='Country',value_name='Population')
gender_equality=pd.melt(gender_equality,id_vars=['Year'],var_name='Country',value_name='Gender Ratio')# Merge the 3 datasets into one on common year and country
income=pd.melt(income,id_vars=['Year'],var_name='Country',value_name='Income')
overall=pd.merge(population,gender_equality,how="inner",on=["Year","Country"])
overall=pd.merge(overall,income,how="inner",on=["Year","Country"])
为了可视化随时间变化的关系,我们需要将 Year 属性设置为运动图中的关键字。我们的 x 轴将是性别比例,y 轴是收入,气泡大小为人口,最后是国家的气泡颜色。
from motionchart.motionchart import MotionChart# setting up the style
%%html
<style>
.output_wrapper, .output {
height:auto !important;
max-height:1000px;
}
.output_scroll {
box-shadow:none !important;
webkit-box-shadow:none !important;
}
</style># plotting the motion chart
mChart = MotionChart(df = overall)
mChart = MotionChart(df = overall, key='Year', x='Gender Ratio', y='Income', xscale='linear'
, yscale='linear',size='Population', color='Country', category='Country')
mChart.to_notebook()
作者图片 Python 中运动图表的 GIF 输出
如果我们研究这个图表,我们知道阿富汗和也门的教育性别比例最低,分别为 23.7 和 30.1。南非的莱索托拥有最高的性别比例(注意右下角的粉红色小点)。
收入和教育中的性别比例之间通常没有明确的关系。在整个时期,由于所有国家的性别比率普遍上升,收入并没有随之增加,也没有减少。有停滞、增加和减少的组合,这与性别比率没有任何明显的关系。
让我们专注于为新加坡建立一个运动图表。
mChart = MotionChart(df = overall.loc[overall.Country.isin(['Singapore'])])
mChart = MotionChart(df = overall.loc[overall.Country.isin(['Singapore'])], key='Year', x='Gender Ratio',
y='Income', xscale='linear', yscale='linear',size='Population', color='Country', category='Country')
mChart.to_notebook()
作者提供的图片 Python 中新加坡运动图表的 GIF 输出
对新加坡来说,有趣的是,除了人口随着时间的推移而增长,教育中的性别比例以及收入似乎也随着时间的推移而不断增长。1970 年收入 11400,2015 年大幅增长到 80900。
摘要
在本文中,我们利用 Python matplotlib、线性回归以及富于想象力的运动图表对三个数据集进行探索性数据分析,主要是人口、教育中的性别比例和收入。通过这些图形方法,我们可以发现一些关于数据的见解,并有可能让我们做出更好的预测。希望你们喜欢这种用 Python 进行探索性数据分析的图形化方法,并喜欢玩你们奇特的运动图表!
轻松绘制的图形功能
"返回方程式的 MS Paint "
图片由 Wokandapix 来自 Pixabay
大家好!几天前,我在研究一个机器学习模型,我需要输入一个与 X 和 Y 相关的函数。唯一的约束是函数必须连续,X 和 Y 的界限必须是[0,1]。
脑子里有了几十条不同的曲线,我开始在纸上画出它们的草图。重点是之后把它们放在绘图计算器上。这是一项单调乏味的任务,我向姐姐寻求帮助。
“你为什么不为它写个程序呢?”,她回答道。
目的
一个已有的应用: MyCurveFit
现有的应用程序采用 X 和 Y 坐标来返回精确的方程。然而,曲线越复杂,用户输入的坐标就越多。此外,用户必须手动选择函数来拟合这些点(例如:线性对指数)。
这个计划
我想要一个“返回方程式的 MS Paint”。
虽然手绘意味着坐标不能精确,但当近似方程足够时,它是一个方便的工具,微调可以留待下次进行。通过比较均方误差,程序也自动选择最佳函数。
应用程序管道
这是整体架构。
我的应用程序管道
当用户在提供的画布上绘制时,提取曲线上的点的 X 和 Y 坐标。它们被输入到不同的模型中,这些模型符合如上所示的相应方程。最后,显示具有最小均方误差的方程。
由于多项式方程最容易过度拟合,因此用户可以选择限制次数。
输入加工
从用户界面到 3D 阵列的绘制
用户界面包含来自 drawingboard.js 资源库的绘图画布。当用户与它交互时,鼠标的移动会被跟踪并保存为 PNG 格式。然后图像被处理成如上所示的三维阵列。(宽 x 高 x 通道)
根据图像的数组表示,计算 X 和 Y 坐标,并分别存储在 X 和 Y 数组中。
模型拟合
Scipy 库提供了给定 X 和 Y 数组来拟合方程的方法。我主要使用了其中的两种。
Polyfit
这个方法返回给定次数的多项式的系数。下面是一个例子。
**from** numpy.polynomial.polynomial **import** polyfitx_array = [0, 1, 2, 3]
y_array = [1, 3, 4, 6]
coef = polyfit(x_array, y_array, deg=2) # Polynomial of Degree 2**print(**coef**)**
# [ 1\. 2\. 0\. ]
# Function: y = 1 + 2*x + 0*x^2
正如我们所见,这可以很容易地扩展到其他功能,如多对数。
**from** math **import** ex_array = [e, e**2, e**3]
y_array = [2, 4, 6]
coef = polyfit(np.log(x_array), y_array, deg=2)**print(**coef**)**
# [ 0\. 2\. 0\. ]
# Function: y = 0 + 2*(log x) + 0*(log x)^2
曲线拟合
这种方法更加灵活,因为它采用了用户定义的函数和要拟合的自定义系数。
正弦函数就是一个例子: y = a * sin(b * x) + c
**from** scipy.optimize **import** curve_fit
**from** math **import** pix_array = [0, pi/2, pi, 3*pi/2]
y_array = [0, 1, 0]# User-defined function with custom a, b, c, d
sine_function = **lambda** x, a, b, c, d: a * np.sin(b*x + c) + dcoef, cov = curve_fit(sine_function, x_array, y_array)**print(**coef**)**
# [ 1\. 1\. 0\. 0\. ]
# Function: y = 1*sin(1*x + 0) + 0
这同样适用于指数函数和双曲正弦函数等其他函数。
然后每个拟合的函数预测 Y 坐标。从输入 Y 中选择具有最小均方误差的一个用于显示。
输出
使用 Matplotlib 库,用不同的颜色绘制预测的 Y 与 X。之后,绘图图形被转换回 PNG 图像用于显示。
样本案例
1.三次多项式、三次多元对数函数和其他函数之间的最佳拟合
正弦函数在这里表现最佳
2.5 次多项式、5 次多对数函数和其他函数之间的最佳拟合
5 次多项式性能最佳
3.不同次数多项式函数的比较
从 1 到 3 的多项式次数
结果看起来不错!
未来的改进
首先,可以添加更多的模型。这很容易做到,因为它们已经从应用程序的其余部分中抽象出来了。欢迎建议!
第二,输入的范围可以变得更鲁棒,因为它当前仅接受[-1,1]。绘图画布可以是动态的,以适应其他范围。
感谢您的阅读!
密码
[## yarkhinephyo/sketch_a_function
在 GitHub 上创建一个帐户,为 yarkhinephyo/sketch_a_function 开发做贡献。
github.com](https://github.com/yarkhinephyo/sketch_a_function)
带 ggplot2 的 R 中的图形
了解如何使用 ggplot2 软件包在 R 中创建专业的图形和绘图
照片由艾萨克·史密斯
介绍
众所周知,R 在图形和可视化方面是一种非常强大的编程语言(当然除了统计学和数据科学!).
为了保持简短,R 中的图形可以通过三种方式完成,通过:
{graphics}
包(R 中的基本图形,默认加载){lattice}
为基础套装增加更多功能的套装{ggplot2}
包(需要预先安装并加载
{graphics}
包自带大量剧情选择(如plot
、hist
、barplot
、boxplot
、pie
、mosaicplot
等)。)和附加的相关特征(例如,abline
、lines
、legend
、mtext
、rect
等)。).对于大多数 R 用户,尤其是初学者到中级用户,这通常是绘制图形的首选方式。
自从 Hadley Wickham 在 2005 年创建以来,**{ggplot2}**
已经成为最受欢迎的 R 包之一,也是最受欢迎的图形和数据可视化包。{ggplot2}
软件包是一种更现代的创建专业质量图形的方法。更多关于这个包的信息可以在 ggplot2.tidyverse.org 找到。
在本文中,我们将看到如何用这个包在 R 中创建常见的图,如散点图、线图、直方图、箱线图、条形图、密度图。如果你不熟悉这些类型的图表,你会找到更多关于每一个的信息(何时使用,它的目的,它显示什么,等等。)在我关于 R 中描述性统计的文章中。
数据
为了说明使用{ggplot2}
包的图,我们将使用包中可用的mpg
数据集。该数据集包含美国环境保护署从 1999 年到 2008 年收集的 38 种流行车型的燃油经济性观察数据(运行?mpg
了解更多数据信息):
library(ggplot2)
dat <- ggplot2::mpg
在继续之前,让我们用transform()
函数转换因子中的cyl
、drv
、fl
、year
和class
变量:
dat <- transform(dat,
cyl = factor(cyl),
drv = factor(drv),
fl = factor(fl),
year = factor(year),
class = factor(class)
)
对于感兴趣的读者,请参阅 R 中的更多数据操作技术。
{ggplot2}
的基本原理
{ggplot2}
包基于“图形语法”的原则(因此{ggplot2}
的名字是“gg”),也就是一个描述和构建图形的连贯系统。主要想法是设计一个图形作为一系列层。
主要层有:
- 包含我们想要表示的变量的数据集。这是通过
ggplot()
功能完成的,并且排在第一位。 - 要在 x 和/或 y 轴上表示的变量,以及要表示的对象的美学元素(如颜色、大小、填充、形状和透明度)。这是通过
aes()
功能(美学的缩写)完成的。 - 类型的图形表示(散点图、线图、条形图、直方图、箱线图等)。).这是通过
geom_point()
、geom_line()
、geom_bar()
、geom_histogram()
、geom_boxplot()
等功能完成的。 - 如果需要,附加层(如标签、注释、比例、轴刻度、图例、主题、小平面等。)可以加进去个性化剧情。
为了创建一个图,我们首先需要在ggplot()
函数中指定数据,然后添加所需的层,如变量、美学元素和图的类型:
ggplot(data) +
aes(x = var_x, y = var_y) +
geom_x()
ggplot()
中的data
是包含变量var_x
和var_y
的数据帧的名称。+
符号用于指示将被添加到绘图的不同层。确保将+
符号写在代码行的末尾,而不是行首,否则 R 抛出错误。- 层
aes()
表示什么样的变量将被用在剧情中,更一般地说,是剧情的美学元素。 - 最后,
geom_x()
中的x
代表剧情类型。 - 其他层通常是不需要的,除非我们想进一步个性化情节。
请注意,为提高代码可读性,每层编写一行代码是一个很好的做法。
用{ggplot2}
创建图
在以下章节中,我们将展示如何绘制以下图形:
- 散点图
- 线形图
- 柱状图
- 密度图
- 箱线图
- 条形图
为了将重点放在不同情节的构建和{ggplot2}
的使用上,我们将限制自己只画基本的(但很漂亮的)情节,不画不必要的图层。为了完整起见,我们将在文章的最后简要讨论和说明不同的层,以进一步个性化一个情节(见本节)。
请注意,如果你在阅读完本教程后仍然难以用{ggplot2}
创建剧情,你可能会发现 {esquisse}插件很有用。这个插件允许你用{ggplot2}
包交互(即通过拖放变量)创建图形。试试看!
散点图
我们首先使用geom_point
创建一个散点图。请记住,散点图用于可视化两个定量变量之间的关系。
- 我们从指定数据开始:
ggplot(dat) # data
2.然后我们添加用aes()
函数表示的变量:
ggplot(dat) + # data
aes(x = displ, y = hwy) # variables
3.最后,我们指出情节的类型:
ggplot(dat) + # data
aes(x = displ, y = hwy) + # variables
geom_point() # type of plot
除了数据集之外,您有时还会看到ggplot()
函数中的美学元素(【T2 与变量】):
ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point()
第二种方法给出的图与第一种方法完全相同。为了获得更好的可读性,我倾向于选择第一种方法,但这更多的是个人喜好的问题,所以选择由你决定。
线形图
线形图,在时间序列或金融中特别有用,可以类似地创建,但使用geom_line()
:
ggplot(dat) +
aes(x = displ, y = hwy) +
geom_line()
线和点的结合
{ggplot2}
的一个优势是能够结合多种类型的情节,并且设计灵活。例如,我们可以通过简单地向初始散点图添加一个层来向散点图添加一条线:
ggplot(dat) +
aes(x = displ, y = hwy) +
geom_point() +
geom_line() # add line
柱状图
可以使用geom_histogram()
绘制直方图(用于可视化分布和检测潜在的异常值):
ggplot(dat) +
aes(x = hwy) +
geom_histogram()
默认情况下,箱的数量等于 30。您可以使用geom_histogram()
函数中的bins
参数来更改该值:
ggplot(dat) +
aes(x = hwy) +
geom_histogram(bins = sqrt(nrow(dat)))
这里我指定箱的数量等于观察数量的平方根(遵循斯特奇法则),但是您可以指定任何数值。
密度图
密度图可以使用geom_density()
创建:
ggplot(dat) +
aes(x = hwy) +
geom_density()
直方图和密度的组合
我们也可以在同一个图上叠加直方图和密度曲线:
ggplot(dat) +
aes(x = hwy, y = ..density..) +
geom_histogram() +
geom_density()
或者叠加几个密度:
ggplot(dat) +
aes(x = hwy, color = drv, fill = drv) +
geom_density(alpha = 0.25) # add transparency
为了增加透明度,增加了参数alpha = 0.25
。关于这个论点的更多信息可以在这个部分中找到。
箱线图
可以使用geom_boxplot()
绘制一个箱线图(对可视化分布和检测潜在的异常值也非常有用):
# Boxplot for one variable
ggplot(dat) +
aes(x = "", y = hwy) +
geom_boxplot()
# Boxplot by factor
ggplot(dat) +
aes(x = drv, y = hwy) +
geom_boxplot()
也可以用geom_jitter()
在箱线图上绘制点,并用varwidth = TRUE
根据每个级别的大小(即观察次数)改变方框的宽度:
ggplot(dat) +
aes(x = drv, y = hwy) +
geom_boxplot(varwidth = TRUE) + # vary boxes width according to n obs.
geom_jitter(alpha = 0.25, width = 0.2) # adds random noise and limit its width
geom_jitter()
层给每个点添加一些随机变化,以防止它们重叠(一个被称为过度绘制的问题)。 1 此外,alpha
参数为点增加了一些透明度(详见本部分)以保持焦点在框上而不是点上。
最后,也可以根据定性变量的水平将箱线图分成几个面板:
ggplot(dat) +
aes(x = drv, y = hwy) +
geom_boxplot(varwidth = TRUE) + # vary boxes width according to n obs.
geom_jitter(alpha = 0.25, width = 0.2) + # adds random noise and limit its width
facet_wrap(~year) # divide into 2 panels
为了视觉上更吸引人的绘图,也可以根据 x 变量对框使用一些颜色:
ggplot(dat) +
aes(x = drv, y = hwy, fill = drv) + # add color to boxes with fill
geom_boxplot(varwidth = TRUE) + # vary boxes width according to n obs.
geom_jitter(alpha = 0.25, width = 0.2) + # adds random noise and limit its width
facet_wrap(~year) + # divide into 2 panels
theme(legend.position = "none") # remove legend
在这种情况下,最好删除图例,因为它变得多余。参见部分中关于图例的更多信息。
如果你对{ggplot2}
中提供的默认颜色不满意,你可以用scale_fill_manual()
层手动改变它们:
ggplot(dat) +
aes(x = drv, y = hwy, fill = drv) + # add color to boxes with fill
geom_boxplot(varwidth = TRUE) + # vary boxes width according to n obs.
geom_jitter(alpha = 0.25, width = 0.2) + # adds random noise and limit its width
facet_wrap(~year) + # divide into 2 panels
theme(legend.position = "none") + # remove legend
scale_fill_manual(values = c("darkred", "darkgreen", "steelblue")) # change fill color manually
条形图
可以使用geom_bar()
绘制柱状图(用于可视化定性变量):
ggplot(dat) +
aes(x = drv) +
geom_bar()
默认情况下,条形的高度对应于感兴趣变量的每个级别的观察频率(在我们的例子中为drv
)。
同样,为了更吸引人的情节,我们可以用fill
参数给条形添加一些颜色:
ggplot(dat) +
aes(x = drv, fill = drv) + # add colors to bars
geom_bar() +
theme(legend.position = "none") # remove legend
我们还可以创建一个包含两个定性变量的柱状图:
ggplot(dat) +
aes(x = drv, fill = year) + # fill by years
geom_bar()
为了比较各组之间的比例,最好使用position = "fill"
使每个条形的高度相同:
ggplot(dat) +
geom_bar(aes(x = drv, fill = year), position = "fill")
要为每组绘制相邻的条形,请使用position = "dodge"
:
ggplot(dat) +
geom_bar(aes(x = drv, fill = year), position = "dodge")
进一步个性化
标题和轴标签
在一个情节中,首先要个性化的是标签,以使情节对观众更具信息性。我们可以使用labs()
功能轻松添加标题、副标题、说明和编辑轴标签:
p <- ggplot(dat) +
aes(x = displ, y = hwy) +
geom_point()p + labs(
title = "Fuel efficiency for 38 popular models of car",
subtitle = "Period 1999-2008",
caption = "Data: ggplot2::mpg. See more at www.statsandr.com",
x = "Engine displacement (litres)",
y = "Highway miles per gallon (mpg)"
)
正如您在上面的代码中看到的,您可以将一个或多个绘图层保存在一个对象中以供以后使用。这样,您可以保存您的“主”图,并添加更多的个性化层,直到您获得所需的输出。这里,我们将主散点图保存在一个名为p
的对象中,我们将在后续的个性化设置中引用它。
您还可以通过theme()
图层和element_text()
功能编辑标题和副标题的对齐、大小和形状:
p + labs(
title = "Fuel efficiency for 38 popular models of car",
subtitle = "Period 1999-2008",
caption = "Data: ggplot2::mpg. See more at www.statsandr.com",
x = "Engine displacement (litres)",
y = "Highway miles per gallon (mpg)"
) +
theme(
plot.title = element_text(
hjust = 0.5, # center
size = 12,
color = "steelblue",
face = "bold"
),
plot.subtitle = element_text(
hjust = 0.5, # center
size = 10,
color = "gray",
face = "italic"
)
)
如果标题或副标题很长,你想把它分成多行,用\n
:
p + labs(
title = "Fuel efficiency for 38 popular \n models of car",
subtitle = "Period 1999-2008",
caption = "Data: ggplot2::mpg. See more at www.statsandr.com",
x = "Engine displacement (litres)",
y = "Highway miles per gallon (mpg)"
) +
theme(
plot.title = element_text(
hjust = 0.5, # center
size = 12,
color = "steelblue",
face = "bold"
),
plot.subtitle = element_text(
hjust = 0.5, # center
size = 10,
color = "gray",
face = "italic"
)
)
坐标轴刻度
可以分别使用 x 轴和 y 轴的scale_x_continuous()
和scale_y_continuous()
调整轴刻度:
# Adjust ticks
p + scale_x_continuous(breaks = seq(from = 1, to = 7, by = 0.5)) + # x-axis
scale_y_continuous(breaks = seq(from = 10, to = 45, by = 5)) # y-axis
日志转换
在某些情况下,绘制变量的对数变换很有用。这可以通过scale_x_log10()
和scale_y_log10()
功能完成:
p + scale_x_log10() +
scale_y_log10()
限制
除了limits
参数之外,控制绘图限制的最方便的方法是再次使用scale_x_continuous()
和scale_y_continuous()
功能:
p + scale_x_continuous(limits = c(3, 6)) +
scale_y_continuous(limits = c(20, 30))
也可以用subset()
或filter()
函数简单地获取数据集的一个子集。如果您需要提醒,请参阅如何子集化数据集。
缩放以获得更好的轴格式
根据您的数据,可以用{scales}
包以某种方式格式化轴。我最常用的格式是comma
和label_number_si()
,它们以更易读的方式格式化大数字。
在本例中,我们将两个变量都乘以 1000 以得到更大的数字,然后对每个轴应用不同的格式:
ggplot(dat) +
aes(x = displ * 1000, y = hwy * 1000) +
geom_point() +
scale_y_continuous(labels = scales::label_number_si()) + # format y-axis
scale_x_continuous(labels = scales::comma) # format x-axis
如您所见,y 轴上的数字自动标有最佳 SI 前缀(“K”表示值≥ 10e3,“M”表示值≥ 10e6,“B”表示值≥ 10e9,“T”表示值≥ 10e12),x 轴上的数字显示为 2,000、3,000 等。而不是 2000,3000 等等。
这两种格式使得大数字更容易阅读。其他格式也是可能的,例如使用美元符号、百分比符号、日期等。参见包文档中的更多信息。
神话;传奇
默认情况下,图例位于图的右侧(当然,当有图例显示时)。为了控制图例的位置,除了使用legend.position
参数之外,我们还需要使用theme()
函数:
p + aes(color = class) +
theme(legend.position = "top")
用"left"
或"bottom"
替换"top"
以改变其位置,用"none"
将其移除。
可以用labs()
层编辑图例的标题:
p + aes(color = class) +
labs(color = "Car's class")
请注意,labs()
中的参数必须与aes()
层中的参数相匹配(在本例中为:color
)。
图例的标题也可以在theme()
层内用legend.title = element_blank()
移除:
p + aes(color = class) +
theme(
legend.title = element_blank(),
legend.position = "bottom"
)
图例现在出现在图的底部,但没有图例标题。
形状、颜色、大小和透明度
有大量的选项可以提高绘图质量或添加附加信息。其中包括:
- 形状,
- 尺寸,
- 颜色,以及
- alpha(透明度)。
例如,我们可以通过添加shape
到geom_point()
来改变散点图中所有点的形状,或者根据另一个变量的值改变形状(在这种情况下,shape
参数必须在aes()
内): 2
# Change shape of all points
ggplot(dat) +
aes(x = displ, y = hwy) +
geom_point(shape = 4)
# Change shape of points based on a categorical variable
ggplot(dat) +
aes(x = displ, y = hwy, shape = drv) +
geom_point()
遵循同样的原则,我们可以基于定性或定量变量修改点的颜色、大小和透明度。以下是一些例子:
p <- ggplot(dat) +
aes(x = displ, y = hwy) +
geom_point()# Change color for all points
p + geom_point(color = "steelblue")
# Change color based on a qualitative variable
p + aes(color = drv)
# Change color based on a quantitative variable
p + aes(color = cty)
# Change color based on a criterion (median of cty variable)
p + aes(color = cty > median(cty))
# Change size of all points
p + geom_point(size = 4)
# Change size of points based on a quantitative variable
p + aes(size = cty)
# Change transparency based on a quantitative variable
p + aes(alpha = cty)
我们当然可以混合几种选项(形状、颜色、大小、alpha)来构建更复杂的图形:
p + geom_point(size = 0.5) +
aes(color = drv, shape = year, alpha = cty)
如果您对默认颜色不满意,您可以使用scale_colour_manual()
层(用于定性变量)和scale_coulour_gradient2()
层(用于定量变量)手动更改它们:
# Change color based on a qualitative variable
p + aes(color = drv) +
scale_colour_manual(values = c("red", "blue", "green"))
# Change color based on a quantitative variable
p + aes(color = cty) +
scale_colour_gradient2(
low = "green",
mid = "gray",
high = "red",
midpoint = median(dat$cty)
)
文本和标签
要在一个点上添加标签(例如行号),我们可以使用geom_text()
和aes()
功能:
p + geom_text(aes(label = rownames(dat)),
check_overlap = TRUE,
size = 2,
vjust = -1
)
要在图上添加文本,我们使用annotate()
功能:
p + annotate("text",
x = 6,
y = 40,
label = "hwy and displ are \n negatively correlated \n (rho = -0.77, p-value < 0.001)",
size = 3
)
阅读 R 中关于相关系数和相关性测试的文章,看看我是如何计算相关系数(rho)和相关性测试的 p 值的。
平滑线和回归线
在散点图中,可以添加拟合数据的平滑线:
p + geom_smooth()
在简单线性回归的情况下,回归线通常显示在图上。这可以通过在geom_smooth()
层添加method = lm
( lm
代表线性模型)来实现:
p + geom_smooth(method = lm)
还可以为分类变量的每个级别绘制一条回归线:
p + aes(color = drv, shape = drv) +
geom_smooth(method = lm, se = FALSE)
se = FALSE
参数删除回归线周围的置信区间。
面状
facet_grid
允许您根据一个或两个定性变量的值将同一个图形分成几个面板:
# According to one variable
p + facet_grid(. ~ drv)
# According to 2 variables
p + facet_grid(drv ~ year)
然后可以为每个面添加一条回归线:
p + facet_grid(. ~ drv) +
geom_smooth(method = lm)
facet_wrap()
也可以使用,如本章节所示。
主题
在{ggplot2}
包中有几个功能可以改变情节的主题。默认主题(即theme_gray()
)之后最常见的主题是黑白(theme_bw()
)、极简(theme_minimal()
)和经典(theme_classic()
)主题:
# Black and white theme
p + theme_bw()
# Minimal theme
p + theme_minimal()
# Classic theme
p + theme_classic()
我倾向于在我的大部分降价报告中使用最小化主题,因为它展现了模式和要点,而不是情节的布局,但这也是个人品味的问题。在 ggplot2.tidyverse.org/reference/ggtheme.html和{ggthemes}
套餐中查看更多主题。
为了避免必须更改您创建的每个图的主题,您可以使用theme_set()
功能更改当前 R 会话的主题,如下所示:
theme_set(theme_minimal())
与{plotly}
的互动剧情
您可以轻松地将使用{ggplot2}
创建的情节与{plotly}
包进行交互:
library(plotly)
ggplotly(p + aes(color = year))
23456720304019992008 显示年份
现在,您可以将鼠标悬停在某个点上,以显示关于该点的更多信息。也有可能放大和缩小,下载图,选择一些观察,等等。关于 R 的{plotly}
的更多信息可以在这里找到。
用{patchwork}
合并图形
有几种方法可以组合{ggplot2}
中制作的剧情。在我看来,最方便的方法是在{patchwork}
包中使用符号,如+
、/
和括号。
我们首先需要创建一些情节并保存它们:
p_a <- ggplot(dat) +
aes(x = displ, y = hwy) +
geom_point()p_b <- ggplot(dat) +
aes(x = hwy) +
geom_histogram()p_c <- ggplot(dat) +
aes(x = drv, y = hwy) +
geom_boxplot()
现在我们的环境中已经保存了 3 个地块,我们可以将它们组合起来。要使图彼此相邻,只需使用+
符号:
library(patchwork)
p_a + p_b + p_c
要上下显示它们只需使用/
符号:
p_a / p_b / p_c
最后,将上面的和下面的组合在一起,混合+
、/
和括号:
p_a + p_b / p_c
(p_a + p_b) / p_c
查看更多结合绘图的方式:
grid.arrange()
来自{gridExtra}
包plot_grid()
来自{cowplot}
包
翻转坐标
翻转图的坐标对于创建水平盒状图很有用,或者当变量的标签太长以至于在 x 轴上相互重叠时也很有用。请参见下面的带和不带翻转坐标:
# without flipping coordinates
p1 <- ggplot(dat) +
aes(x = class, y = hwy) +
geom_boxplot()# with flipping coordinates
p2 <- ggplot(dat) +
aes(x = class, y = hwy) +
geom_boxplot() +
coord_flip()library(patchwork)
p1 + p2 # left: without flipping, right: with flipping
这可以用许多类型的图来完成,而不仅仅是箱线图。例如,如果分类变量有许多级别或标签很长,通常最好翻转坐标以获得更好的视觉效果:
ggplot(dat) +
aes(x = class) +
geom_bar() +
coord_flip()
保存绘图
除非您指定另一个文件夹的路径,否则ggsave()
功能会将最近的绘图保存在您当前的工作目录中:
ggplot(dat) +
aes(x = displ, y = hwy) +
geom_point()ggsave("plot1.pdf")
您也可以指定宽度、高度和分辨率,如下所示:
ggsave("plot1.pdf",
width = 12,
height = 12,
units = "cm",
dpi = 300
)
管理日期
如果数据集中的时间变量是日期格式的,{ggplot2}
包会识别日期格式,并自动使用特定类型的轴刻度。
在我们的数据集中没有日期格式的时间变量,所以让我们借助as.Date()
函数创建一个这种类型的新变量:
dat$date <- as.Date("2020-08-21") - 0:(nrow(dat) - 1)
查看此日期变量及其类的前 6 个观察值:
head(dat$date)## [1] "2020-08-21" "2020-08-20" "2020-08-19" "2020-08-18" "2020-08-17"
## [6] "2020-08-16"str(dat$date)## Date[1:234], format: "2020-08-21" "2020-08-20" "2020-08-19" "2020-08-18" "2020-08-17" ...
新变量date
以日期格式正确指定。
大多数情况下,对于时间变量,我们希望创建一个线形图,在 X 轴上显示日期,在 Y 轴上显示另一个连续变量,如下图所示:
p <- ggplot(dat) +
aes(x = date, y = hwy) +
geom_line()
p
一旦时间变量被识别为日期,我们就可以使用scale_x_date()
层来改变 X 轴上显示的格式。下表显示了最常用的日期格式:
来源:www.statmethods.net
运行?strptime()
查看 r 中更多可用的日期格式。
对于本例,除了未缩写的月份之外,我们还要加上年份:
p + scale_x_date(date_labels = "%B %Y")
也可以用date_breaks
参数控制显示在 X 轴上的断点。在本例中,假设我们希望将日期显示为数字,并将每个 10 天的间隔显示为月份缩写:
p + scale_x_date(date_breaks = "10 days", date_labels = "%d %b")
如果 X 轴上显示的标签因相互重叠而不可读,您可以使用theme()
层和angle
参数旋转它们:
p + scale_x_date(date_breaks = "10 days", date_labels = "%d %b") +
theme(axis.text.x = element_text(angle = 60, hjust = 1))
小费
我最近学到了一个用{ggplot2}
绘图时非常有用的技巧。如果像我一样,您经常注释和取消注释您的图中的一些代码行,您知道如果不删除上一行中的+
符号,您就不能将最后一行转换为注释。
如果您忘记删除代码最后一行中的+
符号,在图的末尾添加一行NULL
将会避免错误。请看这个基本的例子:
ggplot(dat) +
aes(x = date, y = hwy) +
geom_line() + # I do not have to remove the + sign
# theme_minimal() + # this line is a comment
NULL # adding this line doesn't change anything to the plot
这个技巧节省了我很多时间,因为我不需要担心在注释了我的图中的一些代码行之后,要确保删除最后一个+
符号。
如果你觉得这个技巧很有用,你可能会喜欢 RStudio 和 R Markdown 中的这些其他提示和技巧。
走得更远
到目前为止,你已经看到了{ggplot2}
是一个非常强大和完整的软件包,可以在 r 中创建情节。本文只展示了冰山一角,你会发现许多关于如何使用{ggplot2}
在线创建更高级的情节和可视化的教程。如果您想了解比本文所述更多的内容,我强烈建议您从以下内容开始:
- 章节数据可视化和用于交流的图形来自 Garrett Grolemund 和 Hadley Wickham 的《数据科学的研究》一书 R
- Hadley Wickham 的书 ggplot2:优雅的数据分析图形
- 温斯顿·张的书
- 列出了许多扩展
{ggplot2}
的包的 ggplot2 扩展指南 [{ggplot2}](https://www.statsandr.com/blog/files/ggplot2-cheatsheet.pdf)
小抄
感谢阅读。我希望这篇文章能帮助你用{ggplot2}
包创建你的第一个情节。提醒一下,对于简单的图形,有时通过 {esquisse}插件来绘制更容易。过一段时间后,您将很快学会如何自己创建它们,并且很快就能够构建复杂和高级的数据可视化。
和往常一样,如果您有与本文主题相关的问题或建议,请将其添加为评论,以便其他读者可以从讨论中受益。
- 小心使用
geom_jitter()
图层,因为尽管它在大比例下使绘图更具揭示性,但在小比例下也会使其稍不精确,因为点上增加了一些随机性。 ↩︎ - (在撰写本文时)在
shape
参数中接受了 26 种形状。参见本文档了解所有可用的形状。 ↩︎
相关文章
原载于 2020 年 8 月 21 日 https://statsandr.com**T21。