分析工程学介绍
原文:
towardsdatascience.com/analytics-engineering-8b0ed0883379
分析工程师是谁,他们应该做什么
·发布在 Towards Data Science ·阅读时间 6 分钟·2023 年 10 月 22 日
–
图片由 DALL-E2 生成
传统上,数据团队由数据工程师和数据分析师组成。
数据工程师负责建立支持数据操作的基础设施。这包括数据库的配置和 ETL 流程的实施,这些流程用于将数据从外部来源导入到目标系统(可能是另一个数据库)。此外,数据工程师通常负责确保数据的完整性、新鲜度和安全性,以便分析师可以查询数据。数据工程师的典型技能包括 Python(或 Java)、SQL、编排(使用工具如 Apache Airflow)和数据建模。
另一方面,数据分析师应该使用 Excel 或 SQL 构建仪表板和报告,以便向内部用户和部门提供业务洞察。
数据团队的传统组成
从 ETL 到 ELT 的过渡
为了处理数据并获得有价值的洞察,我们首先需要提取数据,对吗?🤯
数据摄取是通过 ETL(最近也用 ELT)流程进行的。ETL 和 ELT 范式都涉及三个主要步骤:提取、转换和加载。目前,我们忽略这些步骤的执行顺序,专注于每个步骤的独立功能。
提取
这个步骤指的是从持久化来源中提取数据。数据来源可以是数据库、API 端点、文件或消息队列。
提取步骤从各种来源中提取数据 — 来源:作者
转换
在转换步骤中,管道预计会对数据的结构和/或格式进行一些更改,以实现某个目标。转换可以是修改(例如,将“United States”
映射到“US”
)、属性选择、数值计算或连接。
转换步骤对输入原始数据进行了一系列转换 — 来源:作者
加载
该步骤指的是将数据(无论是原始的还是转换后的)移动到目标系统的过程。目标通常是 OLTP 系统,如数据库,或 OLAP 系统,如数据仓库。
将数据加载到目标系统 — 来源:作者
ETL: 提取 → 转换 → 加载
ETL 指的是数据提取步骤后跟着转换步骤,最终以加载步骤结束的过程。
ETL 过程的可视化表示 — 来源:作者
ETL 过程中,数据转换步骤发生在目标系统之外的临时环境中,在数据被加载到目标之前进行转换。
ETL 已经存在一段时间,但其应用逐渐开始减少。
-
由于转换发生在中间(临时)服务器上,将转换后的数据移动到目标系统中会产生额外的开销。
-
目标系统不会包含原始数据(即转换前的格式数据)。这意味着每当需要额外的转换时,我们必须重新提取原始数据。
云技术的出现改变了数据摄取和转换的过程。托管在云上的数据仓库使得以非常低的成本存储大量数据成为可能。因此,是否真的需要在每次进行转换时都“实时”应用转换并丢弃原始数据?
ELT: 提取 → 加载 → 转换
ELT 指的是提取步骤后跟着加载步骤,最终的数据转换步骤在最后进行的过程。
ELT 过程的可视化表示 — 来源:作者
与 ETL 相比,ELT 中不需要临时环境/服务器,因为数据转换是在目标系统内进行的,目标系统通常是托管在云上的数据仓库或数据湖。
此外,原始数据存在于目标系统中,因此可以随时用于进一步的转换。
数据分析工程
作为提醒,在较早的数据团队构建中,工程师负责维护 ETL 层,而分析师则负责创建仪表板和报告。但现在的问题是数据分析工程师在这一过程中扮演什么角色?
在较早的数据团队结构中,数据工程师负责 ETL,数据分析师负责报告——来源:作者
分析工程师实际上是数据工程师和分析师之间的桥梁。他们的责任是处理原始数据并应用转换,以便数据分析师可以收集转换后的数据,准备商业智能层的仪表板和报告,以便内部用户能够做出数据驱动的决策。现在,数据工程师可以更多地关注数据平台的摄取层和更广泛的数据基础设施。
在 ELT 流程中,数据工程师负责数据在数据仓库中的提取和加载,分析工程师负责数据转换层,分析师负责业务仪表板的创建——来源:作者
dbt:分析工程的终极工具
分析工程师是能够帮助数据团队扩展和加快速度的人。但要做到这一点,他们还需要利用能够帮助他们完成工作的工具。**数据构建工具(dbt)**就是终极的分析工程工具。
dbt 是一个用于以可扩展且成本效益高的方式构建和管理数据模型的工具。dbt 可以为你处理所有模型之间的依赖关系,而无需花时间弄清楚模型必须按什么顺序执行。此外,它还提供了支持数据质量测试、新鲜度测试和文档编制等功能。
为了更好地理解 dbt 的作用,重要的是要可视化更广泛的背景,看看它在现代数据栈中所处的位置。dbt 实际上位于 ELT 管道中的 T 层,转换在原始数据所在的数据仓库中进行。
使用 dbt 对数据仓库中的原始数据进行转换——来源:作者
dbt 是一个CLI(命令行接口)工具,使分析工程团队能够部署和管理数据模型,遵循软件工程的最佳实践。这些实践包括支持多个环境(开发和生产)、版本控制和 CI/CD(持续集成和持续开发)。数据模型可以用 SQL(jinja 模板)编写,但工具的最新版本也支持使用 Python 定义模型!
最后的想法…
分析工程是数据工程和数据分析交汇处的新兴领域,旨在加快分析产品的开发,提高数据质量,并增强数据的可信度。促进数据产品生命周期的主要工具是 dbt,它极大地改变了数据团队的工作和协作方式。因此,熟悉它非常重要,因为它将在长期内存在。
在即将发布的文章中,我们将更专注于 dbt 以及如何有效地使用它来构建和管理数据模型。因此,请确保订阅,以便在文章发布时收到通知!
使用 Python 分析北极冰趋势
原文:
towardsdatascience.com/analyze-arctic-ice-trends-with-python-581ba4423416
探索过去的预测
·发表在Towards Data Science ·阅读时间 7 分钟·2023 年 6 月 27 日
–
倾斜的冰岛冰山(图源:作者)
测量是所有科学的基石。没有它,我们如何测试我们的假设?
作为数据科学的首选编程语言,Python 使得收集、清理和理解测量数据变得容易。借助 Python,我们可以回测预测,验证模型,并追究预言者的责任。
去年,一个过时的网络迷因在我的 LinkedIn 动态中出现,标记有“#灾难化”的标签。内容是关于他在 2007 年和 2009 年提出的北极海在七年内将无夏季冰的评论。一些事实核查网站将这一说法验证为“基本真实”,并引用了以下引用:
“一些模型表明,Wieslav Maslowski 博士认为,在接下来的五到七年内,整个北极冰盖在某些夏季月份可能会完全无冰,概率为 75%。”
- 阿尔·戈尔,2009 年 12 月
虽然许多人对网络迷因视而不见,但数据科学家能够深入探讨数据并得出自己的结论。在这个快速成功的数据科学项目中,我们将使用 Python 的 pandas 和 Matplotlib 库来审视过去四十年北极海冰的行为,并对评论和迷因进行检验。
关于气候变化的评论
请注意,这既不是一篇反对气候变化的文章,也不是支持气候变化的文章,而是一篇支持数据的文章。无论你对人为气候变化的看法如何,我希望你同意验证模型和确认预测对每个人都至关重要。
同样重要的是,关键主题的思想领袖应避免做出易被驳斥的夸张或轻率的声明。这不仅会损害可信度,还会使话题政治化,使得理性的讨论变得困难,甚至不可能。
在这种情况下,艾尔·戈尔明智地用概率和“建议”和“可能”等词语来缓和他的评论。不幸的是,这些缓和在制作表情包时容易被误用。
国家雪冰数据中心
为了确认或驳斥戈尔的“预测”,我们需要知道在相关时间段内的最小海冰范围。幸运的是,我们可以访问由国家雪冰数据中心提供的全面公共数据集,该中心是科罗拉多大学博尔德分校环境科学合作研究所(CIRES)的一部分。此数据集利用卫星图像追踪和监测北极海冰的变化。
基于卫星图像的 2022 年 9 月海冰范围(感谢国家雪冰数据中心,科罗拉多大学博尔德分校 [1])
数据以月度和日度增量提供。通常,月度总数推荐用于观察海冰趋势。然而,为了确保我们捕捉到每个月的最小测量范围,我们将使用每日数据,并选择最低值的那一天来代表整个月。
虽然按日增量的数据集可以通过提供的链接以 CSV 格式访问,但我已经准备好了文件,并将其存储在这个代码片段中以便使用。
为了解决当前的问题,我们将使用 pandas 准备数据,并使用 Matplotlib 绘制折线图。我们将绘制所有数据,但主要关注每个夏季发生的最小值。
代码
以下代码被输入到 JupyterLab 中,并按单元描述。
导入库
对于这个项目,我们只需要 Matplotlib 和 pandas 这两个稳固的库。你可以使用 conda 安装它们:
conda install matplotlib pandas
和使用 pip:
pip install matplotlib
pip install pandas
Matplotlib 的 mdates
模块将帮助我们在图表上标注戈尔提出无冰北极海的时间跨度。以下是导入的库:
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
加载和准备数据
以下注释的代码使用 pandas 库从 Gist 中加载数据,并为绘图做准备。此外,它生成了年度移动平均值,以捕捉测量中的长期趋势。
# Read the data:
URL = 'https://bit.ly/3NLoDzx'
df = pd.read_csv(URL, skiprows=[1])
# Remove leading whitespace from the column names:
df.columns = df.columns.str.strip()
# Drop unnecessary columns:
df = df.drop(df.columns[[4, 5]], axis=1)
# Group by monthly MINIMUM ice extent:
df = df.groupby(['Year', 'Month']).agg({'Extent': ['min']}).reset_index()
# Create a 'date' column from the 'Year' and 'Month' columns:
cols = ['Year', 'Month']
df['date'] = df[cols].apply(lambda x: '-'.join(x.values.astype(str)),
axis="columns")
df['date'] = pd.to_datetime(df['date'])
# Set the 'date' column as the DataFrame index:
df = df.set_index(df['date'])
# Drop unnecessary year, month, and date columns:
df = df.drop(df.columns[[0, 1, 3]], axis=1)
# Calculate the yearly moving average:
df['yearly_ma'] = df.Extent.rolling(12).mean()
# Check the results:
df.tail(3)
DataFrame 的末尾(作者提供的图片)
绘制数据
以下注释的代码绘制了每月最低冰面数据和年度移动平均值的折线图。阿尔·戈尔 2009 年言论后的七年期用红色突出显示并标记为“戈尔的接下来 7 年”。
# Create the plot:
fig, ax = plt.subplots(figsize=(12, 6))
ax.set_title('Arctic Sea Ice Monthly MINIMUM Extent', size=15)
ax.plot(df['Extent'], lw=2)
ax.plot(df['yearly_ma'], color='k')
ax.set_ylim([0, 20])
ax.tick_params(axis='both',
which='major',
labelsize=12)
ax.grid()
# Add a legend:
ax.legend(['Ice Extent (10⁶ sq km)', 'Yearly Moving Ave'],
frameon=True,
loc=3,
prop={'size': 14},
facecolor='#a1c9f4',
edgecolor='k',
fancybox=True,
shadow=True,
framealpha=1)
# Add a shaded span for Gore's prediction:
ax.axvspan(*mdates.datestr2num(['2009-12-14', '2016-1-1']),
color='red',
alpha=0.3)
# Annotate the 7-year span referenced by Gore in 2009:
ax.text(0.655, 0.8,
"Gore's Next 7 Years",
transform=ax.transAxes,
fontsize=14)
# Set the x and y labels:
font1 = {'family': 'arial',
'color': 'black',
'size': 15}
ax.set_xlabel('Year', fontdict=font1)
ax.set_ylabel('Arctic Sea Ice Extent (10⁶ sq km)',
fontdict=font1)
plt.show()
最终的线图(作者提供的图片)
图中锯齿状的蓝线跟踪了每个月北极海冰的最低冰面。每次波动的峰值代表冬季最低冰面(通常在 3 月最高)。每次波动的低谷代表夏季最低冰面(通常在 9 月最低)。黑线是年度移动平均值,它滤除了季节性“噪声”,显示了 44 年期间海冰面积的整体趋势。
由于我们使用的是每月记录的最低值,而不是更典型的月平均值或中位数值,因此这个图可能与您在网上找到的其他图有所不同。
结果
在 2009 年后的七年里,北极海并未变成无冰状态,尽管在 2012 年 9 月 16 日达到了 334 万平方公里的新低。这比 1981 年 9 月的低点 690 万平方公里以及 2009 年的低点 500 万平方公里要低得多。实际上,从 2009 年到 2012 年,冰面面积有一个稳定的下降趋势。
2012 年之后,数值似乎有所稳定,到 2021 年夏季,移动平均曲线实际上在上升。
虽然阿尔·戈尔并没有“说对”,但他的声明确实留出了实际结果的空间。他引用的模型包括了 25% 的可能性,即部分冰层将在北极海过夏季时依然存在。
我们应该从中学到的一点是,地球气候是一个复杂的系统。关于其行为的具体和短期预测应非常谨慎。虽然制造紧迫感可能很重要,但这很容易适得其反,导致嘲笑和信誉下降。
引用
[1] Fetterer, F., K. Knowles, W. N. Meier, M. Savoie, 和 A. K. Windnagel. Sea Ice Index, Version 3. 2017,由国家雪冰数据中心分发。 doi.org/10.7265/N5K072F8
。访问日期 06–18–2022。
这个项目的具体数据来源网站是 nsidc.org/data/nsidc-0081.html
和 nsidc.org/data/nsidc-0051.html
(数据范围:1978 年 10 月至 2022 年 6 月)。
关于数据的更多详细信息列在下面的表格中:
NSIDC 海冰指数 v3 数据表(由科罗拉多大学博尔德分校国家雪冰数据中心提供)
根据 NSIDC 的引用政策,除非特别说明使用限制,否则可以下载和使用 NSIDC 网站上的照片、图像或文本。卫星图像可以免费下载并使用,但需正确注明来源。除非另有说明,照片和图像可以用于商业目的;但不能重新销售。
许多网站上的图像都附有标题和具体的版权信息。否则,通用的版权格式应为:“图像/照片由科罗拉多大学博尔德分校国家雪冰数据中心提供。”
谢谢!
感谢阅读,请关注我获取更多未来的快速成功数据科学项目。
分析在 Power BI 和 DAX 查询中聚合数据的性能
我们在 Power BI 中经常聚合数据。有时我们需要手动查询数据模型,或者在度量中需要中间表。让我们看看如何做到这一点。
·发布于 Towards Data Science ·8 分钟阅读·2023 年 5 月 1 日
–
Isaac Smith 拍摄的照片,来源于 Unsplash
介绍
你是否曾经问过自己:
Power BI 视觉效果背后发生了什么?
或者
我如何编写查询以获得在 Power BI 视觉效果中显示的结果?
好吧,你可以用性能分析器捕捉查询,并将查询复制到文本编辑器中,或者更好地使用 DAX Studio。
但你是否理解查询中发生了什么?
当你查看 DAX 的函数文档时,无论是在 Microsoft DAX 函数参考 还是在 DAX.Guide,你会发现至少有五个函数用于在查询中生成表:
在这篇文章中,我将通过基础查询来设定场景。然后,我将使用不同的函数从头重建查询,并观察这些函数之间的差异。
我将研究功能差异以及在效率和性能方面的差异。
基础查询
让我们从基础查询开始。
查看 Power BI 中的以下矩阵:
图 1 — 起始视觉效果(图由作者提供)
我用 Performance Analyzer 提取了查询,并在移除 Power BI 在计算国家和大陆级别的总数时需要的所有小计内容后,剩下的结果如下:
DEFINE
VAR __DS0FilterTable =
FILTER(
KEEPFILTERS(VALUES('Date'[YearIndex]))
,AND('Date'[YearIndex] >= -3, 'Date'[YearIndex] <= 0)
)
VAR __DS0Core =
SUMMARIZECOLUMNS(
'Geography'[ContinentName]
,'Geography'[RegionCountryName]
,__DS0FilterTable
,"Sum_Online_Sales", 'All Measures'[Sum Online Sales]
)
EVALUATE
__DS0Core
ORDER BY
'Geography'[ContinentName]
,'Geography'[RegionCountryName]
这里的关键函数是 SUMMARIZECOLUMNS()。
这个函数从两个列 [ContinentName] 和 [RegionCountryName] 中获取唯一值,并对每一行执行 Measure [Sum Online Sales],同时应用在 Variable __DS0FilterTable 中定义的过滤器。
在接下来的所有示例中,我将(尝试)保持 __DS0FilterTable 的定义,如上所示。
SELECTCOLUMNS() 和 ADDCOLUMNS()
使用 SELECTCOLUMNS(),我可以将计算列添加到输入表中,例如,通过 Measure。
输入表可以是一个现有的表或表函数的结果。
让我们尝试这个形式:
DEFINE
VAR __DS0FilterTable =
FILTER (
KEEPFILTERS ( VALUES ( 'Date'[YearIndex] ) ),
AND ( 'Date'[YearIndex] >= -3, 'Date'[YearIndex] <= 0 )
)
EVALUATE
SELECTCOLUMNS ('Geography'
,'Geography'[ContinentName]
,'Geography'[RegionCountryName]
,"Sum_Online_Sales", [Sum Online Sales]
)
这是这个查询的结果:
图 2 — ADDCOLUMNS() 的部分结果(作者提供的图)
如你所见,即使我仅为查询选择了两列,我仍然从地理表中获得了所有行,包括那些没有结果的行。
我想要其他东西。
另一个问题是,使用 SELECTCOLUMNS() 时,我不能引入上面的过滤器。
不管怎样,查看服务器时间,这个查询看起来还不错:
图 3 — ADDCOLUMNS() 的服务器时间(作者提供的图)
大部分工作在存储引擎中完成,并且并行度接近 7.5,非常出色。
当将结果复制到 Excel 时,我们可以毫无问题地删除空行。
SELECTCOLUMNS() 与 ADDCOLUMNS() 非常相似。
根据 DAX.guide,区别在于 SELECTCOLUMNS() 从一个空表开始,然后添加给定的列,而 ADDCOLUMNS() 从所有输入表列开始。
当我们尝试这个查询时:
EVALUATE
ADDCOLUMNS('Geography'
,"Sum_Online_Sales", [Sum Online Sales]
)
我们得到一个包含所有地理表列的表,并且对于每一行,计算 Measure 的结果。
我需要特定的函数来创建一个表,因为我只能定义一个输入表。
我将在本文后面回到这个问题。
SUMMARIZE()
函数 SUMMARIZE() 允许我获取一个总结给定列并添加计算列(例如,通过 Measure)的表。
基于上面的示例,查询将如下所示:
EVALUATE
SUMMARIZE( 'Geography'
,'Geography'[ContinentName]
,'Geography'[RegionCountryName]
,"Sum_Online_Sales", [Sum Online Sales]
)
再次,我们不能在这个查询中添加过滤器。
所以,我们将获取所有年份的结果:
图 4 — 使用 SUMMARIZE() 的结果(作者提供的图)
但再一次,我将得到所有国家的列表,包括那些没有值的。
服务器时间看起来也不错:
图 5 — SUMMARIZE() 的服务器时间(作者提供的图)
但在使用 SUMMARIZE() 函数时存在一些问题。
你可以在下面的参考部分找到描述这些问题的文章。
现在,我将向你展示如何使用正确的形式完成任务。
SUMMARIZECOLUMNS()
SUMMARIZECOLUMNS() 函数将 ADDCOLUMN() 和 SUMMARIZE() 的优点结合成一个强大的函数。
我可以将多个列传递给用作汇总列的函数并添加计算列。我也可以向该函数传递筛选器。
看看这个例子:
DEFINE
VAR __DS0FilterTable =
FILTER (
KEEPFILTERS ( VALUES ( 'Date'[YearIndex] ) ),
AND ( 'Date'[YearIndex] >= -3, 'Date'[YearIndex] <= 0 )
)
EVALUATE
SUMMARIZECOLUMNS(
'Geography'[ContinentName]
,'Geography'[RegionCountryName]
,__DS0FilterTable
,"Sum_Online_Sales", [Sum Online Sales]
)
ORDER BY 'Geography'[ContinentName]
,'Geography'[RegionCountryName]
当你查看本文开头的查询时,你会发现正是这个查询。
结果如下:
图 6 — 使用 SUMMARIZECOLUMNS() 的结果(作者提供的图)
这是我们期望的查询结果。
服务器时间令人印象深刻:
图 7 — SUMMARIZECOLUMNS() 的服务器时间(作者提供的图)
完成了,不是吗?
照片由 Zac Durant 提供,来源于 Unsplash
不,稍等一下。
如果我尝试在上述查询中添加筛选器以将数据限制为一年会怎么样?
结果是,我无法这样做,我只能将一个表作为筛选器传递给 SUMMARIZECOLUMNS()。
CALCULATETABLE()
CALCULATETABLE() 与其他三个函数不同。
我可以像使用 CALCULATE() 一样使用 CALCULATETABLE()。但我将表作为第一个参数,而不是聚合函数或其他度量。然后,我可以将筛选器作为附加参数添加。
那么,让我们尝试将上一个查询的结果限制为一年:
EVALUATE
CALCULATETABLE(SUMMARIZECOLUMNS(
'Geography'[ContinentName]
,'Geography'[RegionCountryName]
,"Sum_Online_Sales", [Sum Online Sales]
)
,'Date'[Year] = 2023
)
ORDER BY 'Geography'[ContinentName]
,'Geography'[RegionCountryName]
如你所见,我将 SUMMARIZECOLUMNS() 函数作为 CALCULATETABLE() 的输入,并在查询中添加了列筛选器。
结果如下:
图 8 — 使用 CALCULATETABLE() 的结果(作者提供的图)
服务器时间非常高效,仅需一个 SE 查询:
图 9 — CALCULATETABLE() 的服务器时间(作者提供的图)
CALCULATETABLE() 可以将整个 DAX 查询合并为一个 SE 查询,使其非常高效。
但不要指望 CALCULATETABLE() 总是能提高效率。稍后,我们将看到一个示例,其中这个函数没有达到相同的效果。
组合函数
生成所需结果的另一种方法是将 ADDCOLUMNS() 和 SUMMARIZE() 函数结合使用,如 SQLBI 发布的文章中所述(参见下面的参考部分)。
DEFINE
VAR __DS0FilterTable =
FILTER (
KEEPFILTERS ( VALUES ( 'Date'[YearIndex] ) ),
AND ( 'Date'[YearIndex] >= -3, 'Date'[YearIndex] <= 0 )
)
EVALUATE
ADDCOLUMNS(
SUMMARIZE('Geography'
,'Geography'[ContinentName]
,'Geography'[RegionCountryName]
)
,"Sum_Online_Sales", CALCULATE([Sum Online Sales]
,KEEPFILTERS(__DS0FilterTable)
)
)
ORDER BY 'Geography'[ContinentName]
,'Geography'[RegionCountryName]
请注意我如何将度量添加到结果中。我使用 CALCULATE 来包含使用 KEEPFILTERS() 的筛选表。必须这样做,否则结果将是错误的。
再次,请阅读下面的 SQLBI 文章,以准确解释为什么这样做是必要的。
结果中的值是正确的,但我们再次看到所有国家,而不仅仅是有结果的国家:
图 10 — 结合 ADDCOLUMNS 和 SUMMARIZE 的结果(作者提供的图)
当我们查看服务器时间时,我们会发现 DAX 需要三个 SE 查询来完成这个查询:
图 11 — 结合 ADDCOLUMNS() 和 SUMMARIZE() 的服务器时间(作者提供的图)
另一种方法是使用 CALCULATETABLE() 引入过滤器:
DEFINE
VAR __DS0FilterTable =
FILTER (
KEEPFILTERS ( VALUES ( 'Date'[YearIndex] ) ),
AND ( 'Date'[YearIndex] >= -3, 'Date'[YearIndex] <= 0 )
)
EVALUATE
CALCULATETABLE(
ADDCOLUMNS(
SUMMARIZE('Geography'
,'Geography'[ContinentName]
,'Geography'[RegionCountryName]
)
,"Sum_Online_Sales", [Sum Online Sales]
)
,__DS0FilterTable
)
ORDER BY 'Geography'[ContinentName]
,'Geography'[RegionCountryName]
结果仍然是相同的,服务器时间没有得到改善。
这证明了 CALCULATETABLE() 只是有时能提高效率。但它可以使查询更具可读性,而不是使用 KEEPFILTERS(),对于 KEEPFILTERS() 的所有效果我仍然难以理解。
结论
SELECTCOLUMNS()/ADDCOLUMNS() 是向表中添加计算列的良好起点。
但是我需要 SUMMARIZE()/SUMMARIZECOLUMNS() 仅对选定的列进行汇总,并能够向结果中添加计算列。
但是当我们想要向表表达式添加过滤器时,SUMMARIZE() 的能力有所减少。
在这种情况下,SUMMARIZECOLUMNS() 是正确的函数。
即使我需要 CALCULATETABLE() 来向查询添加某些类型的过滤器(例如,对单列的过滤器)。
在我的工作中,我总是需要编写查询来将结果与源系统中的数据进行比较,以验证结果。
通过查询和相应结果来记录验证要比截取 Power BI 中设置特定结果的所有过滤器的屏幕截图并从可视化中导出数据要容易得多。
当你希望自动生成一个报告并自动执行并发送给用户时,查询是非常有用的。
在某些情况下,编写查询比在 Power BI 中直接操作要更为合适。
我希望我能激发你探索 DAX 查询的可能性。
Casey Horner 在 Unsplash 提供的照片
参考文献
如果你想了解更多关于在 DAX Studio 中测量性能的内容,请阅读以下文章:
## 如何使用 DAX Studio 从 Power BI 获取性能数据
有时我们会遇到运行缓慢的报告,需要找出原因。我们将学习如何收集性能数据以及……
在 Articles — SQLBI 上,你可以找到关于这些函数的更多深入文章以及为什么要选择一个函数而不是另一个。
例如,SUMMARIZE() 函数的问题在这里有记录:
我使用 Contoso 示例数据集,就像我之前的文章中一样。你可以从 Microsoft 这里 免费下载 ContosoRetailDW 数据集。
Contoso 数据可以在 MIT 许可证下自由使用,如 此处 所述。
我扩大了数据集,以使 DAX 引擎更加高效地工作。
在线销售表包含 7100 万行(而不是 1260 万行),零售销售表包含 1850 万行(而不是 340 万行)。
[## 使用我的推荐链接加入 Medium - Salvatore Cagliari
阅读 Salvatore Cagliari 的每一个故事(以及 Medium 上成千上万其他作者的故事)。你的会员费用直接…
medium.com](https://medium.com/@salvatorecagliari/membership?source=post_page-----fc00027950a3--------------------------------)
用 E-utilities 和 Python 分析科学出版物
如何收集科学文献数据并发现趋势
·
关注 发布于 Towards Data Science · 15 分钟阅读 · 2023 年 5 月 19 日
–
图片来源:Alex Block 摄影于 Unsplash
跟上科学研究趋势对于任何科学家、科学作家或有志于创办初创企业的人都至关重要。谈到搜索生物医学科学文献,大多数人会转向 Google Scholar、PubMed 或他们喜欢的参考管理工具。正如你可能预期的,这些面向公众的搜索工具提供了易用性,但牺牲了效率、控制和可扩展性。因此,它们通常在数据科学中并不实用。相反,你将需要使用由国家生物技术信息中心(NCBI)提供的 E-utilities。[1] NCBI 提供的科学文章存储在 PubMed 数据库中,该数据库主要涵盖生命科学研究,但也包括一些化学和物理相关的期刊。[2]
对于数据科学应用,基于网页的搜索引擎和流行的参考管理工具无法满足需求。
关于如何高效使用 NCBI 进行数据科学的信息散布在各种政府和学术网站上。许多资源上传于 1990 年代,集中于遗传序列的搜索而非原始文献出版物,并且代码示例仅用 Perl 编写。曾经有一门关于使用 NCBI E-Utilities 的课程在 2000 年代初由 NIH 主办,Powerpoint“幻灯片集”以及必要的剪贴画插图可在此处获得。在本文中,我将传达我从这些各种来源中编纂的信息以及常规的反复试验中学到的经验。
注意: NCBI 中可搜索的数据类型很多,包括遗传序列、系统发育树、3-D 结构及其他信息。本文将专注于搜索原始 文献。
NCBI 可以回答哪些数据科学问题?
有许多种科学问题可以通过 NCBI 回答。例如,你可以创建一个作者提供的关键词的聚类图,以检查随时间变化的出版趋势,正如我在本文末尾演示的那样。此外,你还可以使用摘要和出版物中的文本构建 LLMs 和 NLP 模型,以帮助在文献之间建立联系。以下是我将在文章中覆盖的三个步骤:
(1) 查询数据库,
(2) 返回多个出版物的结果,以及
(3) 检索相似文章和文章的全文版本。
最后,我将提供一个完整的文档代码示例,展示如何对一个相对较大的语料库(约 10,000 篇文章)进行这三个步骤,并附上数据可视化。
查询 NCBI 数据库
要有效地查询 NCBI 数据库,你需要了解某些 E-utilities,定义你的搜索字段,并选择你的搜索参数——这些参数控制结果如何返回到你的浏览器中,或者在我们的案例中,我们将使用 Python 来查询数据库。
四个最有用的 E-utilities
NCBI 提供了九个 E-utilities,它们都实现为服务器端的快速 CGI 程序。这意味着你将通过创建以**.cgi结尾的 URL 来访问它们,并在问号后指定查询参数,参数之间用&符号分隔。除了EFetch之外,它们都会提供XML 或 JSON**输出。
**ESearch**
生成符合你搜索查询的 ID 号码列表
以下 E-Utilities 可以与一个或多个 ID 号码一起使用:
-
**ESummary**
期刊、作者列表、资助、日期、参考文献、出版类型 -
**EFetch**
仅 XML,提供**ESummary**
的所有内容,以及摘要、用于研究的资助列表、作者机构和 MeSH 关键词 -
**ELink**
提供与相关引文的链接列表,使用计算出的相似度分数同时提供已发布项目的链接 [你通向文章全文的门户]
NCBI 在其服务器上托管了 38 个数据库,这些数据库涉及各种数据,超出了文献引用的范围。要获取当前数据库的完整列表,你可以使用EInfo而无需搜索词:
[
eutils.ncbi.nlm.nih.gov/entrez/eutils/einfo.fcgi](https://eutils.ncbi.nlm.nih.gov/entrez/eutils/einfo.fcgi)
每个数据库在如何访问及返回的信息方面会有所不同。为了我们的目的,我们将重点关注pubmed和pmc数据库,因为这些是搜索和检索科学文献的地方。
了解搜索 NCBI 的两个最重要的方面是搜索字段和输出。搜索字段种类繁多,将取决于数据库。输出则更为直接,学习如何使用输出是至关重要的,特别是进行大规模搜索时。
搜索字段
你将无法真正利用 E-utilities 的潜力,如果你不了解可用的搜索字段。你可以在NLM 网站上找到这些搜索字段的完整列表及其描述,但要获取特定数据库的最准确搜索词列表,你需要使用这个链接解析自己的 XML 列表:
[
eutils.ncbi.nlm.nih.gov/entrez/eutils/einfo.fcgi?db=pubmed](https://eutils.ncbi.nlm.nih.gov/entrez/eutils/einfo.fcgi?db=pubmed)
db标志设置为数据库(本文将使用pubmed,但文献也可以通过pmc获取)。
用于查询 PubMed MEDLINE 记录的搜索字段列表。(来源:www.nlm.nih.gov/bsd/mms/medlineelements.html
)`
一个特别有用的搜索字段是 Medline 主题词 (MeSH)。索引专家维护 PubMed 数据库,并使用 MeSH 术语来反映期刊文章的主题。每篇被索引的出版物通常由 10 到 12 个由索引专家精心挑选的 MeSH 术语描述。如果未指定搜索词,则查询将针对数据库中所有可用的搜索词执行。
查询参数
每个 E-utility 都通过 URL 行接受多个查询参数,你可以用来控制从查询返回的输出类型和数量。这是你可以设置检索结果数量或搜索日期的地方。以下是一些更重要的参数列表:
数据库参数:
db
应设置为你感兴趣的数据库 — pubmed 或 pmc 用于科学文献
日期参数: 你可以通过使用搜索字段(例如 [pdat] 代表出版日期)来获得对日期的更多控制,但日期参数提供了一种更方便的方式来限制结果。
-
reldate
相对于当前日期的天数,设置 reldate=1 以查询最近的一天 -
mindate
和maxdate
根据格式 YYYY/MM/DD、YYYY 或 YYYY/MM 指定日期 (查询必须同时包含mindate
和maxdate
参数) -
datetype
设置按日期查询时的日期类型 — 选项包括 ‘mdat’(修改日期)、‘pdat’(出版日期)和 ‘edat’(Entrez 日期)
检索参数:
-
rettype
返回的信息类型(对于文献搜索,使用默认设置) -
retmode
输出格式(XML 为默认格式,虽然除 fetch 外的所有 E-utilities 都支持 JSON) -
retmax
是返回记录的最大数量 — 默认值为 20,最大值为 10,000(一万) -
retstart
给定查询的命中列表,retstart
指定索引(当搜索超出一万最大值时很有用) -
cmd
仅与**ELink**
相关,用于指定是否返回类似文章的 IDs 或全文的 URLs
使用 Python 执行查询并存储结果
一旦我们了解了 E-Utilities,选择了搜索字段,并决定了查询参数,我们就准备好执行查询并存储结果 — 甚至是多个页面的结果。
虽然你不需要专门使用 Python 来使用 E-utilities,但它确实使解析、存储和分析查询结果变得更加容易。下面是如何开始你的数据科学项目。
假设你要在 2022 年和 2023 年之间搜索“肌红蛋白”的 MeSH 术语。你现在将 retmax 设置为 50,但请记住 最大值为 10,000,查询速率为 3/s。
import urllib.request
search_url = f'http://eutils.ncbi.nlm.nih.gov/entrez//eutils/esearch.fcgi/' + \
f'?db=pubmed' + \
f'&term=myoglobin[mesh]' + \
f'&mindate=2022' + \
f'&maxdate=2023' + \
f'&retmode=json' + \
f'&retmax=50'
link_list = urllib.request.urlopen(search_url).read().decode('utf-8')
link_list
上述 esearch 查询的输出。
结果以 ID 列表的形式返回,可以在你查询的数据库中进行后续搜索。注意,“count”显示这个查询有 154 个结果,你可以用来获取某组搜索词的总出版物数。如果你想返回所有出版物的 ID,你需要将retmax参数设置为计数值或 154。一般来说,我将其设置为一个非常大的数字,以便能够检索所有结果并进行存储。
在 PubMed 中,布尔搜索很简单,只需在搜索词之间添加+OR+
、+NOT+
或+AND+
到网址中即可。例如:
[http://eutils.ncbi.nlm.nih.gov/entrez//eutils/esearch.fcgi/?db=pubmed&term=**CEO[cois]+OR+CTO[cois]+OR+CSO[cois]**&mindate=2022&maxdate=2023&retmax=10000](http://eutils.ncbi.nlm.nih.gov/entrez//eutils/esearch.fcgi/?db=pubmed&term=CEO%5Bcois%5D+OR+CTO%5Bcois%5D+OR+CSO%5Bcois%5D&mindate=2022&maxdate=2023&retmax=10000)
这些搜索字符串可以使用 Python 构造。在接下来的步骤中,我们将使用 Python 的json 包解析结果,以获取每篇返回出版物的 ID。这些 ID 可以用于创建一个字符串——这个 ID 字符串可以被其他 E-Utilities 使用,以返回关于出版物的信息。
使用 ESummary 来返回关于出版物的信息。
ESummary的目的是返回你可能期望在论文引用中看到的数据(出版日期、页码、作者等)。一旦你获得了ESearch形式的 ID 列表(在上一步中),你可以将这个列表拼接成一个长网址。
URL 的限制是 2048 个字符,每篇出版物的 ID 长度为 8 个字符,因此,为了安全起见,如果你的 ID 列表超过 250 个,你应该将链接列表分成 250 个一批。有关示例,请参见文章底部的笔记本。
ESummary的结果以 JSON 格式返回,并可能包括链接到论文全文的链接。
import json
result = json.loads( link_list )
id_list = ','.join( result['esearchresult']['idlist'] )
summary_url = f'http://eutils.ncbi.nlm.nih.gov/entrez//eutils/esummary.fcgi?db=pubmed&id={id_list}&retmode=json'
summary_list = urllib.request.urlopen(summary_url).read().decode('utf-8')
我们可以再次使用 json 来解析summary_list。使用 json 包时,可以通过 summary[‘result’][id as string]来浏览每篇文章的字段,如下例所示:
summary = json.loads( summary_list )
summary['result']['37047528']
我们可以创建一个数据框架来捕捉每篇文章的 ID,以及期刊名称、出版日期、文章标题、检索全文的网址以及第一作者和最后一作者。
uid = [ x for x in summary['result'] if x != 'uids' ]
journals = [ summary['result'][x]['fulljournalname'] for x in summary['result'] if x != 'uids' ]
titles = [ summary['result'][x]['title'] for x in summary['result'] if x != 'uids' ]
first_authors = [ summary['result'][x]['sortfirstauthor'] for x in summary['result'] if x != 'uids' ]
last_authors = [ summary['result'][x]['lastauthor'] for x in summary['result'] if x != 'uids' ]
links = [ summary['result'][x]['elocationid'] for x in summary['result'] if x != 'uids' ]
pubdates = [ summary['result'][x]['pubdate'] for x in summary['result'] if x != 'uids' ]
links = [ re.sub('doi:\s','http://dx.doi.org/',x) for x in links ]
results_df = pd.DataFrame( {'ID':uid,'Journal':journals,'PublicationDate':pubdates,'Title':titles,'URL':links,'FirstAuthor':first_authors,'LastAuthor':last_authors} )
以下是ESummary返回的所有不同字段的列表,以便你可以创建自己的数据库:
'**uid**','**pubdate**','**epubdate**','**source**','**authors**','**lastauthor**','**title**',
'**sorttitle**','**volume**','**issue**','**pages**','**lang**','**nlmuniqueid**','**issn**',
'**essn**','**pubtype**','**recordstatus**','**pubstatus**','**articleids**','**history**',
'**references**','**attributes**','**pmcrefcount**','**fulljournalname**','**elocationid**',
'**doctype**','**srccontriblist**','**booktitle**','**medium**','**edition**',
'**publisherlocation**','**publishername**','**srcdate**','**reportnumber**',
'**availablefromurl**','**locationlabel**','**doccontriblist**','**docdate**',
'**bookname**','**chapter**','**sortpubdate**','**sortfirstauthor**','**vernaculartitle**'
当你需要摘要、关键词和其他细节(仅限 XML 输出)时,请使用 EFetch。
我们可以使用EFetch返回类似于ESummary的字段,但有个警告是结果仅以 XML 格式返回。EFetch中还有几个有趣的附加字段,包括:摘要、作者选择的关键词、Medline 子标题(MeSH 术语)、资助研究的拨款、冲突声明、研究中使用的化学品列表,以及论文引用的所有参考文献的完整列表。以下是如何使用 BeautifulSoup 获取这些项目的一部分:
from bs4 import BeautifulSoup
import lxml
import pandas as pd
abstract_url = f'http://eutils.ncbi.nlm.nih.gov/entrez//eutils/efetch.fcgi?db=pubmed&id={id_list}'
abstract_ = urllib.request.urlopen(abstract_url).read().decode('utf-8')
abstract_bs = BeautifulSoup(abstract_,features="xml")
articles_iterable = abstract_bs.find_all('PubmedArticle')
# Abstracts
abstract_texts = [ x.find('AbstractText').text for x in articles_iterable ]
# Conflict of Interest statements
coi_texts = [ x.find('CoiStatement').text if x.find('CoiStatement') is not None else '' for x in articles_iterable ]
# MeSH terms
meshheadings_all = list()
for article in articles_iterable:
result = article.find('MeshHeadingList').find_all('MeshHeading')
meshheadings_all.append( [ x.text for x in result ] )
# ReferenceList
references_all = list()
for article in articles_:
if article.find('ReferenceList') is not None:
result = article.find('ReferenceList').find_all('Citation')
references_all.append( [ x.text for x in result ] )
else:
references_all.append( [] )
results_table = pd.DataFrame( {'COI':coi_texts, 'Abstract':abstract_texts, 'MeSH_Terms':meshheadings_all, 'References':references_all} )
现在我们可以使用这个表格来搜索摘要、冲突声明,或使用 MeSH 主题和参考列表制作连接不同研究领域的可视化图表。还有许多其他标签你可以探索,由EFetch返回,以下是如何使用 BeautifulSoup 查看所有这些标签:
efetch_url = f'http://eutils.ncbi.nlm.nih.gov/entrez//eutils/efetch.fcgi?db=pubmed&id={id_list}'
efetch_result = urllib.request.urlopen( efetch_url ).read().decode('utf-8')
efetch_bs = BeautifulSoup(efetch_result,features="xml")
tags = efetch_bs.find_all()
for tag in tags:
print(tag)
使用 ELink 检索相似出版物和全文链接
你可能想找一些与你的搜索查询返回的文章相似的文章。这些文章根据使用概率主题模型的相似度评分进行分组。[5] 要检索给定 ID 的相似度评分,你必须在调用ELink时传递cmd=neighbor_score。以下是一个文章的示例:
import urllib.request
import json
id_ = '37055458'
elink_url = f'http://eutils.ncbi.nlm.nih.gov/entrez//eutils/elink.fcgi?db=pubmed&id={id_}&retmode=json&cmd=neighbor_score'
elinks = urllib.request.urlopen(elink_url).read().decode('utf-8')
elinks_json = json.loads( elinks )
ids_=[];score_=[];
all_links = elinks_json['linksets'][0]['linksetdbs'][0]['links']
for link in all_links:
[ (ids_.append( link['id'] ),score_.append( link['score'] )) for id,s in link.items() ]
pd.DataFrame( {'id':ids_,'score':score_} ).drop_duplicates(['id','score'])
ELink的另一个功能是根据文章的 ID 提供全文链接,如果你将cmd=prlinks传递给ELink,则可以返回这些链接。
如果你只希望访问那些对公众免费的全文链接,你需要使用包含“pmc”(PubMed Central)的链接。访问付费墙后的文章可能需要通过大学订阅——在通过付费墙下载大量全文文章之前,你应该咨询你所在组织的图书管理员。
下面是一个如何检索出版物链接的代码片段:
id_ = '37055458'
elink_url = f'http://eutils.ncbi.nlm.nih.gov/entrez//eutils/elink.fcgi?db=pubmed&id={id_}&retmode=json&cmd=prlinks'
elinks = urllib.request.urlopen(elink_url).read().decode('utf-8')
elinks_json = json.loads( elinks )
[ x['url']['value'] for x in elinks_json['linksets'][0]['idurllist'][0]['objurls'] ]
你还可以在一次调用ELink时检索多个出版物的链接,如下所示:
id_list = '37055458,574140'
elink_url = f'http://eutils.ncbi.nlm.nih.gov/entrez//eutils/elink.fcgi?db=pubmed&id={id_list}&retmode=json&cmd=prlinks'
elinks = urllib.request.urlopen(elink_url).read().decode('utf-8')
elinks_json = json.loads( elinks )
elinks_json
urls_ = elinks_json['linksets'][0]['idurllist']
for url_ in urls_:
[ print( url_['id'], x['url']['value'] ) for x in url_['objurls'] ]
示例数据可视化:来自 C-suite 作者的科学出版物
有时,科学出版物的作者可能是公司的 CEO、CSO 或 CTO。利用 PubMed,我们可以分析最新的生命科学行业趋势。冲突声明(在 2017 年作为搜索词引入 PubMed)提供了一个视角,了解哪些作者提供的关键词出现在声明了 C-suite 执行官身份的出版物中。换句话说,就是作者用来描述其发现的关键词。要实现这个功能,只需在 URL 中包含CEO[cois]+OR+CSO[cois]+OR+CTO[cois]作为搜索词,检索所有返回的结果,然后从每个出版物的 XML 输出中提取关键词。每个出版物包含 4 到 8 个关键词。一旦生成语料库,你可以按每年指定关键词的出版物数量除以该年的出版物总数**来量化关键词的频率。
例如,如果 10 篇出版物列出了关键词“癌症”,而该年共有 1000 篇出版物,那么关键词频率将是 0.001。使用 seaborn clustermap 模块和关键词频率,你可以生成一个可视化图,其中较深的色带表示关键词频率/年值较大(我已经从可视化中去掉了 COVID-19 和 SARS-COV-2,因为它们的频率远大于 0.05,正如预期的那样)。每年,大约有 1000 到 1500 篇论文被检索出来。
使用 Seaborn 的 clustermap 模块生成的,列出 C-suite 作者的出版物的作者指定关键词频率的热图。
从这个可视化中,几个关于列出 C-suite 作者的出版物的数据洞察变得清晰。首先,一个最为明显的簇(位于底部)包含了在过去五年中在数据集中强烈出现的关键词:癌症、机器学习、生物标志物、人工智能——仅举几个例子。显然,行业在这些领域非常活跃并进行大量出版。第二个簇,位于图中部附近,展示了自 2018 年后从数据集中消失的关键词,包括身体活动、公共卫生、儿童、质谱和 mhealth(或移动健康)。这并不是说这些领域在行业中没有发展,只是出版活动有所减缓。查看图的右下角,你可以提取出最近在数据集中出现的术语,包括液体活检和精准医学——这些确实是目前医学领域的两个非常“热门”的领域。通过进一步检查出版物,你可以提取出公司的名称和其他感兴趣的信息。下面是我编写的生成此可视化的代码:
import pandas as pd
import time
from bs4 import BeautifulSoup
import seaborn as sns
from matplotlib import pyplot as plt
import itertools
from collections import Counter
from numpy import array_split
from urllib.request import urlopen
class Searcher:
# Any instance of searcher will search for the terms and return the number of results on a per year basis #
def __init__(self, start_, end_, term_, **kwargs):
self.raw_ = input
self.name_ = 'searcher'
self.description_ = 'searcher'
self.duration_ = end_ - start_
self.start_ = start_
self.end_ = end_
self.term_ = term_
self.search_results = list()
self.count_by_year = list()
self.options = list()
# Parse keyword arguments
if 'count' in kwargs and kwargs['count'] == 1:
self.options = 'rettype=count'
if 'retmax' in kwargs:
self.options = f'retmax={kwargs["retmax"]}'
if 'run' in kwargs and kwargs['run'] == 1:
self.do_search()
self.parse_results()
def do_search(self):
datestr_ = [self.start_ + x for x in range(self.duration_)]
options = "".join(self.options)
for year in datestr_:
this_url = f'http://eutils.ncbi.nlm.nih.gov/entrez//eutils/esearch.fcgi/' + \
f'?db=pubmed&term={self.term_}' + \
f'&mindate={year}&maxdate={year + 1}&{options}'
print(this_url)
self.search_results.append(
urlopen(this_url).read().decode('utf-8'))
time.sleep(.33)
def parse_results(self):
for result in self.search_results:
xml_ = BeautifulSoup(result, features="xml")
self.count_by_year.append(xml_.find('Count').text)
self.ids = [id.text for id in xml_.find_all('Id')]
def __repr__(self):
return repr(f'Search PubMed from {self.start_} to {self.end_} with search terms {self.term_}')
def __str__(self):
return self.description
# Create a list which will contain searchers, that retrieve results for each of the search queries
searchers = list()
searchers.append(Searcher(2022, 2023, 'CEO[cois]+OR+CTO[cois]+OR+CSO[cois]', run=1, retmax=10000))
searchers.append(Searcher(2021, 2022, 'CEO[cois]+OR+CTO[cois]+OR+CSO[cois]', run=1, retmax=10000))
searchers.append(Searcher(2020, 2021, 'CEO[cois]+OR+CTO[cois]+OR+CSO[cois]', run=1, retmax=10000))
searchers.append(Searcher(2019, 2020, 'CEO[cois]+OR+CTO[cois]+OR+CSO[cois]', run=1, retmax=10000))
searchers.append(Searcher(2018, 2019, 'CEO[cois]+OR+CTO[cois]+OR+CSO[cois]', run=1, retmax=10000))
# Create a dictionary to store keywords for all articles from a particular year
keywords_dict = dict()
# Each searcher obtained results for a particular start and end year
# Iterate over searchers
for this_search in searchers:
# Split the results from one search into batches for URL formatting
chunk_size = 200
batches = array_split(this_search.ids, len(this_search.ids) // chunk_size + 1)
# Create a dict key for this searcher object based on the years of coverage
this_dict_key = f'{this_search.start_}to{this_search.end_}'
# Each value in the dictionary will be a list that gets appended with keywords for each article
keywords_all = list()
for this_batch in batches:
ids_ = ','.join(this_batch)
# Pull down the website containing XML for all the results in a batch
abstract_url = f'http://eutils.ncbi.nlm.nih.gov/entrez//eutils/efetch.fcgi?db=pubmed&id={ids_}'
abstract_ = urlopen(abstract_url).read().decode('utf-8')
abstract_bs = BeautifulSoup(abstract_, features="xml")
articles_iterable = abstract_bs.find_all('PubmedArticle')
# Iterate over all of the articles from the website
for article in articles_iterable:
result = article.find_all('Keyword')
if result is not None:
keywords_all.append([x.text for x in result])
else:
keywords_all.append([])
# Take a break between batches!
time.sleep(1)
# Once all the keywords are assembled for a searcher, add them to the dictionary
keywords_dict[this_dict_key] = keywords_all
# Print the key once it's been dumped to the pickle
print(this_dict_key)
# Limit to words that appeared approx five times or more in any given year
mapping_ = {'2018to2019':2018,'2019to2020':2019,'2020to2021':2020,'2021to2022':2021,'2022to2023':2022}
global_word_list = list()
for key_,value_ in keywords_dict.items():
Ntitles = len( value_ )
flattened_list = list( itertools.chain(*value_) )
flattened_list = [ x.lower() for x in flattened_list ]
counter_ = Counter( flattened_list )
words_this_year = [ ( item , frequency/Ntitles , mapping_[key_] ) for item, frequency in counter_.items() if frequency/Ntitles >= .005 ]
global_word_list.extend(words_this_year)
# Plot results as clustermap
global_word_df = pd.DataFrame(global_word_list)
global_word_df.columns = ['word', 'frequency', 'year']
pivot_df = global_word_df.loc[:, ['word', 'year', 'frequency']].pivot(index="word", columns="year",
values="frequency").fillna(0)
pivot_df.drop('covid-19', axis=0, inplace=True)
pivot_df.drop('sars-cov-2', axis=0, inplace=True)
sns.set(font_scale=0.7)
plt.figure(figsize=(22, 2))
res = sns.clustermap(pivot_df, col_cluster=False, yticklabels=True, cbar=True)
结论
阅读完这篇文章后,你应该能够从制定高度定制的科学文献搜索查询,到生成数据可视化以进行更深入的审查。虽然有其他更复杂的方法可以使用各种 E-utilities 的附加功能来访问和存储文章,但我尝试展示了最简单的一组操作,应该适用于对科学出版趋势感兴趣的数据科学家。通过熟悉我所呈现的 E-utilities,你将能更好地理解科学文献中的趋势和联系。如前所述,通过掌握 E-utilities 及其在 NCBI 数据库广阔宇宙中的操作,还有许多其他项目可以解锁。
要了解更多关于访问 NCBI 的信息,你可以下载 2008 年之前举办的一系列 NIH 课程的材料或查看下面的参考资料。要保持对 E-Utilities 的更新,包括可能的新 API,你可能想要订阅 NCBI 那看起来非常 90 年代的邮件列表,请访问这个网站。最后,在 arxiv.org 上搜索“PubMed”将返回一些结果,这些结果可以激发利用这些数据的研究项目。
参考文献
[2] Chapman D. PubMed 的高级搜索功能。J Can Acad Child Adolesc Psychiatry. 2009 年 2 月;18(1):58–9. PMID: 19270851; PMCID: PMC2651214.
[3] Sayers E. E-utilities 深入探讨:参数、语法及更多内容。2009 年 5 月 29 日 [更新于 2022 年 11 月 30 日]。见:Entrez 编程实用工具帮助 [互联网]。Bethesda (MD): 国家生物技术信息中心 (US); 2010-. 可从 www.ncbi.nlm.nih.gov/books/NBK25499/
获得
[4] ftp.ncbi.nlm.nih.gov/pub/PowerTools/eutils/Oct.2007/slides/Lecture1.pdf
[5] Lin J, Wilbur WJ. PubMed 相关文献:基于主题的内容相似性概率模型。BMC Bioinformatics. 2007 年 10 月 30 日;8:423. doi: 10.1186/1471–2105–8–423. PMID: 17971238; PMCID: PMC2212667. 可从 pubmed.ncbi.nlm.nih.gov/17971238/
获得
[6] library.mskcc.org/blog/2019/07/conflict-of-interest-statement-field-in-pubmed/
使用自然语言处理和知识图谱分析您的网站
结合各种自然语言处理技术,构建一个表示您网站的知识图谱
·
关注 发表在 Towards Data Science · 14 分钟阅读 · 2023 年 1 月 5 日
–
网站反映了公司。大多数情况下,它用于向用户提供各种产品和服务的信息并推动销售。然而,网站随着时间的推移而增长和变化,许多小的和大的变化被引入。因此,网站最终可能会变得混乱,无法实现其最初的使命。因此,定期评估网站的结构和内容以使其尽可能优化是有意义的。优化网站是一个庞大的业务,因此有多种商业工具可以帮助你进行 SEO 和其他建议。然而,我将向你展示如何通过一点编码知识创建网站内容的全面而详细的表示,从而允许你分析和改进它。
你可以使用任何现有的网络爬虫工具提取网站的结构。此外,利用各种自然语言处理技术评估网站的内容也是有意义的。由于大多数网站是受版权保护的,我决定在本教程中使用 Neo4j 文档网站作为示例。文档网站的内容在 CC 4.0 许可证 下提供。不过,你可以将类似的工作流程应用于任何你希望的网页。
从文档中提取信息以构建知识图谱。图像由作者提供。
这可能看起来有点神奇(如果你忽略我的箭头),如何利用你网站上的信息构建知识图谱。在这篇文章中,我旨在为信息提取带来更多的清晰度,并为你提供可以自己使用的工具。我曾经使用类似的方法处理过 医疗文档、新闻 或甚至 加密货币报告,现在我们将利用 NLP 和知识图谱来分析一个网站。
数据收集和建模工作流程
数据收集和建模工作流程。图像由作者提供。
数据收集和预处理分为三个部分。
-
网络爬虫:一个 Python 脚本,用于遍历文档网页并收集链接和文本
-
NLP 流程:从文本中提取关键词并计算文本嵌入,以检测相似/重复内容
-
知识图谱:将结果存储为知识图谱以进行进一步分析
数据收集和预处理的代码可在 GitHub 上的 Jupyter 笔记本 中找到。
你不需要自己运行数据收集和处理,因为这需要几个小时。我已经准备了一个 Neo4j dump ,如果你想跟随文章中的分析,可以使用这个数据。
网页抓取器
我通常使用Python Selenium进行网页抓取,但你可以使用任何其他库或语言来从网站中提取相关信息。我不会详细介绍代码,因为这篇文章的目标不是教你如何抓取网站。然而,你可以查看处理网页抓取的Jupyter notebook。
对于 Neo4j 文档网站,我避免抓取左侧和顶部导航栏的链接,因为这会在图中引入大量噪音,因为大多数页面都有相同的导航栏。
在抓取过程中会忽略导航栏中的链接。图片由作者提供。
在 Neo4j 文档网站中,我希望捕捉到用户如何在不使用导航栏的情况下遍历文档。否则,我们将引入噪音,因为所有页面都会链接到导航栏中的相同页面。此外,我专注于从文档网页中提取文本和链接,因此一些产品或营销页面的内容没有被抓取。
自然语言处理
自然语言处理步骤包括提取关键词和计算文本嵌入,以检测相似和重复的内容。在考虑训练自己的 NLP 模型之前,查看HuggingFace 模型库是否有任何公开可用的模型适合你的用例总是一个好主意。
经过一些研究,我发现了一个由Yankı Ekin Yüksel提供的关键词提取模型。我非常喜欢使用 transformers 和 HuggingFace 加载和运行模型的简单性。
以下代码加载关键词提取模型并准备一个 NLP 流水线。
tokenizer = AutoTokenizer.from_pretrained("yanekyuk/bert-uncased-keyword-extractor")
model = AutoModelForTokenClassification.from_pretrained(
"yanekyuk/bert-uncased-keyword-extractor"
)
nlp = pipeline("ner", model=model, tokenizer=tokenizer)
你不需要下载模型或担心文件路径。相反,你可以简单地将模型名称定义为 tokenizer 和模型的参数,transformers 库会为你完成所有工作。
流水线返回的令牌不一定是一个单词。因此,我们需要在 NLP 流水线完成后从令牌构建回单词。
def extract_keywords(text):
"""
Extract keywords and construct them back from tokens
"""
result = list()
keyword = ""
for token in nlp(text):
if token['entity'] == 'I-KEY':
keyword += token['word'][2:] if \
token['word'].startswith("##") else f" {token['word']}"
else:
if keyword:
result.append(keyword)
keyword = token['word']
# Add the last keyword
result.append(keyword)
return list(set(result))
extract_keywords("""
Broadcom agreed to acquire cloud computing company VMware in a $61 billion (€57bn) cash-and stock deal.
""") # ['cloud computing', 'vmware', 'broadcom']
这个示例展示了模型从给定文本中提取了云计算、vmware和broadcom。这些结果似乎非常适合我们的用例,因为我们正在分析 Neo4j 文档,其中应该包含许多技术关键词。
接下来,我们还需要计算文本嵌入,以帮助我们识别相似和重复的内容。我在 HuggingFace 模型库中搜索了一下,发现了一个可以用来识别相似句子或段落的句子变换模型。此外,该模型只需三行代码即可加载和使用。
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
def generate_embeddings(text):
embeddings = model.encode(text)
return [float(x) for x in embeddings.tolist()]
我们需要将结果转换为浮点数列表,因为 Neo4j Driver 不支持 NumPy 数组。
知识图谱构建
在完成网页抓取和自然语言处理步骤后,我们可以继续构建知识图谱。你可能已经猜到,我们将使用 Neo4j 来存储我们的知识图谱。你可以使用免费云实例或设置本地环境。
初始导入后的图形模式定义如下。
初始图形模式。图像由作者提供。
在我们图表的中心是网页。我们知道它们的 URL 地址、文本嵌入值,以及网页抓取工具是否提取了页面文本。页面还可以链接或重定向到其他页面,这通过相应的关系表示。作为 NLP 工作流的一部分,我们还检测了网站上的关键词,并将其作为单独的节点存储。
如果你对数据导入的代码实现感兴趣,可以查看预处理笔记本。否则,我们将直接进入网络分析部分。
网络分析
我准备了一个 Neo4j 数据库转储 ,如果你不想抓取 Neo4j 文档但仍想跟随网络分析示例,可以使用这个转储。
Neo4j 文档网站知识图谱的样本子图。图像由作者提供。
我将带你了解一些我认为有趣的网站网络分析示例。我们将使用图数据科学 Python客户端,这是一个理想的工具,可以用来从 Python 进行 Neo4j 网络分析。
包含所有相关网络分析代码的 Jupyter Notebook可在 GitHub 上获得。
总体统计数据
首先,我们将通过使用apoc.meta.stats
过程来评估数据集的大小,统计节点和关系的数量。
gds.run_cypher("""
CALL apoc.meta.stats()
YIELD labels, relTypesCount
""")
我们的知识图谱包含 15370 页和 4199 个关键字,还有 62365 个链接和 723 个重定向。我没有预料到这么多页面。然而,考虑到文档涵盖了多个产品的多个版本,网站上的页面数量激增是有意义的。此外,许多链接指向 Neo4j 网站外的页面。
接下来,我们将评估从多少页面成功检索到内容信息。
gds.run_cypher("""
MATCH (p:Page)
RETURN p.has_text AS has_text,
count(*) AS count
""")
我们已经成功检索了 9688 个网页的内容并计算了文本嵌入。网络爬虫专注于从文档网站中恢复内容,同时大多忽略了产品和类似页面的结构和文本。因此,Neo4j 网站上有 2972 个我们尚未检索到内容的页面。最后,Neo4j 网站链接到其主要域外的 2710 个页面。在网络爬虫过程中,Neo4j 文档网站外的页面被明确忽略。
出于好奇,我们可以列出 Neo4j 最常链接的十个随机外部网页。
gds.run_cypher("""
MATCH (p:Page)
WHERE p.has_text IS NULL
RETURN p.url AS page,
count{(p)<-[:LINKS_TO|REDIRECTS]-()} AS links
ORDER BY links DESC
LIMIT 5
""")
结果
最常链接的网页实际上是一个本地 URL,即 Neo4j 浏览器的默认地址。以下是一些 GitHub 上 APOC 发布的链接。最后,看起来 Neo4j 有一些支持与微软 NLP 和 AWS 云 API 集成的产品或服务,否则他们可能不会在文档中链接这些内容。
识别死链
我们将通过识别死链或损坏的链接来进行处理。损坏的链接是指指向不存在网页的链接。与大多数网站一样,Neo4j 文档有一个指定的 404 网页。网络爬虫将“404”文本值分配给任何响应 404 页面的 URL。
gds.run_cypher("""
MATCH (:Page)-[:LINKS_TO|REDIRECTS]->(:Page{is_404:true})
RETURN count(*) AS brokenLinkCount
""")
数据集中有 241 个损坏的链接。考虑到数据库中总共有 62 千个链接,损坏的链接数量听起来很少。然而,如果你在你的网站上执行了此分析,你可以将结果转发给相关团队以修复这些链接。
查找最短路径
大多数网站的设计旨在轻轻推动用户沿着路径到达最终目标。例如,如果你经营一个电子商务网站,那么目标可能是购买事件。借助 Neo4j,你可以分析用户可能遵循的所有路径,以到达期望的目的地。由于我们处理的是文档网站,我们无法探讨用户如何最终完成网站上的购买。然而,我们可以应用相同的技术,评估网站各部分之间的最短路径。
以下代码查找两个给定网页之间的所有最短路径。
gds.run_cypher("""
MATCH (start:Page {url:"https://neo4j.com/docs"}),
(end:Page {url:"https://console.neo4j.io"})
MATCH p=shortestPath((start)-[:LINKS_TO|REDIRECTS*..10]->(end))
RETURN [n in nodes(p) | n.url] AS path
""")
结果显示,用户必须遍历以下网页才能从文档主页到达 Aura 控制台页面:
将你的网站表示为知识图谱可以显著提高对网页设计流程的理解,从而帮助你优化它们。
链接分析
接下来,我们将使用中心性算法对网页的重要性进行排名。例如,假设我们简单地将网页的排名定义为传入链接的数量。在这种情况下,我们可以利用度中心性算法来对网页的重要性进行排名。
要执行图数据科学库中的任何图算法,我们首先需要在内存中投影一个图。
G, metadata = gds.graph.project('structure', 'Page',
['LINKS_TO', 'REDIRECTS'])
使用图数据科学库的投影功能,你可以选择要用图算法评估的知识图谱的特定子图。在这个例子中,我们选择了Page节点和LINKS_TO及REDIRECTS关系。为了简便起见,我们将链接和重定向视为相同。然而,进行更深入的网络分析时,我们可以定义一些权重,也许将重定向视为比链接更重要。
以下代码将计算传入度中心性,即网页所拥有的传入链接或重定向的数量。
df = gds.degree.stream(G, orientation="REVERSE")
df["url"] = [d["url"] for d in gds.util.asNodes(df["nodeId"].to_list())]
df.sort_values("score", ascending=False, inplace=True)
df.head()
需要注意的一点是,我们使用了图数据科学 Python 客户端与数据库进行交互,因此如果你习惯于 Cypher 过程调用,语法可能会略有不同。结果如下:
开发者知识库页面有 598 个传入链接。许多链接也指向开发者博客和图形摘要的特定标签。我认为,许多文档网站都指向可以在图形摘要的博客中找到的具体示例。如果我们要更好地理解预期流程,我们可以深入分析这些链接的来源等。
有时传入链接的数量不足以作为排名指标。Google 的创始人意识到这个问题,因此他们提出了最著名的图算法 PageRank,它考虑了传入链接的数量及其来源。例如,如果网页直接链接自主页或仅有少数人访问的外围文档页面,则有所不同。
以下代码将计算 PageRank 分数并将其与度数据框合并。
pr_df = gds.pageRank.stream(G)
pr_df["pagerank"] = pr_df.pop("score")
combined_df = df.merge(pr_df, on="nodeId")
combined_df.sort_values("pagerank", ascending=False, inplace=True)
现在我们可以根据 PageRank 分数检查前五个最重要的网页。
得分列表示传入链接和重定向的数量,而 PageRank 列则保存 PageRank 分数。
有趣的是,只有开发者知识库页面在使用 PageRank 而不是度中心性时保留了其位置。看来 GraphConnect 非常重要,因为它仍然是第二个最重要的网页。作为一名网页 UX 设计师,你可以利用这些信息尝试更改网站结构,以便最新的 GraphConnect 可能会更重要。记住,我们只是对这个网络分析进行了初步探索。然而,你可以找到有趣的模式,然后深入了解网页流程并进行优化。
关键字分析和共现主题聚类
在本次分析的最后部分,我们将查看关键字分析。
网页及其关键字的图形表示。图像由作者提供。
在正确的网页上拥有合适的关键字是搜索引擎优化的关键方面之一。通过检查页面上最频繁的关键字,我们可以获得页面的高层次概述。
gds.run_cypher("""
MATCH (k:Keyword)
RETURN k.name AS keyword,
count {(k)<-[:HAS_KEYWORD]-()} AS mentions
ORDER BY mentions DESC
LIMIT 5
""")
结果
结果看起来正是我们预期的。网页讨论了节点、neo4j、图表和 Java。不确定为什么会有剪贴板。也许文档中有很多“复制到剪贴板”的部分。
我们可以稍微深入,查看 URL 地址中包含“graph-data-science”的网页上最频繁的关键字。通过这种方式,我们主要筛选出 Neo4j 图数据科学库文档。
gds.run_cypher("""
MATCH (p:Page)-[:HAS_KEYWORD]->(k:Keyword)
WHERE p.url CONTAINS "graph-data-science"
RETURN k.name AS keyword,
count(*) AS mentions
ORDER BY mentions DESC
LIMIT 5
""")
结果
它看起来与整体关键字存在情况非常相似,只是“algorithm”这个关键字在这里出现得更频繁。现在,我们可以继续通过其他部分或单独页面深入挖掘关键字。知识图谱是进行深入分析或通过指定的用户流程分析关键字和内容分布的绝佳工具。此外,如果你使用了能够检测短尾和长尾关键字的 NLP 模型,这将大大有助于 SEO 分析和优化。
最后,我们还可以只用几行代码执行关键字共现聚类。关键字共现聚类可以理解为识别主题的任务,其中主题由文本语料库中频繁共现的多个关键字组成。
关键字共现主题聚类输出的示意图。图像由作者提供。
Neo4j 中关键字共现聚类或主题聚类的工作流程如下:
-
投影一个包含相关信息的内存图。
-
创建一个 CO_OCCUR 关系,以便在文本中常常一起出现的关键字之间建立联系。
-
运行像 Louvain 方法这样的社区检测算法来识别关键字的社区或集群。
我们将开始通过投影一个包含所有相关信息的内存图。我们需要投影 Page 和 Keyword 节点以及连接的 HAS_KEYWORD 关系。由于我们要检查共同出现的关键词集群,而不是相似网页的组,因此我们需要在图投影中反转关系的方向。
附注:如果你离开自然方向并按照示例进行操作,你将根据提到的关键词识别出相似网页的集群
G, metadata = gds.graph.project(
"keywords", ["Page", "Keyword"], {"HAS_KEYWORD": {"orientation": "REVERSE"}}
)
接下来,我们需要在经常一起出现的关键词之间创建CO_OCCUR关系。为了解决这个任务,我们将使用 Node Similarity 算法。Node Similarity 默认使用Jaccard 相似系数来计算两个节点之间的相似性。
在这个示例中,每个关键词都有一组包含它的网页。如果基于网页的关键词对之间的 Jaccard 系数大于 0.40,则会在它们之间创建一个新的CO_OCCUR关系。我们使用变异模式将算法的结果存储回内存中的投影图。
gds.nodeSimilarity.mutate(
G, mutateRelationshipType="CO_OCCUR", mutateProperty="score",
similarityCutoff=0.4
)
最后,我们将使用 Louvain 方法算法,一个社区检测算法,来识别关键词的集群。该算法输出每个节点及其社区 ID。因此,我们需要根据社区 ID 对结果进行分组,以创建形成主题或集群的关键词列表。
topic_df = gds.louvain.stream(G, nodeLabels=["Keyword"], relationshipTypes=["CO_OCCUR"])
topic_df["keyword"] = [
n["name"] for n in gds.util.asNodes(topic_df["nodeId"].to_list())
]
topic_df.groupby("communityId").agg(
{"keyword": ["size", list]}
).reset_index().sort_values([("keyword", "size")], ascending=False).head()
结果
由于我们遵循的主题聚类工作流程是一种无监督技术,我们需要手动分配总体主题名称。例如,我们可以观察到第一个最大主题包含 chewbacca、jedi、christmas day、independence day 等关键词。这是一个有趣的节日与星球大战的混合。我们可以探索为什么节日和星球大战混合在一起。此外,第二大主题似乎谈论了各种 panama 和 paradise papers 以及涉及的公司和人物。
总结
在我看来,知识图谱和自然语言处理技术是天作之合。如前所述,我见过类似的方法来分析医学文档、新闻甚至加密报告。这个想法是使用 NLP 和其他工具从非结构化数据中提取有价值的信息,然后用于构建知识图谱。知识图谱提供了一种友好且灵活的提取信息的结构,可用于支持各种分析工作流程。
本博客文章的所有代码都可以在GitHub上找到。
分析加州电动汽车的采纳率
使用 DMV 数据与 Pandas 和 GeoPandas
·
关注 发表在 Towards Data Science ·12 min read·2023 年 4 月 26 日
–
特斯拉(图片由 Matt Weissinger 提供,来源于 pexels.com)
加州正在推动积极的社会变革,朝着净零排放的未来迈进,而其中一个重要部分就是其公民用于日常生活的车辆。结合 通货膨胀减少法案(为新购电动车提供最高 $7,500 的税收抵免,并为二手电动车提供最高 $4,000 的税收抵免——具体取决于车辆组装地点和电池材料来源),加州还实施了 先进清洁汽车 II(ACC II)法规,要求汽车制造商到 2026 年销售中至少 35%必须是电动车。2026 年之后,该要求每年线性增加,直到 2035 年所有销售必须是电动车。
本分析专注于加州人在这些新激励措施下的电动车(EV)采纳率。我将 EV 采纳率定义为:
EV 采纳率 = (总电动车购买量)/(总车辆购买量)
在本分析中,我们将探讨加州是否在轨道上达到 2026 年 35% EV 采纳率的目标,并进一步分析地理层面和汽车制造商层面的进展。
重要说明: 由于 35%的要求最终是基于车辆销售,而 DMV 提供的是车辆注册数量(而非车辆销售数量),因此本分析通过注册数量来近似销售情况。具体而言,对于每年的 DMV 数据,我仅使用了注册日期前三年的车辆来近似为新车购买。
数据
-
DMV 车辆计数数据(包括按车型年份、汽车制造商、重量类别、燃料类型和邮政编码划分的车辆注册数量),涵盖 2018 年、2020 年、2021 年和 2022 年
地理数据:
过程
技术方法(GeoPandas 教程)
如果你只对结果感兴趣,可以跳过这一部分。
这个项目让我有机会学习如何使用 GeoPandas,一个用于数据分析项目的 Python 库,特别是有空间组件的数据。
使用 GeoPandas 的一般工作流程是将你想要绘制的数据(例如邮政编码中的车辆和电动车数量)与一个相关的几何(例如邮政编码的几何边界)连接在一个名为 GeoDataFrame 的结构中。GeoDataFrame 是 GeoPandas 的核心,是 Pandas DataFrame 对象的子类,并包含一个几何列。
对我而言,我有邮政编码级别的车辆计数,但我想在县级别绘制车辆计数。我从所需的库导入开始,并读取了邮政编码和县边界的 geojson 文件。
import geopandas as gpd
import matplotlib.pyplot as plt
zip_codes = gpd.read_file(zip_code_geojson_path)
counties = gpd.read_file(county_geojson_path)
GeoDataFrames 只能有一个“活动”几何列。无论哪个列是活动的,都将用于连接、绘图或其他应用。你可以使用 GeoDataFrame.set_geometry() 方法将几何设置为不同的列。此外,当两个 GeoDataFrames 连接时,一个活动几何列将被丢弃(因为 GeoDataFrame 只能有一个活动几何列)
由于我想将邮政编码和县的 GeoDataFrames 结合起来,但又保留两者的几何信息,我重命名了邮政编码几何列。我还制作了县几何列的副本。
重命名 zip_code 几何列
zip_codes.rename_geometry(‘zip_code_geometry’, inplace=True)
创建副本县几何列
counties[‘county_geometry’] = counties.geometry
由于一些邮政编码的边界与多个县的边界重叠,并且我只想为一个县分配一个邮政编码,我取了每个邮政编码边界的质心(即对象的几何中心),然后查看该邮政编码的质心是否位于一个县的边界内。实际上,我将每个邮政编码的整体形状简化为其中心点,然后确定给定邮政编码的中心点位于哪个县的范围内。
为此,我首先将每个 GeoDataFrame 的 CRS(坐标参考系统)从 4326(默认值)设置为 3857。这实际上将我们的坐标系统从地球模型转换为地图:
zip_codes.to_crs(3857, inplace=True)
counties.to_crs(3857, inplace=True)
然后,我计算了邮政编码的质心,并将这些质心设置为活动几何:
计算邮政编码质心
zip_codes[‘zip_code_centroid’] = zip_codes.centroid
将邮政编码的活动几何设置为质心列
zip_codes.set_geometry(‘zip_code_centroid’, inplace=True)
最后,我将两个 GeoDataFrames 连接起来:
zip_codes_with_county = gpd.sjoin(zip_codes, counties, how=‘inner’, predicate=‘intersects’)
一旦我有了一个包含邮政编码名称、县名称、邮政编码几何形状和县几何形状的 GeoDataFrame,我将按邮政编码将车辆计数和电动车计数合并到我的 GeoDataFrame 中,并将计数汇总到县级。这使我得到了一个包含 58 行(即加利福尼亚 58 个县)的 GeoDataFrame,其中包括县名称、县地理信息、车辆计数和电动车计数。非常适合绘图!
下面是绘图代码的一个示例。在其中,我还包含了一个额外的 GeoDataFrame,展示了加利福尼亚的一些城市,以作为我的图中的地标:
电动车采纳率 2022
fig, ax = plt.subplots(figsize = (10, 10))
county_gdf.plot(ax=ax,
column=’ev_rate_2022’,
legend=True,
legend_kwds={‘shrink’:0.5},
cmap = ‘Greens’)
city_gdf.plot(ax=ax,
color = ‘orange’,
markersize = 10)
for idx, row in city_gdf.iterrows():
plt.annotate(text=row[‘city’], xy=row[‘coords’], horizontalalignment=’center’, color=’Black’)
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
for edge in [‘right’, ‘bottom’, ‘top’,’left’]:
ax.spines[edge].set_visible(False)
ax.set_title(‘CA EV Adoption Rate (2022) by County’, size=18, weight=’bold’)
这段代码生成了下一节(结果)中的加利福尼亚首张地图。
结果
电动车采纳率(整体):
上面是一个图表,展示了 2018 年至 2022 年整个加利福尼亚州的电动车采纳率。电动车被定义为电池电动汽车、氢燃料电池汽车或插电式混合动力车。然而,电动车不包括混合动力汽油车(因为这些不符合税收优惠要求或汽车制造商规定)。例如,2019 年的丰田普锐斯将不算在内。
我们可以注意到,从 2021 年到 2022 年的销售出现了上升。这一增长从约 6.6%上升到 9.5%。这一增长似乎主要来自 ZEV 购买的增加。
如果我们假设 2021 年到 2022 年的线性外推会继续,那么到 2026 年我们可能达不到 35%的电动车采纳率。然而,DMV 数据反映的是每年年初的车辆计数,因此没有计算新激励结构推出后的时间段(2022 年 8 月)。上图中的蓝色圆圈对此进行了补偿。它展示了 2022 年全州的电动车采纳率,数据来自 energy.ca.gov。如果我们将蓝色圆圈包含在趋势中并进行线性外推,那么到 2026 年实现 35%的目标看起来是有可能的。
尽管如此,假设线性外推是一种过于简单化的处理方式,甚至可能不是趋势的正确形状。总体而言,很难预测未来 4 年将会怎样,但从 2021 年到 2022 年的增长是一个令人鼓舞的迹象,另外来自energy.ca.gov的数据点也是一个积极的信号。
电动汽车采纳率(县级):
我们还可以查看县级的电动汽车采纳率,以了解州内各区域购买电动汽车的趋势:
在上图中,我们可以看到湾区的电动汽车采纳率远远高于全州的其他地区。在州的其他部分,电动汽车采纳率通常在沿海县和湾区-太浩走廊沿线较高。具体来说,以下 5 个县的电动汽车采纳率最高:
有一种假设认为,湾区的电动汽车采纳率较高是因为它反映了居住在那里公民的财富和政治倾向(虽然这超出了本次分析的范围,我们可以利用最新人口普查数据和加州人在 2022 年对 30 号提案的投票情况进一步探讨这个问题)。
加利福尼亚州电动汽车采纳率最低的地区通常集中在州的东北部。这 5 个电动汽车采纳率最低的县是:
加利福尼亚州的东北地区人口稀少。这些地区可能有居民对电动汽车的采纳持保留态度,因为这些地区往往面临严酷的天气(或许居民认为电动汽车在这些条件下会比他们习惯的车辆功能下降)。此外,该州这部分地区的充电基础设施可能也很少。这里的大异类是帝国县,这是加利福尼亚州最东南的县。它的居民数量比其他县多,并且是沙漠地区(与红木覆盖的山区相对)。它也可能面临基础设施短缺的问题。虽然这超出了本次分析的范围,但我们可以通过查看电动汽车充电器位置数据来确定基础设施的缺乏是否与电动汽车采纳率相关。
如果我们对每个县 2021 年和 2022 年的电动汽车采纳率进行线性外推,我们可以估算出哪些县会在 2026 年达到目标,哪些县不会。
线性外推 — 基于 2021 年到 2022 年的全县增长数据
然而,这种推断不包括新激励结构后的电动汽车采用率。如果我们假设年增长率等于 2021 年到 2022 年全县平均增长率和 2022 年到 2023 年州级增长率的平均值,我们可以得出以下预测:
线性推断 —— 基于 2021 年到 2022 年全县平均增长和 2022 年到 2023 年州级增长的平均值
类似于我们之前看到的电动汽车采用率图表,电动汽车采用率较高的县通常是我预计将在 2026 年达到目标的县。如果我们考虑所有未预计达到目标的县,并计算它们预计的未达到目标的程度,按其各自的人口比重加权,我们可以确定哪些县最“重要”,需要专注于。这些县可以被认为是改进空间最大、对整个加州推向 2026 年目标最有帮助的地区。以下是最有机会的前 5 个县:
这些县大多位于州东南角和南中央谷地区。它们人口众多,电动汽车使用率较低。如果加州在 2026 年电动汽车采用率落后 35%,这个地区可能是一个重要原因。洛杉矶县特别显著,我预测它在 2026 年的电动汽车采用率为 33.7%(几乎达到 35%的目标,但还未完全达到),但由于人口众多,它出现在最重要县的前列。
值得注意的是,上述模型用于估计 2026 年电动汽车采用率非常简单,只是我们可以考虑预测未来电动汽车采用的一种方式。在今后的工作迭代中,我们可以增加更多复杂性以获得更准确的结果。
**电动汽车采用率(汽车制造商水平):
我们还可以按汽车制造商的水平查看数据,评估哪些汽车制造商正在朝着达到 2026 年目标的方向前进,哪些滞后。
重要提示: 我注意到 DMV 的数据中有大约 28%的电动汽车标记为“其他/未知”,其制造商不明确。我不确定这些电动汽车属于哪些组,但如果它们被正确分配,这些结果可能会有所不同。
如果我们纯粹看注册最多的电动汽车,我们可以看到以下情况:
2022 DMV 数据
我们可以看到特斯拉占据主导地位,丰田排名第二。此后,其他电动汽车销售商的长尾排列。大多数汽车制造商的电动汽车销售率在低个位数,少数豪华品牌略高。从这些数据可以清楚地看出,汽车制造商需要做大量工作才能在 2026 年实现 35%的电动汽车销售目标。
我与几位行业专业人士讨论了这个列表,他们指出缺少一些特定的汽车制造商,特别是日产、起亚和现代。我做了一些调查,发现日产特别在 2022 年注册了许多较旧的电动车(例如 2018 年或 2019 年生产的电动车),因此被我的规则过滤掉了,该规则只考虑了注册年份过去 3 年内生产的汽车。如果我多考虑一年,日产就被包含在了第 8 位的列表中。在这种情况下,本田也进入了列表。这项调整对现代和起亚的结果影响不大。这两家汽车制造商可能是上述“其他/未知”组的重要组成部分。
如果我们按照总体销售最多的车辆来排序数据,我们可以看到以下信息:
2022 年 DMV 数据
从这张图表可以看出,除了特斯拉之外,所有大型汽车制造商都有很多工作要做,以实现 35%的电动车销售目标。
IRS 更新 关于哪些车辆将符合 IRA 税收优惠的最新信息已于 2023 年 4 月 17 日公开。如果在可预见的未来此列表没有变化,税收优惠将主要惠及雪佛兰、福特、特斯拉、吉普和林肯,而对日产、沃尔沃、奥迪、现代、起亚、斯巴鲁、丰田和大众产生不利影响(尽管我看到关于大众 ID.4 是否符合条件的信息存在冲突)。这并不意外,因为 IRA 旨在刺激美国及其盟国的汽车制造业(从而促进汽车制造商的就业),并减少对中国等“外国关注实体”(FEC)的依赖。
我们还可以从县级别考察加利福尼亚州,看看每个县的主要电动车销售商:
特斯拉远远领先于其他品牌,丰田排在第二位。之后,是否有任何电动车销售商在特定地区具有明显优势尚不明确。
人均车辆数(县级别):
虽然这与电动车直接无关,我还查看了加利福尼亚州每个县的人均车辆数,并考察了该数据从 2018 年到 2022 年的趋势。随着我们向更可持续的社会过渡,我们不仅要向电动车转型,还需要在总体上减少个人车辆的使用,依赖公共交通、自行车和拼车。这部分分析研究了加利福尼亚州每个人的车辆数量,以及该数据在地区上的趋势是上升还是下降。
2022 年每人车辆数
2018 年到 2022 年每人车辆数的百分比变化
正如您在第一幅图中看到的那样,Sierra 山脉地区是拥有最高人均车辆的县,而旧金山县(虽然可能难以看清)和湾区则是最低的。一般来说,我们在中央谷地区看到的人均车辆较少,但这可能更多是因为较低的财富而不是湾区,而湾区则可能是因为较高的交通便利性。中央谷地区人均车辆较少也可能与车辆注册较少有关。
这幅图展示了过去 5 年各县每人拥有的车辆数量发生了多大变化。在大多数县,这个数量要么保持相对稳定,要么有所增加。在湾区,这个数量显著下降。我对此的一个假设是远程办公的出现。我猜想,由于许多科技行业的人(特别是在湾区集中的人群)因疫情开始远程办公,他们不需要通勤,因此对汽车的需求减少,因此他们卖掉了自己的车(尽管超出了本次分析的范围,二手车销售数据可能用于进一步探讨这一点)。
结论:
加利福尼亚州的电动车市场格局复杂而广泛,我们在这里只是初步探索了一部分。总体而言,看起来加州人在 2026 年达到 35%电动车普及率的目标方面走在了正确的方向上,但各个汽车制造商还有很多工作要做才能实现这一目标。未来几年的发展将会非常有趣!
希望您喜欢这篇分析!您可以在我的 GitHub 页面上查看所有代码。如果您喜欢这篇分析,请关注我以获取我的未来工作通知。
注意: 所有图片(除非另有说明)均为作者所拍摄。
分析 Chess960 数据
使用超过 1400 万局 Chess960 游戏来找出是否有比其他变体更好的变体
·
关注 发表于 Towards Data Science · 12 分钟阅读 · 2023 年 1 月 13 日
–
在这篇文章中,我分析了 Lichess 上所有可用的 Chess960 游戏。通过这些信息,并使用贝叶斯 A/B 测试,我展示了没有哪个起始位置比其他位置更有利于任何一方。
原始文章发布于 这里。所有图片和图表,除非另有说明,均由作者提供。
介绍
世界费舍尔随机棋锦标赛最近在雷克雅未克举行,GMHikaru 胜出。费舍尔随机棋,也被称为 Chess960,是经典棋局的一种独特变体,它随机化棋子的起始位置。这一改变的目的是通过消除记忆开局的优势来平衡比赛,使玩家不得不依靠自己的技巧和创造力。
在跟踪这一事件的过程中,我心中出现了一个问题:是否有某些初始 Chess960 变体会给某一方带来不公平的优势?目前标准棋局的初始位置给白方略微的优势,白方通常赢得约 55% 的比赛点数(参考),Stockfish 给白方的评分为 +0.3(参考)。然而,这一优势相对较小,这可能是该位置成为标准位置的原因之一。
关于这个主题已有一些工作。Ryan Wiley 写了这篇博客文章,他在文章中分析了 lichess 上的数据,并得出了一些变体优于其他变体的结论。文章中提到某些棋局对白方的胜率更高,但他没有展示这一说法的显著性。这让我觉得也许他的发现需要重新审视。他还训练了一个机器学习模型,以变体和玩家的 ELO 为输入来预测比赛结果。最终模型的准确率为 65%。
另一方面,还有这个仓库,其中包含了 450 万场比赛的统计数据(每个变体约 4500 场比赛)。在这个仓库中,列出了白方和黑方的最大差异,但同样没有给出统计显著性。
最后,还有一些关于这个主题的计算机分析研究。在这个电子表格中,包含了所有起始位置的 Stockfish 深度 ~40 评估。有趣的是,没有一个位置 Stockfish 给黑方提供优势。还有这个数据库,其中包含了不同计算机引擎之间的 Chess960 比赛。然而,我目前只对分析人类比赛感兴趣,所以对这类比赛不会给予过多关注,也许在未来的文章中会涉及。
由于之前的工作没有解决给每种 Chess960 变体的获胜机会赋予统计信心的问题,我决定尝试一下。
简而言之
在这篇文章中,我分析了所有在 Lichess 上进行的 Chess960 比赛。通过这些信息,我展示了
-
使用贝叶斯 AB 测试,我展示了没有任何起始位置比其他位置更有利于任何一方。
-
此外,变体过去的获胜率并不预测该变体未来的获胜率。
-
并且 Stockfish 的评估并不预测每个变体的实际获胜率
-
最后,知道正在使用的变体并不能帮助预测获胜者。
数据
Lichess——世界上最伟大的国际象棋平台——维护了一个 数据库 ,其中包含了他们平台上所有已进行的游戏。为了进行分析,我下载了所有可用的 Chess960 数据(直到 2022 年 12 月 31 日)。对于所有进行的游戏,我提取了变体、玩家的 ELO 以及最终结果。这些数据可以在 Kaggle 上获得。用于下载和处理数据的脚本和笔记本可以在这个 repo 上找到。
我使用的数据是根据 Creative Commons CC0 许可协议 发布的,这意味着你可以将其用于研究、商业目的、出版等任何你喜欢的用途。你可以下载、修改和再分发这些数据,无需请求许可。
数学框架
贝叶斯 A/B 测试
根据上述的先前工作,一些变体比其他变体更好。但我们如何确保这些差异在统计上是显著的呢?为了解答这个问题,我们可以使用著名的 A/B 测试策略。也就是说,我们首先假设变体A比变体B具有更大的获胜机会。零假设则是A和B具有相同的获胜率。为了排除零假设,我们需要证明在零假设的假设下观察到的数据是如此极端,以至于继续相信零假设没有意义。为此,我们将使用贝叶斯 A/B 测试 1。
在贝叶斯框架下,我们为每个变体分配一个获胜率的概率分布。也就是说,不是说变体A的获胜率是X%
,而是说变体A的获胜率有某种概率分布。建模这种问题时,自然的选择是使用贝塔分布 (ref)。
贝塔分布被定义为
其中 B(a, b) = Γ(a)Γ(b)/Γ(a+b), Γ(x) 是伽马函数,对于正整数 Γ(n) = (n-1)!。对于给定的变体,参数α可以解释为白方胜利的次数加一,而β则为白方失败的次数加一。
现在,对于两种变体A和B,我们想知道A的胜率比B的胜率大的可能性。数值上,我们可以通过从A和B中抽取N个值,即w_A和w_B,并计算w_A > w_B的比例来做到这一点。然而,我们可以通过解析的方法计算这一点,从
注意,Beta 函数可能会给出非常大的数字,因此为了避免溢出,我们可以使用log
进行转换。幸运的是,许多统计软件包都有对数 Beta 函数的实现。通过这种转换,项被转换为
这在 python 中实现,使用scipy.special.betaln
对数 B(a, b)的实现,如
import numpy as np
from scipy.special import betaln as logbeta
def prob_b_beats_a(n_wins_a: int,
n_losses_a: int,
n_wins_b: int,
n_losses_b: int) -> float:
alpha_a = n_wins_a + 1
beta_a = n_losses_a + 1
alpha_b = n_wins_b + 1
beta_b = n_losses_b + 1
probability = 0.0
for i in range(alpha_b):
total += np.exp(
logbeta(alpha_a + i, beta_b + beta_a)
- np.log(beta_b + i)
- logbeta(1 + i, beta_b)
- logbeta(alpha_a, beta_a)
)
return probability
使用这种方法,我们可以计算一个变体比另一个变体更好的可能性,并由此定义一个阈值α,使得我们可以说,如果Pr(p_A>p_B)<1-α,则变体B显著优于变体A。
下面你可以看到一些 Beta 分布的图。第一幅图中,参数为α_A= 100,β_A=80,α_B=110 和β_B=70。
参数为α_A= 100,β_A=80,α_B=110 和β_B=70 的 Beta 分布
在第二幅图中,参数为α_A= 10,β_A=8,α_B=11 和β_B=7。
参数为α_A= 10,β_A=8,α_B=11 和β_B=7 的 Beta 分布
注意,即使在两种情况下胜率相同,但分布看起来不同。这是因为在第一种情况下,我们对实际胜率的确定性更高,这是因为我们观察到了比第二种情况更多的点。
家庭错误率
通常,在 A/B 测试中,人们仅比较两个变体,例如:白色背景与蓝色背景的网站转换率。然而,在这个实验中,我们不仅仅是在比较两个变体,而是比较所有可能的变体对——记住我们想找出是否有至少一个变体优于另一个变体——因此,我们所做的比较数量是 960959/2 ~ 5e5。这意味着使用典型值α=0.05*是不正确的,因为我们需要考虑到我们进行的比较数量。例如,假设所有初始位置的胜率分布相同,使用标准值会有一定的概率
至少观察到一个假阳性!这意味着即使在任何变体对之间没有统计显著差异的情况下,我们仍将观察到至少一个假阳性。如果我们想保持相同的α但将比较数量从 2 增加到,我们需要定义一个有效的α,如
并解决
插入我们的值后,我们最终得到α_eff =1e-7。
列车/测试分割
在前面的部分,我们发展了理论,以确定一种变体是否比另一种变体更好,基于观察到的数据。也就是说,在看到一些数据后,我们建立了变体 B 优于变体 A
的形式的假设。然而,我们不能使用用于生成假设的数据来验证假设的真实性。我们需要在尚未使用的数据集上测试这个假设。
为了实现这一点,我们将把完整的数据集拆分成两个不相交的train
和test
数据集。train
数据集将与贝叶斯 A/B 测试框架一起使用,以生成B>A形式的假设。然后,使用test
数据集,我们将检查这些假设是否成立。
请注意,这种方法只有在赢率的分布随时间不变时才有意义。这似乎是一个合理的假设,因为,据我所知,过去几年没有发生过重大的理论进展来改变某些变体的获胜概率。实际上,最小化理论和准备对游戏结果的影响是 Chess960 的目标之一。
数据准备
在前面的部分,我们隐含地假设一局游戏要么赢,要么输,但它也可能是平局。我为胜利分配了1
分,为平局分配了1/2
分,为失败分配了0
分,这是国际象棋比赛中通常的做法。
结果
在这一部分,我们将应用上面解释的所有技术到 lichess 数据集中。在数据集中,我们有超过 1300 万局游戏,这大约是每种变体 1.4 万局。然而,数据集包含了大量不同的玩家和时间控制(从 ELO 900 到 2000,从快速棋到经典棋)。因此,使用所有游戏进行比较将意味着忽视混杂变量。为避免这个问题,我只使用了 ELO 在(1800, 2100)范围内且使用快速棋时间控制的玩家的游戏。我意识到这些筛选条件与世界费舍尔随机国际象棋锦标赛等顶级比赛的现实情况不符,但在 lichess 数据中,高等级玩家(>2600)的经典 Chess960 游戏并不多,所以我将只使用游戏数量更多的组。在应用这些筛选条件后,我们得到一个包含约 240 万局游戏的数据集,每种变体约有 2500 局。
列车/测试分割是通过时间分割完成的。2022-06-01
之前的所有游戏都属于训练数据集,而该日期之后的所有游戏都属于测试数据集,这大约占训练数据的 80%和测试数据的 20%。
生成假设
第一步是通过 A/B 测试生成一组假设。要比较的变化对数量相当大(1e5),测试所有这些变化对会花费很多时间,因此我们只比较获胜率最高的 20 个变化与获胜率最低的 30 个变化。这意味着我们将有 900 对变化进行比较。在这里,我们看到 train
数据集中差异较大的变化对。
注意到这些变化的 α 大于 α_eff,这意味着差异不显著。由于这些是差异较大的变化,我们知道没有任何变化对具有统计学上显著的差异。
尽管差异不显著,但从这张表格可以假设变化 rnnqbkrb
比变化 bbqrnkrn
更差。如果我们在 test
数据集中检查这些变化值,我们会得到
注意到“差”的变化的获胜率仍然低于“好”的变化,然而,它已经从 0.473
增加到 0.52
,这差距还是很大的。这提出了一个新问题:过去的变化表现是否能保证未来的表现?
过去与未来的表现
在上一部分,我们已经看到如何生成和测试假设,但我们也注意到一些变化的表现会随时间变化。在这一部分,我们将更详细地分析这个问题。为此,我计算了 train
和 test
数据集中的获胜率,并将它们绘制在一起进行比较。
训练与测试的获胜率
如我们所见,过去的获胜率与未来的获胜率之间没有关系!
评估与率
我们已经看到过去的表现不能保证未来的表现,但 Stockfish 评估能否预测未来的表现?在接下来的图表中,我展示了 Stockfish 对每个变化的评估以及数据集中相应的获胜率。
Stockfish 评估与每个变化的获胜率
机器学习模型
直到现在,我们已经看到在 Chess960 游戏中没有更好的变化,并且过去的表现不能保证未来的表现。在这一部分,我们将看看是否可以根据变化和玩家的 ELO 预测哪一方将赢得比赛。为此,我将训练一个机器学习模型。
模型的特征包括白棋和黑棋的 ELO、正在进行的变体以及使用的时间控制。由于变体特征的基数很大,我将使用CatBoost
,该模型专门设计用于处理分类特征。此外,作为基准,我将使用一个模型,该模型预测如果White ELO > Black ELO
则白棋获胜,White ELO == Black ELO
则平局,White ELO < Black ELO
则白棋失败。通过这个实验,我想了解变体对预期胜率的影响。
在接下来的表格中,我展示了两个模型的分类报告。
- CatBoost 模型
- 基准模型
从这些表格中,我们可以看到 CatBoost 和基准模型的结果几乎相同,这意味着知道正在进行的变体并不有助于预测游戏结果。注意,结果与这里获得的结果相符(准确率 ~65%),但在链接的博客中假设知道变体有助于预测赢家,而我们已经看到这并不成立。
结论与评论
在这篇文章中,我展示了
-
使用标准阈值来确定显著结果在进行多次比较时不有效,需要进行调整。
-
胜率没有统计学上的显著差异,即:我们不能说某个变体比另一个变体对白棋更有利。
-
过去的比率不能暗示未来的比率。
-
Stockfish 评估无法预测胜率。
-
知道正在进行的变体并不有助于预测比赛结果。
然而,我意识到我使用的数据并不代表我最初想研究的问题。这是因为 Lichess 上的数据偏向于非职业玩家,即使我使用了来自具有良好 ELO(从 1800 到 2100)的玩家的数据,但他们距离参加 Chess960 世界杯的玩家(>2600)还很远。问题在于,ELO >2600 的玩家数量非常少(根据chess.com的数据显示为 209),而且并不是所有人都在 Lichess 上定期进行 Chess960 比赛,因此具有这种特征的棋局数量几乎为零。
从数据科学的角度分析 FC 巴塞罗那的防守
原文:
towardsdatascience.com/analyzing-fc-barcelonas-defense-from-a-data-science-perspective-76797018b0b3
体育分析
通过视觉数据比较来说明巴萨防守的漏洞
·发布于 Towards Data Science ·13 分钟阅读·2023 年 8 月 10 日
–
照片由 Alessio Patron 提供,来源于 Unsplash
简介
几天前我发布了我的第一个体育分析文章。对这一话题仍然充满兴趣,现在我又在写关于足球的内容。
在下文中 — 链接见下方 — 我使用了频率统计来演示进球事件的随机性。但我将其更进一步。文中解释的随机模型 — 受到泊松分布的影响 — 在许多与足球无关的领域也适用。
通过频率统计理解进球事件
towardsdatascience.com
今天我们将更进一步,尽管这将以足球为中心,但我们将要了解的过程和知识对于任何数据科学家都是相关的。
从足球角度来看,我们将专注于防守,分析巴萨的防守,看看在哪些方面可以改进,无论是在团队还是个人层面。
由于防守是一个广泛的术语 — 包括铲球、扑救、拦截和许多其他高级统计数据 — 我将更加具体,专注于射门和失球。
在 2015–16 赛季的西甲中,巴萨是第二少失球的球队(29 球),仅次于马竞(18 球)。尽管这已经不错,但仍有改进的空间。
目标不是提供解决方案,那是教练组的工作。我们今天作为数据科学家或体育分析师的目标是发现问题并提出假设,以便教练组可以根据这些信息解决场上的问题。
这是我们今天将要探讨的内容的简要总结:
-
背景和上下文。
-
获取数据、转换和准备数据。
-
分析 FCB 的射门和丢球。
-
更深入地分析每个球员的射门和丢球。
所有的代码都将在我的 GitHub 仓库 [1]中提供,链接在资源部分,因此我将在此跳过部分代码,以免大段代码分散读者对内容的关注。
上下文
没有背景信息的数据科学问题是无法解决的。我们需要深入理解我们所处理的数据。如果没有这样做,我们无法得出有用的结论。
数据需要上下文才能转化为信息。
让我们回到过去。2015-16 赛季西甲联赛是一个有趣的赛季。F.C.巴萨以 91 分获得冠军,紧随其后的是只差 1 分的皇家马德里(90 分),而马德里竞技获得 88 分。
最后一轮比赛将决定一切。但三队都赢得了各自的比赛,因此排名没有变化。巴萨赢得了冠军。
在那个赛季,巴萨还赢得了国王杯和欧洲超级杯,但在冠军联赛和西班牙超级杯中惨遭失败。在冠军联赛中,他们在四分之一决赛被马德里竞技淘汰,在西班牙超级杯中,他们以总比分 1-5 输给了毕尔巴鄂竞技。
显然,巴萨的防守存在问题。在那次西甲赛季中,他们被进球次数比马德里竞技多了 11 次(增加了 61%)。看起来 MSN 进攻组合(梅西-苏亚雷斯-内马尔)在大多数比赛中弥补了球队的防守。
但奇迹是不存在的。
四名常规防守首发球员是阿尔维斯、皮克、马斯切拉诺和阿尔巴。这些都是世界级的防守球员,但显然他们并没有打满所有比赛时间。替补球员包括马蒂厄、罗贝托、阿德里亚诺、巴尔特拉、维尔马伦……如果看替补席,水平下降了不少。
我们可以怪罪替补球员吗?也许不能。我想指出的是,阿尔维斯和阿尔巴都是相当进攻的边后卫……这是否导致他们的球队丢了更多的射门或进球?
我们今天的任务是分析防守,看看是否能发现潜在的缺陷。
获取数据、转换和准备数据
现实世界中的原始数据永远不会被清理和准备好以供数据科学家使用。大多数时候,代码将用于处理数据集并将其转换为我们项目所需的精确数据。
对于今天的目的,我们将使用 StatsBomb 的免费开放数据[2]。我们还将使用statsbombpy
[3]模块对数据进行处理。
你可以通过运行以下命令来安装它:
pip install statsbombpy
我们还将使用一些你可能需要安装的其他模块。如果尚未安装,请先安装这些模块,然后导入它们:
import matplotlib
import matplotlib.pyplot as plt
from mplsoccer import VerticalPitch
import numpy as np
import pandas as pd
from statsbombpy import sb
我们现在准备好检索数据。由于我们需要检查 2015–16 赛季 FCB 的射门和失球,我们首先需要获取该比赛和赛季的所有比赛数据:
competition_row = sb.competitions()[
(sb.competitions()['competition_name'] == 'La Liga')
& (sb.competitions()['season_name'] == '2015/2016')
]
competition_id = pd.unique(
competition_row['competition_id']
)[0]
season_id = pd.unique(
competition_row['season_id']
)[0]
matches = sb.matches(competition_id=competition_id, season_id=season_id)
然后,对于每场比赛,我们可以轻松地通过以下方式检索其事件:
match_events = sb.events(match_id=match_id)
因此,我们提取了所有巴萨比赛中的所有事件,并创建了一个包含所有事件的数据框:all_events
。我们还创建了两个额外的列,因为它们对于后续分析会很有用:
-
一个叫做
minutes
的列,其在同一场比赛的所有行中具有相同的值:它的持续时间。 -
另一个叫做
time
,它只是minute
和second
列中值的连接。
最终,all_events
数据框被过滤以仅保留对手球队的射门。结果如下:
shots_against_team.head()
shots_against_team DF 中的前 5 行 — 图片由作者提供
StatsBomb 的数据非常棒。完整且准确。今天对我们有用的是射门发生的位置,由x
和y
列标记。因此,我们可以使用这些数据来进行可视化。
分析 FCB 的射门和失球
一旦我们拥有所有需要的数据,我们可以开始分析。像往常一样,绘图是我做的第一件事,因为这是理解我们所处理数据的最佳方式。
我们将使用来自mplsoccer
[4]模块的VerticalPitch
类来展示射门和进球的位置:
# Set up pitch (layout)
pitch = VerticalPitch(line_zorder=2, line_color='black', half = True)
fig, axs = pitch.grid(nrows=1, ncols=1, axis=False, endnote_height=0.05)
# Plot each shot
for row in shots_against_team.itertuples():
if row.shot_outcome == 'Goal':
# If it was a goal, we want to see it clearly
alpha = 1
else:
# Increase transparency if it wasn't a goal
alpha = 0.2
pitch.scatter(
row.x,
row.y,
alpha = alpha,
s = 100,
color = "red",
ax=axs['pitch'],
edgecolors="black"
)
这段简单的代码允许我们绘制半个足球场,并将射门用红色标记,透明度根据是否进球而有所不同。此外,我还添加了两个额外的点,分别表示平均射门和进球的位置(绿色和蓝色)。
2015–16 赛季西甲联赛中 FCB 的射门和失球 — 图片由作者提供
两个平均值都相当集中(略微偏向左侧),而进球位置比射门平均位置更靠近球门。毫不奇怪,射门越接近球门,进球越容易。
继续分析射门,唯一无法忽视的就是:在禁区外,射门似乎更多来自右侧(尽管相对均匀)。但在禁区内,射门明显偏向左侧。
那是阿尔维斯和皮克的一侧。
如果我们关注进球,来自左侧的进球更为分散,而右侧的进球在图中看起来更为集中或聚集。
以下代码片段用于创建射门热图。如果我们对进球应用相同的方法,我们会得到热图,用于更好地展示射门和进球在场地不同区域的分布。
pitch = VerticalPitch(line_zorder=2, line_color='black', half = True)
fig, axs = pitch.grid(nrows=1, ncols=2, axis=False, endnote_height=0.05)
shot_bin_statistic = pitch.bin_statistic(
shots_against_team.x,
shots_against_team.y,
statistic='count',
bins=(6, 5),
normalize=False
)
#normalize by number of games
shot_bin_statistic["statistic"] = shot_bin_statistic["statistic"]/len(team_matches)
#make a heatmap
pcm = pitch.heatmap(shot_bin_statistic, cmap='Reds', edgecolor='grey', ax=axs['pitch'][0])
#legend to our plot
ax_cbar = fig.add_axes((-0.05, 0.093, 0.03, 0.786))
cbar = plt.colorbar(pcm, cax=ax_cbar)
axs['pitch'][0].set_title('Shots conceded heatmap')
fig.suptitle(f"Shots and Goals Against {team} in 2015/16 La Liga season", fontsize = 30)
如果我们绘制两个热图,我们会得到:
2015–16 赛季西甲联赛中 FCB 的射门和失球 — 图片由作者提供
现在,显然,大多数射门和进球都来自中心和离球门最近的位置。一旦球在那儿,只有射门才有意义。
射门热图几乎是完全对称的,这因为我们将其分成了 5 个区间或列。如果我们选择更多区间,可能会更清楚地看到左侧偏斜。
但我们可以在进球热图中看到这一点。巴萨在阿尔维斯-皮克的一侧丢球比在马斯切拉诺-阿尔巴的一侧更多。
在分析一个团队的防守时,有几个因素非常重要。最终目标是查看他们失败的原因以及我们如何减少丢球的数量。一个团队可以通过多种方式实现这一点,但减少对方射门次数显然是一种减轻丢球数量的方式。
然而,今天我们还没有将射门与进球联系起来。我们将通过构建一个新的KPI来实现这一点,这是今天的第一个 KPI。
关键绩效指标(KPIs)在任何数据科学或分析项目中都极其重要。选择正确的 KPIs 可以让我们评估和正确地评估我们使用的策略,而选择不准确的 KPIs 则会导致无用和误导性的分析。
今天的第一个 KPI 将是每次射门进球比率,作为检查对手在射门时得分机会的方式:
# Count goals per heatmap bin
goal_bin_statistic = pitch.bin_statistic(
shots_against_team.loc[shots_against_team['shot_outcome'] == 'Goal'].x,
shots_against_team.loc[shots_against_team['shot_outcome'] == 'Goal'].y,
statistic='count',
bins=(6, 5),
normalize=False
# Count shots per heatmap bin
shot_bin_statistic = pitch.bin_statistic(
shots_against_team.x,
shots_against_team.y,
statistic='count',
bins=(6, 5),
normalize=False
)
# Create goal_shot_ratio KPI by dividing goals/shots
goal_shot_ratio = goal_bin_statistic.copy()
goal_shot_ratio['statistic'] = np.divide(goal_bin_statistic['statistic'], shot_bin_statistic['statistic'])
goal_shot_ratio['statistic'] = np.nan_to_num(goal_shot_ratio['statistic'])
下面是这个新统计数据绘制成热图的结果:
对 FCB 的每次射门进球比率 — 图片由作者提供
我们可以清晰地看到之前提到的左侧偏斜。当对方从阿尔维斯和皮克的一侧射门时,在禁区内丢球的机会几乎是从马斯切拉诺和阿尔巴的一侧射门丢球概率的两倍。
这并不意味着皮克比马斯切拉诺差,我当然不这样认为。我们只是说,从左侧射门比从右侧射门更容易导致丢球。
深入分析 — 球员分析
我觉得我们一直在责怪皮克和阿尔维斯,但我们甚至还没有比较他们的个人数据。因此,我们将深入分析这些球员的个人表现,再次从射门和丢球的角度来看。
如果这种不对称的原因是因为替补球员在首发球员疲劳或受伤时替换他们,那会怎么样?如果在个人层面上,他们的射门和丢球表现是一样的呢?
这就是我们现在要分析的内容,但我们首先需要准备数据。我们需要知道每个球员在每场比赛中打了多少分钟,球队在他们在场时丢了多少球……
我们将使用statsbombpy
的阵容数据,结合我们已经建立的事件数据框来获取这些数据:
all_lineups = None
for match_id in pd.unique(all_events['match_id']):
match_lineups = sb.lineups(match_id=match_id)['Barcelona']
match_lineups['match_id'] = match_id
match_lineups['match_duration'] = all_events[all_events['match_id'] == match_id]['minutes'].unique()[0]
match_lineups['from'] = match_lineups['positions'].apply(lambda x: x[0]['from'] if x else np.nan)
match_lineups['to'] = match_lineups.apply(lambda x: x['positions'][-1]['to'] if x['positions'] and x['positions'][-1]['to'] is not None else ('90:00' if x['positions'] else np.nan), axis=1)
match_lineups['minutes_played'] = match_lineups.apply(lambda x: parse_positions(x['positions'], x['match_duration']), axis=1)
if all_lineups is None:
all_lineups = match_lineups.copy()
else:
all_lineups = pd.concat([all_lineups, match_lineups], join="inner")
all_lineups = all_lineups.reset_index(drop=True)
我们只是添加了一些有用的新列,并使用了一个自定义函数 — parse_position()
— 来解析球员在比赛中在场的分钟数(也包括额外的加时)。
all_lineups 数据框的前 5 行 — 图片由作者提供
现在我们可以继续计算每个球员在场时,球队接到的射门和丢球的数量。
for match_id in pd.unique(all_lineups['match_id']):
match_shots = shots_against_team[
shots_against_team['match_id'] == match_id
]
for player_tup in all_lineups[all_lineups['match_id'] == match_id].itertuples():
# For whatever reason, the 'from' column is being mapped to '_10'
shots_conceded = match_shots[
(match_shots['time'] >= player_tup._10)
& (match_shots['time'] <= player_tup.to)
]
goals_conceded = len(
shots_conceded[shots_conceded['shot_outcome'] == 'Goal']
)
shots_conceded = len(shots_conceded)
all_lineups.at[player_tup.Index,'shots_conceded'] = shots_conceded
all_lineups.at[player_tup.Index,'goals_conceded'] = goals_conceded
新列在all_lineups
数据框中的样子如下:
每场比赛按球员统计的射门和进球数 — 图片来源于作者
这很有用,因为它允许我们看到每场比赛当特定球员在场时的表现。让我们进一步深入探讨。
除了我们在前一部分创建的 KPI 之外,我们现在将生成两个新的 KPI:
-
每分钟射门数
-
每分钟进球数
grouped = all_lineups.groupby('player_id')[
['minutes_played', 'shots_conceded', 'goals_conceded']
].sum()
grouped['shots_per_minute'] = \
grouped['shots_conceded'] / grouped['minutes_played']
grouped['goals_per_minute'] = \
grouped['goals_conceded'] / grouped['minutes_played']
如果我们排除那些每场比赛平均上场时间少于 10 分钟的球员,并按shots_per_minute
变量对数据框进行排序,我们会得到以下结果:
显示球员与进球和被射门相关的统计数据的数据框 — 图片来源于作者
这再清楚不过了。前 6 名中有 5 名是替补防守球员:巴尔特拉、阿德里亚诺、马修、罗伯托和维尔马伦。这太疯狂了。
难怪路易斯·恩里克偏爱皮克、马斯切拉诺、阿尔维斯和阿尔巴。
每 90 分钟的射门和进球数 — 图片来源于作者
转化为每 90 分钟的射门数,当巴尔特拉在场时,球队每场比赛平均接收 11.31 次射门。与之相比,马斯切拉诺在场时每 90 分钟接收 8.51 次射门。
就每 90 分钟的进球数而言,我们仍然看到马修、阿德里亚诺和罗伯托位于表现最差的 6 名球员中。显然效率不高。
我们也看到了皮克,这解释了我们在热图上看到的左侧偏斜。
但让我们将这个放到团队背景中。来看看团队比例:
Barcelona conceded 356 shots. An average of 9.37 shots per match.
Barcelona conceded 26 goals. An average of 0.68 goals per match.
这是相关的:
-
左侧图中的所有球员,除了维尔马伦外,每场比赛承受的射门数都高于球队平均水平。换句话说,这 5 名球员在防守上表现不佳。由于他们大多数是防守球员,这意味着球队在他们在场时防线挣扎。幸运的是,射门的增加并没有直接转化为进球。
-
如果我们看右侧图,所有 6 名球员都高于球队平均水平。当他们在场时,球队承受了更多的进球。这次,一半是替补防守球员。
结论
所有数据科学项目都需要得出结论。这可能是最重要的部分——提取洞见并与利益相关者分享是进行前期分析和研究的原因。
在我们的足球案例中,这就是我们如何结束分析并与那赛季的教练路易斯·恩里克分享:
作为一个团队,我们在那个赛季的防守排名第二。不算差,但距离最佳防守还相当远(相差 11 个进球)。我们承受的进球数超过了预期,减少这一数字的最佳方法是减少巴萨让对手射门的次数。
如果我们关注这些射门和进球,危险的射门(在禁区内)似乎偏向于皮克的一侧。我们不是在指责他,我们需要看看为什么会这样。
更深入的分析证明,如果我们要责怪某人,那不会是首发防守球员。在场时,对手的射门次数减少。是否转化为进球数量较少取决于多个因素,但确实存在相关性。
实际上,当像维尔马伦、马蒂厄、巴尔特拉、阿德里亚诺和/或罗伯托这样的球员上场时,球队每分钟受到的射门和进球数量最多。因此,在我看来,重点应该是寻找更好的防守选项来支持由阿尔维斯、比克、马斯切拉诺和阿尔巴建立的防守核心,或提升现有防守球员的表现。
比克和马斯切拉诺大部分时间主导了防守,这帮助巴萨赢得了他们赢得的奖杯。但即便比克在被射门方面是最好的防守者,当对手射门时,他们进球的几率还是比马斯切拉诺高。因果关系还是偶然性,我们永远无法知道(我会选择第二种)。
如果你对 2016 年转会窗口期间发生的事情感到好奇:阿德里亚诺、巴尔特拉和维尔马伦离开了俱乐部。马蒂厄再待了一个赛季,但他的出场次数大幅下降,在所有比赛中只踢了 16 场:他基本上是最后的选择。
罗伯托是唯一一个留下来并且保持了相当多的上场时间的人。
那个夏天,巴萨还尝试通过签下乌姆蒂蒂和迪涅来增强阵容——如果他们知道结果会是这样就好了……
从某种程度上说,今天的分析对 FCB 来说并不新鲜:他们已经在 2015–16 赛季结束时进行了类似分析,并看到了我们在他们防守中看到的相同问题。不知何故,他们未能签下足够的优质球员来改善情况,并在下赛季没有赢得西甲。
**Thanks for reading the post!**
I really hope you enjoyed it and found it insightful.
Follow me and subscribe to my mail list for more
content like this one, it helps a lot!
**@polmarin**
资源
[4] mplsoccer — Readthedocs.io
使用 Python 分析地理空间数据
原文:
towardsdatascience.com/analyzing-geospatial-data-with-python-7244c1b9e302
一篇实用的数据分析文章,包含 Python 代码。
·发表于Towards Data Science ·阅读时间 8 分钟·2023 年 8 月 19 日
–
简介
地理空间数据科学是我感兴趣的领域之一。我觉得将数据可视化在地图上的方式非常迷人,而且——很多时候——数据点之间的关系能够迅速提供深刻的洞见。
我相信这一数据科学子领域的应用对任何业务都非常有用,例如杂货店、汽车租赁、物流、房地产等等。在这篇文章中,我们将探讨来自AirBnb的一个数据集,地点是美国北卡罗来纳州的阿什维尔市。
附注:在那个城市里有一处美国最惊人的房地产之一——我敢说是世界上最惊人的。这处房产属于范德比尔特家族,曾经是国家上最大的私人财产。嗯,值得一游览,但这不是本文的核心主题。
位于北卡罗来纳州阿什维尔的比尔特莫尔庄园。照片由Stephanie Klepacki拍摄,Unsplash提供。
本练习使用的数据集为阿什维尔市的 AirBnb 出租数据。可以直接从insideairbnb.com/get-the-data
网站下载,符合知识共享署名 4.0 国际许可协议。
让我们开始工作吧。
地理空间数据科学
本文的知识大多来源于下述书籍(《使用 Python 进行应用地理空间数据科学》,作者:David S. JORDAN)。所以,让我们开始导入一些模块到我们的会话中吧。
import pandas as pd
import geopandas as gpd
import matplotlib.pyplot as plt
import pysal
import splot
import re
import seaborn as sns
import folium
# For points map
import geoplot.crs as gcrs
import geoplot as gplt
现在注意,有些数据点对你来说可能是新的,就像对我一样。如果需要,使用 pip install module_name
安装所需的任何软件包。在我的情况下,pysal
和 geoplot
对我来说是新的,所以它们必须被安装。
接下来,我们将从 AirBnb 读取数据。
# Open listings file
listings = pd.read_csv('/content/listings.csv',
usecols=['id', 'property_type', 'neighbourhood_cleansed',
'bedrooms', 'beds', 'bathrooms_text', 'price',
'latitude','longitude'])
#listings.columns
listings.sample(4)
阿什维尔,NC 的 AirBnb 列表。图像由作者提供。
如果我们执行 listings.info()
,我们会看到 price
变量被识别为对象类型。这是因为数字前面有美元符号 $
。这在 Python 中很容易纠正。
# Correct Price to Float (Replace $ and , with nothing)
listings['price'] = (listings['price']
.replace("[$,]", "", regex=True)
.astype(float)
)
探索性分析
现在我们可以检查该变量的统计信息。
#price stats
listings.price.describe()
count 3239.000000
mean 179.771843
std 156.068212
min 14.000000
25% 95.000000
50% 135.000000
75% 212.500000
max 2059.000000
Name: price, dtype: float64
有趣。平均价格接近 $180,但中位数为 $135。 这表明我们可能有一些高值扭曲了分布,使其向右偏斜。为了确认这一点,我们可以检查价格范围。
# Check price range
sns.displot(listings['price'], kde=True);
阿什维尔,NC 列表的价格。图像由作者提供。
正如预期的那样,我们的数据大多数集中在 $500 左右。其余的大部分是异常值,数量很少。我们可以通过运行代码 np.quantile(listings[‘price’], 0.97)
并检查第 97 百分位数是 $538 美元来确认这一点。
非常好。
下一步是开始将数据绘制到地图上。让我们可视化数据点并开始绘制一些洞察。为此,我们首先需要将 Pandas 数据框转换为 Geopandas。
# Convert Pandas df to Geopandas df
listings_gpd = gpd.GeoDataFrame(listings,
geometry=gpd.points_from_xy(listings.longitude, listings.latitude, crs=4326))
# Look at the geometry variable created
listings_gpd.head(2)
查看创建的数据集,可以观察到它现在带有一个 geometry
变量。
由 geopandas 创建的‘geometry’变量。图像由作者提供。
完成后,使用 geoplot 绘制点是很简单的。我们只使用了 $538 以下的值,这样颜色看起来分布更均匀,否则数据的偏斜会使点变成一大块紫色混合。
# Points map using geoplot
ax = gplt.webmap(listings_gpd.query('price < 538'), projection=gcrs.WebMercator())
gplt.pointplot(listings_gpd.query('price < 538'), ax=ax, hue= 'price', legend=True);
数据点:来自北卡罗来纳州阿什维尔的 AirBnb 列表。图像由作者提供。
好的。从这里开始,我们可以看到一些不错的东西:
-
大多数列表(如预期的那样)都在平均值附近浮动。
-
I-40 高速公路沿线有租赁物业的集中区域。
-
深蓝色和浅蓝色点混合得很好,显示价格在 100 到 200 美元之间。
-
红点的出现非常稀疏,所以在阿什维尔找不到非常贵的租赁房源应该不是很常见。
热图
接下来,我认为我们可以开始创建热图。
JORDAN 的书中展示的一个选项是使用 geoplot。我们可以从 insideairbnb.com 下载 geojson 文件,并用它来创建热图。
在这里,我们将使用 geopandas 读取文件,并将其转换为 4326 坐标系统,这是 GPS 的标准。
# Reading the Asheville polygon shapefile (geojson)
geofile = '/content/neighbourhoods.geojson'
asheville = gpd.read_file(geofile)
asheville = asheville.to_crs(4326)
然后,使用这些几行代码创建热图非常简单。首先,我们创建一个密度图(kde)以便在地图上投影,然后使用 polyplot 显示这两个图层。
# Heatmap
ax = gplt.kdeplot(listings_gpd,
fill=True, cmap='Reds',
clip=asheville.geometry,
projection=gcrs.WebMercator())
# Plotting the heatmap on top of the geometry
gplt.polyplot(asheville, ax=ax, zorder=1);
这是漂亮的结果。列表在城市中心区域非常集中。
阿什维尔列表的热图。图像由作者提供。
继续,我们将使用Folium
模块创建按区域划分的价格热图和一个分层地图。
首先是热图。考虑到我们希望看到不同颜色的数据点,按价格范围分组,这里有一个代码来创建这些组。首先我们使用pandas.cut
为每 100 美元创建区间,然后使用map()
将标签转换为颜色。这将在我们的下一步中使用。
from numpy import Inf
# Create clip levels for prices
listings['price_bins']= pd.cut(listings.price,
bins= [-Inf, 100, 200, 300, 400, 500, Inf],
labels= ['0-100', '100-200', '200-300', '300-400', '400-500', '500+'])
# Create bin colors
listings['colors'] = listings.price_bins.map({'0-100': 'lightblue', '100-200':'blue', '200-300':'gold', '300-400':'orange', '400-500':'red', '500+':'black'})
我们来创建一个基础地图,起点为北卡罗来纳州阿什维尔市中心。
# Creating a base map
m = folium.Map(location= [35.5951, -82.5515], zoom_start=10)
然后我们可以添加点。
# Adding the points
for lat, lon, ptcolor in zip(listings.latitude, listings.longitude, listings.colors):
folium.CircleMarker(
location=[lat, lon],
radius=2,
opacity=0.5,
color=ptcolor,
fill=True,
fill_color=ptcolor,
).add_to(m)
现在我们将创建热图。我们必须将数据转换为值的列表。仅包括纬度、经度和价格。热图将按价格显示。然后,我们导入HeatMap from folium.plugins
并将其添加到基础地图中。
from folium import plugins
# Preparing data for plot
data = listings[['latitude','longitude', 'price']].values
data =data.tolist()
# Create Heat Map with Folium
hm = plugins.HeatMap(data,gradient={0.1: 'blue', 0.2: 'lime', 0.4: 'yellow', 0.6: 'orange', 0.9: 'red'},
min_opacity=0.1,
max_opacity=0.9,
radius=20,
use_local_extrema=False)
# Add to base map
hm.add_to(m);
# Display
m
这是结果。一个漂亮的热图,展示了有价值的见解。
阿什维尔 AirBnb 列表的热图。图像由作者提供。
看看市中心附近的价格更高。而在比尔特莫景点附近,列表并不那么密集。那里有一些,价格相对较低,可能是因为离市中心较远。
市中心与比尔特莫周边。图像由作者提供。
分层地图
最后,如果我们想从这些数据创建一个快速的分层地图,以下是代码片段。我们可以使用之前创建的asheville
文件作为我们的地理数据,listings
文件是价格来源,neighbourhood
变量是 geojson 与数据框之间的链接。
# Add a choropleth layer
folium.Choropleth(
geo_data=asheville,
name="choropleth",
data=listings,
columns=["neighbourhood", "price"],
key_on="feature.properties.neighbourhood",
fill_color="RdBu_r",
fill_opacity=0.5,
line_opacity=0.5,
legend_name="Prices",
).add_to(m)
m
阿什维尔 AirBnb 列表的分层地图。图像由作者提供。
从地图上看,我们看到最高的价格在右侧。如果你不熟悉这个地区,阿什维尔是一个位于蓝岭山脉旁边的城市,是北卡罗来纳州一个著名而美丽的地方,特别是在秋季。蓝岭公路是美国最著名的公路之一,经常被提及为最美的驾车路线之一。所以让我们绘制另一个分层地图,这次启用地形模式,然后我们可以看到山脉的位置。
# Base map with Terrain mode
m = folium.Map(location= [35.5951, -82.5515], zoom_start=10, tiles="Stamen Terrain")
# Add a choropleth layer to a terrain map
folium.Choropleth(
geo_data=asheville,
name="choropleth",
data=listings,
columns=["neighbourhood", "price"],
key_on="feature.properties.neighbourhood",
fill_color="RdBu_r",
fill_opacity=0.5,
line_opacity=0.5,
legend_name="Prices",
).add_to(m)
m
红线是蓝岭公路的位置。
分层地图叠加在地形图上。图像由作者提供。
这是蓝岭公路的一种视角。我想你可以理解为什么那些出租物业更贵了吧?真是美丽的景色!
蓝岭公路的视图。图片来自作者的个人收藏。
在你离开之前
我希望你喜欢这个小项目。地理空间数据非常强大,可以带来许多见解。要使用它,像 Geopandas、Geoplot 和 Folium 这样的包是必不可少的。
这是本练习的完整代码:
github.com [## Studying/Python/Geospatial 在 master · gurezende/Studying
这是一个包含我的测试和新包学习的代码库 - Studying/Python/Geospatial 在 master ·…
如果你喜欢这些内容,请关注我的博客获取更多更新。此外,你也可以在LinkedIn找到我。
gustavorsantos.medium.com [## Gustavo Santos - Medium
阅读 Gustavo Santos 在 Medium 上的文章。数据科学家。我从数据中提取见解,以帮助人们和公司……
参考资料
JORDAN, David S. 2023.应用地理空间数据科学与 Python. 第 1 版。Pactk Publishing.
medium.com [## 使用 Folium 在 Python 中创建交互式地图和 choropleth 地图
学习如何创建简单的地图并使用 Folium 包添加基于颜色的值编码(choropleth)……
medium.com [## 快速入门 - Folium 0.14.0 文档