Python 网络科学(二)
原文:
annas-archive.org/md5/3df7c5feb0bf40d7b9d88197a04b0b37译者:飞龙
第五章:更简便的抓取方法!
在上一章中,我们讲解了网页抓取的基础知识,即从网络上获取数据以供自己使用和项目应用。在本章中,我们将探讨更简便的网页抓取方法,并且介绍社交媒体抓取的内容。上一章非常长,因为我们需要覆盖很多内容,从定义抓取到解释如何使用Requests库和BeautifulSoup来收集网页数据。我会展示一些更简单的方法,获取有用的文本数据,减少清理工作。请记住,这些简单的方法并不一定能取代上一章中解释的内容。在处理数据或进行软件项目时,当事情没有立即按预期工作时,拥有多个选择是非常有用的。但目前,我们将采用更简便的方法来抓取网页内容,并且介绍如何抓取社交媒体的文本数据。
首先,我们将介绍Newspaper3k Python 库,以及Twitter V2 Python 库。
当我说Newspaper3k是一种更简便的收集网页文本数据的方法时,这是因为Newspaper3k的作者在简化收集和丰富网页数据的过程上做得非常出色。他们已经完成了你通常需要自己做的许多工作。例如,如果你想收集关于网站的元数据,比如它使用的语言、故事中的关键词,或者甚至是新闻故事的摘要,Newspaper3k都能提供给你。这本来是需要很多工作的。它之所以更简便,是因为这样你不需要从头开始做。
第二步,你将学习如何使用Twitter V2 Python 库,因为这是一个非常简便的方法来从 Twitter 抓取推文,这对于自然语言处理(NLP)和网络分析非常有用。
本章将涵盖以下主题:
-
为什么要介绍 Requests 和 BeautifulSoup?
-
从 Newspaper3k 入门
-
介绍 Twitter Python 库
技术要求
在本章中,我们将使用 NetworkX 和 pandas Python 库。这两个库现在应该已经安装完毕,所以可以直接使用。如果还没有安装,您可以通过以下命令安装 Python 库:
pip install <library name>
例如,要安装 NetworkX,可以执行以下命令:
pip install networkx
我们还将讨论一些其他的库:
-
Requests -
BeautifulSoup -
Newspaper3k
Requests 应该已经包含在 Python 中,通常不需要单独安装。
BeautifulSoup可以通过以下方式安装:
pip install beautifulsoup4
Newspaper3k可以通过以下方式安装:
pip install newspaper3k
在第四章中,我们还介绍了一个draw_graph()函数,它使用了 NetworkX 和 scikit-network 库。每次进行网络可视化时,你都需要使用到这段代码,记得保留它!
你可以在本书的 GitHub 仓库中找到本章的所有代码:github.com/PacktPublishing/Network-Science-with-Python。
为什么要介绍Requests和BeautifulSoup?
我们都喜欢事物变得简单,但生活充满挑战,事情并不总是如我们所愿。在抓取过程中,这种情况常常发生,简直让人发笑。最初,你可以预期更多的事情会出错而不是成功,但只要你坚持不懈并了解自己的选择,最终你会获得你想要的数据。
在上一章中,我们介绍了Requests Python 库,因为它让你能够访问和使用任何公开可用的网页数据。使用Requests时,你有很大的自由度。它能为你提供数据,但使数据变得有用则是一个既困难又耗时的过程。接着,我们使用了BeautifulSoup,因为它是一个强大的 HTML 处理库。通过BeautifulSoup,你可以更具体地指定从网页资源中提取和使用的数据类型。例如,我们可以轻松地从一个网站收集所有的外部链接,甚至获取网站的完整文本,排除所有 HTML 代码。
然而,BeautifulSoup默认并不会提供完美清洁的数据,特别是当你从成百上千的网站抓取新闻故事时,这些网站都有不同的页头和页脚。我们在上一章中探讨的使用偏移量来裁剪页头和页脚的方法,在你只从少数几个网站收集文本时非常有用,你可以轻松为每个独特的网站设置一些规则进行处理,但当你想扩展抓取的网站数量时,就会遇到可扩展性问题。你决定抓取的网站越多,你就需要处理更多的随机性。页头不同,导航结构可能不同,甚至语言可能不同。抓取过程中遇到的困难正是让我觉得它非常有趣的一部分。
介绍Newspaper3k
如果你想做自然语言处理(NLP)或将文本数据转化为社交网络分析和网络科学中使用的网络数据,你需要干净的数据。Newspaper3k提供了继BeautifulSoup之后的下一步,它进一步抽象了数据清理的过程,为你提供了更干净的文本和有用的元数据,减少了工作量。许多性能优化和数据清理工作都被抽象了出来。而且,由于你现在已经了解了上一章中关于数据清理的一些方法,当你看到Newspaper3k的数据清洁度时,你可能会更好地理解背后发生的事情,并且希望你会对他们的工作感到非常赞赏,感谢他们为你节省的时间。
现在,我们可以认为Newspaper3k是从网上收集文本的“简便方法”,但它之所以简便,是因为它建立在之前的基础之上。
在你的网页抓取和内容分析项目中,你仍然可能需要使用Requests和BeautifulSoup。我们需要先了解这些内容。当进行网页抓取项目时,不要一开始就从最基础的部分开始,一边重造每个需要的工具,也许我们应该通过问自己几个问题,来决定从哪里开始:
-
我可以用
Newspaper3k做吗? -
不行吗?好吧,那我能用
Requests和BeautifulSoup做吗? -
不行吗?好吧,那我至少可以使用
Requests获取一些数据吗?
你在哪里获得有用的结果,就应该从哪里开始。如果你能从Newspaper3k中获得所需的一切,就从那里开始。如果用Newspaper3k不能得到你需要的内容,但用BeautifulSoup可以,那就从BeautifulSoup开始。如果这两种方法都不可行,那你就需要使用Requests来获取数据,然后写清理文本的代码。
我通常建议人们从基础开始,只有在必要时才增加复杂性,但对于数据收集或文本抓取,我不推荐这种方法。重造 HTML 解析器几乎没有用处,没有必要去制造不必要的麻烦。只要它能满足你的项目需求,使用任何能快速提供有用数据的工具。
什么是 Newspaper3k?
Newspaper3k是一个 Python 库,用于从新闻网站加载网页文本。然而,与BeautifulSoup不同,Newspaper3k的目标不在于灵活性,而是快速获取有用数据快速。我最为钦佩的是 Newspaper3k 的清洗数据的能力,结果相当纯净。我曾比较过使用BeautifulSoup和Newspaper3k的工作,后者给我留下了非常深刻的印象。
Newspaper3k不是BeautifulSoup的替代品。它可以做一些BeautifulSoup能做的事情,但并不是所有的,而且它的设计并没有考虑到处理 HTML 时的灵活性,这正是BeautifulSoup的强项。你给它一个网站,它会返回该网站的文本。使用BeautifulSoup时,你有更多的灵活性,你可以选择只查看链接、段落或标题。而Newspaper3k则提供文本、摘要和关键词。BeautifulSoup在这一抽象层次上略低一些。理解不同库在技术栈中的位置非常重要。BeautifulSoup是一个高层次的抽象库,但它的层次低于Newspaper3k。同样,BeautifulSoup的层次也高于Requests。这就像电影《盗梦空间》——有层层叠叠的结构。
Newspaper3k 的用途是什么?
Newspaper3k对于获取网页新闻故事中的干净文本非常有用。这意味着它解析 HTML,剪切掉无用的文本,并返回新闻故事、标题、关键词,甚至是文本摘要。它能返回关键词和文本摘要,意味着它背后具有一些相当有趣的自然语言处理能力。你不需要为文本摘要创建机器学习模型,Newspaper3k会透明地为你完成这项工作,而且速度惊人。
Newspaper3k似乎受到了在线新闻解析的启发,但它不限于此。我也曾用它来抓取博客。如果你有一个网站想尝试抓取,不妨给Newspaper3k一个机会,看看效果如何。如果不行,可以使用BeautifulSoup。
Newspaper3k的一个弱点是,它无法解析使用 JavaScript 混淆技术将内容隐藏在 JavaScript 中,而不是 HTML 中的网站。网页开发者有时会这样做,以阻止抓取,原因多种多样。如果你将Newspaper3k或BeautifulSoup指向一个使用 JavaScript 混淆的网站,它们通常只会返回很少甚至没有有用的结果,因为数据隐藏在 JavaScript 中,而这两个库并不适用于处理这种情况。解决方法是使用像Selenium配合Requests这样的库,这通常足以获取你需要的数据。Selenium超出了本书的范围,而且经常让人觉得麻烦,不值得花费太多时间。因此,如果你在 JavaScript 混淆面前卡住了,可以查阅相关文档,或干脆跳过并抓取更简单的网站。大多数网站是可以抓取的,那些不能抓取的通常可以忽略,因为它们可能不值得投入太多精力。
开始使用Newspaper3k
在使用Newspaper3k之前,你必须先安装它。这非常简单,只需要运行以下命令:
pip install newspaper3k
在之前的安装过程中,我曾收到一个错误,提示某个 NLTK 组件未下载。留意奇怪的错误信息。解决办法非常简单,只需要运行一个 NLTK 下载命令。除此之外,库的表现一直非常好。安装完成后,你可以立即将它导入到 Python 代码中并使用。
在上一章中,我展示了灵活但更为手动的抓取网站方法。许多无用的文本悄悄混入其中,数据清理相当繁琐,且难以标准化。Newspaper3k将抓取提升到了另一个层次,让它变得比我见过的任何地方都要简单。我推荐你尽可能使用Newspaper3k进行新闻抓取。
从一个网站抓取所有新闻 URL
使用Newspaper3k从域名中提取 URL 非常简单。这是加载网页域名中所有超链接所需的全部代码:
import newspaper
domain = 'https://www.goodnewsnetwork.org'
paper = newspaper.build(domain, memoize_articles=False)
urls = paper.article_urls()
但是,我想指出一个问题:当你以这种方式抓取所有 URL 时,你也会发现我认为是“垃圾 URL”的一些链接,这些 URL 指向网站的其他区域,而不是文章。这些 URL 可能有用,但在大多数情况下,我只想要文章的 URL。如果我不采取任何措施去删除垃圾,URL 会像这样:
urls
['https://www.goodnewsnetwork.org/2nd-annual-night-of-a-million-lights/',
'https://www.goodnewsnetwork.org/cardboard-pods-for-animals-displaced-by-wildfires/',
'https://www.goodnewsnetwork.org/category/news/',
'https://www.goodnewsnetwork.org/category/news/animals/',
'https://www.goodnewsnetwork.org/category/news/arts-leisure/',
'https://www.goodnewsnetwork.org/category/news/at-home/',
'https://www.goodnewsnetwork.org/category/news/business/',
'https://www.goodnewsnetwork.org/category/news/celebrities/',
'https://www.goodnewsnetwork.org/category/news/earth/',
'https://www.goodnewsnetwork.org/category/news/founders-blog/']
请注意,如果你在不同的时间爬取一个网站,你可能会得到不同的结果。新内容可能已被添加,旧内容可能已被删除。
在我大多数抓取中,那些位于前两个 URL 下方的内容就是我认为的垃圾。我只需要文章 URL,那些是指向特定分类页面的 URL。有几种方法可以解决这个问题:
-
你可以删除包含“category”一词的 URL。在这种情况下,这看起来非常合适。
-
你可以删除那些 URL 长度超过某个阈值的 URL。
-
你可以将这两种选项合并成一种方法。
对于这个例子,我决定选择第三个选项。我将删除所有包含“category”一词的 URL,以及长度小于 60 个字符的 URL。你也许想尝试不同的截止阈值,看看哪个最适合你。简单的清理代码如下:
urls = sorted([u for u in urls if 'category' not in u and len(u)>60])
现在我们的 URL 列表看起来更加干净,只包含文章 URL。这正是我们需要的:
urls[0:10]
…
['https://www.goodnewsnetwork.org/2nd-annual-night-of-a-million-lights/',
'https://www.goodnewsnetwork.org/cardboard-pods-for-animals-displaced-by-wildfires/',
'https://www.goodnewsnetwork.org/couple-living-in-darkest-village-lights-sky-with-huge-christmas-tree/',
'https://www.goodnewsnetwork.org/coya-therapies-develop-breakthrough-treatment-for-als-by-regulating-t-cells/',
'https://www.goodnewsnetwork.org/enorme-en-anidacion-de-tortugasen-tailandia-y-florida/',
'https://www.goodnewsnetwork.org/good-talks-sustainable-dish-podcast-with-shannon-hayes/',
'https://www.goodnewsnetwork.org/gopatch-drug-free-patches-good-gifts/',
'https://www.goodnewsnetwork.org/horoscope-from-rob-brezsnys-free-will-astrology-12-10-21/',
'https://www.goodnewsnetwork.org/how-to-recognize-the-eight-forms-of-capital-in-our-lives/',
'https://www.goodnewsnetwork.org/mapa-antiguo-de-la-tierra-te-deja-ver-su-evolucion/']
现在我们有了一个干净的 URL 列表,我们可以遍历它,抓取每个故事,并加载文本供我们使用。
在继续之前,你应该注意到,在这个单一的网络域上,他们发布的故事是多语言的。他们发布的大部分故事是英语的,但也有一些不是。如果你将 Newspaper3k 指向该域(而不是指向单独的故事 URL),它可能无法正确地识别该域的语言。最好在故事级别做语言查找,而不是在域级别。我会展示如何在故事级别做这个。
从网站抓取新闻故事
现在我们有了一个包含故事 URL 的列表,我们想从中抓取文章文本和元数据。下一步是使用选定的 URL,收集我们需要的任何数据。对于这个例子,我将下载并使用我们 URL 列表中的第一个故事 URL:
from newspaper import Article
url = urls[0]
article = Article(url)
article.download()
article.parse()
article.nlp()
这个代码片段中有几行比较令人困惑,我会逐行解释:
-
首先,我从报纸库中加载
Article函数,因为它用于下载文章数据。 -
接下来,我将
Article指向我们的 URL 列表中的第一个 URL,也就是urls[0]。此时它并没有执行任何操作;它只是被指向了源 URL。 -
然后,我从给定的 URL 下载并解析文本。这对于获取完整的文章和标题很有用,但它不会捕获文章的关键词。
-
最后,我运行
Article的nlp组件来提取关键词。
通过这四个步骤,我现在应该已经拥有了这篇文章所需的所有数据。让我们深入看看,看看我们有什么!
-
文章标题是什么?
title = article.titletitle…'After Raising $2.8M to Make Wishes Come True for Sick Kids, The 'Night of a Million Lights' Holiday Tour is Back' -
干净利落。那文本呢?
text = article.texttext[0:500]…'The Night of A Million Lights is back—the holiday spectacular that delights thousands of visitors and raises millions to give sick children and their weary families a vacation.\n\n'Give Kids The World Village' has launched their second annual holiday lights extravaganza, running until Jan. 2\n\nIlluminating the Central Florida skyline, the 52-night open house will once again provide the public with a rare glimpse inside Give Kids The World Village, an 89-acre, whimsical nonprofit resort that provide' -
文章摘要是什么?
summary = article.summarysummary…'The Night of A Million Lights is back—the holiday spectacular that delights thousands of visitors and raises millions to give sick children and their weary families a vacation.\nWhat began as an inventive pandemic pivot for Give Kids The World has evolved into Central Florida's most beloved new holiday tradition.\n"Last year's event grossed $2.8 million to make wishes come true for children struggling with illness and their families," spokesperson Cindy Elliott told GNN.\nThedisplay features 1.25M linear feet of lights, including 3.2 million lights that were donated by Walt Disney World.\nAll proceeds from Night of a Million Lights will support Give Kids The World, rated Four Stars by Charity Navigator 15 years in a row.' -
文章是用什么语言写的?
language = article.meta_langlanguage…'en' -
文章中发现了哪些关键词?
keywords = article.keywordskeywords…['million','kids','children','lights','world','true','tour','wishes','sick','raising','night','village','guests','holiday','wish'] -
这篇文章配的是什么图片?
image = article.meta_imgimage…'https://www.goodnewsnetwork.org/wp-content/uploads/2021/12/Christmas-disply-Night-of-a-Million-Lights-released.jpg'
而且,你还可以使用 Newspaper3k 做更多的事情。我鼓励你阅读该库的文档,看看还有哪些功能能对你的工作有所帮助。你可以在newspaper.readthedocs.io/en/latest/阅读更多内容。
优雅地抓取并融入其中
在构建任何爬虫时,我通常会做两件事:
-
与人群融为一体
-
不要过度抓取
这两者之间有些重叠。如果我与真实的网页访问者融为一体,我的爬虫就不那么显眼,也不太容易被封锁。其次,如果我不进行过度抓取,我的爬虫也不太可能被注意到,从而减少被封锁的可能性。然而,第二点更为重要,因为对 web 服务器进行过度抓取是不友好的。最好在 URL 抓取之间设置 0.5 或 1 秒的等待:
-
对于第一个想法,即与人群融为一体,你可以伪装成一个浏览器用户代理。例如,如果你希望你的爬虫伪装成在 macOS 上运行的最新 Mozilla 浏览器,可以按如下方式操作:
from newspaper import Configconfig = Config()config.browser_user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 12.0; rv:95.0) Gecko/20100101 Firefox/95.0'config.request_timeout = 3 -
接下来,为了在每次抓取 URL 之间添加 1 秒钟的暂停,你可以使用
sleep命令:import timetime.sleep(1)
user_agent 配置通常足以绕过简单的机器人检测,且 1 秒的暂停是一个友好的操作,有助于与人群融为一体。
将文本转换为网络数据
为了将我们刚刚抓取的文本转换为网络数据,我们可以重用前一章创建的函数。提醒一下,这个函数是使用一种名为命名实体识别(NER)的 NLP 技术来提取文档中提到的人物、地点和组织:
-
这是我们将使用的函数:
import spacynlp = spacy.load("en_core_web_md")def extract_entities(text):doc = nlp(text)sentences = list(doc.sents)entities = []for sentence in sentences:sentence_entities = []sent_doc = nlp(sentence.text)for ent in sent_doc.ents:if ent.label_ in ['PERSON', 'ORG', 'GPE']:entity = ent.text.strip()if "'s" in entity:cutoff = entity.index("'s")entity = entity[:cutoff]if entity != '':sentence_entities.append(entity)sentence_entities = list(set(sentence_entities))if len(sentence_entities) > 1:entities.append(sentence_entities)return entities -
我们可以简单地将抓取的文本传入此函数,它应该返回一个实体列表:
entities = extract_entities(text)entities…[['Night', 'USA'], ['Florida', 'Kissimmee'], ['GNN', 'Cindy Elliott'], ['the Centers for Disease Control and Prevention', 'CDC'], ['Florida', 'Santa'], ['Disney World', 'Central Florida']] -
完美!现在,我们可以将这些实体传递给另一个函数,以获取我们可以用来构建网络的
pandasDataFrame 边缘列表数据。 -
接下来,我们将使用
get_network_data函数,其代码如下:import pandas as pddef get_network_data(entities):final_sources = []final_targets = []for row in entities:source = row[0]targets = row[1:]for target in targets:final_sources.append(source)final_targets.append(target)df = pd.DataFrame({'source':final_sources, 'target':final_targets})return df -
我们可以通过传入实体列表来使用它:
network_df = get_network_data(entities)network_df.head()
经检查,效果很好。网络边缘列表必须包含一个源节点和一个目标节点,而现在我们已经有了这两者:
图 5.1 – pandas DataFrame 实体边缘列表
很好。第四行很有趣,因为 NER 成功识别出了两种不同的表示 CDC 的方式,既有全称也有缩写。第一行似乎有一个误报,但我会在下一章讲解如何清理网络数据。现在这一切都很完美。
端到端 Network3k 抓取与网络可视化
现在我们有了所有演示两个事项所需的东西。我想展示如何抓取多个 URL,并将数据合并为一个 DataFrame 以供使用和存储,你将学习如何将原始文本转换为网络数据并可视化它。在上一章中我们做过这个,但再做一次会更有助于加深记忆。
将多个 URL 抓取结果合并到一个 DataFrame 中
在这两个演示之间,这部分是最基础和最重要的。我们将在下一个演示中使用此过程的结果。在大多数实际的抓取项目中,重复抓取单个 URL 没有什么意义。通常,你想对你抓取的任何域名重复这些步骤:
-
抓取所有的 URL。
-
删除你已经抓取过文本的 URL。
-
抓取剩余 URL 的文本。
对于这个演示,我们只会做步骤 1和步骤 3。对于你的项目,你通常需要想出一个方法来删除你已经抓取的 URL,这取决于你将抓取后的数据写入哪里。本质上,你需要检查你已经抓取的内容,并忽略你已经使用过的 URL。这可以防止重复工作、不必要的抓取噪音、不必要的抓取负担以及重复的数据。
以下代码抓取给定域名的所有 URL,抓取每个发现的 URL 的文本,并创建一个pandas DataFrame,供使用或输出到文件或数据库。我还加入了一个额外的 Python 库:tqdm。tqdm库在你想了解一个过程需要多长时间时非常有用。如果你在后台自动化中使用这个功能,你可能不需要tqdm的功能,但现在它很有用,因为你正在学习。
你可以通过运行pip install tqdm来安装tqdm:
图 5.2 – TQDM 进度条的实际应用
这是完整的 Python 代码,它接受一个域名并返回一个抓取的故事的pandas DataFrame:
import newspaper
from newspaper import Article
from tqdm import tqdm
def get_story_df(domain):
paper = newspaper.build(domain, memoize_articles=False)
urls = paper.article_urls()
urls = sorted([u for u in urls if 'category' not in u and len(u)>60])
titles = []
texts = []
languages = []
keywords = []
for url in tqdm(urls):
article = Article(url)
article.download()
article.parse()
article.nlp()
titles.append(article.title)
texts.append(article.text)
languages.append(article.meta_lang)
keywords.append(article.keywords)
df = pd.DataFrame({'urls':urls, 'title':titles, 'text':texts, 'lang':languages, 'keywords':keywords})
return df
要使用这个函数,你可以运行以下代码,并指向任何感兴趣的新闻域名:
domain = 'https://www.goodnewsnetwork.org'
df = get_story_df(domain)
df.head()
现在,你应该有一个干净的新闻故事 DataFrame 可供使用。如果你遇到 404(页面未找到)错误,可能需要在函数中添加一些 try/except 异常处理代码。我将这些和其他边缘情况留给你处理。不过,URL 抓取和文章文本抓取的时间间隔越短,遇到 404 错误的可能性就越小。
让我们来检查一下结果!
图 5.3 – 抓取的 URL 数据的 pandas DataFrame
酷!tqdm进度条一直工作到完成,我们还可以看到最终故事的语言已被设置为西班牙语。这正是我们想要的。如果我们试图通过抓取主页(landing page)来检测整个域的语言,语言检测组件可能会给出错误的结果,甚至什么都不返回。这个网站既有英语故事也有西班牙语故事,我们可以在故事级别看到这一点。
捕捉一段文本的语言对于 NLP 工作非常有用。通常,经过一种语言训练的机器学习分类器在应用到另一种语言时会遇到困难,而在使用无监督机器学习(聚类)处理文本数据时,用不同语言写的数据会聚集在一起。我的建议是,利用捕捉到的语言数据将数据按语言拆分,以便进行后续的 NLP 扩展工作。这样你会获得更好的结果,同时分析结果也会变得更加简单。
接下来,让我们使用这些故事来创建网络数据和可视化!
将文本数据转换成网络进行可视化
本书中,我们已经多次提取文本、提取实体、创建网络数据、构建网络并进行可视化。我们在这里也会做同样的事情。不同之处在于,我们现在有一个包含多篇新闻文章的pandas DataFrame,每篇文章都可以转换成一个网络。对于这个示例,我只做两次。从现在开始,你应该不会再有困难将文本转换成网络,你也可以重用已经写好的代码。
我们的实体提取是基于英语语言的 NLP 模型构建的,因此我们只使用英语语言的故事。为了简化演示,我们将使用 DataFrame 中的第二篇和第四篇文章,因为它们给出了有趣且干净的结果:
-
首先,我们将使用第二篇文章。你应该能看到我正在加载
df['text'][1],其中[1]是第二行,因为索引是从0开始的:text = df['text'][1]entities = extract_entities(text)network_df = get_network_data(entities)G = nx.from_pandas_edgelist(network_df)draw_graph(G, show_names=True, node_size=4, edge_width=1, font_size=12)
这是网络可视化:
图 5.4 – 文章实体关系的网络可视化(第二篇文章)
这看起来不错,但非常简单。一篇新闻文章通常涉及几个个人和组织,所以这并不令人惊讶。我们仍然可以看到一些野生动物保护组织之间的关系,一个人与大学之间的关系,以及澳大利亚和考拉之间的关系。所有这些看起来都很现实。
-
接下来,让我们尝试第四篇文章:
text = df['text'][3]entities = extract_entities(text)network_df = get_network_data(entities)G = nx.from_pandas_edgelist(network_df)draw_graph(G, show_names=True, node_size=4, edge_width=1, font_size=12)
这是网络可视化。这一部分更加有趣且复杂:
图 5.5 – 文章实体关系的网络可视化(第四篇文章)
这是一个比新闻报道中常见的实体集合更加丰富的集合,事实上,这个故事中涉及的实体和关系比我们在上一章研究的《变形记》这本书还要多。这看起来很棒,我们可以进一步研究这些被揭示出来的关系。
从本书这一章开始,我将主要使用 Twitter 数据来创建网络。我想解释如何对任何文本进行这种操作,因为这让我们能够自由地揭示任何文本中的关系,而不仅仅是社交媒体文本。然而,你应该已经理解这一点了。本书剩余部分将更多关注分析网络,而不是创建网络数据。一旦文本数据转化为网络,其余的网络分析信息同样具有相关性和实用性。
介绍 Twitter Python 库
Twitter 是一个非常适合 NLP 项目的宝藏。它是一个非常活跃的社交网络,且审查制度不太严格,这意味着用户在讨论各种话题时都很自在。这意味着 Twitter 不仅适合研究轻松的话题,也可以用来研究更为严肃的话题。你有很大的灵活性。
与其他社交网络相比,Twitter 也有一个相对简单的 API。它很容易入门,并且可以用来捕捉数据,这些数据可以为未来的 NLP 研究提供支持。在我个人的 NLP 研究中,学习如何抓取 Twitter 数据极大地推动了我的 NLP 学习。当你有了自己感兴趣的数据时,学习 NLP 变得更加有趣。我曾使用 Twitter 数据来理解各种网络、创造原创的 NLP 技术以及生成机器学习训练数据。我虽然不常用 Twitter,但我发现它对所有与 NLP 相关的内容来说都是一座金矿。
什么是 Twitter Python 库?
多年来,Twitter 一直在开放其 API,以便软件开发者和研究人员能够利用他们的数据。由于文档有些零散且令人困惑,使用该 API 是一项挑战,因此创建了一个 Python 库来简化与 API 的交互。我将解释如何使用这个 Python 库,但你需要自行探索 Twitter API,了解 Twitter 为防止过度使用 API 所设置的各种限制。
你可以在developer.twitter.com/en/docs查看更多关于 Twitter API 的信息。
Twitter 库有哪些用途?
一旦你学会了使用 Twitter 库和 API,你就可以完全自由地用它来做任何研究。你可以用它来了解 K-Pop 音乐圈,或者用它来关注机器学习或数据科学领域的最新动态。
另一个用途是分析整个受众。例如,如果一个账户有 50,000 个粉丝,你可以使用 Twitter 库加载关于所有 50,000 个粉丝的数据,包括他们的用户名和描述。利用这些描述,你可以使用聚类技术来识别更大群体中的各种子群体。你还可以使用这类数据潜在地识别机器人和其他形式的人工放大。
我建议你找到一些你感兴趣的东西,然后追寻它,看看它会引导你到哪里。这种好奇心是培养 NLP 和社交网络分析技能的极好驱动力。
可以从 Twitter 收集哪些数据?
即使与两年前相比,Twitter 似乎已经扩展了其 API 提供的功能,允许更多不同类型的数据科学和 NLP 项目。然而,在他们的 版本一(V1)API 中,Twitter 会返回一个包含大量数据的字典,这些数据都是他们可以提供的。这在他们的 V2 API 中有所变化,现在他们要求开发者明确指定请求哪些数据。这使得知道 Twitter 提供的所有数据变得更加困难。任何与 Twitter API 一起工作的人都需要花时间阅读文档,以了解有哪些可用的数据。
对于我的研究,我通常只对以下几个方面感兴趣:
-
谁在发布内容?
-
他们发布了什么?
-
它是什么时候发布的?
-
他们提到了谁?
-
他们使用了什么话题标签?
所有这些内容都可以轻松地通过 Twitter API 获取,我会向你展示如何操作。但这并不是 Twitter 提供的全部功能。我最近对 V2 API 曝露的一些新发现的数据印象深刻,但我还没有足够的了解来写关于它的内容。当你遇到感觉应该由 API 曝露的内容时,查阅文档。在我与 Twitter API 工作的经验中,一些本应默认暴露的内容,现在相比于 V1,可能需要做更多额外的工作。尝试找出如何获取你需要的内容。
获取 Twitter API 访问权限
在你能使用 Twitter API 做任何事情之前,首先你需要做的就是获得访问权限:
-
首先,创建一个 Twitter 账户。你不需要用它发布任何内容,但你确实需要有一个账户。
-
接下来,访问以下 URL 请求 API 访问权限:
developer.twitter.com/en/apply-for-access。
申请访问权限的时间可能从几分钟到几天不等。你需要填写一些表格,说明你将如何使用这些数据,并同意遵守 Twitter 的服务条款。在描述你使用 Twitter 数据的方式时,你可以说明你是为了学习 NLP 和社交网络分析而使用这些数据。
- 一旦你获得了访问权限,你将拥有自己的开发者门户。在身份验证部分搜索,直到看到类似这样的内容:
图 5.6 – Twitter 认证 Bearer Token
具体来说,你需要一个 Bearer Token。生成一个并将其保存在安全的地方。你将用它来进行 Twitter API 的身份验证。
一旦你生成了 Bearer Token,你就可以通过 Twitter Python 库开始与 Twitter API 进行交互了。
Twitter 身份验证
在进行身份验证之前,你需要安装 Twitter Python 库:
-
你可以通过运行以下代码来实现:
pip install python-twitter-v2 -
接下来,在你选择的笔记本中,尝试导入库:
from pytwitter import Api -
接下来,你需要使用 Bearer Token 来进行 Twitter 身份验证。将以下代码中的
bearer_token文本替换为你自己的 Bearer Token,然后尝试进行身份验证:bearer_token = 'your_bearer_token'twitter_api = Api(bearer_token=bearer_token)
如果没有失败,那么你应该已经通过身份验证,并准备好开始抓取推文、连接以及更多数据。
抓取用户推文
我创建了两个辅助函数,用于将用户推文加载到 pandas DataFrame 中。如果你需要更多的数据,可以扩展 tweet_fields,并可能需要添加 user_fields:
-
请查看以下代码块中的
search_tweets()函数,了解如何将user_fields添加到 Twitter 调用中。此函数不使用user_fields,因为我们已经传入了用户名:def get_user_id(twitter_api, username):user_data = twitter_api.get_users(usernames=username)return user_data.data[0].id -
第一个函数接受一个 Twitter 用户名并返回其
user_id。这是很重要的,因为某些 Twitter 调用需要user_id而不是username。以下函数使用user_id来查找该用户的推文:def get_timeline(twitter_api, username):tweet_fields = ['created_at', 'text', 'lang']user_id = get_user_id(twitter_api, username)timeline_data = twitter_api.get_timelines(user_id, return_json=True, max_results=100, tweet_fields=tweet_fields)df = pd.DataFrame(timeline_data['data'])df.drop('id', axis=1, inplace=True)return df
在这个函数中,twitter_api.get_timelines() 函数完成了大部分工作。我已经指定了我需要的 tweet_fields,传入了用户的 user_id,指定了要获取该用户最近的 100 条推文,并指定返回的数据格式为 JSON,方便转换为 pandas DataFrame。如果调用这个函数,我应该能立即获得结果。
-
让我们看看圣诞老人都在谈些什么:
df = get_timeline(twitter_api, 'officialsanta')df.head()
现在我们应该能看到圣诞老人五条推文的预览:
图 5.7 – 圣诞老人推文的 pandas DataFrame
完美。现在我们已经获取到了圣诞老人最近的 100 条推文。
-
我还为你提供了一个额外的辅助函数。这个函数会提取
text字段中的实体和标签,我们将利用这些信息绘制社交网络:def wrangle_and_enrich(df):# give some space for splitting, sometimes things get smashed togetherdf['text'] = df['text'].str.replace('http', ' http')df['text'] = df['text'].str.replace('@', ' @')df['text'] = df['text'].str.replace('#', ' #')# enrich dataframe with user mentions and hashtagsdf['users'] = df['text'].apply(lambda tweet: [clean_user(token) for token in tweet.split() if token.startswith('@')])df['tags'] = df['text'].apply(lambda tweet: [clean_hashtag(token) for token in tweet.split() if token.startswith('#')])return df -
我们可以将这个函数作为一个增强步骤添加进去:
df = get_timeline(twitter_api, 'officialsanta')df = wrangle_and_enrich(df)df.head()
这为我们提供了额外的有用数据:
图 5.8 – 丰富版圣诞老人推文的 pandas DataFrame
这非常适合我们在本章接下来的工作,但在继续之前,我们也将看看如何抓取连接信息。
抓取用户关注列表
我们可以轻松抓取一个账户所关注的所有账户。可以通过以下函数来完成:
def get_following(twitter_api, username):
user_fields = ['username', 'description']
user_id = get_user_id(twitter_api, username)
following = twitter_api.get_following(user_id=user_id, return_json=True, max_results=1000, user_fields=user_fields)
df = pd.DataFrame(following['data'])
return df[['name', 'username', 'description']]
在这里,我指定了max_results=1000。这是 Twitter 一次最多返回的结果数,但你可以加载比 1000 条更多的数据。你需要传入一个'next_token'键,继续获取1000条粉丝的数据集。你也可以做类似的操作来加载某个用户的超过 100 条推文。理想情况下,如果你有这个需求,应该在编程中使用递归。你可以使用前面的函数加载第一批数据,如果需要构建递归功能,应该能够扩展它。
你可以调用以下函数:
df = get_following(twitter_api, 'officialsanta')
df.head()
这将为你提供以下格式的结果:
图 5.9 – 圣诞老人追踪的 Twitter 账户的 pandas DataFrame
对于调查一个群体内部存在的子群体,包含账户描述是非常有用的,因为人们通常会描述自己的兴趣和政治倾向。为了捕获描述,你需要将描述包含在user_fields列表中。
抓取用户粉丝
抓取粉丝几乎是相同的。以下是代码:
def get_followers(twitter_api, username):
user_fields = ['username', 'description']
user_id = get_user_id(twitter_api, username)
followers = twitter_api.get_followers(user_id=user_id, return_json=True, max_results=1000, user_fields=user_fields)
df = pd.DataFrame(followers['data'])
return df[['name', 'username', 'description']]
你可以调用这个函数:
df = get_followers(twitter_api, 'officialsanta')
df.head()
这将为你提供与之前所示相同格式的结果。确保包含账户描述。
使用搜索词抓取
收集关于搜索词的推文也很有用。你可以用这个来探索谁参与了关于某个搜索词的讨论,同时也可以收集这些推文,以便阅读和处理。
以下是我为根据搜索词抓取推文编写的代码:
def search_tweets(twitter_api, search_string):
tweet_fields = ['created_at', 'text', 'lang']
user_fields = ['username']
expansions = ['author_id']
search_data = twitter_api.search_tweets(search_string, return_json=True, expansions=expansions, tweet_fields=tweet_fields, user_fields=user_fields, max_results=100)
df = pd.DataFrame(search_data['data'])
user_df = pd.DataFrame(search_data['includes']['users'])
df = df.merge(user_df, left_on='author_id', right_on='id')
df['username'] = df['username'].str.lower()
return df[['username', 'text', 'created_at', 'lang']]
这个函数比之前的函数稍微复杂一些,因为我指定了tweet_fields和user_fields,这是我感兴趣的内容。为了捕获用户名,我需要在author_id上指定一个扩展,最后,我想要 100 条最新的推文。如果你想包含额外的数据,你需要探索 Twitter API,找出如何添加你感兴趣的数据字段。
你可以像这样调用该函数:
df = search_tweets(twitter_api, 'natural language processing')
df = wrangle_and_enrich(df)
df.head()
我还通过wrangle_and_enrich()函数调用,丰富了pandas DataFrame,使其包括用户提及和话题标签。这样就得到了以下的pandas DataFrame:
图 5.10 – Twitter 搜索推文的 pandas DataFrame
这些搜索推文非常适合用于创建社交网络可视化,因为这些推文来自多个账户。在数据的前两行,你可能会在视觉上注意到intempestades、pascal_bornet和cogautocom之间有关系。如果我们要可视化这个网络,它们将显示为相连的节点。
将 Twitter 推文转换为网络数据
将社交媒体数据转换为网络数据比处理原始文本要容易得多。幸运的是,推特的情况正好符合这一点,因为推文通常很简短。这是因为用户经常在推文中互相标记,以增加可见性和互动,而且他们经常将推文与话题标签关联,这些关联可以用来构建网络。
使用用户提及和话题标签,我们可以创建几种不同类型的网络:
-
账户到提及网络 (@ -> @):有助于分析社交网络。
-
账户到话题标签网络 (@ -> #):有助于找到围绕某个主题(话题标签)存在的社区。
-
提及到话题标签网络 (@ -> #):类似于之前的网络,但链接到被提及的账户,而不是推文账户。这对于寻找社区也很有用。
-
话题标签到话题标签网络 (# -> #):有助于发现相关的主题(话题标签)和新兴的趋势话题。
此外,你可以使用 NER 从文本中提取额外的实体,但推文通常比较简短,因此这可能不会提供太多有用的数据。
在接下来的几个部分中,你将学习如何构建第一类和第三类网络。
账户到提及网络 (@ -> @)
我创建了一个有用的辅助函数,将pandas DataFrame 转换为此账户到提及网络数据:
def extract_user_network_data(df):
user_network_df = df[['username', 'users', 'text']].copy()
user_network_df = user_network_df.explode('users').dropna()
user_network_df['users'] = user_network_df['users'].str.replace('\@', '', regex=True)
user_network_df.columns = ['source', 'target', 'count'] # text data will be counted
user_network_df = user_network_df.groupby(['source', 'target']).count()
user_network_df.reset_index(inplace=True)
user_network_df.sort_values(['source', 'target'], ascending=[True, True])
return user_network_df
这个函数中涉及的内容非常多:
-
首先,我们从
dfDataFrame 中复制用户名、用户和文本字段,并将它们用于user_network_dfDataFrame。users字段的每一行包含一个用户列表,因此我们会“展开”users字段,为每个用户在 DataFrame 中创建一行。我们还会删除不包含任何用户的行。 -
接下来,我们移除所有的
@字符,这样数据和可视化结果会更加易读。 -
然后,我们重命名 DataFrame 中的所有列,为创建我们的图形做准备。NetworkX 的图形需要源和目标字段,
count字段也可以作为额外数据传入。 -
接下来,我们进行聚合,并统计 DataFrame 中每个源-目标关系的数量。
-
最后,我们对 DataFrame 进行排序并返回。虽然我们不需要对 DataFrame 进行排序,但我习惯这样做,因为它有助于查看 DataFrame 或进行故障排除。
你可以将search_tweets DataFrame 传递给这个函数:
user_network_df = extract_user_network_data(df)
user_network_df.head()
你将得到一个边列表 DataFrame,表示用户之间的关系。我们将利用这个来构建和可视化我们的网络。仔细观察,你应该能看到有一个额外的count字段。我们将在后面的章节中使用这个字段作为选择哪些边和节点在可视化中显示的阈值:
图 5.11 – 账户到提及 pandas DataFrame 边列表
这个 DataFrame 中的每一行表示一个用户(源)与另一个用户(目标)之间的关系。
提及到话题标签网络 (@ -> #)
我创建了一个有用的辅助函数,将 pandas DataFrame 转换为 提及 到话题标签的网络数据。这个函数与之前的类似,但我们加载了用户和话题标签,而完全不使用原账户的用户名:
def extract_hashtag_network_data(df):
hashtag_network_df = df[['users', 'tags', 'text']].copy()
hashtag_network_df = hashtag_network_df.explode('users')
hashtag_network_df = hashtag_network_df.explode('tags')
hashtag_network_df.dropna(inplace=True)
hashtag_network_df['users'] = hashtag_network_df['users'].str.replace('\@', '', regex=True)
hashtag_network_df.columns = ['source', 'target', 'count'] # text data will be counted
hashtag_network_df = hashtag_network_df.groupby(['source', 'target']).count()
hashtag_network_df.reset_index(inplace=True)
hashtag_network_df.sort_values(['source', 'target'], ascending=[True, True])
# remove some junk that snuck in
hashtag_network_df = hashtag_network_df[hashtag_network_df['target'].apply(len)>2]
return hashtag_network_df
You can pass the search_tweets DataFrame to this function.
hashtag_network_df = extract_hashtag_network_data(df)
hashtag_network_df.head()
你将获得一个用户关系的边列表 DataFrame。如前所示,还会返回一个 count 字段,我们将在后面的章节中将其用作选择显示哪些节点和边的阈值:
图 5.12 – 提及到话题标签的 pandas DataFrame 边列表
这个 DataFrame 中的每一行显示一个用户(源)和一个话题标签(目标)之间的关系。
从头到尾抓取 Twitter 数据
我希望之前的代码和示例已经展示了如何轻松使用 Twitter API 来抓取推文,也希望你能看到如何轻松地将推文转化为网络。在本章的最终演示中,我希望你能按照以下步骤进行:
-
加载一个包含与网络科学相关的推文的
pandasDataFrame。 -
丰富该 DataFrame,使其包括用户提及和话题标签作为独立字段。
-
创建账户到提及网络数据。
-
创建提及到话题标签的网络数据。
-
创建账户到提及网络。
-
创建提及到话题标签的网络。
-
可视化账户到提及的网络。
-
可视化提及到话题标签的网络。
让我们在代码中顺序进行操作,重用本章中使用的 Python 函数。
这是前六步的代码:
df = search_tweets(twitter_api, 'network science')
df = wrangle_and_enrich(df)
user_network_df = extract_user_network_data(df)
hashtag_network_df = extract_hashtag_network_data(df)
G_user = nx.from_pandas_edgelist(user_network_df )
G_hash = nx.from_pandas_edgelist(hashtag_network_df)
真的就这么简单。虽然背后有很多复杂的操作,但你越多地练习网络数据,编写这类代码就会变得越简单。
这两个网络现在都已准备好进行可视化:
-
我将从账户到提及的网络可视化开始。这是一个社交网络。我们可以这样绘制它:
draw_graph(G_user, show_names=True, node_size=3, edge_width=0.5, font_size=12)
这应该会渲染出一个网络可视化图:
图 5.13 – 账户到提及社交网络可视化
这有点难以阅读,因为账户名重叠了。
-
让我们看看没有标签的网络长什么样:
draw_graph(G_user, show_names=False, node_size=3, edge_width=0.5, font_size=12)
这将给我们一个没有节点标签的网络可视化图。这样我们可以看到整个网络的样子:
图 5.14 – 账户到提及社交网络可视化(无标签)
哇!对我来说,这真是既美丽又有用。我看到有几个用户群体或集群。如果我们仔细观察,将能够识别出数据中存在的社区。我们将在第九章中进行此操作,本章专门讲解社区检测。
-
现在,让我们看看提及到话题标签的网络:
draw_graph(G_hash, show_names=True, node_size=3, edge_width=0.5, font_size=12)
这应该会渲染出一个网络可视化图:
图 5.15 – 从提及到话题标签的网络可视化
与“提及账户”网络不同,这个网络更易于阅读。我们可以看到与各种话题标签相关联的用户。没有标签的话显示这些数据没有任何价值,因为没有标签它是无法读取和使用的。这标志着演示的结束。
总结
在本章中,我们介绍了两种更简单的方法来抓取互联网上的文本数据。Newspaper3k轻松抓取了新闻网站,返回了干净的文本、标题、关键词等。它让我们跳过了使用BeautifulSoup时的一些步骤,能够更快地得到清洁数据。我们使用这些清洁的文本和命名实体识别(NER)来创建并可视化网络。最后,我们使用了 Twitter 的 Python 库和 V2 API 来抓取推文和连接,我们也用推文来创建和可视化网络。通过本章和前一章所学,你现在在抓取网络和将文本转换为网络的过程中有了更大的灵活性,这样你就能探索嵌入的和隐藏的关系。
这里有个好消息:收集和清理数据是我们要做的工作中最困难的部分,这标志着数据收集和大部分清理工作的结束。在本章之后,我们将大部分时间都在享受与网络相关的乐趣!
在下一章中,我们将研究图形构建。我们将利用本章中使用的技术,创建用于分析和可视化的网络。
第六章:图形构建与清洗
到目前为止,我们已经覆盖了很多内容。在前几章中,我们介绍了自然语言处理(NLP)、网络科学和社交网络分析,并学习了如何将原始文本转换为网络数据。我们甚至还可视化了其中的一些网络。我希望看到文本转化为可视化网络对你产生的影响和我当时的感觉一样强烈。第一次尝试时,我使用的是《创世纪》一书(来自《圣经》),能将几千年前的文本转换为一个实际的交互式网络,真是让我大吃一惊。
在前两章中,我们学习了从互联网上的网站和社交网络收集文本数据的几种不同方式,并利用这些文本数据创建网络。好消息是,我不需要再展示更多的文本抓取方法了。你已经有了足够的选择,应该能够将这些知识作为其他类型抓取的基础。
坏消息是,现在该讨论每个人的“最爱”话题:数据清洗!说实话,这是我处理网络数据时最喜欢的部分。清洗工作需要一些努力,但其实并不复杂。现在是时候播放一些音乐,泡一杯热饮,放松一下,开始寻找需要修复的小问题了。
为了让本章特别有趣,我们将使用来自 《爱丽丝梦游仙境》 的社交网络。我已经按照前几章中描述的过程创建了这个网络。由于我们已经讨论了几遍如何将文本转换为实体、实体转换为网络数据以及网络数据转换为图形的步骤,所以这次我将跳过这些内容的解释。原始的网络数据已经上传到我的 GitHub,我们将在本章中使用这些数据。
本章我们将涵盖以下内容:
-
从边列表创建图形
-
列出节点
-
移除节点
-
快速视觉检查
-
重命名节点
-
移除边
-
持久化网络
-
模拟攻击
技术要求
本章中,我们将使用 NetworkX 和 pandas Python 库。我们还将导入一个 NLTK 分词器。到现在为止,这些库应该已经安装好了,所以它们应该可以立即使用。
本章的所有代码都可以在本书的 GitHub 仓库中找到,地址是 github.com/PacktPublishing/Network-Science-with-Python。
从边列表创建图形
我们将使用这个文件作为我们的原始边列表:raw.githubusercontent.com/itsgorain/datasets/main/networks/alice/edgelist_alice_original.csv。让我们来看一下:
-
在创建图形之前,我们必须导入两个我们将使用的库:
pandas和networkx。我们使用pandas将边列表读取到一个 DataFrame 中,然后将该 DataFrame 传递给networkx来创建图形。你可以按如下方式导入这两个库:import pandas as pdimport networkx as nx -
导入库后,让我们使用 pandas 将 CSV 文件读取为 DataFrame,然后显示它,如下代码块所示:
data = 'https://raw.githubusercontent.com/itsgorain/datasets/main/networks/alice/edgelist_alice_original.csv'network_df = pd.read_csv(data)network_df.head()
如果你在 Jupyter notebook 中运行这个,你应该会看到以下的 DataFrame:
图 6.1 – 《爱丽丝梦游仙境》边列表的 pandas DataFrame
在继续之前,我想说的是,如果你能将任何两个事物表示为具有某种关系的形式,那么你就可以将它们作为网络数据。在我们的 DataFrame 中,源节点和目标节点是人、地点和组织,就像我们在 命名实体识别(NER)工作中配置的那样,但你也可以制作以下类型的网络:
-
配料和菜肴
-
学生和老师
-
行星和星系
我可以一直说下去。意识到网络在我们周围是如此普遍,并且在所有事物中都能看到它们:这就是解锁网络分析力量的前奏。这是关于理解事物之间关系的。我对文学和安全感兴趣,我的大部分网络分析都涉及人类语言和安全之间的交集。你可能有其他的兴趣,因此你会在其他类型的网络中找到更多的用处。试着从本书中汲取灵感,激发你去研究感兴趣的话题的新方式。
既然这一点已经说清楚了,我们有了边列表的 DataFrame,接下来让我们把它转换成图形。最简单的形式就是下面这样:
G = nx.from_pandas_edgelist(network_df)
真的吗?就这样?是的,就是这样。当我第一次了解到从 pandas DataFrame 转换为可用的图形竟然这么简单时,我立刻被吸引住了。就是这么简单。但它之所以如此简单,有几个原因:
-
首先,
networkx函数期望.csv文件包含这些列,我们不需要重命名任何列或指定哪些列是源节点和目标节点。 -
其次,我们没有指定使用哪种图,因此
networkx默认使用nx.Graph()。这是最简单的图形形式,只允许节点之间有一条边,并且不包括方向性。
在笔记本中,如果我们检查 G,我们会看到以下内容:
G
<networkx.classes.graph.Graph at 0x255e00fd5c8>
这验证了我们正在使用默认的图类型 Graph。
有几种方法可以将网络数据加载到 networkx 中,但我更喜欢使用边列表。边列表最简单的形式是带有两个字段的表格数据:source 和 target。由于这种简单性,它们可以轻松地存储为纯文本或数据库中。你不需要一个复杂的图形数据库来存储边列表。
有些人在处理网络数据时更喜欢使用邻接矩阵。邻接矩阵不容易存储在数据库中,而且也不易扩展。你可以选择你喜欢的方式,但边列表非常容易使用,所以我建议你学习如何使用、创建和存储它们。
图的类型
NetworkX 提供了四种不同类型的图:
-
Graph
-
DiGraph
-
MultiGraph
-
MultiDiGraph
你可以在这里了解更多关于它们的信息:networkx.org/documentation/stable/reference/classes/。我将简要概述每种图的类型,并分享我对何时使用它们的看法。
图
Graph是 NetworkX 提供的默认和最简单的图形式。在一个简单的图中,节点之间只能有一条边。如果你的边列表包含多个源和目标之间的边,它将被简化为一条边。这并不总是坏事。存在减少网络复杂度的方法,其中一种方法就是聚合数据——例如,计算两个节点之间存在的边的数量,并将该值作为加权计数,而不是让边列表如下所示:
source, target
而是像这样:
source, target, edge_count
以这种形式,图依然有效,因为多个边被简化成了一条边,原本存在的边数也被减少到了计数。这是一种非常好的方法,能够在保持所有信息的同时简化网络数据。
对于我的大部分工作,图是非常合适的。如果我没有选择默认的图,那是因为我需要方向性,因此我选择了DiGraph。
创建默认图网络可以通过以下代码完成:
G = nx.from_pandas_edgelist(network_df)
DiGraph
DiGraph类似于图,主要区别在于它是有向的。DiGraph 代表有向图。就像图一样,每个节点之间只能有一条边。关于聚合的大部分内容仍然适用,但如果遇到自环,你可能需要进行处理。
当你需要理解信息的方向性和流动时,这些非常有用。知道两者之间存在关系并不总是足够的。通常,最重要的是理解影响的方向性,以及信息如何传播。
例如,假设我们有四个人,分别是 Sarah、Chris、Mark 和 John。Sarah 写了很多东西,并与她的朋友 Chris 分享她的想法。Chris 有些影响力,并将从 Sarah(和其他人)那里获得的信息分享给他的追随者,这些人包括 Mark 和 John。在这种情况下,数据流动是这样的:
Sarah -> Chris -> (Mark and John)
在这个数据流中,Sarah 是一个重要人物,因为她是全新信息的发起者。
Chris 也是一个重要人物,因为信息通过他流向了许多其他人。我们将在后续章节中学习如何捕捉这种重要性,当我们讨论“介于中心性”时。
最后,Mark 和 John 是这些信息的接收者。
如果这不是一个有向图,我们无法直观地看出是谁创造了信息,或者谁是信息的最终接收者。这种方向性使我们能够追溯到源头,并跟踪信息流动。
有向图在映射生产数据流方面也非常有用,这些数据流发生在生产中的服务器和数据库上。当流程以这种方式映射时,如果某个环节停止工作,你可以一步步回溯,直到找出问题所在。使用这种方法,我能够在几分钟内排查出以前需要几天时间才能解决的问题。
创建一个有向图就像这样简单:
G = nx.from_pandas_edgelist(network_df, create_using=nx.DiGraph)
如果你检查G,你应该看到如下内容:
<networkx.classes.digraph.DiGraph at 0x255e00fcb08>
MultiGraph
一个MultiGraph可以在任意两个节点之间有多个边。MultiGraph不保留任何方向性上下文。说实话,我不使用MultiGraph。我更喜欢将多个边汇总成计数,并使用Graph或DiGraph。不过,如果你想创建一个MultiGraph,可以使用以下命令:
G = nx.from_pandas_edgelist(network_df, create_using = nx.MultiGraph)
如果你检查G,你会看到如下内容:
<networkx.classes.multigraph.MultiGraph at 0x255db7afa88>
MultiDiGraph
一个MultiDiGraph可以在任意两个节点之间有多个边,并且这些图形也传达了每条边的方向性。我不使用MultiDiGraph,因为我更喜欢将多个边汇总成计数,然后使用Graph或DiGraph。如果你想创建一个MultiDiGraph,可以使用以下命令:
G = nx.from_pandas_edgelist(network_df, create_using = nx.MultiDiGraph)
如果你检查G,你应该看到如下内容:
<networkx.classes.multidigraph.MultiDiGraph at 0x255e0105688>
总结图形
为了确保我们覆盖了所有内容,让我们回顾一下这些图形:
-
让我们使用默认图形重新创建我们的图:
G = nx.from_pandas_edgelist(network_df)
很好。我们已经将所有数据加载到G中。这是一个很小的网络,因此在 Jupyter 笔记本中加载速度非常快,我想你也会很快加载完成。考虑到操作速度如此之快,我常常会觉得不过瘾,就会想:“就这些?我花了那么多功夫创建这些数据,结果就这样?”嗯,是的。
-
然而,有一个函数对于快速概览图形非常有用:
print(nx.info(G))
如果我们运行这个命令,我们会看到如下内容:
Graph with 68 nodes and 68 edges
整洁。这是一个很小、简单的网络。节点和边数这么少,应该足够清晰地帮助清理。
还有其他方法可以快速检查图形,但这是最简单的一种。现在,让我们来看看清理工作;我们将在后面的章节中了解更多关于网络分析的内容。
列出节点
在从文本构建网络之后,我通常会列出已添加到网络中的节点。这让我可以快速查看节点名称,从而估算出需要清理和重命名节点的工作量。在我们的实体提取过程中,我们有机会清理实体输出。实体数据用于创建网络数据,而网络数据又用于生成图形本身,因此在多个步骤中都可以进行清理和优化,越是在前期做得好,后期需要做的工作就越少。
不过,查看节点名称仍然很重要,目的是识别任何仍然成功进入网络的异常:
-
获取节点列表的最简单方法是运行以下
networkx命令:G.nodes
这将给你一个NodeView:
NodeView(('Rabbit', 'Alice', 'Longitude', 'New Zealand', "Ma'am", 'Australia', 'Fender', 'Ada', 'Mabel', 'Paris', 'Rome', 'London', 'Improve', 'Nile', 'William the Conqueror', 'Mouse', 'Lory', 'Eaglet', 'Northumbria', 'Edwin', 'Morcar', 'Stigand', 'Mercia', 'Canterbury', 'â\x80\x98it', 'William', 'Edgar Atheling', "â\x80\x98I'll", 'Said', 'Crab', 'Dinah', 'the White Rabbit', 'Bill', 'The Rabbit Sends', 'Mary Ann', 'Pat', 'Caterpillar', 'CHAPTER V.', 'William_', 'Pigeon', 'Fish-Footman', 'Duchess', 'Cheshire', 'Hare', 'Dormouse', 'Hatter', 'Time', 'Tillie', 'Elsie', 'Lacie', 'Treacle', 'Kings', 'Queens', 'Cat', 'Cheshire Cat', 'Somebody', 'Mystery', 'Seaography', 'Lobster Quadrille', 'France', 'England', 'â\x80\x98Keep', 'garden_.', 'Hm', 'Soup', 'Beautiful', 'Gryphon', 'Lizard'))
-
这样是可读的,但看起来可能更容易一些。这个函数将稍微整理一下:
def show_nodes(G):nodes = sorted(list(G.nodes()))return ', '.join(nodes)
这可以如下运行:
show_nodes(G)
这将输出一个更清晰的节点列表:
"Ada, Alice, Australia, Beautiful, Bill, CHAPTER V., Canterbury, Cat, Caterpillar, Cheshire, Cheshire Cat, Crab, Dinah, Dormouse, Duchess, Eaglet, Edgar Atheling, Edwin, Elsie, England, Fender, Fish-Footman, France, Gryphon, Hare, Hatter, Hm, Improve, Kings, Lacie, Lizard, Lobster Quadrille, London, Longitude, Lory, Ma'am, Mabel, Mary Ann, Mercia, Morcar, Mouse, Mystery, New Zealand, Nile, Northumbria, Paris, Pat, Pigeon, Queens, Rabbit, Rome, Said, Seaography, Somebody, Soup, Stigand, The Rabbit Sends, Tillie, Time, Treacle, William, William the Conqueror, William_, garden_., the White Rabbit, â\x80\x98I'll, â\x80\x98Keep, â\x80\x98it"
现在我们有了一个清理过的*《爱丽丝梦游仙境》*社交网络中的节点列表。立刻,我的目光被最后三个节点吸引。它们甚至不像是名字。我们将删除它们。我还看到CHAPTER V., Soup和其他一些非实体节点被加入了。这是使用自然语言处理(NLP)进行词性标注(pos_tagging)或命名实体识别(NER)时常见的问题。这两种方法经常会在单词首字母大写的情况下出错。
我们有一些工作要做。我们将删除错误添加的节点,并重命名一些节点,使它们指向白兔。
在检查图时,我列出的是节点,而不是边。你可以使用以下方式列出边:
G.edges
这会给你一个EdgeView,像这样:
EdgeView([('Rabbit', 'Alice'), ('Rabbit', 'Mary Ann'), ('Rabbit', 'Pat'), ('Rabbit', 'Dinah'), ('Alice', 'Longitude'), ('Alice', 'Fender'), ('Alice', 'Mabel'), ('Alice', 'William the Conqueror'), ('Alice', 'Mouse'), ('Alice', 'Lory'), ('Alice', 'Mary Ann'), ('Alice', 'Dinah'), ('Alice', 'Bill'), ('Alice', 'Caterpillar'), ('Alice', 'Pigeon'), ('Alice', 'Fish-Footman'), ('Alice', 'Duchess'), ('Alice', 'Hare'), ('Alice', 'Dormouse'), ('Alice', 'Hatter'), ('Alice', 'Kings'), ('Alice', 'Cat'), ('Alice', 'Cheshire Cat'), ('Alice', 'Somebody'), ('Alice', 'Lobster Quadrille'), ('Alice', 'â\x80\x98Keep'), ('Alice', 'garden_.'), ('Alice', 'Hm'), ('Alice', 'Soup'), ('Alice', 'the White Rabbit'), ('New Zealand', "Ma'am"), ('New Zealand', 'Australia'), ('Ada', 'Mabel'), ('Paris', 'Rome'), ('Paris', 'London'), ('Improve', 'Nile'), ('Mouse', 'â\x80\x98it'), ('Mouse', 'William'), ('Lory', 'Eaglet'), ('Lory', 'Crab'), ('Lory', 'Dinah'), ('Northumbria', 'Edwin'), ('Northumbria', 'Morcar'), ('Morcar', 'Stigand'), ('Morcar', 'Mercia'), ('Morcar', 'Canterbury'), ('William', 'Edgar Atheling'), ("â\x80\x98I'll", 'Said'), ('the White Rabbit', 'Bill'), ('the White Rabbit', 'The Rabbit Sends'), ('Caterpillar', 'CHAPTER V.'), ('Caterpillar', 'William_'), ('Duchess', 'Cheshire'), ('Duchess', 'Cat'), ('Duchess', 'Lizard'), ('Hare', 'Hatter'), ('Hare', 'Lizard'), ('Dormouse', 'Hatter'), ('Dormouse', 'Tillie'), ('Dormouse', 'Elsie'), ('Dormouse', 'Lacie'), ('Dormouse', 'Treacle'), ('Hatter', 'Time'), ('Kings', 'Queens'), ('Mystery', 'Seaography'), ('France', 'England'), ('Soup', 'Beautiful'), ('Soup', 'Gryphon')])
我通常不列出边,因为当我删除或重命名节点时,边会自动修正。指向已删除节点的边会被删除,指向已重命名节点的边会连接到新的节点。EdgeView也更难阅读。
有了清理过的节点列表,这是我们的攻击计划:
-
删除不良节点。
-
重命名白兔节点。
-
添加我知道的任何缺失节点。
-
添加我能识别的任何缺失的边。
让我们从这些步骤的第一步开始。
删除节点
接下来我们要做的是删除那些错误进入网络的节点,通常是由于pos_tagging或NER的假阳性结果。你可能会听到我提到这些节点是“不良”节点。我也可以称它们为“无用”节点,但重点是这些节点不属于网络,应该被删除。为了简化,我称它们为不良节点。
删除节点的一个原因是清理网络,使其更贴近现实,或者贴近某段文本中描述的现实。然而,删除节点也可以用于模拟攻击。例如,我们可以从爱丽丝梦游仙境社交网络中删除关键角色,模拟如果红心皇后实现她的愿望处决多个角色的结果。我们将在本章中进行此操作。
模拟攻击也有助于增强防御。如果一个节点是单点故障,并且它的删除会对网络造成灾难性后果,你可以在某些位置添加节点,这样即使关键节点被删除,网络仍能保持完整,信息流动不受影响:
-
在
networkx中,有两种删除节点的方法:一次删除一个,或者一次删除多个。你可以像这样删除单个节点:G.remove_node('â\x80\x98it') -
你可以一次性删除多个节点,像这样:
drop_nodes = ['Beautiful', 'CHAPTER V.', 'Hm', 'Improve', 'Longitude', 'Ma\'am', 'Mystery', 'Said', 'Seaography', 'Somebody', 'Soup', 'Time', 'garden_.', 'â\x80\x98I\'ll', 'â\x80\x98Keep']G.remove_nodes_from(drop_nodes)
我更喜欢第二种方法,因为它还可以用来移除单个节点,只要drop_nodes变量仅包含一个节点名称。你可以简单地不断扩展drop_nodes,直到列出所有不良实体,然后继续刷新剩余节点的列表。现在我们已经移除了一些节点,让我们看看剩下哪些实体:
show_nodes(G)
'Ada, Alice, Australia, Bill, Canterbury, Cat, Caterpillar, Cheshire, Cheshire Cat, Crab, Dinah, Dormouse, Duchess, Eaglet, Edgar Atheling, Edwin, Elsie, England, Fender, Fish-Footman, France, Gryphon, Hare, Hatter, Kings, Lacie, Lizard, Lobster Quadrille, London, Lory, Mabel, Mary Ann, Mercia, Morcar, Mouse, New Zealand, Nile, Northumbria, Paris, Pat, Pigeon,
Queens, Rabbit, Rome, Stigand, The Rabbit Sends, Tillie, Treacle, William, William the Conqueror, William_, the White Rabbit'
现在看起来干净多了。接下来,我们将通过重命名和合并某些节点,进一步清理网络,特别是与白兔相关的节点。
快速视觉检查
在继续进行更多清理之前,让我们对网络进行一次快速的视觉检查。我们将重用本书中一直使用的draw_graph函数:
draw_graph(G, show_names=True, node_size=5, edge_width=1)
这输出了以下网络:
图 6.2 – 快速视觉检查网络
好的,我们看到什么了?我可以看到有一个大型的连接实体集群。这是《爱丽丝梦游仙境》网络的主要组成部分。
我们还看到了什么?爱丽丝是主要组件中最中心的节点。这很有意义,因为她是故事中的主角。想到主要角色,我看到许多我熟悉的名字,比如睡鼠、柴郡猫和白兔。但令我感兴趣的是,不仅它们被展示出来,而且我还能开始看到哪些角色对故事最重要,基于与它们连接的实体数量。然而,我也注意到红心皇后和红心国王缺席了,这让我有些失望。命名实体识别(NER)未能将它们识别为实体。从我所看到的,NER 在处理奇幻和古代名字时存在困难,因为它是基于更现实的数据训练的。它对真实名字的处理要好得多。我们将手动添加皇后宫廷的几位成员,包括国王和皇后。
我还可以看到一些奇怪的节点,它们似乎是故事的一部分,但与主要组件没有连接。为什么狮鹫与一切都断开连接?狮鹫认识谁?我们应该在故事文本中寻找这些关系。我们将手动添加这些边。
最后,我看到一些与地球上的地方有关的节点,比如尼罗河、法国、英格兰、新西兰和澳大利亚。我们可以保留这些,因为它们从技术上讲是故事的一部分,但我打算将它们移除,这样我们就可以更多地关注仙境中的角色关系社交网络。我们将移除这些。
让我们从移除非仙境节点开始:
drop_nodes = ['New Zealand', 'Australia', 'France', 'England', 'London', 'Paris', 'Rome', 'Nile', 'William_', 'Treacle', 'Fender', 'Canterbury', 'Edwin', 'Mercia', 'Morcar', 'Northumbria', 'Stigand']
G.remove_nodes_from(drop_nodes)
现在,让我们再次可视化这个网络:
图 6.3 – 快速视觉检查网络(已清理)
看起来好多了。我们仍然有狮鹫像孤岛一样漂浮着,但我们很快会处理它。尽管如此,红心皇后到底在哪呢?我写了一个辅助函数来帮助调查这个问题:
from nltk.tokenize import sent_tokenize
def search_text(text, search_string):
sentences = sent_tokenize(text)
for sentence in sentences:
if search_string in sentence.lower():
print(sentence)
print()
通过这个函数,我们可以传入任何文本和任何搜索字符串,它将输出包含搜索字符串的句子。这将帮助我们找到 NER 未能找到的实体和关系。我使用的是 NLTK 的句子分割器,而不是 spaCy,因为它更快且能更容易地得到我目前需要的结果。有时,NLTK 是更快且更简便的方式,但在这个情况下不是。
注意
要运行以下代码,你需要使用第四章或第五章中的一种方法加载text变量。我们已经展示了多种加载《爱丽丝梦游仙境》文本的方法。
让我们找一下与皇后相关的文本:
search_text(text, 'queen')
这里是一些结果:
An invitation from the Queen to play croquet.""
The Frog-Footman repeated, in the same solemn tone, only changing the order of the words a little, "From the Queen.
"I must go and get ready to play croquet with the Queen,"" and she hurried out of the room.
"Do you play croquet with the Queen to-day?""
"We quarrelled last March just before _he_ went mad, you know "" (pointing with his tea spoon at the March Hare,) " it was at the great concert given by the Queen of Hearts, and I had to sing âTwinkle, twinkle, little bat!
"Well, I'd hardly finished the first verse,"" said the Hatter, "when the Queen jumped up and bawled out, âHe's murdering the time!
The Queen's Croquet-Ground A large rose-tree stood near the entrance of the garden: the roses growing on it were white, but there were three gardeners at it, busily painting them red.
"I heard the Queen say only yesterday you deserved to be beheaded!""
运行这个函数会得到比这些更多的结果——这些只是其中的一些。但我们已经可以看到Queen of Hearts认识Frog-Footman,而Frog-Footman在我们的网络中,因此我们应该添加Queen of Hearts和其他缺失的角色,并在它们与互动的角色之间添加边。
添加节点
我们需要添加缺失的节点。由于《爱丽丝梦游仙境》是一个幻想故事,而 NER 模型通常是用更现代和现实的文本进行训练的,因此 NER 难以识别一些重要的实体,包括红心皇后。这其中有几个教训:
-
首先,永远不要盲目相信模型。它们所训练的数据将对它们做得好或做得不好产生影响。
-
其次,领域知识非常重要。如果我不了解《爱丽丝梦游仙境》的故事,我可能根本不会注意到皇家人物的缺失。
-
最后,即使有缺陷,NER 和这些方法仍然能完成大部分的工作,将文本转换为网络,但你的领域知识和批判性思维将带来最佳的结果。
就像删除节点一样,networkx有两种添加节点的方法:一次一个,或者一次添加多个:
-
我们可以只添加
'Queenof Hearts':G.add_node('Queen of Hearts') -
或者,我们也可以一次性添加所有缺失的节点:
add_nodes = ['Queen of Hearts', 'Frog-Footman', 'March Hare', 'Mad Hatter', 'Card Gardener #1', 'Card Gardener #2', 'Card Gardener #3', 'King of Hearts', 'Knave of Hearts', 'Mock Turtle']G.add_nodes_from(add_nodes)
我仍然偏好批量处理方式,因为我可以不断扩展add_nodes列表,直到我对结果满意。如果我们现在可视化网络,这些新增的节点将呈现为孤岛,因为我们尚未在它们与其他节点之间创建边:
图 6.4 – 添加缺失节点的网络
看起来不错。接下来,我们来添加那些缺失的边。
添加边
我们使用了search_text函数来识别不仅是缺失的角色,还有这些角色之间缺失的关系。采用的方法如下:
-
找出“红心皇后”认识谁;做笔记,因为这些是缺失的边。
-
添加红心皇后和其他任何缺失的节点。
-
找出每个缺失角色认识谁;做笔记,因为这些是缺失的边。
这涉及使用search_text函数进行大量查找,并在我的 Jupyter 笔记本中将关系跟踪为注释。最终,它看起来是这样的:
图 6.5 – 识别出的缺失边
这些是我们需要添加的已识别边。我们可能遗漏了一些,但这已经足够满足我们的需求:
-
我们可以一次性添加一条边:
G.add_edge('Frog-Footman', 'Queen of Hearts') -
另外,我们也可以一次性添加多个。我还是更喜欢批量方式。要使用批量方式,我们将使用一个元组列表来描述边:
add_edges = [('Alice', 'Mock Turtle'), ('King of Hearts', 'Alice'), ('King of Hearts', 'Card Gardener #1'),('King of Hearts', 'Card Gardener #2'), ('King of Hearts', 'Card Gardener #3'),('King of Hearts', 'Dormouse'), ('King of Hearts', 'Frog-Footman'), ('King of Hearts', 'Kings'),('King of Hearts', 'Lizard'), ('King of Hearts', 'Mad Hatter'), ('King of Hearts', 'March Hare'),('King of Hearts', 'Mock Turtle'), ('King of Hearts', 'Queen of Hearts'), ('King of Hearts', 'Queens'),('King of Hearts', 'White Rabbit'), ('Knave of Hearts', 'King of Hearts'),('Knave of Hearts', 'Queen of Hearts'),('Queen of Hearts', 'Alice'), ('Queen of Hearts', 'Card Gardener #1'),('Queen of Hearts', 'Card Gardener #2'), ('Queen of Hearts', 'Card Gardener #3'),('Queen of Hearts', 'Dormouse'), ('Queen of Hearts', 'Frog-Footman'), ('Queen of Hearts', 'Kings'),('Queen of Hearts', 'Lizard'), ('Queen of Hearts', 'Mad Hatter'), ('Queen of Hearts', 'March Hare'),('Queen of Hearts', 'Mock Turtle'), ('Queen of Hearts', 'Queens'), ('Queen of Hearts', 'White Rabbit')]G.add_edges_from(add_edges)
现在我们的网络看起来如何?
图 6.6 – 添加了缺失边的网络
现在看起来好多了,我们已经把女王的宫廷安排好。不过,Gryphon 仍然是一个孤岛,所以让我们查找一下缺失的关系或关系:
search_text(text, 'gryphon')
这给了我们一些文本来查看,我用它来识别缺失的边。让我们添加它们:
add_edges = [('Gryphon', 'Alice'), ('Gryphon', 'Queen of Hearts'), ('Gryphon', 'Mock Turtle')]
G.add_edges_from(add_edges)
现在,让我们再一次可视化这个网络:
图 6.7 – 添加了缺失边的网络(最终版)
啊!当一个断开的网络终于连接起来,所有的孤岛/孤立节点都得到处理时,真是一种美妙的感觉。这是干净且易于阅读的。我们已经成功地移除了垃圾节点,添加了缺失节点,并将缺失的节点连接到它们应该共享边的节点!我们可以继续前进了!
重命名节点
这个网络看起来足够好,以至于我们可能会忍不住认为我们的清理工作已经完成。然而,还有一些事情需要做,特别是对白兔的处理,还有一些其他角色。我可以看到有三个节点与白兔有关:
-
theWhite Rabbit -
Rabbit -
TheRabbit Sends
如果我们将这三个节点都重命名为White Rabbit,那么它们将合并成一个节点,它们的边也会正确连接。还有一些其他的节点也应该被重命名。以下是重命名节点的方法:
relabel_mapping = {'Cheshire':'Cheshire Cat', 'Hatter':'Mad Hatter', 'Rabbit':'White Rabbit','William':'Father William', 'the White Rabbit':'White Rabbit', 'The Rabbit Sends':'White Rabbit', 'Bill':'Lizard Bill', 'Lizard':'Lizard Bill', 'Cat':'Cheshire Cat', 'Hare':'March Hare'}
G = nx.relabel_nodes(G, relabel_mapping)
我们传入一个包含节点及其重命名的 Python 字典。例如,我们将Cheshire改为Cheshire Cat,将Hatter改为Mad Hatter。
现在我们的节点看起来如何?
show_nodes(G)
'Ada, Alice, Card Gardener #1, Card Gardener #2, Card Gardener #3, Caterpillar, Cheshire Cat, Crab, Dinah, Dormouse, Duchess, Eaglet, Edgar Atheling, Elsie, Father William, Fish-Footman, Frog-Footman, Gryphon, King of Hearts, Kings, Knave of Hearts, Lacie, Lizard Bill, Lobster Quadrille, Lory, Mabel, Mad Hatter, March Hare, Mary Ann, Mock Turtle, Mouse, Pat, Pigeon, Queen of Hearts, Queens, Tillie, White Rabbit, William the Conqueror'
很好。看起来完美。我们的网络在视觉上如何呢?
图 6.8 – 重命名后的网络
完美。White Rabbit 已经正确放置,节点的颜色和位置表明它是一个核心角色,就在Alice旁边,离Queen of Hearts和King of Hearts也不远。
移除边
很可能你会遇到需要删除边的情况。这不仅对清理网络有用,还可以用于模拟攻击,或者识别团体和社区。例如,我常常使用所谓的最小割或最小边割来找到将网络分割成两部分所需的最少边数。我用这个方法来进行社区检测,也用它来发现社交媒体上的新兴趋势。
对于爱丽丝梦游仙境网络,实际上没有需要删除的边,因此我将首先演示如何删除一些边,然后再演示如何将它们放回去:
-
你可以逐一删除边:
G.remove_edge('Dormouse', 'Tillie') -
或者,你也可以一次性删除几条边:
drop_edges = [('Dormouse', 'Tillie'), ('Dormouse', 'Elsie'), ('Dormouse', 'Lacie')]G.remove_edges_from(drop_edges)
这个可视化效果怎么样?
图 6.9 – 删除边的网络
这看起来正如预期的那样。如果我们删除的是Elsie、Tillie 和 Lacie 的节点,而不是它们的边,那么节点和边都会被删除。相反,我们删除的是边,这有点像剪断一根绳子。这三个节点现在变成了孤岛,孤立无援,什么也没有连接。
让我们把它们放回去:
add_edges = [('Dormouse', 'Elsie'), ('Dormouse', 'Lacie'), ('Dormouse', 'Tillie')]
G.add_edges_from(add_edges)
现在网络看起来怎么样?
图 6.10 – 添加边的网络
完美。Elsie、Tillie 和 Lacie 已经回到它们应该在的位置,连接到 Dormouse。这样一来,我认为这个网络非常适合我们的用途。
持久化网络
我希望将这个网络持久化,以便我们在后面的章节中可以使用它,而无需再次进行所有这些工作。在本书中,我们将多次使用这个网络:
outfile = r'C:\blah\blah\blah\networks\alice\edgelist_alice_cleaned.csv'
final_network_df = nx.to_pandas_edgelist(G)
final_network_df.to_csv(outfile, header=True, index=False)
我正在使用微软 Windows。你的输出文件路径可能不同。
模拟攻击
我们已经完成了将一个粗略的网络边列表转换成网络、清理网络并持久化清理后的网络边列表的端到端工作流,因此接下来让我们进行一个模拟。
在大多数网络中,一些节点充当着关键枢纽。这些节点可以通过查看节点的度数(边数),或检查 PageRank 或各种中心性指标来发现。我们将在后面的章节中使用这些方法来识别重要节点。现在,我们有可以利用的领域知识。我们这些知道这个故事的人,可能能够脱口而出几个故事中的重要主角:爱丽丝、疯帽子、柴郡猫等等。而且,我们这些熟悉这个故事的人,也可能非常清楚红心女王反复喊着“砍掉他们的头!”
在一个网络中,如果你移除最连接的和最重要的节点,通常会发生类似于星球大战中死亡星爆炸的场景。瞬间,许多节点变成了孤立节点,它们的边缘也与被移除的中央节点一起被摧毁。这对网络来说是灾难性的,信息流被中断。你能想象当关键节点从网络中被移除时,现实世界会带来怎样的影响吗?你的互联网断了。电力中断了。供应链被打乱了。超市没有货物了,等等等等。理解网络并模拟中断可以为如何加强供应链和信息流提供一些思路。这就是这个练习的意义所在。
但我们要玩得开心。我们将让红心皇后大获全胜。我们将让她执行四个故事中的主要角色,然后看看会发生什么:
-
首先,让我们执行这些操作:
drop_nodes = ['Alice', 'Dormouse', 'White Rabbit', 'Mad Hatter']G.remove_nodes_from(drop_nodes)
我们决定红心皇后成功执行了Alice、Dormouse、White Rabbit和Mad Hatter。如果真的发生了这种情况,那将是一个可怕的故事,但我们要将它演绎出来。我选择这四个角色是因为我知道他们是故事中的关键人物。移除他们应该会摧毁网络,这是我想要展示的。
- 仅仅移除这四个关键节点后,剩余的网络会是什么样子?这会带来什么后果?
图 6.11 – 被摧毁的爱丽丝网络
灾难。我们可以看到几个节点被孤立了。中心依然有一个主组件,我们还有两个较小的组件,每个组件包含两到四个节点。但总的来说,网络已经被打碎,信息流被中断。需要重新建立新的关系,新的层级结构也需要建立。女王的宫廷已经变得主导,仅仅通过移除四个节点就实现了这一点。
-
让我们仔细看看主组件:
components = list(nx.connected_components(G))main_component = components[4]G_sub = G.subgraph(main_component)draw_graph(G_sub, show_names=True, node_size=4, edge_width = 0.5)
这段代码中有一些需要理解的地方。首先,nx.connected_components(G)将图转换成了一个连接组件的列表。列表中的一个组件将是主组件,但它不一定是列表中的第一个组件。经过一些调查,我们发现第四个组件是主组件,所以我们将其设置为main_component,然后可视化该组件的子图。我们看到的是这样的:
图 6.12 – 女王的宫廷子图
女王的宫廷完好无损,包含了一些在执行发生之前不幸被困在网络中的角色。
本章到此为止!
总结
在这一章中,我们从原始数据开始,执行了一系列步骤来清理网络,甚至进行了一个非常简单的攻击模拟。
我希望到目前为止,查看和操作网络开始变得更加自然。随着我越来越多地与网络打交道,我开始在每一件事物中看到它们,并且它们影响着我对世界的理解。我们使用一个虚构的故事来展开这一章,因为它的规模适中,便于讲解构建、清理和一些简单的分析。随着你对网络的了解越来越深,你可能会发现现实中的网络通常更加杂乱、复杂且庞大。我希望这个简单的网络能为你提供所需的工具和实践,最终帮助你去解决更加雄心勃勃的问题。
在接下来的章节中,我们将会非常有趣。下一章我们将讨论如何分析整体网络。你将学到各种有用的知识,比如如何识别网络中最有影响力的节点。从现在开始,我们将进行大量的网络分析和可视化。
第三部分:网络科学与社交网络分析
在这些章节中,我们学习如何分析网络并寻找洞察。我们从整体网络分析的讨论开始,逐步缩小到节点层面,探讨自我中心网络。接着,我们会寻找网络中存在的社区和子群体。最后,我们通过展示图数据如何对监督式和无监督式机器学习有用来结束本书。
本节包括以下章节:
-
第七章*, 整体网络分析*
-
第八章*, 自我中心网络分析*
-
第九章*, 社区检测*
-
第十章*, 网络数据上的监督式机器学习*
-
第十一章*, 网络数据上的无监督机器学习*
第七章:整体网络分析
在前几章中,我们花了很多时间讲解如何通过文本构建网络以及如何清理网络数据。在本章中,我们将开始进行整体网络分析。为了简便起见,我将其简称为WNA。WNA 用来了解网络的整体情况,分析网络的密度、哪些节点在不同方面最为重要、存在哪些社区等。我将介绍一些我认为有用的内容,这些内容与大多数社交网络分析(SNA)或网络科学书籍中的内容有所不同。我每天都在进行应用网络科学,我的目标是展示一些可以让读者快速开始网络分析的选项。
网络科学和 SNA 都是非常丰富的主题,如果你觉得本章某一部分特别有趣,我鼓励你自己进行研究,进一步了解。在本书中,我会引用一些 NetworkX 文档中的特定部分。请注意,这些参考页面上还有许多没有覆盖的功能,了解那些鲜为人知的函数,它们的功能及使用方式,会非常有帮助。
NetworkX 的在线文档分享了期刊文章的链接,供你阅读和学习。
在阅读本章时,我希望你考虑自己工作中遇到的问题,并尝试找出你可以将我所描述的方法应用到自己工作的方式。一旦你开始处理网络问题,你会发现它们无处不在,一旦学会如何分析和操作它们,机会的世界便会向你展开。
我们将涵盖以下主题:
-
创建基准 WNA 问题
-
WNA 实践
-
比较中心性
-
可视化子图
-
调查连通分量
-
理解网络层次
技术要求
本章中,我们将使用 Python 库 NetworkX 和 pandas。这两个库现在应该已经安装完成,可以随时使用。如果没有安装,你可以通过以下命令安装这些 Python 库:
pip install <library name>
比如,要安装 NetworkX,你可以使用以下命令:
pip install networkx
在 第四章中,我们还介绍了一个draw_graph()函数,它同时使用了 NetworkX 和 Scikit-Network。每次进行网络可视化时,你都需要用到这个代码,记得随时备好!
你可以在 GitHub 仓库中找到本章所有代码:github.com/PacktPublishing/Network-Science-with-Python。
创建基准 WNA 问题
在进行任何分析之前,我通常会记录下自己的一些问题。这有助于我明确自己要寻找的目标,并为我设定一个框架,去追寻这些答案。
在进行任何类型的 WNA 时,我关注的是寻找每一个问题的答案:
-
网络有多大?
-
网络有多复杂?
-
网络在视觉上是什么样子的?
-
网络中最重要的节点是什么?
-
是不是有孤岛,还是只有一个大大陆?
-
网络中可以找到哪些社区?
-
网络中存在哪些桥梁?
-
网络的层次揭示了什么?
这些问题为我提供了一个起点,可以作为我进行网络分析时的任务清单。这使我在进行网络分析时有了一个有条理的方法,而不仅仅是追随自己的好奇心。网络是嘈杂且混乱的,而这个框架为我提供了一个保持专注的工具。
修改后的 SNA 问题
在本章中,我们将使用一个 K-pop 社交网络。你可以在第二章中了解更多关于此网络数据的信息。
我的目标是了解网络的形态,以及信息如何在个体和社区之间流动。我还希望能够探索网络的不同层次,就像剥洋葱一样。核心通常特别有趣。
由于这是一个社交网络,我有一些额外的问题,超出了之前的基本问题:
-
社交网络有多大?这意味着什么?
-
网络的复杂性和相互连接程度如何?
-
网络在视觉上是什么样子的?
-
网络中最重要的人和组织是谁?
-
网络中只有一个巨大的集群吗,还是有孤立的人群?
-
网络中可以找到哪些社区?
-
网络中存在哪些桥梁?
-
网络的层次揭示了什么?
社交网络分析重访
在第二章,《网络分析》中,我描述了网络科学和社会网络分析(SNA)的定义、起源和用途。尽管这两个领域是独立的研究领域,但它们有很多重叠,因此我认为社交网络是一组应该整合到网络科学中的技术。这是因为 SNA 可以很好地利用网络科学的工具和技术,而将网络科学应用于社交网络会使其更加有趣。我个人并不区分这两者。
什么是社交网络分析?在我看来,它是从社会角度看网络分析的一种不同视角。网络科学涉及的是网络如何构建、网络的属性以及网络如何随时间演变。而在社交网络分析中,我们更加关注个体。我们想知道网络中哪些人和组织是重要的,哪些个体作为社区之间的桥梁,哪些社区存在以及它们存在的原因。
内容分析是 NLP 与网络科学结合最为重要的领域。NLP 允许提取实体(人、地点和组织)并预测文本的情感分类。网络科学和 SNA 则使我们能够更深入地理解这些网络中存在的关系。因此,通过 NLP 和网络分析,你不仅可以获得内容背景,还能获得关系背景。这是一个强大的协同效应,1 + 1 = 3。
在本章中,我们不会进行任何自然语言处理(NLP)。我将解释网络科学和社会网络分析(SNA)的一些功能。那么,让我们开始吧!
WNA(网络分析)实战
正如前一章所提到的,在 NetworkX 中,你可以构建无向图、有向图、多重图或多重有向图。在本章中,我们将使用无向图,因为我想展示某些功能如何帮助理解网络。需要知道的是:我接下来展示的内容如果使用其他类型的网络,会有不同的意义。当使用有向网络时,你还有更多的选择,比如研究in_degrees和out_degrees,不仅仅是总度数。
加载数据并创建网络
我们需要做的第一件事是构建图形。没有图形,我们就无法进行分析:
-
你可以像这样从我的 GitHub 读取 K-pop 边列表:
import pandas as pddata = 'https://raw.githubusercontent.com/itsgorain/datasets/main/networks/kpop/kpop_edgelist.csv'df = pd.read_csv(data)df['source'] = df['source'].str[0:16]df['target'] = df['target'].str[0:16]df.head()
预览 pandas DataFrame,我们可以看到有'source'和'target'两列。这正是 NetworkX 用来构建图形所需要的。如果你想为图的列命名不同的名称,NetworkX 也允许你指定自己的源和目标列。
-
查看边列表的形状,我们可以看到边列表中有 1,286 条边:
df.shape[0]1286
记住,边是指一个节点与另一个节点之间,或者一个节点与其自身之间的关系,这被称为自环。
-
现在我们已经准备好了 pandas 边列表,我们可以用它来构建无向图:
import networkx as nxG = nx.from_pandas_edgelist(df)G.remove_edges_from(nx.selfloop_edges(G))G.remove_node('@') # remove a junk node -
最后,让我们检查
G,确保它是一个无向 NetworkX 图:G<networkx.classes.graph.Graph at 0x217dc82b4c8>
这看起来完美无缺,所以我们可以开始分析了。
网络的大小和复杂性
我们要调查的第一件事是网络的大小、形状和整体复杂性。让我来定义一下我的意思:
-
网络大小:网络中节点和边的数量
-
网络复杂性:网络中的聚类程度和密度。聚类指的是在网络中实际存在的三角形数量,密度则类似于指网络中节点之间的互联程度。
NetworkX 使得查找网络中节点和边的数量变得非常容易。你只需使用nx.info(G),如下所示:
nx.info(G)
'Graph with 1163 nodes and 1237 edges'
我们的网络有 1,163 个节点和 1,237 条边。简单来说,我们的 K-pop 社交网络由 1,163 个人和组织组成,在这 1,163 个人和组织之间,有 1,237 个已识别的互动。由于这是 Twitter 数据,因此在此情况下,互动意味着两个账户在同一条推文中被提到,意味着它们以某种方式是相关的。回到自然语言处理和内容分析的重要性,我们可以利用这些已识别的关系进一步挖掘这些关系到底是什么类型的。它们是合作关系吗?他们在争论吗?他们一起写了论文吗?社会网络分析(SNA)无法给出这些答案。我们需要内容分析来解决这些问题。但这一章是关于网络分析的,所以让我们继续。
这是一个密集的网络吗?除非你分析的是一个紧密的社群,否则互联网社交网络往往是稀疏的,而非密集的。
让我们看看网络的聚类和密度是什么样子的:
-
首先,让我们检查一下平均聚类:
nx.average_clustering(G)0.007409464946430933
聚类的结果约为0.007,这表明这是一个稀疏的网络。如果聚类返回的结果是1.000,那就表示每个节点都与网络中的其他节点连接。在 SNA 的背景下,这意味着网络中的每个人和组织彼此相识并进行互动。但在 K-pop 中,情况显然不是这样的。并非所有的音乐人都认识他们的粉丝,粉丝们也不一定和他们最喜欢的偶像是朋友。
-
密度是什么样子的呢?from networkx.classes.function import densitydensity(G)0.001830685967059492
密度给出的结果约为0.002,进一步验证了这个网络的稀疏性。
我们先不要继续。我想确保这些概念被理解。让我们构建一个完全连接的图——一个“完全”图——包含 20 个节点,并重复前面几段的步骤。NetworkX 有一些方便的函数用于生成图形,我们将使用nx.complete_graph进行演示:
-
让我们构建图表吧!
G_conn = nx.complete_graph(n=20) -
首先,让我们调查一下网络的大小:
nx.info(G_conn)'Graph with 20 nodes and 190 edges'
很棒。我们有一个包含 20 个节点的网络,这 20 个节点之间有 190 条边。
-
但这真的是一个完全连接的网络吗?如果是的话,那么我们应该会得到
1.0的聚类和密度值:nx.average_clustering(G_conn)1.0density(G_conn)1.0 -
完美。那正是我们预期的结果。但这个网络到底是什么样子的呢?让我们使用我们在本书中一直使用的相同函数来绘制可视化图:
draw_graph(G_conn, edge_width=0.3)
这将绘制出没有节点标签的网络。
图 7.1 – 完全图
如你在网络可视化中看到的,每个节点都与其他节点相连。这是一个完全连接的网络。我们的 K-pop 网络是一个稀疏连接的网络,因此其可视化图将看起来非常不同。
网络可视化与思考
我们知道完全连接的网络是什么样子,我们也知道 K-pop 社交网络是稀疏连接的,但这到底是什么样子的呢?让我们看看:
draw_graph(G, node_size=1, show_names=False)
这将创建一个没有标签的节点和边的网络可视化。
图 7.2 – K-pop 网络
需要注意的一点是,即使只有一千个节点,这仍然需要几秒钟才能渲染完成,并且无法从网络中提取任何真正的洞察。我们看到一堆小点,看到这些小点与其他小点之间的许多线条。我们还可以注意到,网络有一个核心部分,并且随着我们向网络的边缘推进,网络的稀疏度增加。本章稍后会探讨网络层的概念。关键是,除了考虑它看起来很酷之外,我们对这种可视化几乎做不了什么。至少我们可以将其可视化,而在本章的后续部分,我将解释如何“剥洋葱”以理解网络中的各种层次。
但现在为了展示一些内容,这里有一种非常快速的方法来删除所有只有一个边的节点,而这些节点占据了大部分网络。如果你这样做,你可以非常迅速地去噪网络。这是一个巨大的时间节省,因为我之前做完全相同事情的方法是如下:
-
使用列表推导识别每个只有一个边的节点。
-
从网络中移除它。
这一行代码消除了所有这些需求。K_core将G图转换为另一个只包含两个或更多边的节点的图:
draw_graph(nx.k_core(G, 2), node_size=1, show_names=False)
很简单。现在网络看起来怎么样?
图 7.3 – 简化后的 K-pop 网络
我希望你能看到,这一个单独的步骤迅速揭示了所有只有一个边的节点下存在的网络结构。有几种方法可以简化网络,我经常使用这种方法。
重要节点
我们现在已经了解了网络的一般形态,但我们更关心的是了解谁是最重要的人物和组织。在网络科学中,存在着所谓的中心性得分,它根据节点的位置以及信息流动的方式来表示节点在网络中的重要性。NetworkX 提供了数十种不同的中心性度量方法。你可以在networkx.org/documentation/stable/reference/algorithms/centrality.html了解它们。
我将介绍一些我常用的中心性,但这些不一定是最重要的中心性。每种中心性在揭示不同的背景时都有其用途。谷歌的创始人们也创造了他们自己的中心性,著名的叫做 PageRank。PageRank 是许多数据专业人士常用的中心性,但它可能还不够全面。为了全面了解,你应该理解节点的重要性,不仅要看它们是如何连接的,还要看信息是如何流动的。让我们探索几种衡量网络中节点重要性的方法。
度数
判断网络中某个事物或人的重要性最简单的方法就是根据它与其他节点之间的连接数。以 Twitter 或 Facebook 等流行社交网络为例,网红通常连接非常广泛,而我们则会对连接非常少的账户产生怀疑。我们正在借助代码提取这个概念,从我们的网络中获取这一洞察。
在网络中,实体(如人、地方、组织等)称为节点,节点与节点之间的关系称为边。我们可以通过调查网络中节点的度数来计算每个节点的边数:
degrees = dict(nx.degree(G))
degrees
{'@kmg3445t': 1,
'@code_kunst': 13,
'@highgrnd': 1,
'@youngjay_93': 1,
'@sobeompark': 1,
'@justhiseung': 1,
'@hwajilla': 1,
'@blobyblo': 4,
'@minddonyy': 1,
'@iuiive': 1,
'@wgyenny': 1,
...
}
现在,我们有一个包含节点及其度数的 Python 字典。如果我们将这个字典放入 pandas DataFrame 中,我们可以轻松地排序并可视化度数:
-
首先,我们将其加载到 pandas DataFrame 中,并按度数降序排序(从高到低):
degree_df = pd.DataFrame(degrees, index=[0]).Tdegree_df.columns = ['degrees']degree_df.sort_values('degrees', inplace=True, ascending=False)degree_df.head()
这将展示一个 Twitter 账户及其度数的 DataFrame。
图 7.4 – 节点度数的 pandas DataFrame
-
现在,让我们创建一个水平条形图,以便快速获取一些洞察:
import matplotlib.pyplot as plttitle = 'Top 20 Twitter Accounts by Degrees'_= degree_df[0:20].plot.barh(title=title, figsize=(12,7))plt.gca().invert_yaxis()
这将通过度数可视化 Twitter 账户之间的连接。
图 7.5 – 按度数排列的 Twitter 账户水平条形图
一个显著的现象是,即使是比较连接最多的前 20 个节点,度数也迅速下降。在最连接的节点之后,度数下降非常明显。网络中连接最多的节点属于歌手/词曲创作人/演员边伯贤(Byun Baek-hyun),他是组合 Exo 的成员,更为人知的名字是 Baekhyun。这很有意思。为什么他会有这么多连接?是人们在连接他,还是他在连接其他人?每一个洞察通常会引发更多可以探索的问题。把这些问题写下来,根据价值进行优先级排序,然后你可以利用这些问题进行更深入的分析。
度数中心性
度数中心性类似于根据节点的度数判断其重要性。度数中心性是网络中一个节点与其他节点连接的比例。一个节点的度数越多,它与其他节点连接的比例就越高,因此度数和度数中心性可以互换使用:
-
我们可以计算网络中每个节点的度数中心性:
degcent = nx.degree_centrality(G)degcent{'@kmg3445t': 0.0008605851979345956,'@code_kunst': 0.011187607573149742,'@highgrnd': 0.0008605851979345956,'@youngjay_93': 0.0008605851979345956,'@sobeompark': 0.0008605851979345956,'@justhiseung': 0.0008605851979345956,'@hwajilla': 0.0008605851979345956,'@blobyblo': 0.0034423407917383822,'@minddonyy': 0.0008605851979345956,'@iuiive': 0.0008605851979345956,...} -
我们可以用它来创建另一个 pandas DataFrame,按度数中心性降序排序:
degcent_df = pd.DataFrame(degcent, index=[0]).Tdegcent_df.columns = ['degree_centrality']degcent_df.sort_values('degree_centrality', inplace=True, ascending=False)degcent_df.head()
这将显示一个 Twitter 账户及其度数中心性的 DataFrame。
图 7.6 – 节点度数中心性的 pandas DataFrame
-
最后,我们可以将其可视化为一个横向条形图:
title = 'Top 20 Twitter Accounts by Degree Centrality'_= degcent_df[0:20].plot.barh(title=title, figsize=(12,7))plt.gca().invert_yaxis()
这将绘制一个按度数中心性排序的 Twitter 账户横向条形图。
图 7.7 – 按度数中心性排序的 Twitter 账户横向条形图
你注意到度数和度数中心性的条形图除了数值外,看起来几乎一模一样吗?这就是我说它们可以互换使用的原因。使用度数通常会更容易解释和辩护。
介数中心性
介数中心性与信息如何在网络中流动有关。如果一个节点位于其他两个节点之间,那么这两个节点中的任何一个的信息必须通过位于它们之间的节点传递。信息通过位于中间的节点流动。这个节点可以被视为一个瓶颈,或者是一个优势的地方。拥有他人所需信息的节点可以提供战略上的优势。
然而,通常情况下,介数中心性较高的节点位于多个节点之间,而不仅仅是两个节点之间。这通常出现在一个启动网络中,其中一个核心节点连接到几十个或更多的其他节点。想象一下一个社交媒体上的网红。这个人可能与 2200 万粉丝相连,但这些粉丝之间很可能互不相识。他们肯定认识这个网红(或者是一个虚假的机器人)。这个网红就是一个核心节点,介数中心性将会体现这一点。
在我们了解如何计算介数中心性之前,请注意,计算介数中心性对于大型或密集的网络来说非常耗时。如果你的网络较大或密集,且导致介数中心性的计算速度慢到无法使用,考虑使用其他中心性指标来计算重要性:
-
我们可以计算网络中每个节点的介数中心性:
betwcent = nx.betweenness_centrality(G)betwcent{'@kmg3445t': 0.0,'@code_kunst': 0.016037572215773392,'@highgrnd': 0.0,'@youngjay_93': 0.0,'@sobeompark': 0.0,'@justhiseung': 0.0,'@hwajilla': 0.0,'@blobyblo': 0.02836579219003866,'@minddonyy': 0.0,'@iuiive': 0.0,'@wgyenny': 0.0,'@wondergirls': 0.0013446180439736057,'@wg_lim': 0.0026862711087984274,...} -
我们可以用它来创建另一个 pandas DataFrame,按介数中心性降序排序:
betwcent_df = pd.DataFrame(betwcent, index=[0]).Tbetwcent_df.columns = ['betweenness_centrality']betwcent_df.sort_values('betweenness_centrality', inplace=True, ascending=False)betwcent_df.head()
这将显示一个 Twitter 账户及其介数中心性的 DataFrame。
图 7.8 – 节点介数中心性的 pandas DataFrame
-
最后,我们可以将其可视化为一个横向条形图:
title = 'Top 20 Twitter Accounts by Betweenness Centrality'_= betwcent_df[0:20].plot.barh(title=title, figsize=(12,7))plt.gca().invert_yaxis()
这将绘制一个按介数中心性排序的 Twitter 账户横向条形图。
图 7.9 – 按介数中心性排序的 Twitter 账户横向条形图
请注意,条形图与度数和度中心性的图表非常不同。还要注意,@youtube、@spotifykr 和 @kchartsmaster 是具有最高中介中心性的节点。这可能是因为艺术家和其他人在推特中提到 YouTube、Spotify 和 KChartsMaster。这些节点位于节点和其他节点之间。
接近中心性
接近中心性与节点之间的接近程度有关,这与被称为最短路径的概念相关,计算大规模或密集网络的最短路径是计算上昂贵且缓慢的。因此,接近中心性可能比中介中心性还要慢。如果由于网络的大小和密度,获取接近中心性结果太慢,你可以选择其他中心性度量来评估重要性。
最短路径将在另一个章节中讨论,它与从一个节点到另一个节点所需的跳数或握手次数有关。这是一个非常缓慢的操作,因为涉及到许多计算:
-
我们可以计算网络中每个节点的接近中心性:
closecent = nx.closeness_centrality(G)closecent{'@kmg3445t': 0.12710883458078617,'@code_kunst': 0.15176930794223495,'@highgrnd': 0.12710883458078617,'@youngjay_93': 0.12710883458078617,'@sobeompark': 0.12710883458078617,'@justhiseung': 0.12710883458078617,'@hwajilla': 0.12710883458078617,'@blobyblo': 0.18711010406907921,'@minddonyy': 0.12710883458078617,'@iuiive': 0.12710883458078617,'@wgyenny': 0.07940034854856182,...} -
我们可以用它来创建另一个 pandas 数据框,按接近中心性降序排序:
closecent_df = pd.DataFrame(closecent, index=[0]).Tclosecent_df.columns = ['closeness_centrality']closecent_df.sort_values('closeness_centrality', inplace=True, ascending=False)closecent_df.head()
这将展示一个包含 Twitter 账户及其接近中心性的数据框。
图 7.10 – pandas 数据框,节点的接近中心性
-
最后,我们可以将其可视化为水平条形图:
title = 'Top 20 Twitter Accounts by Closeness Centrality'_= closecent_df[0:20].plot.barh(title=title, figsize=(12,7))plt.gca().invert_yaxis()
这将绘制一个根据接近中心性排序的 Twitter 账户水平条形图。
图 7.11 – 根据接近中心性排序的 Twitter 账户水平条形图
请注意,结果与我们之前看到的其他任何中心性度量都不同。@blackpink 排名第一,其次是 @youtube、@kchartsmaster 和 @spotifykr。BLACKPINK 是著名的 K-pop 女团,它们在 K-pop 网络中有很强的连接性,能够获得影响力。其他 K-pop 艺术家可能需要调查 BLACKPINK 在做什么,才能使其处于一个战略上有利的网络位置。
PageRank
最后,PageRank 是 Google 搜索背后的算法。Google 的创始人在 1999 年发表了这篇论文:ilpubs.stanford.edu:8090/422/1/1999-66.pdf。如果你曾经在 Google 上搜索过任何内容,那么返回的结果部分是因为 PageRank,尽管自 1999 年以来,搜索算法可能已经发生了显著的演变。
PageRank 的数学公式不仅考虑了目标节点的入度和出度,还考虑了连接节点的入度和出度。这也是为什么搜索引擎优化(SEO)成为一项重要工作,因为人们了解到,要获得 Google 的高排名,一个网站应该尽可能多地拥有外部链接,并且还要链接到其他信息来源。如需了解 PageRank 背后的数学原理,请查看斯坦福大学的 PDF 文档。
PageRank 是一个非常快速的算法,适用于大规模和小规模的网络,并且作为重要性度量非常有用。许多图解决方案在其工具中提供 PageRank 功能,许多人将 PageRank 视为首选的中心性。就个人而言,我认为你应该了解几种中心性,了解它们的应用场景及其局限性。PageRank 在大型和密集网络中也非常有用,因此我建议在进行任何中心性分析时都包含它:
-
我们可以计算网络中每个节点的 PageRank 得分:
pagerank = nx.pagerank(G)pagerank{'@kmg3445t': 0.00047123124840596525,'@code_kunst': 0.005226313735064201,'@highgrnd': 0.00047123124840596525,'@youngjay_93': 0.00047123124840596525,'@sobeompark': 0.00047123124840596525,'@justhiseung': 0.00047123124840596525,'@hwajilla': 0.00047123124840596525,'@blobyblo': 0.0014007295303692594,'@minddonyy': 0.00047123124840596525,...} -
我们可以使用这个来创建另一个按 PageRank 降序排序的 pandas DataFrame:
pagerank_df = pd.DataFrame(pagerank, index=[0]).Tpagerank_df.columns = ['pagerank']pagerank_df.sort_values('pagerank', inplace=True, ascending=False)pagerank_df.head()
这将显示一个 Twitter 账户及其 PageRank 得分的数据框。
图 7.12 – 节点的 PageRank 得分的 pandas DataFrame
-
最后,我们可以将其可视化为水平条形图:
title = 'Top 20 Twitter Accounts by Page Rank'_= pagerank_df[0:20].plot.barh(title=title, figsize=(12,7))plt.gca().invert_yaxis()
这将绘制一个按 PageRank 排序的 Twitter 账户水平条形图。
图 7.13 – 按 PageRank 排序的 Twitter 账户水平条形图
这些结果实际上与度和度中心性所得到的条形图非常相似。再次,Exo 的 Baekhyun 位居榜首。
边的中心性
在结束这一部分关于中心性的内容之前,我想指出,你并不仅限于节点的中心性。也有边的中心性。例如,边介数中心性可以用来识别连接最多节点的边。如果你剪断了这条连接最多节点的边,网络通常会被分割成两个大块,称为连通分量。这实际上对识别社区或新兴趋势非常有用,我们将在后续章节中深入探讨。
比较中心性
为了了解不同中心性之间的差异,或者将多种中心性一起使用(例如,在构建机器学习分类器并希望使用图度量时),将不同的中心性合并成一个 pandas DataFrame 可能非常有用。你可以通过 pandas 的concat函数轻松实现:
combined_importance_df = pd.concat([degree_df, degcent_df, betwcent_df, closecent_df, pagerank_df], axis=1)
combined_importance_df.head(10)
这将把我们的所有中心性和 PageRank DataFrame 合并成一个统一的 DataFrame。这样可以更方便地比较不同类型的中心性。
图 7.14 – 综合重要性度量的 pandas DataFrame
你可能会注意到,如果你按不同类型的中心性排序,一些中心性结果非常相似,而其他则差异很大。我留给你这个结论:没有一个中心性能够统治所有的网络。它们是不同的,应在不同的情境下使用。如果你正在绘制信息流的图谱,那么介数中心性非常有用,只要网络的规模是可管理的。如果你只想查看网络中哪些节点最为连接,可以通过查看节点的度数来最轻松地做到这一点。如果你想了解哪些节点与每个其他节点的距离最短,可以尝试使用接近中心性。如果你想要一个能很好地识别重要节点,并且即使在大型网络上也能高效运行的算法,可以尝试 PageRank:
combined_importance_df.sort_values('pagerank', ascending=False)[0:10]
这将展示一个包含 Twitter 账户和综合网络中心性及 PageRank 得分的 DataFrame。
图 7.15 – 按 PageRank 排序的综合重要性度量的 pandas DataFrame
只需要知道,即使是 PageRank 和介数中心性也可能给出非常不同的结果,因此你应该学习几种不同的衡量重要性的方法,并了解你想要做什么。这些对初学者来说可能是非常陌生的,但不要害怕,跳进去学习吧。NetworkX 文档中的文档和相关期刊将足以帮助你入门。
如果你刚刚开始进行社交网络分析和网络科学,中心性可能是这一章中最不寻常的部分。从这一章节开始,接下来的概念应该就不那么陌生了。
可视化子图
在网络分析中,我们常常希望查看网络的一部分,以及该部分中的节点如何彼此连接。例如,如果我有一个 100 个感兴趣的网页域名或社交媒体账号的列表,那么创建一个包含所有节点的子图来进行分析和可视化可能会非常有用。
对于子图的分析,本章中的所有内容仍然适用。例如,你可以在子图中使用中心性来识别一个社区中的重要节点。你还可以使用社区检测算法来识别子图中存在的社区,尤其是当这些社区尚未被识别时。
当你想去除网络中的大部分噪音并研究某些节点之间的交互时,可视化子图也非常有用。可视化子图与我们可视化整个网络、个人图和时序图的方式是一样的。但创建子图需要稍微花费一点工作。首先,我们需要识别出感兴趣的节点,然后我们需要构建一个仅包含这些节点的子图,最后,我们将可视化这个子图:
-
举个例子,让我们选择网络中 PageRank 得分最高的 100 个节点:
subgraph_nodes = pagerank_df[0:100].index.to_list()subgraph_nodes['@b_hundred_hyun','@zanelowe','@haroobomkum','@itzailee','@spotifykr','@shxx131bi131','@thinktwicekpop','@leehi_hi','@bambam1a','@bighitent','@ericnamofficial','@twicetly',...]
很简单。我这里只展示了几个节点,因为整个屏幕滚动下去时,节点会显示得很远。
-
接下来,我可以构建一个子图,如下所示:
G_sub = G.subgraph(subgraph_nodes) -
最后,我可以像可视化任何其他网络一样进行可视化:
draw_graph(G_sub, node_size=3)
在这个例子中,我省略了节点名称,但我也可以很容易地将它们添加进来。我觉得不加节点名称会使这个示例的可视化更简洁。
图 7.16 – 按 PageRank 排序的前 100 个 K-pop Twitter 账号的子图可视化
就这样。实际上,关于子图创建的内容并不多,除了它是可行的以及如何做。只要知道方法,过程是简单的。
调查岛屿和大陆——连接组件
如果你查看子图的可视化图,你可能会注意到有一个大的节点簇,几个小的节点岛屿(有两条或更多的边),以及几个孤立节点(没有边的节点)。这是许多网络中常见的现象。通常,网络中会有一个巨大的超级簇,几个中等大小的岛屿,以及许多孤立节点。
这带来了挑战。当一些人刚开始接触网络分析时,他们通常会对网络进行可视化,并使用 PageRank 来识别重要节点。但这远远不够。提取网络中的洞察有很多不同的方法,我将在本书的过程中向你展示几种方法。
但是有一种非常简单的方式可以去除噪声,那就是识别网络中存在的大陆和岛屿,利用它们创建子图,然后分析和可视化这些子图。
这些大陆和岛屿在正式术语中称为连接组件。连接组件是一个网络结构,其中每个节点至少与另一个节点相连。实际上,NetworkX 允许孤立节点存在于自己的连接组件中,这让我感到奇怪,因为孤立节点除了可能自连接外,实际上没有与任何其他节点相连(自环存在)。
在网络中找到所有存在的连接组件是非常容易的:
components = list(nx.connected_components(G))
len(components)
我这里做了两件事:首先,我将我们G图的所有连接组件加载到一个 Python 列表中,然后计算存在的组件数量。K-pop 网络中有 15 个连接组件。
很好,但这 15 个中哪些是大陆,哪些是岛屿呢?通过一个简单的循环,我们可以计算每个连接组件中存在的节点数:
for i in range(len(components)):
component_node_count = len(components[i])
print('component {}: {}'.format(i, component_node_count))
这将给我们一个连接组件的列表,以及属于该连接组件的节点数:
component 0: 909
component 1: 2
component 2: 3
component 3: 4
component 4: 2
component 5: 2
component 6: 80
component 7: 129
component 8: 3
component 9: 7
component 10: 4
component 11: 4
component 12: 2
component 13: 10
component 14: 2
完美。注意到其中一个组件有 909 个节点。这是网络中可能存在的大型“大陆”之一。还要注意到,组件中有 80 和 129 个节点。这比最大连通组件中的节点数量少得多,但仍然是一个相当大的节点数。我把这些看作是岛屿。最后,注意到还有几个组件的节点数在 2 到 10 之间。这些就像是小岛屿。
每一个连通组件都可以作为子图进行分析和可视化。为了简化可视化,我将创建一个辅助函数,扩展我的主要draw_graph函数:
def draw_component(G, component, node_size=3, show_names=True)
check_component = components[component]
G_check = G.subgraph(check_component)
return draw_graph(G_check, show_names=show_names, node_size=node_size)
让我们试试看吧。让我们可视化一个随机组件,组件 13:
draw_component(G, component=13, node_size=5)
它是怎么呈现的?
图 7.17 – 连通组件 #13 的子图可视化
看起来不错。我们已经成功地可视化了整个网络中的一个单一组件。接下来,让我们可视化最大的组件:
draw_component(G, 0, show_names=False, node_size=2)
图 7.18 – 连通组件 #0 的子图可视化
再次回到那个巨大的、混乱的线团。虽然我们成功地进行了可视化,但我们可以通过去除所有只有一个边的节点来大大简化它,比如这样做。
连通组件有点不寻常,就像中心性一样,但如果你把它们看作是存在于网络中的岛屿和大陆,那就能去除很多神秘感。总的来说,在一个网络中,通常会有几个连通组件,我把它们看作是大陆、岛屿或孤立点。在大多数网络中,通常至少有一个大型大陆,几个岛屿,以及零到多个孤立点。孤立点的数量取决于图的构建方式。使用我们在前几章中提到的 NER 方法,实际上是没有孤立点的。
我们将在第九章中查看更多关于连通组件的内容。
社区
社区检测算法在各种形式的网络分析中非常有用。在 WNA 中,它们可以用来识别整个网络中存在的社区。当应用于以自我为中心的网络(自我图)时,它们可以揭示围绕单个节点存在的社区和团体;在时间网络中,它们可以用来观察社区随时间的演变。
社区检测在社会网络分析(SNA)中很常见,因为在人口庞大的群体中存在不同的社群,识别这些社群是非常有用的。社区检测有多种方法,网络上有很多关于社区检测算法如何工作的资料。本书主要讲解应用网络科学,因此我只会演示其中一种,叫做Louvain 算法。与中心性一样,没有“最佳”的算法。我曾经参加过一些讨论,其中有人提到了一种边缘算法,并坚信它更好;也有讨论中人们更倾向于使用 Louvain 算法。
您可以在这里了解更多关于 Louvain 算法的信息:python-louvain.readthedocs.io/en/latest/。
-
Louvain 算法并未随 NetworkX 一起提供。您需要安装它,安装方法非常简单,如下所示:
pip install python-louvain -
之后,您可以通过以下方式导入该库进行使用:
import community as community_louvain -
为了节省大量时间并跳过数学部分,Louvain 算法可以识别节点所属的各种分区(社区)。与我们通常的网络可视化相比,可视化这些分区有点棘手,因为
scikit-network对节点着色的灵活性并不高。为了节省时间,我将回到我以前的网络可视化实践,并使用 NetworkX 进行可视化。下面是绘制图形和着色社区的代码:def draw_partition(G, partition):import matplotlib.cm as cmimport matplotlib.pyplot as plt# draw the graphplt.figure(3,figsize=(12,12))pos = nx.spring_layout(G)# color the nodes according to their partitioncmap = cm.get_cmap('flag', max(partition.values()) + 1)nx.draw_networkx_nodes(G, pos, partition.keys(), node_size=20, cmap=cmap, node_color=list(partition.values()))nx.draw_networkx_edges(G, pos, alpha=0.5, width=0.3)return plt.show() -
现在我们已经有了可视化功能,我们需要先识别分区,然后我们需要可视化网络。让我们将这两步一起完成。我在经过一些调整后使用
resolution=2,因为社区布局看起来最优:partition = community_louvain.best_partition(G, resolution=2)draw_partition(G, partition)
看起来怎么样?
图 7.19 – 社区分区的可视化
这些图像对我来说虽然凌乱,但却令人着迷。我可以轻松地看到一些以前从未注意到的、易于区分的社区。但是它们是什么呢?哪些节点属于每个社区?将这个分区列表转换为 pandas DataFrame 非常简单,我们可以利用它来识别社区,统计每个社区中节点的数量,确定某个节点属于哪个社区,并可视化各个社区:
-
首先,让我们从分区列表创建一个 pandas DataFrame:
community_df = pd.DataFrame(partition, index=[0]).Tcommunity_df.columns = ['community']community_df.head()
现在看起来怎么样?
图 7.20 – pandas DataFrame 中的社区分区
-
这看起来不错。我们可以看到,它已经按分区编号排序,我称之为
community。现在它已经是一个 pandas DataFrame,统计每个社区中属于的节点数量变得很简单:community_df['community'].value_counts()
这将为我们提供一个社区列表(左侧数字)以及该社区中节点的数量(右侧数字):
21 170
10 133
14 129
16 104
2 91
3 85
13 80
23 70
0 66
15 55
4 51
22 48
1 36
17 10
19 7
9 4
20 4
5 4
8 3
18 3
12 2
11 2
7 2
6 2
24 2
我们可以轻松看到哪些社区拥有最多的节点。我们应该使用子图来分析和可视化这些社区,正如前面所解释的那样。
-
那么我们如何识别每个社区中的节点呢?我们可以直接在 pandas 中进行。以下是一个简单的辅助函数:
def get_community_nodes(commmunity_df, partition):community_nodes = community_df[community_df['community']==partition].index.to_list()return community_nodes -
我们可以直接使用这个功能,但我更倾向于将这些
community节点提取出来,创建一个子图并进行可视化。以下是实现这一功能的辅助函数:def draw_community(G, community_df, partition, node_size=3, show_names=False):community_nodes = get_community_nodes(community_df, partition)G_community = G.subgraph(community_nodes)return draw_graph(G_community, node_size=node_size, show_names=show_names)
我们来试试一个:
draw_community(G, community_df, 1, show_names=True)
图 7.21 – 社区 #1 的子图可视化
运行后,我可以看到这个可视化效果。如果你看到的是不同的结果,不用担心。在处理网络时,像连通分量和社区编号等东西,在下一次运行时位置不一定相同。
非常酷。这感觉与可视化连通分量非常相似,但社区不一定是孤岛或大陆。例如,多个社区可以出现在一个大型的连通分量中。算法寻找分隔节点群体的边界,然后据此标记社区。
如果你从事网络工作,特别是如果你有兴趣识别社交网络中存在的社群,你会想要尽可能多地了解如何识别团体和社区。尝试不同的算法。我选择了 Louvain,因为它快速且可靠,即使在大型网络中也是如此。
桥接节点
简单来说,桥接节点是位于两个不同社区之间的节点。在小型社交网络中,这些节点通常很容易被目视识别,因为会有一个或几个看起来像橡皮筋的连接强度,如果被剪断,两个群体就会分开。就像桥梁让人们可以跨越水面从一块陆地走到另一块陆地一样,网络中的桥接节点也能让信息从一个社区传播到另一个社区。作为人类,身处桥接节点是一个强有力的位置,因为信息和资源必须通过你才能传递到另一方。
在复杂的网络中,桥接节点更难以目视识别,但它们通常存在,位于两个社区之间。我们的 K-Pop 网络相当复杂,因此网络比在较小的社交网络中更不容易看出,但它们确实存在。
-
你可以像这样在网络中找到桥接节点:
list(nx.bridges(G))[('@kmg3445t', '@code_kunst'),('@code_kunst', '@highgrnd'),('@code_kunst', '@youngjay_93'),('@code_kunst', '@sobeompark'),('@code_kunst', '@justhiseung'),('@code_kunst', '@hwajilla'),('@code_kunst', '@blobyblo'),('@code_kunst', '@minddonyy'),('@code_kunst', '@iuiive'),('@code_kunst', '@eugenius887'),...] -
这是一个非常长的桥接节点列表,我这里只展示了一部分行,但我们可以结合 pandas 来识别最重要的桥接节点:
bridges = [s[0] for s in list(nx.bridges(G))]pd.Series(bridges).value_counts()[0:10]@b_hundred_hyun 127@zanelowe 90@haroobomkum 84@itzailee 78@spotifykr 60@shxx131bi131 57@thinktwicekpop 53@leehi_hi 53@bambam1a 49@bighitent 46 -
删除桥接节点的一个副作用是,它可能类似于删除高度中心的节点——网络会碎裂成一大群孤立节点和一些较小的连通分量。让我们把拥有最多边的 10 个桥接节点删除:
cut_bridges = pd.Series(bridges).value_counts()[0:10].index.to_list()G_bridge_cut = G.copy()G_bridge_cut.remove_nodes_from(cut_bridges) -
做完这一步后,我们的网络可能看起来像一颗超级新星爆发,碎片飞向太空。让我们来看看:
draw_graph(G_bridge_cut, show_names=False)
这应该会绘制一个没有节点标签的网络。
图 7.22 – 切割主要桥梁后的网络可视化
如我们所见,网络中心仍然存在一个密集连接的组件,少数由几个节点组成的小型连接组件,以及许多独立的孤立节点。切割桥梁并不总是如此具有破坏性。在我处理的其他网络中,存在一个由两个社区组成的核心结构,几个节点位于这两个社区之间作为桥梁。当移除这些桥梁时,网络的核心社区就会分裂开来,几乎没有或没有孤立节点。
识别桥梁是有原因的。在社交网络中,这些节点是信息必须经过的节点,才能到达另一边的社区。如果你想在这些网络中战略性地定位自己,理解桥梁节点的作用,并模仿他们所做的,和两边的人建立联系,将会是通向权力的捷径。
同样,如果你的目标是禁用一个网络,识别并移除重要的桥梁将会停止信息从一个社区流向另一个社区。这将造成高度的干扰。这对于破坏暗网(如犯罪、仇恨等)可能非常有用。
这些是可以从网络中提取的有用见解,没有网络分析很难识别。识别桥梁并为处理它们制定计划,可以提供战略优势。你可以利用它们来获取权力,也可以用来破坏网络,或者将网络社区分离,进行更清晰的分析。
使用 k_core 和 k_corona 理解网络的层次
网络可以被看作是洋葱,通常也以类似的方式进行可视化,孤立节点位于最外层,接下来是具有单一边缘的节点,然后是具有两条边的节点,依此类推,直到到达网络的核心。NetworkX 提供了两种方法来“剥洋葱”,即 k_core 和 k_corona。
k_core
NetworkX 的 k_core 函数允许我们轻松将网络简化为仅包含具有 k 条或更多边缘的节点,其中 "k" 是一个介于 0 和网络中任意节点的最大边数之间的数字。因此,您得到的是一个包含 k 条或更多边缘的网络“核心”。如果执行 k_core(G, 2),则返回的图形将仅包含那些具有两条或更多边缘的节点,同时移除了孤立节点和仅有一个边缘的节点,一步完成。
这一单一的网络去噪步骤可能看起来没什么大不了,但如果通过列表推导或循环来实现,需要更多的步骤、更多的思考以及更多的调试。这一步骤轻松地完成了清理工作。因此,当我最关注去除孤立节点和单边节点后的网络形态时,k_core(G, 2) 是我代码中常见的操作。
比如说,这就是我们完整的 K-pop 网络渲染出来的样子。很难看清任何东西,因为那些单边节点已经将网络可视化变成了一团乱麻。
图 7.23 – 整个网络的可视化
然而,我们可以很容易地删除所有度数小于两个的节点:
G_core = nx.k_core(G, 2)
那么网络现在看起来怎么样?
draw_graph(G_core, show_names=True, node_size=3)
这应该绘制出我们的G_core网络,并显示节点标签。
图 7.24 – 带有 k_core 和 k=2 的整个网络可视化
很明显,这样要更容易解释。
学习k_core是我学习如何分析图形和社交网络过程中最重要的时刻之一。我以前是通过不太直接的方法来去噪网络,识别度数少于两个的节点,将它们添加到列表中,然后从网络中删除它们。这个单一功能节省了我大量的时间。
k_corona
正如k_core允许我们提取网络的核心一样,k_corona让我们可以研究网络的每一层。k_corona不是为了找出核心,而是为了研究网络每一层发生的事情。例如,如果我们只想查看那些有零条或一条边的节点,我们可以这样做:
G_corona = nx.k_corona(G, 1)
这将显示一堆孤立节点,并且可能还会有一些节点之间存在一条边:
-
首先,让我们可视化
k_corona(G, 1)的结果:draw_graph(G_corona, show_names=False, node_size=2)
这应该渲染出所有拥有一个或更少边的节点的网络可视化。没有任何边的节点称为孤立节点,将以点的形式显示。
图 7.25 – k_corona k=1 层的可视化
如我们所见,有很多孤立节点。你能识别出那些只有一条边连接的节点吗?我看不出来。这就像在读那本《在哪里是沃尔多?》一样。那么,我们如何识别出这个层次中有边相连的节点呢?如何移除所有度数小于一的节点呢?想一想。
-
没错,我们将使用
k_core进行清理:G_corona = nx.k_corona(G, 1)G_corona = nx.k_core(G_corona, 1)
如果我们将其可视化,可以看到有五个连接组件,每个组件包含两个节点,每个节点与该组件中另一个节点之间有一条边。
draw_graph(G_corona, show_names=True, node_size=5, font_size=12)
这将绘制出G_corona网络,孤立节点已被移除,并且显示节点标签。
图 7.26 – k_corona k=1 层的简化可视化
-
有没有简单的方法来提取这些节点,以便我们可以进一步分析它们?是的,很简单:
corona_nodes = list(G_corona.nodes)corona_nodes
这将显示我们corona_nodes中所有节点的列表:
['@day6official',
'@9muses_',
'@bol4_official',
'@8_ohmygirl',
'@withdrama',
'@elris_official',
'@hunus_elris',
'@miiiiiner_misog',
'@into__universe',
'@shofarmusic']
-
第二层网络看起来如何,即每个节点具有两个度数的层?这些层上的节点是否彼此连接?让我们创建并呈现这个可视化:
G_corona = nx.k_corona(G, 2)draw_graph(G_corona, show_names=True, node_size=3)
这将呈现一个所有节点边数为两个或更少的网络可视化。
图 7.27 – k=2 层的 k_corona 可视化
它看起来非常类似于第一层的k_corona,但我们可以更容易地看到一些节点连接到其他节点。我们还可以看到这一层中孤立节点显著减少。我们可以重新进行k_core步骤来清理,但我认为你已经理解了重点。
就我个人而言,我并不经常使用k_corona。我对逐层剥离网络并没有太大兴趣,但这是一个选择,也许对你而言比对我更有用。然而,我几乎每次处理网络时都会使用k_core,用于去噪声网络,并研究存在于社交网络核心的核或核。我建议你了解这两者,但可能你对k_core的需求比对k_corona更大。不过,k_corona为分析开启了一些有趣的可能性。
挑战自己!
在结束这一章之前,我想向你提出一个挑战。你已经学会了如何使用文本创建网络,边缘列表的样子以及如何在 pandas 中构建一个,如何创建网络,如何清理网络,现在你已经介绍了整体网络分析。你现在拥有开始你的网络分析之旅所需的每一样工具。我将在后续章节中详细解释如何做更多事情,但你已经拥有了开始并迷上网络分析所需的所有工具。
我想挑战你,思考一下你自己的数据,关于你处理的数据,你玩耍的社交网络,你协作的工作网络,以及更多。我希望你考虑如何将这些活跃的网络描述成边缘列表(只是源和目标列),绘制网络可视化,并分析这些网络。你有能力做到这一点,而网络无处不在。我建议,当你学习调查社交网络和各种网络时,使用对你真正有趣的数据。你不需要在网上找数据集。你可以轻松地自己创建一个,或者像我们在之前章节中探索的那样,抓取社交媒体上的数据。
我挑战你,在这一章节停下来,玩上一会儿。在网络中迷失。重新阅读本书中的前几章。探索。变得怪异。玩得开心。这是我最喜欢的学习方式。我非常享受构建我自己的数据集,并分析我最感兴趣的事物。
总结
我们在本章中走了很长一段路。本章内容可以独立成书,但我的目标是快速带你了解网络中可以做的事情。正如我在开始时所说,这本书不会是一本数学书。我想为你开启新的能力和机会,我相信本章和本书能够为你实现这一点。
在本章中,我们涉及了很多内容:解释了整体网络分析,描述了有助于分析的问题,并花了很多时间进行实际的网络分析。我们从整体上看待网络,同时也研究了节点中心性、连接组件和层级。
在下一章中,我们将学习自我中心网络分析。我们将其称为自我网络,为了简洁。在那一章中,我们将聚焦于感兴趣的节点,了解它们周围的社区和节点。你可以把自我中心网络分析想象成一次放大。