BnF Ms. Fr. 640语义标记的可视化

概述
数据丰富的学术版本里藏着宝贵的编辑注释,可以提取、分析、可视化,服务于各种学术目的。2020年发布的Secrets of Craft and Nature in Renaissance France(《文艺复兴时期法国的工艺与自然之秘》)便是如此:它的元数据文件可从GitHub仓库下载。本文展示如何把这些变量汇成一个相关矩阵,再以不同方式画出来。
数据
Making and Knowing Project生成了一份电子表格,载有手稿内容的最新信息:entry_metadata.csv。该文件可从Making & Knowing的GitHub仓库取得。另外,也可以借助Matthew Kumar出色的manuscript-object——BnF Ms. Fr. 640的Python版本——生成定制的.csv文件,加入更多标记。
设置Python
数据整理用Pandas,热图用Matplotlib和seaborn,最后用NetworkX生成基于相关的网络。
对这类变量,我们避开Pearson法,改用𝜙𝐾法。请务必先了解这种相关方法及其对应的库PhiK。
#install packages
pip install phik
# import modules
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import networkx as nx
准备数据
首先,从该版本GitHub仓库的metadata文件夹下载最新的元数据文件。
我们只选需要的列。本演示中,我选了英文译文tl里的全部语义标签;您也可以选法文转录tc或规范化版本tcn里的标签。
数据是分号分隔的值,得让Python替我们数。于是用stack-unstack方法,配合正则表达式[^;\s][^\;]*[^;\s]*来数。
为了让矩阵更好读,我们把每一列重新命名。赶时间的话,这一步可以跳过,只需记住此时的数据框叫tagsrn。
# load the edition's metadata
df = pd.read_csv('entry_metadata.csv')
# select the tags you want to correlate
dftags = df[['al_tl', 'bp_tl', 'cn_tl', 'df_tl', 'env_tl', 'm_tl', 'md_tl', 'ms_tl', 'mu_tl', 'pa_tl', 'pl_tl', 'pn_tl', 'pro_tl', 'sn_tl', 'tl_tl', 'tmp_tl', 'wp_tl', 'de_tl', 'el_tl', 'it_tl', 'la_tl', 'oc_tl', 'po_tl']]
# count comma separated values
tagcount = dftags.stack(dropna=False).str.count(r'[^;\s][^\;]*[^;\s]*').unstack()
# rename columns
tagsrn = tagcount.rename(columns={'al_tl': 'animals', 'bp_tl': 'body parts', 'cn_tl': 'currency', 'df_tl': 'definitions', 'env_tl': 'environment', 'm_tl': 'material', 'md_tl': 'medical', 'ms_tl': 'measurement', 'mu_tl': 'music', 'pa_tl': 'plant', 'pl_tl': 'toponym', 'pn_tl': 'person', 'pro_tl': 'profession', 'sn_tl': 'sensory', 'tl_tl': 'tool', 'tmp_tl': 'temporal', 'wp_tl': 'weapons', 'de_tl': 'German', 'el_tl': 'Greek', 'it_tl': 'Italian', 'la_tl': 'Italian', 'oc_tl': 'Occitan', 'po_tl': 'Poitevin'})
计算相关
数据框清理干净,就可以计算各变量之间的相关系数了。这一步要紧的是吃透您的数据,确保用的是最合适的相关方法。pandas-profiling包对此特别有帮助。
# calculate correlation coefficient with the phi k method
cortag = tagsrn.phik_matrix()
cortag就是我们的相关矩阵。现在可以试试不同类型的可视化了。
可视化
首先可以试的,是用seaborn的heatmap模块把它画成一个颜色编码的矩阵。
相关热图
f, ax = plt.subplots(figsize=(16, 14))
ax = sns.heatmap(cortag, linewidths=.03, vmin=0, cmap="Oranges", square=True)

熟悉这份文本的人,一眼就能看出这张热图很有道理。譬如,人名与拉丁语强相关,因为把人名拉丁化是当时的风气,尤以十六世纪的人文主义者为甚。
有人会说,这张热图不过是在陈述显而易见的事。他们不全错;乍看之下,医学标签正是一例——它与身体部位、计量和植物相关,本在意料之中。
但要是逐行细读热图,或许会发现一些有趣而意外的相关。例如,医学标签与意大利语、拉丁语词汇相关,这就为Ms. Fr. 640中医学配方的来源提供了线索。同样,职业、定义与计量三者之间的相关,显示出职业身份在多大程度上构造了十六世纪的技术话语。
相关聚类图
热图适合“探索”阶段,可在听众眼里未免有些凌乱,尤其当您正在讨论——或仍在寻找——手稿中特定的语义簇时。Seaborn的clustermap模块也许能给出有趣的结果。
clustermap = sns.clustermap(cortag, figsize=(12, 13), dendrogram_ratio=(.1, .2), vmin=0, cmap="Oranges", cbar_pos=(-.06, .12, .03, .68))

聚类图看起来像一只像素化的(没错,这个词已收入《牛津英语词典》)昆虫;除此之外,它清楚地把孤立的标签(顶部和左侧)与联系更紧密的标签分了开来。我们还能分辨出孤立的簇,如音乐和普瓦图方言(谁能想到!),以及更为核心的簇,如计量、材料、定义和武器。职业的联系更广,但至少在这个相关矩阵里,它不属于哪个特定的簇。
相关网络
若想把矩阵里的相关进一步提炼,网络图是个优雅的办法,尤其适合向人介绍手稿内容的场合。
为此,我们要把矩阵转成边和节点的列表,并定一个阈值,把较弱的相关从图中剔除。
# transform the data
links = cortag.stack().reset_index()
links.columns = ['var1', 'var2','value']
# threshold
links_filtered = links.loc[(links['value'] > .6) & (links['var1'] != links['var2'])]
links_filtered
# create edges
G = nx.from_pandas_edgelist(links_filtered, 'var1', 'var2')
# draw network using Kamada & Kawai's algorithm
plt.figure(3,figsize = (12,12))
nx.draw_kamada_kawai(G, with_labels = True, node_color = 'red', node_size = 400, edge_color = 'black', linewidths = 1, font_size = 14)

边和节点太多的话,可以调整阈值,让结果更清爽。要不然,也可以用.write_gexf()函数把图导出,到Gephi里把玩。
nx.write_gexf(G, 'graph.gexf')
结果见本文开头。
更新:环形加权网络
我一直在找把相关矩阵显示为加权网络的办法,找到了Julian West分享的这个有趣的思路,在此改编到我们的数据集上。
# create graph weighted by correlation coefficients (unfiltered)
Gx = nx.from_pandas_edgelist(links, 'var1', 'var2', edge_attr=['value'])
# determine a threshold to remove some edges
threshold = 0.4
# list to store edges to remove
remove = []
# loop through edges in Gx and find correlations which are below the threshold
for var1, var2 in Gx.edges():
corr = Gx[var1][var2]['value']
#add to remove node list if abs(corr) < threshold
if abs(corr) < threshold:
remove.append((var1, var2))
# remove edges contained in the remove list
Gx.remove_edges_from(remove)
print(str(len(remove)) + ' edges removed')
去掉几条边之后,就可以决定边的颜色和粗细了。
# determine the colors of edges
def assign_colour(correlation):
if correlation <= 0.8:
return '#ff872c' # orange
else:
return '#f11d28' # red
def assign_thickness(correlation, benchmark_thickness=3, scaling_factor=3):
return benchmark_thickness * abs(correlation)**scaling_factor
def assign_node_size(degree, scaling_factor=50):
return degree * scaling_factor
我们还让节点的大小与其连接数成正比。
# assign node size depending on number of connections (degree)
node_size = []
for key, value in dict(Gx.degree).items():
node_size.append(assign_node_size(value))
结果是一张加权图:容纳了更多节点和多得多的边,却依然清晰可读、信息丰富。

