Python大数据分析9:案例-谁是一百年前南方小镇的社交明星?

这次我们根据一个有趣的数据,来看看如何使用网络可视化分析方法。网络可视化分析方法可以让我们以一种全新的眼光来看待平淡无奇的数据,并找到新的发现。

一些社会学家和种族学家在20世纪30年代做了一个很小的数据实验,实验目标是描绘出美国南部一个小镇一组女士的社交结构关系,他们使用的数据是报纸上公开发表的数据,共有18位女士参加了14个不同的社交活动。接近100年过去了,我们今年所能知道的仅仅只有这些数据。这些数据描述了这些女士分别参加了哪些社会活动。

为了方便生成适合网络分析的数据结构,我们需要对数据进行必要的处理。原始数据中有两类数据,一类是女士,一类是活动。一般比较常见的网络图都是由一种类型的数据节点构成,同时由于我们需要知道女士之间的联系,因此需要建立女士之间的关系。

我们可以假设如果两个女士参加一个活动,就意味着两人存在着一个联系,

共同参与的越多,联系应该越紧密。为此我们先来生成女士之间的联系:

# coding:utf-8

import pandas as pd

frame = pd.read_csv(‘C:\\temp\\SouthernLadies.csv’)

frame = pd.merge(frame, frame, left_on=’Activity’, right_on=’Activity’)

print(frame)

这里使用了数据连接的方法。通过将现有的数据自己和自己连接,按照左表和右表活动相等即可连接全部记录。

从结果可以看到,诸如刚才的W01女士和W02女士都建立了连接。这里数据有几个特点:第一,这里至少一半的数据冗余,因为自己会和自己连接,而且W01女士和W02女士连接,就会有W02女士和W01女士连接;第二,确实不同女士可以因为参加同一活动而被连接起来,这种连接会随着共同参与活动的多少而有总数的变化。

我们先处理第一个问题:

frame = frame[frame[‘Lady_x’] < frame[‘Lady_y’]]

print(frame)

这里使用了一个小技巧,我们利用女士号码小于的关系筛选了记录,可以看出但凡等于和大于的都被去除,正好对应自己和自己的连接,以及前后错位的冗余连接。七百多条记录只剩下三百多记录。

再看第二个问题,两位女士共同参加的活动越多,就会形成更多的连接,因此可以对现有连接结果按照连接女士汇总下:

frame = frame[[‘Activity’]].groupby([frame[‘Lady_x’], frame[‘Lady_y’]]).count()

print(frame)

此时,就是按照两个连接女士的号码作为分组条件,汇总统计各自的总数。看的出来,确实存在较大的差异,数值越高,女士的关系应该更为紧密。

此时取出索引的值,生成列表,即可得到网络图所有边的信息:

edges = frame.index.tolist()

print(edges)

对于节点,我们可以在未汇总前直接统计所有女士:

frame = frame.groupby(frame[‘Lady’]).count()

nodes = frame.index.tolist()

print(nodes)

这里仍然利用了分组的方法,取出了分组的所有索引,并生成了一个节点列表,供后续网络图生成时使用。

到此为止,我们已经完成了所需数据内容的整理。这也是数据分析的关键内容,一定要根据分析的目标和方法的特点,有选择的进行数据进行必要的处理,通常这一块是最能体现数据分析特点的地方,不同的整理方法往往也会产生不同的最终分析效果。

将上述两个代码融合下:

# coding:utf-8
import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt

frame = pd.read_csv('C:\\temp\\SouthernLadies.csv')
frame1 = frame.groupby(frame['Lady']).count()
nodes = frame1.index.tolist()

frame2 = pd.merge(frame, frame, left_on='Activity', right_on='Activity')
frame2 = frame2[frame2['Lady_x'] < frame2['Lady_y']]
frame2 = frame2[['Activity']].groupby([frame2['Lady_x'], frame2['Lady_y']]).count()
edges = frame2.index.tolist()

g = nx.Graph()
g.add_nodes_from(nodes)
g.add_edges_from(edges)
nx.draw(g)
plt.show()

这里前面的代码就是生成节点列表和边列表,为了反复使用数据,可以看出我们保留了最初的DataFrame,通过存储的frame1和frame2分别计算了节点和边。

最后的部分就是绘图,其实绘图并不复杂,利用导入的networkx,生成图,并添加节点和边,绘制即可,注意networkx不能独立使用,必须配合matplotlib,因此最后不要忘了pyplot的show方法。

这是绘制的结果,显然还需要进一步调整,比如增加必要的标签显示和颜色美化。

nx.draw(g, with_labels=True, node_shape=”s”, alpha=0.9, node_color=’green’,

        node_size=1500, font_size=16, font_weight=”bold”, font_color=’white’)

plt.show()

这些属性大都见名知意,大家可以自行调整。

从中可以看出,有些节点处于一种社交中心的特点,比如W09、W14和W15等。

但是,这里的边都是一样的粗细,无法看出前面我们利用共同参加活动所计算出来的重要性关系。

于是,我们再次使用加权的边来绘制。为此需要做两步:第一步是将权值也保存到列表中:

frame2 = pd.merge(frame, frame, left_on='Activity', right_on='Activity')
frame2 = frame2[frame2['Lady_x'] < frame2['Lady_y']]
frame2 = frame2[['Activity']].groupby([frame2['Lady_x'], frame2['Lady_y']]).count()
weights = frame2['Activity'].tolist()
edges = frame2.index.tolist()

g = nx.Graph()
g.add_nodes_from(nodes)
g.add_edges_from(edges)
nx.draw(g, width=weights, edge_color=weights,
        with_labels=True, node_shape="s", alpha=0.9, node_color='green',
        node_size=1500, font_size=16, font_weight="bold", font_color='white')
plt.show()

首先我们将总数统计列保存为一个列表weights,显然和它和边列表元素个数一样,并且一一对应。然后,我们再draw函数中设置width属性即可表达每条边的宽度和颜色,以体现这个权值。

图中可以看出,W03和W04等节点呈现出比较明显的、与其他节点的强关联性,浅色权值更高,说明她们的好伙伴更多一些,并且隐隐约约还能看到两个高度关联的团体。

为此我们需要将无关的低权值边去除,以便更高的看清结果。

我们在分组后增加一条删除汇总个数较低的边,比如小于4:

frame2 = frame2.drop(index=(frame2.loc[(frame2[‘Activity’] < 4)].index))

weights = frame2[‘Activity’].tolist()

edges = frame2.index.tolist()

从结果来看,虽然更能看到这两个社交团体,但是由于存在很多无关节点,默认布局很不好看。

因此我们需要去除这些已经不再连接边的独立节点。我们先使用一种最为直观的方法,就是从边中直接得到需要显示的点:

# coding:utf-8
import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt

frame = pd.read_csv('C:\\temp\\SouthernLadies.csv')
frame2 = pd.merge(frame, frame, left_on='Activity', right_on='Activity')
frame2 = frame2[frame2['Lady_x'] < frame2['Lady_y']]
frame2 = frame2[['Activity']].groupby([frame2['Lady_x'], frame2['Lady_y']]).count()
frame2 = frame2.drop(index=(frame2.loc[(frame2['Activity'] < 4)].index))
weights = frame2['Activity'].tolist()
edges = frame2.index.tolist()

frame2 = frame2.reset_index()
nodes1 = frame2.drop_duplicates('Lady_x')['Lady_x'].tolist()
nodes2 = frame2.drop_duplicates('Lady_y')['Lady_y'].tolist()
nodes = nodes1 + nodes2

g = nx.Graph()
g.add_nodes_from(nodes)
g.add_edges_from(edges)
nx.draw(g, width=weights, edge_color=weights,
        with_labels=True, node_shape="s", alpha=0.9, node_color='green',
        node_size=1500, font_size=16, font_weight="bold", font_color='white')
plt.show()

我们去除了最早的节点生成方法,利用当前索引中已有的节点,通过去除Lady_x和Lady_y重复的元素,转换为列表再拼装,虽然有重复,但是问题不大,得到最终只有这些权值大于等于4的边才有的节点。

进一步调整边权值的阈值为5,可以发现更为明显,两个高度关联的女士社交团体就被识别了出来。

进一步调整边权值的阈值为3,又可以发现这两个团体是如何关联的,看得出来,一号团体的W03、W09,和二号团体的W10、W13和W14,是形成这两个团体联系的关键人物。

当然这样做,效果不错,但是获取点的方法太麻烦了,其实更为有效的方法可以直接从边中获取所有的边和节点:

其中我们直接将索引恢复到原有数据中,形成包括Lady_x、Lady_y和统计列的三列数据。

print(frame2.head(1))

frame2 = frame2.reset_index()

print(frame2.head(1))

这样就可以直接利用这个完整的DataFrame来生成网络图。

这是综合后的代码:

# coding:utf-8
import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt

frame = pd.read_csv('C:\\temp\\SouthernLadies.csv')
frame2 = pd.merge(frame, frame, left_on='Activity', right_on='Activity')
frame2 = frame2[frame2['Lady_x'] < frame2['Lady_y']]
frame2 = frame2[['Activity']].groupby([frame2['Lady_x'], frame2['Lady_y']]).count()
frame2 = frame2.drop(index=(frame2.loc[(frame2['Activity'] < 3)].index))
weights = frame2['Activity'].tolist()

frame2 = frame2.reset_index()
edges = frame2.values.tolist()
g = nx.from_pandas_edgelist(frame2, 'Lady_x', 'Lady_y', create_using=nx.Graph())

nx.draw(g, width=weights, edge_color=weights,
        with_labels=True, node_shape="s", alpha=0.9, node_color='green',
        node_size=1500, font_size=16, font_weight="bold", font_color='white')
plt.show()

其中关键的语句就是from_pandas_edgelist方法,里面很清楚的给出边的起始节点列和终止节点列,绘图会自动绘制相关边和点。图形显示和以前没有区别,但是代码更为简单。

最后让我们把布局再美化下:

fig = plt.figure()

nx.draw(g, pos=nx.circular_layout(g),

        width=weights, edge_color=weights,

        with_labels=True, node_shape=”s”, alpha=0.9, node_color=’green’,

        node_size=1500, font_size=16, font_weight=”bold”, font_color=’white’)

fig.set_facecolor(“#000000”)

plt.show()

这里才有了黑色背景,请注意fig获取必须在draw之前进行才有效果,set_facecolor方法设置为黑色,这里颜色还是以前介绍的表示方法,红绿蓝都为0,为纯黑色。

另外,还采取了新的布局模式,circular_layout表示环形布局。

发表评论

电子邮件地址不会被公开。 必填项已用*标注