数学史上的图论可以追溯到柯尼斯堡七桥问题(大约1730年代)。它提问是否可以在以下限制条件下遍历柯尼斯堡市的七座桥梁。欧拉于1736年研究并解决了此问题,他把问题归结为如“一笔画”问题。他的《柯尼斯堡七桥》的论文圆满解决了这一问题,同时开创了数学一个新分支---图论。
平均路径长度:所有可能节点对应的最短路径长度的平均值。给出了图的“紧密度”度量,可用于了解此网络中某些内容的流动速度。
BFS和DFS:广度优先搜索和深度优先搜索是用于在图中搜索节点的两种不同算法。它们通常用于确定我们是否可以从给定节点到达某个节点。这也称为图遍历。BFS的目的是尽可能接近根节点遍历图,而DFS算法旨在尽可能远离根节点。
中心性(Centrality):用于分析网络的最广泛使用和最重要的概念工具之一。中心性旨在寻找网络中最重要的节点。可能存在对“重要”的不同理解,因此存在许多中心性度量标准。中心性标准本身就可以分成好多类。有一些标准是以沿着边的流动为特征,还有一些标准以步行结构(Walk Structure)为特征。
一些最常用的标准是:
度中心性(Degree Centrality) - 第一个也是概念上最简单的中心性定义。表示连接到某节点的边数。在有向图中,我们可以有2个度中心性度量。流入和流出的中心性。
紧密中心性(Closeness Centrality) - 从某节点到所有其他节点的最短路径的平均长度。
中介中心性(Betweenness Centrality) - 某节点在多少对节点的最短路径上。
这些中心性度量有不同变种,并且可以使用各种算法来实现定义。总而言之,这方面有大量的定义和算法。
网络密度:图的边数的度量。实际定义将根据图的类型和所提问问题的上下文而不同。对于完备的无向图,密度为1,而空图(empty)为0。在某些情况下(包含循环时),图密度可能大于1。
图随机化(Graph Randomization):尽管一些图度量指标可能很容易计算,但要理解它们的相对重要性并不容易。在这种情况下,我们使用网络/图随机化。我们计算了手头的图和随机生成的另一些类似图的度量。例如,这些相似图可以有相同数量的密度和节点。通常我们生成1000个相似的随机图并计算每个图的度量标准,然后与手头图的相同度量进行比较,以得出某些基准(benchmark)。
我们将在Python中使用networkx包。它可以安装在Anaconda的Root环境中(如果你使用的是Anaconda的Python分发版)。你也可以pip install安装它。
让我们看一下使用Networkx软件包可以完成的一些常见事情。包括导入和创建图以及可视化图的方法。
图形创建
import networkx as nx
# Creating a Graph
G = nx.Graph() # Right now G is empty
# Add a node
G.add_node(1)
G.add_nodes_from([2,3]) # You can also add a list of nodes by passing a list argument
# Add edges
G.add_edge(1,2)
e = (2,3)
G.add_edge(*e) # * unpacks the tuple
G.add_edges_from([(1,2), (1,3)]) # Just like nodes we can add edges from a list
通过传递包含节点和属性dict的元组,可以在创建节点和边的时候添加节点和边的属性。除了逐个节点或逐个边地构建图形之外,还可以通过一些经典的图操作来生成它们,例如
subgraph(G, nbunch) - induced subgraph view of G on nodes in nbunch
union(G1,G2) - graph union
disjoint_union(G1,G2) - graph union assuming all nodes are different
cartesian_product(G1,G2) - return Cartesian product graph
compose(G1,G2) - combine graphs identifying nodes common to both
complement(G) - graph complement
create_empty_copy(G) - return an empty copy of the same graph class
convert_to_undirected(G) - return an undirected representation of G
convert_to_directed(G) - return a directed representation of G
访问边和节点
可以使用G.nodes和G.edges方法访问节点和边。可以使用括号/下标法访问各个节点和边
G.nodes()
NodeView((1, 2, 3))
G.edges()
EdgeView([(1, 2), (1, 3), (2, 3)])
G[1] # same as G.adj[1]
AtlasView({2: {}, 3: {}})
G[1][2]
{}
G.edges[1, 2]
{}
图可视化
Networkx提供了可视化图的基本功能,但其主要目标是帮助图分析而不是图的可视化。图可视化很难,我们将使用专门用于此任务的工具。Matplotlib提供了一些便利功能。但是GraphViz可能是最好的工具。
%matplotlib inline
import matplotlib.pyplot as plt
nx.draw(G)
首先必须安装Graphviz。然后使用该命令pip install pygraphviz --install-option =“<>。在安装选项中,你必须提供Graphviz 中lib和include文件夹的路径。
import pygraphviz as pgv
d={'1': {'2': None}, '2': {'1': None, '3': None}, '3': {'1': None}}
A = pgv.AGraph(data=d)
print(A) # This is the 'string' or simple representation of the Graph
Output:
strict graph "" {
1 -- 2;
2 -- 3;
3 -- 1;
}
PyGraphviz可以很好地控制边和节点的各个属性。我们可以使用它获得非常漂亮的可视化。
# Let us create another Graph where we can individually control the colour of each node
B = pgv.AGraph()
# Setting node attributes that are common for all nodes
B.node_attr['style']='filled'
B.node_attr['shape']='circle'
B.node_attr['fixedsize']='true'
B.node_attr['fontcolor']='#FFFFFF'
# Creating and setting node attributes that vary for each node (using a for loop)
for i in range(16):
B.add_edge(0,i)
n=B.get_node(i)
n.attr['fillcolor']="#%2x0000"%(i*16)
n.attr['height']="%s"%(i/16.0+0.5)
n.attr['width']="%s"%(i/16.0+0.5)
B.draw('star.png',prog="circo") # This creates a .png file in the local directory. Displayed below.
Image('images/star.png', width=650) # The Graph visualization we created above.
通常,可视化被认为是与图分析独立的任务。分析后的图将导出为Dotfile。然后单独显示该Dotfile以展示我们想表达的内容。
我们将寻找一个通用数据集(不是专门用于图的数据集)并进行一些操作(在pandas中),以便它可以以边列表(edge list)的形式输入到图中。边列表是一个元组列表,其中的元组包含定义每条边的顶点
我们将关注的数据集来自航空业。它有一些关于航线的基本信息。有某段旅程的起始点和目的地。还有一些列表示每段旅程的到达和起飞时间。如你所想,这个数据集非常适合作为图进行分析。想象一下通过航线(边)连接的几个城市(节点)。如果你是航空公司,你可以问如下几个问题:
从A到B的最短途径是什么?分别从距离和时间角度考虑。
有没有办法从C到D?
哪些机场的交通最繁忙?
哪个机场位于大多数其他机场“之间”?这样它就可以变成当地的一个中转站。
import pandas as pd
import numpy as np
data = pd.read_csv('data/Airlines.csv')
data.shape
(100, 16)
data.dtypes
year int64
month int64
day int64
dep_time float64
sched_dep_time int64
dep_delay float64
arr_time float64
sched_arr_time int64
arr_delay float64
carrier object
flight int64
tailnum object
origin object
dest object
air_time float64
distance int64
dtype: object
我们注意到起始点和目的地看起来像节点的好人选。然后可以将所有东西想象为节点或边的属性。单条边可以被认为是一段旅程。这样的旅程将有不同的时间,航班号,飞机尾号等相关信息。
我们注意到年,月,日和时间信息分散在许多列上。所以我们想创建一个包含所有这些信息的日期时间列。我们还需要将预计的(scheduled)和实际的(actual)到达离开时间分开。所以我们最终应该有4个日期时间列(预计到达时间、预计起飞时间、实际到达时间和实际起飞时间)。
此外,时间列的格式不正确。下午4:30被表示为1630而不是16:30。该列没有分隔符。一种方法是使用pandas字符串方法和正则表达式。
我们还应该注意到sched_dep_time和sched_arr_time是int64 类型而dep_time和arr_time是float64 类型。
另一个麻烦是NaN值。
# converting sched_dep_time to 'std' - Scheduled time of departure
data['std'] = data.sched_dep_time.astype(str).str.replace('(\d{2}$)', '') + ':' + data.sched_dep_time.astype(str).str.extract('(\d{2}$)', expand=False) + ':00'
# converting sched_arr_time to 'sta' - Scheduled time of arrival
data['sta'] = data.sched_arr_time.astype(str).str.replace('(\d{2}$)', '') + ':' + data.sched_arr_time.astype(str).str.extract('(\d{2}$)', expand=False) + ':00'
# converting dep_time to 'atd' - Actual time of departure
data['atd'] = data.dep_time.fillna(0).astype(np.int64).astype(str).str.replace('(\d{2}$)', '') + ':' + data.dep_time.fillna(0).astype(np.int64).astype(str).str.extract('(\d{2}$)', expand=False) + ':00'
# converting arr_time to 'ata' - Actual time of arrival
data['ata'] = data.arr_time.fillna(0).astype(np.int64).astype(str).str.replace('(\d{2}$)', '') + ':' + data.arr_time.fillna(0).astype(np.int64).astype(str).str.extract('(\d{2}$)', expand=False) + ':00'
现在时间列被转换成了我们想要的格式。最后,我们可能希望将年,月和日列合并到日期列中。这一步不是绝对必要的。但是,一旦转换为日期时间(datetime)格式,我们就可以轻松获取年,月,日(和其他)信息。
data['date'] = pd.to_datetime(data[['year', 'month', 'day']])
# finally we drop the columns we don't need
data = data.drop(columns = ['year', 'month', 'day'])
现在使用networkx函数导入数据集,该函数直接读如pandas DataFrame。就像图创建一样,多种方法可以将数据从多种格式中输入到图中。
import networkx as nx
FG = nx.from_pandas_edgelist(data, source='origin', target='dest', edge_attr=True,)
FG.nodes()
输出:
NodeView(('EWR', 'MEM', 'LGA', 'FLL', 'SEA', 'JFK', 'DEN', 'ORD', 'MIA', 'PBI', 'MCO', 'CMH', 'MSP', 'IAD', 'CLT', 'TPA', 'DCA', 'SJU', 'ATL', 'BHM', 'SRQ', 'MSY', 'DTW', 'LAX', 'JAX', 'RDU', 'MDW', 'DFW', 'IAH', 'SFO', 'STL', 'CVG', 'IND', 'RSW', 'BOS', 'CLE'))
FG.edges()
输出:
EdgeView([('EWR', 'MEM'), ('EWR', 'SEA'), ('EWR', 'MIA'), ('EWR', 'ORD'), ('EWR',
'MSP'), ('EWR', 'TPA'), ('EWR', 'MSY'), ('EWR', 'DFW'), ('EWR', 'IAH'), ('EWR', 'SFO'),
('EWR', 'CVG'), ('EWR', 'IND'), ('EWR', 'RDU'), ('EWR', 'IAD'), ('EWR', 'RSW'), ('EWR',
'BOS'), ('EWR', 'PBI'), ('EWR', 'LAX'), ('EWR', 'MCO'), ('EWR', 'SJU'), ('LGA', 'FLL'),
('LGA', 'ORD'), ('LGA', 'PBI'), ('LGA', 'CMH'), ('LGA', 'IAD'), ('LGA', 'CLT'), ('LGA',
'MIA'), ('LGA', 'DCA'), ('LGA', 'BHM'), ('LGA', 'RDU'), ('LGA', 'ATL'), ('LGA', 'TPA'),
('LGA', 'MDW'), ('LGA', 'DEN'), ('LGA', 'MSP'), ('LGA', 'DTW'), ('LGA', 'STL'), ('LGA',
'MCO'), ('LGA', 'CVG'), ('LGA', 'IAH'), ('FLL', 'JFK'), ('SEA', 'JFK'), ('JFK', 'DEN'),
('JFK', 'MCO'), ('JFK', 'TPA'), ('JFK', 'SJU'), ('JFK', 'ATL'), ('JFK', 'SRQ'), ('JFK',
'DCA'), ('JFK', 'DTW'), ('JFK', 'LAX'), ('JFK', 'JAX'), ('JFK', 'CLT'), ('JFK', 'PBI'),
('JFK', 'CLE'), ('JFK', 'IAD'), ('JFK', 'BOS')])
nx.draw_networkx(FG, with_labels=True) # Quick view of the Graph. As expected we see 3 very busy airports
nx.algorithms.degree_centrality(FG) # Notice the 3 airports from which all of our 100 rows of data originates
nx.density(FG) # Average edge density of the Graphs
输出:
0.09047619047619047
nx.average_shortest_path_length(FG) # Average shortest path length for ALL paths in the Graph
输出:
2.36984126984127
nx.average_degree_connectivity(FG) # For a node of degree k - What is the average of its neighbours' degree?
输出:
{1: 19.307692307692307, 2: 19.0625, 3: 19.0, 17: 2.0588235294117645, 20: 1.95}
从可视化中(上面的方式)可以明显看出 - 从一些机场到其他机场有多条路径。 假如想要计算2个机场之间的最短路线。我们可以想到几种方法:
距离最短的路径。
飞行时间最短的路径。
我们可以通过距离或飞行时间来给路径赋予权重,并用算法计算最短路径。请注意,这是一个近似的解决方案 - 实际问题是计算当你到达中转机场时的航班可用性加候机的等待时间,这才是一种更完整的方法,也是人们计划旅行的方式。出于本文的目的,我们将假设你到达机场时可以随时使用航班并使用飞行时间作为权重,从而计算最短路径。
让我们以JAX和DFW机场为例:
# Let us find all the paths available
for path in nx.all_simple_paths(FG, source='JAX', target='DFW'):
print(path)
# Let us find the dijkstra path from JAX to DFW.
# You can read more in-depth on how dijkstra works from this resource - https://courses.csail.mit.edu/6.006/fall11/lectures/lecture16.pdf
dijpath = nx.dijkstra_path(FG, source='JAX', target='DFW')
dijpath
输出:
['JAX', 'JFK', 'SEA', 'EWR', 'DFW']
# Let us try to find the dijkstra path weighted by airtime (approximate case)
shortpath = nx.dijkstra_path(FG, source='JAX', target='DFW', weight='air_time')
shortpath
输出:
['JAX', 'JFK', 'BOS', 'EWR', 'DFW']
本文对图论和网络分析这一非常有趣的领域进行了粗浅的介绍。对理论和Python软件包的了解将为任何数据科学家的工具库增加一个有价值的工具。 对于上面使用的数据集,可以提出一系列其他问题,例如:
在给定成本,飞行时间和可用性的情况下,找到两个机场之间的最短路径?
作为一家航空公司,你们拥有一队飞机。你了解航班的需求。假设你有权再运营2架飞机(或者为你的机队添加2架飞机),把这两架飞机投入到哪条航线可以最大限度地提高盈利能力?
你可以重新安排航班和时刻表以优化某个参数吗?(如时效性或盈利能力等)