TowardsDataScience 2023 博客中文翻译(七十五)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

合作图神经网络

原文:towardsdatascience.com/co-operative-graph-neural-networks-34c59bf6805e

新的 GNN 架构

绝大多数图神经网络(GNNs)遵循信息传递范式,其中节点状态基于聚合的邻居消息进行更新。在本文中,我们描述了合作图神经网络(Co-GNNs),这是一种新的信息传递架构,其中每个节点被视为一个玩家,可以选择‘监听’、‘广播’、‘监听 & 广播’或‘隔离’。标准的信息传递是一种特殊情况,其中每个节点‘监听 & 广播’所有邻居。我们展示了 Co-GNNs 是异步的、更具表现力的,并且可以解决标准信息传递 GNNs 的常见问题,如过度压缩和过度平滑。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Michael Bronstein

·发布于 数据科学之路 ·阅读时长 11 分钟·2023 年 12 月 6 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Co-GNNs 中节点操作的示意图:标准、监听、广播和隔离。图片来源:DALL-E 3。

本文由 Ben Finkelshtein、Ismail Ceylan 和 Xingyue Huang 共同撰写,基于论文 B. Finkelshtein 等人,合作图神经网络(2023)arXiv:2310.01267。

图神经网络(GNNs)是一类用于在图结构数据上进行学习的流行架构,例如分子、生物相互作用组和社交网络。大多数 GNN 遵循信息传递范式 [1],在每一层中,图节点沿图的边缘交换信息。每个节点的状态通过对来自相邻节点的消息进行排列不变的聚合操作(通常是求和或取均值)来更新 [2]。

虽然消息传递范式在图 ML 中具有很大影响力,但它有着公认的理论和实际限制。消息传递图神经网络(MPNN)与图同构测试的形式等价 [3] 为它们的表达能力提供了理论上的上限。因此,即使是非常简单的非同构图(例如下面的 6-圈和两个三角形)也无法通过消息传递区分,除非有额外的信息,如位置编码或结构编码 [4],或更复杂的信息传播机制 [5]。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

两个非同构图的示例,通过 1-WL(因此,通过消息传递)在没有额外信息的情况下无法区分。

消息传递与信息瓶颈

消息传递的一个实际限制与图上的信息流相关。为了从 k 跳邻居处接收信息,一个 MPNN 需要至少 k 层,这通常导致节点接收域的指数增长。增长的信息量必须被压缩为固定大小的节点嵌入,这可能导致信息丢失,称为 过度压缩 [6]。

我们最近表明,MPNN 中是否发生过度压缩取决于任务、架构选择(例如层数)和图 [7]。使图“更友好”以应对过度压缩是图 ML 文献中一种常见的技术,通用名称为 “图重连线。”

每个节点既 发送接收 来自其邻居的信息是经典消息传递的另一个限制。动态图重连线 [8]、注意力 [9] 或门控 [10] 等机制允许修改节点的邻域或降低邻居消息的权重。

合作图神经网络

在最近的一篇论文 [11] 中,我们提出了一种可学习的消息传递推广方法,允许每个节点决定如何从或向邻居传播信息,从而实现更灵活的信息流动。我们将节点视为可以在每一层采取以下动作的参与者:

标准: 广播给那些监听的邻居 监听广播的邻居。所有节点选择此动作将产生经典的消息传递方案。

监听: 监听广播的邻居。

广播: 广播给监听的邻居。

隔离: 既不监听也不广播。当所有节点都隔离时,图上不会传播任何信息,预测以节点为单位进行,类似于 DeepSets [12]。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图上的节点操作示例:所有节点广播和听(标准消息传递用“S”表示),所有节点隔离(节点预测,用“I”表示),以及混合操作的通用案例(听“L”,广播“B”,听和广播“S”,隔离“I”)。

这些操作之间的相互作用以及局部和动态改变它们的能力使整体方法比标准消息传递更丰富。它允许我们以可学习(非启发式)和任务依赖的方式将输入图与计算解耦。

为了实现这一新颖的消息传递方案,我们引入了一类新的 GNN 架构,称为合作 GNN(“Co-GNN”)。与传统 MPNN 的主要区别在于,额外的*“行动网络”为每一层的每个节点选择四个操作之一。所选择的操作会影响节点特征的更新方式,由另一个环境网络*进行更新,该网络与行动网络共同训练[13]。

Co-GNN 的优势

任务特定的: 标准消息传递根据节点的局部邻域更新节点,这完全与任务无关。通过允许每个节点只从相关邻居处接收信息,Co-GNN 可以确定最适合目标任务的计算图[14]。

定向的: 节点所能采取的行动结果相当于对输入图的特殊形式的“定向重连”。一条边可以被删除(例如,如果两个邻居都在听而没有广播);保持无向(例如,如果两个邻居都执行标准的听和广播操作);或变成有向(例如,如果一个邻居在听而其邻居在广播)。

动态的: Co-GNN 不在预先固定的计算图上操作,而是在通过选择节点操作学习到的计算图上操作,这在各层之间是动态的。每个节点学习与相关邻居交互,并且只有在这些邻居仍然相关时才这样做。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

经典 MPNN 中的计算图示例(这是我们架构的一个特殊案例,其中所有节点选择“标准”操作,用黑色轮廓表示)和 Co-GNN。这一示例展示了将消息从源节点 w 直接路由到目标节点 v 的可能性。

既基于特征也基于结构: 标准消息传递完全由图的结构决定:具有相同邻域的两个节点接收相同的聚合消息。在 Co-GNN 中情况并非如此,它可以为具有不同节点特征的两个节点学习不同的操作。这允许即使邻域相同,也为不同节点传递不同的消息。

异步的: 标准消息传递在每次迭代中同步更新所有节点,这并不总是最优的[15]。Co-GNN 的设计允许在节点之间进行异步更新。

比 1-WL 更具表现力: 对于 1-WL 无法区分的节点对,有非平凡的概率会采样不同的动作,从而使其直接邻域不同[16]。这产生了高概率的唯一节点标识符,并允许我们区分任何一对图,前提是存在注射图池化函数[17]。

适合长程任务: 长程任务需要在远距离节点之间传播信息。我们的消息传递范式可以通过学习专注于连接这两个节点的最短路径,来有效地过滤无关信息,从而最大化信息流向目标节点[18]。

可以防止过度压缩: 在我们之前的工作[19–20]中,我们将过度压缩形式化为r 层 MPNN 输出在节点u处对远处节点v的输入的缺乏敏感性。这可以通过形式为的偏导数(雅可比矩阵)的界限来量化

|∂xʳ⁾/∂x⁽⁰⁾| < c(Aʳ)ᵤᵥ,

其中xʳ⁾表示第r层节点u处的特征,c封装了与架构相关的常数(例如,激活函数的 Lipschitz 正则性、宽度等),A是归一化的邻接矩阵,捕捉图的效果。图重连技术相当于修改A以增加上界,从而减少过度压缩的影响。在 Co-GNNs 中,每个节点的动作导致有效的图重连,传递特征从一个节点到另一个节点(如上例所示),从而最大化雅可比矩阵的界限[21]。

可以防止过度平滑:过度平滑”是指节点嵌入随着消息传递层数的增加而在图中变得越来越相似的趋势。我们在[10]中表明,通过梯度门控机制可以缓解过度平滑,该机制自适应地禁用来自具有相似特征的邻居的节点更新。Co-GNNs 通过选择 BROADCAST 或 ISOLATE 动作来模拟这一机制。

实验结果

为了更好地理解我们新的消息传递方案中学习到的计算图的效果以及它如何适应不同任务,我们观察了在 Co-GNN 不同层之间保留的有向边的比例(一个经典的 MPNN 在输入图上执行消息传递,其比例为 1)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在同质图 Cora(蓝色)和异质图 Roman-empire(红色)中,每一层保留的有向边的比例显示了不同类型图中不同的自适应行为。

我们在两个数据集上训练了一个 10 层的 Co-GNN:同质Cora和异质Roman-Empire [22]。我们观察到在保留的边的比例演变方面的相反趋势。在同质数据集中,保留的边的比例随着深度逐渐减少,而在异质数据集中则增加。保留的边的比例减少意味着信息在较少的节点之间传播,我们认为这是一种应对过度平滑现象的方法,类似于梯度门控[10]。

在被认为是 GNNs 的困难测试案例的异质数据集[23]上,Co-GNNs 在各个方面都取得了最先进的结果,尽管它们使用的行动和环境网络架构相对简单,超越了更复杂的模型如 Graph Transformers。这些结果令人振奋,因为它们确立了 Co-GNNs 作为异质环境中一种强有力的方法。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

异质节点分类的性能结果(准确率 %)。前三名模型分别用红色、蓝色、灰色标记。

可视化行动

我们使用Minesweepers数据集[23]可视化 Co-GNN 在每一层的拓扑,该数据集是一个半监督节点分类任务,使用一个规则的 100×100 网格,其中每个节点连接到八个邻居节点。每个节点有一个热编码的输入特征,显示相邻地雷的数量。随机选择的 50%节点具有未知特征,用一个单独的二进制特征表示。任务是正确识别节点是否为地雷。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们观察到在早期层次(1-4),行动网络学习隔离黑色节点的正确部分,这类似于人类玩这个游戏的方式:没有邻近地雷(标记为 0 的节点)的节点在确定黑色节点是否为地雷时最初没有帮助。因此,行动网络优先处理来自网格左侧区域的信息,那里的地雷更多,因此最初主要关注对任务更具信息性的节点。

在确定最重要的信息并将其传播通过网络之后,这些信息还需要与最初标记为 0 的节点进行通信。这导致在更深的层次(7-8)中几乎完全连接的网格。

结论

我们的新型消息传递方案使每个节点可以自适应地选择其行动及其在 Co-GNNs 形式下的架构,提供了多种优势,并帮助克服了传统消息传递方法的已知缺陷。我们相信这在理论和实践方面都是一个有前景的方向。

[1] J. Gilmer 等,量子化学的神经消息传递(2017)ICML

[2] 这使得层具有置换等变性。

[3] K. Xu 等人,《图神经网络的强大能力?》(2019)ICLR,以及 C. Morris 等人,《魏斯费勒和莱曼走向神经网络:高阶图神经网络》(2019)AAAI 证明了消息传递与 B. Weisfeiler 和 A. Lehman 经典论文中描述的图同构测试之间的等价关系,《图的规范形式的化简及其中出现的代数》(1968)Nauchno-Technicheskaya Informatsia 2(9):12–16。请参见我们关于这一主题的之前的博客文章

[4] 请参见我们关于 GNNs 中结构编码的之前的博客文章。

[5] 关于这些构造的几个示例,请参见我们之前的博客文章,子图 GNNs 和拓扑消息传递。

[6] U. Alon 和 E. Yahav,图神经网络瓶颈及其实际影响(2021)ICML

[7] 从几何角度对过度挤压的理论分析首先由 J. Topping 和 F. Di Giovanni 等人 完成,通过曲率理解图上的过度挤压和瓶颈(2022),ICLR,并由 F. Di Giovanni 等人 进一步扩展,关于消息传递神经网络中的过度挤压:宽度、深度和拓扑的影响(2023),ICML。最近,过度挤压与表达能力的关系由 F. Di Giovanni 等人 链接,过度挤压如何影响 GNNs 的能力?(2023),arXiv:2306.03589。

[8] Wang 等人,点云上的动态图卷积神经网络(2019)ACM Trans. Graphics 38(5):146,同时参见我们关于潜在图学习的博客文章。

[9] P. Veličković 等人,图注意力网络(2018)ICLR

[10] K. Rusch 等人,图上的深度多率学习的梯度门控(2023)ICLR

[11] B. Finkelshtein 等人,合作图神经网络(2023)arXiv:2310.01267。

[12] M. Zaheer 等人DeepSets(2017),NIPS

[13] 动作网络预测每个节点的动作概率(参见我们论文[11]中的方程 1),然后使用直通 Gumbel-softmax 估计器对节点动作进行采样。随后,环境网络根据采样动作更新每个节点的状态,依据是我们论文[11]中的方程 1。

[14] 例如,如果任务只需要来自具有某种程度的邻居的信息,那么动作网络可以学习仅关注这些节点(参见我们论文[11]第 6.1 节中的实验)。

[15] L. Faber 和 R. Wattenhofer,图学习中的异步神经网络(2022),arXiv:2205.12245。

[16] 在我们的论文[11]中,第 5.1 条命题。采样过程引入的方差有助于区分那些 1-WL 不可区分的节点,但也使得 Co-GNN 模型仅在期望值上不变。

[17] 参见例如 A. Loukas,图神经网络不能学习的内容:深度与宽度(2020) ICLR 和 R. Abboud、R. Dimitrov 和 I. Ceylan,用于图属性预测的最短路径网络(2022) LoG

[18] 在我们的论文[11]中,第 5.2 条定理。

[19] J. Topping ,通过曲率理解图上的过度挤压和瓶颈(2022),ICLR

[20] F. Di Giovanni 关于消息传递神经网络中的过度挤压:宽度、深度和拓扑的影响(2023),ICML

[21] 在我们的论文[11]中,第 5.2 条定理。

[22] 同质性 表明节点的邻居具有与节点本身相似的属性。早期的 GNN 基准如CoraPubmed 主要是同质的。更近期的评估包括异质图,这对 GNN 来说更具挑战性。

[23] O. Platonov 对 GNN 在异质性下评估的批判性分析:我们真的在进步吗?(2023),ICLR

有关图上的深度学习的更多文章,请查看 Michael 的 其他文章 ,订阅 他的帖子 以及 YouTube 频道,获取 Medium 会员资格,或关注 MichaelBenXingyue 和* Ismail 在 Twitter 上的动态。

在你的本地硬件上理解代码

原文:towardsdatascience.com/code-understanding-on-your-own-hardware-dd38c4f266d6?source=collection_archive---------8-----------------------#2023-07-05

设置一个 LLM 来讨论你的代码——使用 LangChain 和本地硬件

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Dorian Drost

·

关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 7 月 5 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我保证你的代码不会离开你的本地硬件。照片由Clément Hélardot提供,发布在Unsplash

在今天,大型语言模型(LLMs)能够执行的各种任务中,代码理解可能对你尤其感兴趣,如果你是一名软件开发者或数据科学家。拥有一个你可以询问代码问题的聊天机器人不是很好吗?数据预处理在哪里实现的? 是否已经有验证用户身份的函数?calculate_vector_dim 和 calculate_vector_dimension 函数之间有什么区别? 你不必自己搜索正确的文件,只需问机器人,它会给你答案,并指向包含相关代码片段的文件。这种机制叫做语义搜索,你可以想象它的实用性。

在这个教程中,我将展示如何实现一个完全符合要求的 LangChain 机器人。此外,我将关注具体的数据隐私问题,即不把你的代码交出去。你或你的公司生产的代码是私有财产,可能包含敏感信息或宝贵的知识。你可能不希望,或者公司政策可能不允许你将其发送到另一个公司托管的 LLM,那个公司可能位于外国。因此,在本教程中,我将展示如何设置一个运行在本地硬件上的代码理解机器人,以便你的代码不会离开你的基础设施。

我们现在就开始吧!首先,我会给你简要介绍一下语义搜索的一般过程,然后我们再实现一个用于代码理解的机器人。

语义搜索简介

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在语义搜索中,关键是找到相关的文档。照片由 Markus Spiske 拍摄,来源于 Unsplash

首先,让我简要说明一下语义搜索的一般思路。这种方法包括两个主要步骤:检索和 LLM 本身生成答案。在检索步骤中,选择包含相关信息的文档,然后将这些文档输入 LLM 以生成自然语言答案。例如,如果你问一个关于名为transform_vectors的函数的问题,检索步骤会选择那些与回答该问题相关的文件。这可能包括实现transform_vectors函数的文件,也包括使用它的文件或提及它的文档部分。在第二步,这些文件的内容会被作为提示提供给 LLM,提示可能如下所示:

"""Answer the question below given the context. 
<document 1>
<document 2>
...
<document n>

Question: <user question>
Answer:
"""

LLM 使用来自提供的文档的信息生成自然语言答案。

这就是语义搜索的主要思想。现在我们开始实现吧!首先,我们需要安装我们的要求并读取数据。

安装要求

在我们开始之前,请确保你已经设置了运行 Python 的环境,并安装了以下软件包:

pip install langchain==0.0.191
pip install transformers

读取文档

现在我们需要读取数据并将其转换为 LangChain 可以使用的格式。在这个演示中,我将下载 LangChain 本身的代码,但你当然可以使用你自己的代码库:

import os

folder_name = "sample_code"
os.system(f"git clone https://github.com/hwchase17/langchain {folder_name}")

我们加载所有文件并将其转换为一个 Document,即每个 Document 将包含代码库中的一个文件。

from langchain.docstore.document import Document

documents = []
for root, dirs, files in os.walk(folder_name):
    for file in files:
        try:
            with open(os.path.join(root, file), "r", encoding="utf-8") as o:
                code = o.readlines()
                d = Document(page_content="\n".join(code), metadata={"source": os.path.join(root, file)})
                documents.append(d)
        except UnicodeDecodeError:
            # some files are not utf-8 encoded; let's ignore them for now.
            pass

检索

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

哪些与回答我们的提问相关?这是检索的任务来决定的。照片由 Ed Robertson 提供,来源于 Unsplash

现在我们已经创建了 Documents,我们需要对其进行索引以使其可搜索。对 Document 进行索引意味着计算一个数值向量,该向量捕捉 Document 中最相关的信息。与纯文本不同,数字向量可以用于进行数值计算,这意味着我们可以轻松计算相似度,然后用来确定哪些 Documents 对回答给定问题是相关的。

从技术层面来看,我们将利用嵌入创建的索引,并将其存储在 VectorStore 中。已有一些作为服务提供的 VectorStores(例如 DeepLake),这些服务有一些方便的优势,但在我们的场景中,我们不希望将代码交给他人,所以我们在本地机器上创建一个 VectorStore。最简单的方法是使用 Chroma,它在内存中创建一个 VectorStore 并允许我们持久化它。

from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import Chroma

hfemb = HuggingFaceEmbeddings(model_name="krlvi/sentence-t5-base-nlpl-code-x-glue")
persist_directory = "db"
db = Chroma.from_documents(documents, hfemb, persist_directory=persist_directory)
db.persist()

from_documents 函数中,索引被计算并存储在 Chroma 数据库中。下次,我们可以加载持久化的 Chroma 数据库,而不是再次调用 from_documents 函数:

db = Chroma(persist_directory=persist_directory, embedding_function=hfemb)

正如你在上面看到的,作为一个嵌入,我使用了krlvi/sentence-t5-base-nlpl-code-x-glue,这是一个在开源 GitHub 库的代码上训练的嵌入。可以想象,我们使用的嵌入必须在代码(以及其他数据)上进行过训练,以便能够利用我们提供的数据。仅在自然语言上训练的嵌入,表现可能会较差。

现在我们有了 VectorStore 和嵌入,我们可以直接从 Chroma 数据库创建检索器:

retriever = db.as_retriever()

LLM

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

LLM 必须对文档进行推理,并给出用户问题的答案。照片由 Tingey Injury Law Firm 提供,来源于 Unsplash

我们需要的最后一个组件是一个 LLM。最简单的解决方案是使用托管的 LLM,例如通过使用 OpenAI 接口。然而,我们不想将我们的代码发送到这样的托管服务。相反,我们将在自己的硬件上运行 LLM。为此,我们使用HuggingFacePipeline,它允许我们在 LangChain 框架中使用来自 HuggingFace 的模型。

from langchain import HuggingFacePipeline
import transformers

model_id = "mosaicml/mpt-7b-instruct"
config = transformers.AutoConfig.from_pretrained(model_id,trust_remote_code=True)
tokenizer = transformers.AutoTokenizer.from_pretrained(model_id)
model = transformers.AutoModelForCausalLM.from_pretrained(model_id, config=config, trust_remote_code=True)
pipe = transformers.pipeline("text-generation", model=model, tokenizer=tokenizer, max_new_tokens=100)
llm = HuggingFacePipeline(pipeline=pipe)

正如你所见,我使用了mosaic mpt-7b模型,它只需要~16GB 的 GPU 内存。我创建了一个AutoModelForCausalLM,它被传递到transformers.pipeline中,最终被转换成一个HuggingFacePipelineHuggingFacePipeline实现了与 LangChain 中典型 LLM 对象相同的接口。也就是说,你可以像使用 OpenAI LLM 接口一样使用它。

如果你的机器上有多个 GPU,你需要指定使用哪个。在这种情况下,我想使用索引为 0 的 GPU:

config.init_device="cuda:0"
model.to(device='cuda:0')
pipe = transformers.pipeline("text-generation", model=model, tokenizer=tokenizer, max_new_tokens=100, device=0)

我设置的一些额外参数可以解释如下:

  • trust_remote_code:这必须设置为 true,以允许运行来自 LangChain 之外的模型。

  • max_new_tokens:这定义了模型在回答中可能生成的最大 token 数量。如果这个值太低,模型的回答可能会在完全回答问题之前被截断。

将所有内容连接起来

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们拥有所需的所有组件。我们只需将它们连接起来。照片由John Barkiple拍摄,来源于Unsplash

现在我们拥有了所有需要的组件,并可以将它们组合成一个ConversationalRetrievalChain

from langchain.chains import ConversationalRetrievalChain

qa_chain = ConversationalRetrievalChain.from_llm(llm=llm, retriever=retriever, return_source_documents=True)

最终,我们可以查询链以回答我们的问题。结果对象将包括一个自然语言答案和一个source_documents的列表,这些文档被查阅以得出答案。

result = qa_chain({"question":"What is the return type of the create_index function in the KNNRetriever?", "chat_history":[]})
print(f"Answer: {result['answer']}")
print(f"Sources: {[x.metadata['source'] for x in result['source_documents']]}")

这是答案:

Answer:  The return type of the create_index function in the KNNRetriever is np.ndarray.
Sources: ['sample_code/langchain/retrievers/knn.py', 'sample_code/langchain/vectorstores/elastic_vector_search.py', 'sample_code/langchain/vectorstores/elastic_vector_search.py', 'sample_code/langchain/vectorstores/opensearch_vector_search.py']

总结

完成了!嗯,差不多。通过上述代码,我们现在可以提出关于源代码的问题。然而,根据你的需求,可能需要更改一些步骤。

  • 使用你自己的源代码作为Documents,而不是 LangChain 的代码。

  • 尝试不同的嵌入。如果嵌入不合适,检索器可能无法找到正确的文档,最终问题可能无法准确回答。

  • 尝试不同的模型。外面有更大、更强大的模型,但有些可能太大而无法在你的硬件上运行。你需要找到性能不错但仍能以令人满意的方式运行模型的最佳平衡点。

  • 尝试不同的Documents预处理方式以促进检索步骤*。*一个常见的例子是将它们分成相等长度的块

我相信还有更多可以尝试的,以获得更好的性能。只需动手尝试并根据你的需求调整机器人。

进一步阅读

有关使用 LangChain 进行代码理解的更多示例,请查看他们的文档:

在 HuggingFace 上,你可以找到可以在 LangChain 中轻松使用的模型和嵌入:

喜欢这篇文章吗? 关注我 以便获取我未来的文章更新。

编程曾经很难,直到我学会了这两件事!

原文:towardsdatascience.com/coding-was-hard-until-i-learned-these-2-things-1219840d0a0a

这里是帮助我从“有志编程者”变成实际获得工作领域的经历。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Natassha Selvaraj

·发布在Towards Data Science ·7 分钟阅读·2023 年 10 月 2 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来源:米哈伊尔·尼洛夫来自 Pexels

你是否曾尝试过编程却发现自己惨败?

你开始时充满热情,报名参加一个承诺教你“关于编程的所有知识”的在线课程,但课程结束后,你感觉比开始时更迷茫。

有一种障碍你就是无法突破,它让你感到恐惧、焦虑和自尊心低落。

你惊讶地看到其他人将编程看得如此简单 —— 这位程序员获得了 Twitter、Microsoft 和 Amazon 的工作机会 —— 而你却在没有观看在线教程的情况下,挣扎着写一个程序。

如果你有这样的感觉,你并不孤单。这五年前的我也曾面临同样的情况。

从那时起,我学会了编程,并获得了数据科学职位,推出了在线课程,随后开发了多个被动收入来源,这些都推动我实现了财务自由。

在我详细介绍我成为程序员的步骤之前,我想分享一个令人深思的故事,这个故事改变了我对学习编程的态度。

如果你更喜欢视频版本,请观看:

点击这里观看视频版本

那个在编程上失败的天才

在学校时,我有一个朋友,我将在本文中称呼他为“迪伦”。

迪伦和我相识已久——我从五年级起就认识他了。他是班里最聪明的孩子。如果有人需要帮助做作业或备考,大家都会去找他。

有趣的是,迪伦并不是那种会长时间学习的孩子。

他的智力看起来很自然,几乎没有费力。他只需读或听到一个概念一次,就能轻松地内化它。

迪伦在每个科目上都是班里的佼佼者。他是高级数学学生,在“公文式”补习班中比我们年级高出 4 年级。

到我们上八年级时,迪伦已经在做大学水平的数学题。

毕业后,迪伦获得了英国一所最负盛名大学的奖学金。之前我和他谈过,他告诉我他计划学习编程,因为他想成为一名软件工程师。

然而,仅仅两个月后,迪伦告诉我他的计划改变了。他不再想成为软件工程师,因为编程“不适合他”。

当他告诉我这些时,我感到非常惊讶。

班里的天才觉得编程太难了?

与此同时,我注意到许多学校成绩中等的学生最终成为了软件工程师、网页开发者和数据科学家——这些职业需要大量的编程专长。

对我来说,越来越明显的是,在编程方面有些特质比异常的技能或智力更为重要——例如毅力、心态变化和自信。

在这篇文章中,我将详细讲解我发展出的两个关键特质,这些特质帮助我成为一个更好的编码员。

这些是生活方式和心态的变化,帮助我从一个“有抱负的程序员”变成了一个需要每天编码的数据科学家。

1. 培养成长型心态

2012 年,斯坦福大学心理学家卡罗尔·德韦克创造了“成长型心态”这一术语。

本质上,德韦克解释了有两种类型的心态——固定型心态和成长型心态。

比如说你遇到一个对你来说有点过于困难的问题——当你卡住时,你的初步反应是什么样的?

你是否感到焦虑,或者因为自己不够聪明而想要放弃?

还是……你觉得自己只是还没有解决它。只需多做一点准备,你最终会做到的。

如果你属于第一类,你就拥有德韦克所描述的固定心态。固定心态的人相信天赋和智力等特质是与生俱来的。要么你有“它”,要么你没有。

让我们回到迪伦的故事。

作为一个快速学习者,这个孩子在没有努力的情况下获得了好成绩,几乎没有犯错误。

他在学校里是完美的榜样,常常因为“聪明才智”和比其他人更聪明而受到赞美。

所以……当这个家伙第一次尝试学习编程并最初遇到困难时,你认为发生了什么?

当他在网上寻找答案时,陌生人和网络上的随机人能够写出他几乎无法理解的程序?

当他在执行最基本的任务时遇到错误的困扰?

我告诉你发生了什么。

迪伦进入了“战斗或逃跑”模式。他感到沮丧。他决定编程对他来说实在不适合。

你看,迪伦体现了固定心态。他在得到认可的环境中表现良好,而在面临失败的可能性时则崩溃。

在很长一段时间里,我也是这样。

当我第一次尝试学习编程时,我认为世界上有两种人——适合编程的人和不适合编程的人。

结果证明我说对了。

然而,那些适合编程的人,并不是比你更聪明或更有天赋。

他们只是具备接受从零开始学习编程需要努力和耐心的能力。它不能一蹴而就。

换句话说,他们拥有成长心态

成长心态意味着你把自己的编程能力视为可以通过努力培养的东西。当你未能学会一个概念或遇到错误时,你不会将失败视为描述自己的方式。

相反,你应该把它视为一个学习和成长的机会。

对许多人来说,成长心态并不是自然而然就具备的。

当我第一次开始学习编程时,我会为一些简单的问题苦苦挣扎,而这些问题我的同龄人几分钟内就能解决。

这使我开始质疑自己的智力,并怀疑自己在这个领域的能力。

然而,随着时间的推移,我意识到编程与我们在学校学习的其他科目不同。

学习编程不像背诵课本以备考。你不能仅仅花几周时间学习它,然后期望在完成课程后构建全栈应用程序。

这是一个终身的努力,永无止境——一个需要反复进行的练习,每小时的付出都会带来小的改进。

我喜欢把它看作去健身房或者骑自行车。你不能通过一次健身锻炼就 overnight 增肌。你需要每周都去,并且重复这个动作,直到你变得更好。

如果你跳过一次训练或一个月不出现,你最终会失去进展并需要从头开始。

把编程当作锻炼来看待。

看到任何明显的进展可能需要几个月,但持续出现并专注于过程是唯一的进步方式。

2. 将编程变成每日习惯

既然我们已经确定了一致性是学习编程的关键,那么你如何将其转化为一个足够重复的习惯以实际看到进展?

学习编程是困难的。作为人类,研究表明我们的脑袋天生倾向于选择最简单的路径。

那么……你如何抵抗选择最简单路径的冲动,并说服自己每天都编程呢?

当然,一开始当你充满动力时,坚持成为程序员的目标很容易。困难在于,当你开始对看不到明显进展感到沮丧时,如何保持在轨道上。

帮助我将编程变成日常习惯的一个技巧来自詹姆斯·克利尔在他畅销书《原子习惯》中提供的建议。

在这本书中,詹姆斯·克利尔指出,习惯是在你重复做某事到足够次数时形成的,它会成为你日常生活的一部分。

为了将任何行动变成日常习惯,他建议设定一个“实施意图”。

实施意图

实施意图让你能够非常具体地确定你要做什么、何时做以及在哪里做。

例如,不要说“我要学习编程”,而是说“我要每天从上午 11 点到下午 4 点在我家附近的咖啡馆编程。”

这个实施意图对我来说是具体的,但你可以为自己制定一个类似的并坚持下去。

这样的实施意图可以确保你将编程融入日常生活,并坚持下去,即使在那些你真的不想做的日子里。

我发现有用的另一个技巧叫做诱惑捆绑

诱惑捆绑

这就是将你必须做的事情与享受做的事情结合起来的行动,以便将其变成日常习惯。

例如,我们大多数人发现去健身房很困难且不愉快。然而,我们喜欢看 Netflix 和听音乐。

如果你将观看自己最喜欢的电视节目与每天晚上锻炼的任务结合起来,你可以让大脑期待锻炼的时间。

作为咖啡爱好者,我只会在编程时才允许自己喝冰拿铁,这让我期待打开电脑和写代码的时刻。

这帮助我每天早晨起床并编程,最终把这变成了一个我喜欢的日常习惯。

收获

总之,学习编程是一项困难的任务,需要时间和大量的练习。

刚开始时可能没有什么回报,你投入的时间也很少见成效。

然而,就像去健身房或骑自行车一样,你必须把它变成日常习惯。关注过程而不是执着于结果,你会随着时间的推移不可避免地进步。

数据科学中的认知偏见:类别规模偏见

原文:towardsdatascience.com/cognitive-biases-in-data-science-the-category-size-bias-8dbd851608c3

数据偏见黑客

数据科学家的偏见破解指南

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Maham Haroon

·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 11 月 29 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Andy Li 拍摄的照片,来源于 Unsplash

想象一下,你发现自己置身于一个风景如画的街区,那里有两家面包店。第一家是一家小型家族经营的面包店,温馨地坐落在街角。第二家则是一座宏伟的三层大楼,招牌上展示了其丰富的选择和先进的烤箱。

当你开始寻找完美的面包时,你被那家高大的面包店所吸引。建筑的巨大和宏伟给人留下了深刻的印象,使你假设较大的面包店一定生产出最好的面包。

在这个情境中,你无意中屈服于一种被称为类别规模偏见的心理倾向。这种偏见让你相信,较大的面包店更可能提供优质的面包。

实际上,面包店的规模与其面包的质量并不一定相关。较小的家族经营的面包店可能拥有一份代代相传的秘密配方,而较大的面包店则可能更注重数量而非手工艺。

这种偏见反映了我们将更大类别与更好结果联系起来的倾向,即使这些类别中的具体特征可能与我们的假设不符。这种现象被称为类别规模偏见。

类别大小偏差指的是我们倾向于将结果视为更可能发生的情况,当它们属于较大类别时,而不是较小类别,即使每个结果的可能性是相等的。

尽管这种偏差基于经过实验验证的研究,但对证据的解释仍然存在持续的变化。

在数据科学领域,类别大小偏差可能通过特定假设表现出来。例如:

假设 1:较大、更复杂的模型总是比较小的模型提供更好的预测。

在类别大小偏差的背景下,通常认为神经网络或机器学习模型的性能随着其规模或复杂性而提高。因此,无论数据或任务是否符合模型的特性,人们往往会关注复杂模型。这种倾向类似于从众效应,即认为更新、更复杂和更知名的算法是最前沿的,即使它们并不适合某些任务。例如,考虑使用大型语言模型(LLMs)来处理相对简单的任务。

例如,向神经网络中添加额外的层,即使它们实际上并没有使模型变得更好,因为大多数任务只需两层就能很好地处理。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来源于作者

警告: 需要注意的是,大型、更复杂的模型往往能够提供更好的预测,这在许多情况下是成立的,特别是在处理需要高度精确度或涉及庞大且多样化数据集的复杂任务时。

权衡: 在这种情况下的权衡包括性能和资源消耗。尽管复杂或更大的模型需要更多的资源,但增加的资源消耗并不会自动转化为更好的模型性能。尽管这在所有情况下可能不是必要的或有影响的,但在关键问题中,考虑这些因素确实可能会带来显著的差异。

假设 2:仅依赖较高的准确率忽视类别不平衡

这个问题是一个被广泛认可的问题。在使用诸如准确率之类的指标时,更容易忽视潜在的偏差。举例来说,考虑一个包含 1000 名患者的数据库,其中 5 名患者有某种疾病。如果一个模型通过始终将几乎所有实例都分类为阴性(因为大多数实例是阴性的)来实现 99.5%的准确率,那么可能会认为这个模型表现良好。然而,实际上,这个模型的表现是不足的,因为它没有将任何实例分类为阳性。

进一步说明,我们来看一个基本的例子。我们将生成 100 个 0 到 1 之间的随机数。如果一个数字超过 0.92,我们称其为正类;否则为负类。我们将使用逻辑回归作为我们的模型。下面的代码片段演示了这种情况:

import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, roc_auc_score

# Generate synthetic data
np.random.seed(42)
thresh = 0.92
X = np.random.uniform(0, 1, 100).reshape(-1, 1)
y = (X > thresh).astype(int).ravel()

model = LogisticRegression()
model.fit(X, y)

y_pred = model.predict(X)
y_prob = model.predict_proba(X)[:, 1]

#Calulcate performance metrics
accuracy = accuracy_score(y, y_pred)
precision, recall, _ = precision_recall_curve(y, y_prob)
average_precision = average_precision_score(y, y_prob)
f1 = f1_score(y, y_pred)

尽管模型达到令人满意的准确率 0.92,但检查其他指标如召回率和 f1-score 却显示性能远远不如。在左侧绘制的图表中,明显没有任何正实例被正确分类为正类。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图像

警告: 尽管准确性通常是一个很好的衡量标准,但在某些情况下,优先考虑更高的准确性是合理的。在类不平衡最小且误分类成本相对较低的情况下,优先考虑准确性可能是一个合理的选择。

权衡: 在这种情况下的权衡集中在性能本身,涉及到相当大的风险。然而,在轻微不平衡或数据已处理的情况下,这种担忧可能不那么重要。

假设 3:将更大的数据集与性能改进等同起来

虽然较大的数据集通常带来更多特征、附加信息和更高的现实预测概率,但这种情况只在一定程度上成立。所需的数据量取决于具体问题。例如,在分类任务中,类较少或有很多有用特征的情况可能适用于较小的数据集。同样,涉及低阶函数近似的任务可能对较小的数据集感到满意。

让我们通过一个例子来理解这个问题。首先,我们生成虚假的数据,其中一半是有用的,包含 5 个类别,大约 20,000 个样本。

import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.datasets import make_classification
from sklearn.metrics import accuracy_score

# Generate a synthetic dataset
samples = 20000
X, y = make_classification(n_samples=samples, n_features=10, n_informative=5, n_clusters_per_class=5, random_state=42)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
indices = np.arange(X_train.shape[0])
np.random.shuffle(indices) 

随后,我们使用支持向量分类(SVC)对数据子集进行类别分离,特别是 9,000 个实例:

count_small = 9000
X_train_small = X_train[:count_small]
y_train_small = y_train[:count_small]
start_time = time.time()

model_small = SVC(probability =True)
model_small.fit(X_train_small, y_train_small)  # Using only a small subset for training

y_pred_small = model_small.predict(X_test)
accuracy_small = accuracy_score(y_test, y_pred_small) 

在测量该任务所用的时间时,我们观察到 6.9590 秒,准确率为 0.8430。随后,我们利用整个数据集来训练 SVC,包括大约 16,000 个实例用于训练和 4,000 个实例用于测试:

model_large = SVC(probability =True)
model_large.fit(X_train, y_train)

y_pred_large = model_large.predict(X_test)
accuracy_large = accuracy_score(y_test, y_pred_large)

这次,算法花费了 20.4149 秒,约为之前时间的 3 倍,准确率为 0.8452——尽管数据量几乎翻倍,但结果非常接近。对两个模型的 ROC 曲线进行比较,结果几乎相同。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图像

警告: 复杂任务通常需要更多数据,但有一个点是更多的数据不一定意味着更好的结果。

权衡: 尽管额外的数据很少会对模型造成伤害,但权衡在于计算成本,而不是性能。当模型中添加无关的信息时,这种成本可能很高,这突显了数据预处理和清理的重要性,特别是在复杂任务中。

假设 4:将较长、更复杂的算法等同于更优性能

尽管并非总是如此,但存在一种观点,认为较长、复杂的算法优于较短、简单的算法。有时,即使一个简单的算法可以完美地完成任务,仍会偏向于使用更复杂的算法。尽管这种观点并非总是毫无根据,但问题在于这种信念的根本理由。如果算法确实需要复杂性和长度,那么应用这些特性也无可厚非。

为了说明这一假设,我们比较两个代码块。第一个函数看起来相当简单:

def is_even_simple(num):
    return num % 2 == 0

另一方面,第二个函数似乎涉及更多工作,时间和空间复杂度均有所体现。然而,两个函数的最终目标是完全相同的:

def is_even_complex(num):
    if num < 0:
        return False
    elif num == 0:
        return True
    else:
        while num >= 2:
            num -= 2
        if num == 0:
            return True
        else:
            return False

我故意将这个例子保持简单,但这一理念同样适用于可能过于关注特定问题或做不同事情的更复杂的代码。

警告: 这不适用于添加单元测试、注释或真正提升代码质量的改进。此外,较长且复杂的算法在某些情况下并非毫无依据地被认为更优。对于那些本质上需要细致决策边界或涉及复杂关系的任务,更复杂的算法可能确实是必要的。

权衡: 这种偏差的代价通常表现为计算资源的消耗。虽然它可能不会显著影响较简单的任务,但对于更复杂的任务,开销变得更加明显。

避免类别大小偏差:提高意识和缓解策略

尽管类别大小偏差可能不是最具破坏性的偏差,但它可能导致资源消耗。

将问题分解为更简单、更小的任务,并在可能的情况下从那里开始,以更清晰地理解潜在问题,这是更好的做法。

意识到我们潜意识中倾向于偏袒某个选项是避免陷入偏见的关键。一种有价值的方法是挑战假设,利用苏格拉底式提问技术。

理解苏格拉底式提问:全面指南 [## 理解苏格拉底式提问:全面指南

苏格拉底式提问鼓励人们更深入地思考问题,超越自身的视角……

理解苏格拉底式提问:全面指南

另一种方法是尝试证明与最初偏好的假设相反的假设。

花时间逐个评估数据,并将每个问题区别对待,可以提供宝贵的见解。

总结:

总之,本文突出了类别规模偏差在数据科学领域中的影响。关键的结论是,要时刻留意这些偏差,并始终考虑问题的背景。

即将到来…

我们的分析不仅仅依赖于算法和模型,还受到深层次的认知偏差的重大影响。本文探讨了类别规模偏差及其对数据科学决策的影响。然而,这只是个开始。在本系列的后续文章中,我将专注于揭示更多认知偏差及其对数据研究和分析的影响。从因果关系的假设到轶事证据的吸引力,从确认偏差到从众效应,这些文章将探索人类偏差与数据科学的交集。

我的目标是对可能影响我们分析追求的偏差进行更深入的研究,并促进更细致和偏差意识的数据科学实践。

我添加了一些额外的资源或进一步的探索。

资源

[## 认知偏差和启发式列表 - 认知决策实验室

以下是行为科学领域中最重要的认知偏差和启发式的列表。

认知偏差列表:180 多种启发式的可视化 [## 认知偏差列表:180 多种启发式的可视化

认知偏差是指倾向于选择性地搜索或解释数据,以确认现有的观点…

www.teachthought.com [## 推理阶梯 - 如何避免仓促得出结论

使用推理阶梯来探索我们在思维中从事实到决策或结论所经过的七个步骤…

www.mindtools.com

使用 Lang-SAM 和深度学习在图像中求和硬币值

原文:towardsdatascience.com/coin-counting-using-lang-sam-b469827808a7?source=collection_archive---------9-----------------------#2023-10-03

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Gamze Zorlubas

·

关注 发表在 Towards Data Science ·10 min read·Oct 3, 2023

在计算机视觉的最新进展中,图像分割取得了显著的进展。一个突出例子是“Segment Anything”模型(SAM),这是一个动态深度学习工具,通过输入提示从图像中预测对象掩码。得益于其先进的编码和解码能力,SAM 能够处理各种分割挑战,对研究人员和开发者都极为宝贵。

Lang-SAM 是一个基于 SAM 的项目。它通过文本提示提取图像中所有对象实例的掩码。它智能地结合了文本描述,弥合了自然语言处理和计算机视觉之间的差距。这种融合允许更加上下文感知、精确和详细的分割,将复杂的图像挑战扩展到超越传统能力的范围。

在探索了 SAM 模型的能力后,我发现了一个典型的用例:估算包含各种其他对象的图像中硬币的总价值。让我们深入了解 SAM 的操作,并查看我如何将其应用到我的硬币计数项目中,以生成数据集和测试神经网络。

1. Segment-Anything

Facebook 的研究团队 FAIR 在 2022 年推出了他们的分割模型 SAM。SAM 令人惊叹的是,它能够识别和分离图像的部分,而不是专门为此训练的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1:由 Meta 提供的 SAM 模型架构,通过segment-anything.com/下载

SAM 的核心有三个主要部分:它理解图像,接受提示或命令,然后根据该命令创建掩码。为了训练 SAM,Facebook 创建了有史以来最大的图像数据集 SA-1B,通过详细的三步过程。技术上,SAM 使用了与其他流行模型类似的系统,但具有自己独特的特征。有时在给定模糊命令时,它会进行多次猜测并选择最佳结果。在测试中,SAM 在 23 个不同数据集上的表现优于其他模型。它们甚至将 SAM 与其他工具结合使用,用于寻找和突出图像中的特定对象等任务。

尽管 SAM 使用文本编码器进行了文本提示训练,但 Meta 尚未发布带有文本编码器的权重。因此,当前公开模型中仅提供框或点提示。

2. Language-Segment-Anything (Lang-SAM)

为了解决 SAM 的文本提示问题,Luca Medeiras 创建了一个名为Language-Segment-Anything (Lang-SAM)的开源项目。Lang-SAM 依次部署了 GroundingDino 和 SAM。GroundingDino 是一个文本到边界框的模型,用户输入图像和文本提示,该模型根据文本提示找到这些对象的掩码。这些边界框然后用作 SAM 模型的输入提示,SAM 生成识别对象的精确分割掩码。

以下是用 Python 运行 Lang-SAM 的代码片段:

from  PIL  import  Image
from lang_sam import LangSAM
from lang_sam.utils import draw_image

# Initialize LangSAM model
model = LangSAM()
# Load the image and convert it to RGB
image_pil = Image.open('./assets/image.jpeg').convert("RGB")
# Set the text prompt for the segmentation
text_prompt = 'bicycle'
# Perform prediction to obtain masks, bounding boxes, labels, and logits
masks, boxes, labels, logits = model.predict(image_pil, text_prompt)
# Draw segmented image using the utility function
image = draw_image(image_pil, masks, boxes, labels)

使用上述代码,我对图像中的自行车进行了分割测试。结果在下图中可视化。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2:Lang-SAM 的示例分割结果 — 作者提供的图像

Lang-SAM 的用例:硬币总和计数

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3:硬币计数工作流程 — 作者提供的图像

首先,我们决定硬币计数的工作流程。大致来说,我们将有包含各种硬币的图像。

作为工作流程的第一步,我们可以对输入图像中的每个硬币进行分割。此步骤可以通过使用 Lang-SAM 来完成,因为它允许我们简单地输入“硬币”作为文本提示。在获得硬币掩码后,我们可以使用卷积神经网络来估算硬币的类别。这个神经网络可以是一个自定义的网络,我们用使用 Lang-SAM 生成的数据集来训练。架构细节和训练方法在第 2 步中给出。在最后一步中,估算的类别将被简单地汇总。

第 1 步:使用 Lang-SAM

为了在图像中分割硬币,我编写了以下函数,该函数以图像作为输入,通过使用 Lang-SAM 模型返回每个硬币的掩码和框。单个硬币的框仅在后续的可视化过程中使用,因此目前并不重要。

def find_coin_masks(image):
    # Suppress warning messages
    warnings.filterwarnings("ignore")
    text_prompt = "coin"
    try:
        model = LangSAM()
        masks, boxes, _, _ = model.predict(image, text_prompt)

        if len(masks) == 0:
            print(f"No objects of the '{text_prompt}' prompt detected in the image.")
        else:
            # Convert masks to numpy arrays
            masks_np = [mask.squeeze().cpu().numpy() for mask in masks]
            boxes_np = [box.squeeze().cpu().numpy() for box in boxes]
            return masks_np, boxes_np

    except (requests.exceptions.RequestException, IOError) as e:
        print(f"Error: {e}") 

从包含多个硬币的输入图像中,提供的上述函数生成了分割掩码,如下图所示。然而,生成的分割掩码为图像的原始分辨率。由于分割图像的 95%左右被空白区域占据,这可能被视为冗余信息。这种过多的数据在输入到神经网络进行后续训练阶段时会造成计算挑战。为了解决这个问题,我将引入一个后续函数,以裁剪和聚焦相关的分割区域,优化数据以便进一步处理。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4:find_coin_masks 函数的输入和输出 — 作者提供的图像

我创建了另一个名为generate_coin_images的函数。该函数首先使用find_coin_mask获取原始大小的掩码。接着,它裁剪掉掩码周围的黑色区域。最终掩码被调整为 500x500 像素的标准大小。如果包含硬币的区域大于这个尺寸,它会调整以适应 500x500 的大小,确保我们在下一步中有一致的输入。

def generate_coin_images(image_dir):
    # Load the image and convert it to RGB format
    image = Image.open(image_dir).convert("RGB")

    # Use the previously defined function to obtain masks and bounding boxes
    masks, boxes = find_coin_masks(image)

    # Convert image to a numpy array for further processing
    image = np.array(image)

    # List to store final coin images
    coins = []
    for index in range(len(masks)):
        # Apply mask to image and obtain relevant segment
        mask = np.broadcast_to(np.expand_dims(masks[index],-1), image.shape)
        masked_image = mask * image

        # Find the bounding box coordinates for the non-zero pixels in the masked image
        nonzero_indices = np.nonzero(masked_image[:,:,0])
        nonzero_indices = np.array(nonzero_indices)
        y_min, y_max, x_min, x_max = find_boundary_of_coin(nonzero_indices)

        # Crop the masked image to the bounding box size
        masked_image = masked_image[y_min:y_max,x_min:x_max]  
        # Creating a 500x500 mask 
        if (y_max - y_min)<500 and (x_max - x_min)<500:
            difference_y = 500 - (y_max - y_min)
            difference_x = 500 - (x_max - x_min)
            if difference_y != 0:
                if difference_y % 2 == 0:
                    masked_image = np.pad(masked_image, [(difference_y//2, difference_y//2), (0, 0), (0, 0)])
                else:
                    masked_image = np.pad(masked_image, [((difference_y-1)//2, (difference_y-1)//2 + 1), (0, 0), (0, 0)])
            if difference_x != 0:
                if difference_x % 2 == 0:
                    masked_image = np.pad(masked_image, [(0, 0), (difference_x//2, difference_x//2), (0, 0)])
                else:
                    masked_image = np.pad(masked_image, [(0, 0), ((difference_x-1)//2, (difference_x-1)//2 + 1), (0, 0)])
            coins.append(masked_image)
        else:
            dim = (500, 500)
            resized_masked_image = cv2.resize(masked_image, dim, interpolation = cv2.INTER_AREA)
            coins.append(resized_masked_image)

    return coins, boxes

generate_coin_images函数生成硬币图像,如下所示。稍后我们将在创建数据集以训练神经网络时以及在测试流程中使用此函数。我们可以说这个函数是项目的核心。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5:generate_coin_images 函数的输入和输出 — 作者提供的图像

第 2 步:创建硬币估算神经网络

第 2.1 步:数据集生成

认识到缺乏专门的欧洲硬币数据集,我主动为我的项目创建了一个。我从这个GitHub 页面获取了六种不同欧洲硬币面值的照片:2 欧元、1 欧元、50 分、20 分、10 分和 5 分。每张图片只包含一枚硬币,确保数据集的一致性。

利用generate_coin_image函数(我之前描述过的),我提取并保存了每个硬币的掩码版本。这些图像然后被系统地组织到以各自面额为基础的文件夹中。

为了清晰起见,训练数据集由 2,739 张图像组成,分布在六个类别中,如下所示:

  • 2 欧元:292 张图像

  • 1 欧元:301 张图像

  • 50 美分:747 张图像

  • 20 美分:444 张图像

  • 10 美分:662 张图像

  • 5 美分:293 张图像

验证集由 73 张图像组成,分布在六个类别中,如下所示:

  • 2 欧元:5 张图像

  • 1 欧元:12 张图像

  • 50 美分:8 张图像

  • 20 美分:17 张图像

  • 10 美分:16 张图像

  • 5 美分:15 张图像

output_dir = "coin_dataset/training/"
dataset_dir = "coin_images/"
subfolders = os.listdir(dataset_dir) 

for subfolder in subfolders:
    files = os.listdir(os.path.join(dataset_dir,subfolder)) 
    if '.DS_Store' in files:
        files.remove('.DS_Store')
    if '.git' in files:
        files.remove('.git')
    files = [file for file in files if file.endswith('.jpg') or file.endswith('.png')] 

    for file in files:

        # Generate coin images with generate_coin_images function and loop through them
        padded_coins, boxes = generate_coin_images(os.path.join(dataset_dir,subfolder,file))

        for padded_coin in padded_coins:

            # Convert the numpy array image back to PIL Image object
            image = Image.fromarray((padded_coin).astype(np.uint8))
            if os._exists(os.path.join(output_dir, subfolder, '.DS_Store')):
                os.remove(os.path.join(output_dir, subfolder, '.DS_Store'))
            last_index = find_last_index(os.listdir(os.path.join(output_dir, subfolder)))
            image_name = f"img_{last_index+1}.png"
            subfolder_for_padded_coins = os.path.join(output_dir, subfolder, image_name)
            image.save(subfolder_for_padded_coins)

下图提供了我们分割程序的可视化表示,显示了处理 1 欧元硬币照片以创建数据集的过程。分割后,单个硬币图像被存储在‘1e/’目录中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6:数据集生成工作流程的输入和输出 — 作者提供的图片

步骤 2.2:训练

神经网络的架构包括两个主要组件:几个卷积层,用于从输入图像中提取空间特征,以及两个密集层,负责最终分类。

具体来说,网络从一个 500x500x3 形状的 RGB 输入图像开始。随着网络经过卷积层,通道数增加,每个卷积后跟一个 ReLU 激活。通过在这些层中使用步幅为 2,特征图的空间维度在每个阶段都减少,产生编码效果。

在卷积阶段之后,空间特征被展平并传递到两个全连接层。最终层的输出提供了一个跨类别的概率分布,使用 softmax 激活。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7:硬币估计器的神经网络架构 — 作者提供的图片

我使用 Adam 优化器和交叉熵损失来训练模型。训练持续进行,直到验证损失出现饱和点,这发生在第 15 个 epoch。

步骤 2.3:性能基准测试

在完成训练后,我利用下面提供的脚本对最后一个 epoch 的检查点进行了基准测试。我使用了下面提供的compute_accuracy函数,该函数接受模型和数据加载器作为参数,并计算给定数据集中的准确预测百分比。

def compute_accuracy(model, data_loader, device):
    correct_predictions = 0
    total_predictions = 0

    with torch.no_grad():
        for inputs, labels in data_loader:
            inputs, labels = inputs.to(device), labels.to(device)

            # Forward pass
            outputs = model(inputs)

            # Get the predicted class index by finding the max value in the output tensor along dimension 1
            _, predicted = torch.max(outputs.data, 1)  
            total_predictions += labels.size(0)

            # Update correct predictions count: 
            # Sum up all instances where the predicted class index equals the true class index
            correct_predictions += (predicted == labels).sum().item()

    return (correct_predictions / total_predictions) * 100

# Compute the accuracy on the training dataset and validation sets
train_accuracy = compute_accuracy(model, train_loader, device)
val_accuracy = compute_accuracy(model, val_loader, device)

print(f"Training set accuracy: {train_accuracy:.2f}%")
print(f"Validation set accuracy: {val_accuracy:.2f}%")

随后计算的训练集和验证集的平均准确率如下:

  • 训练集:87%

  • 验证集:95%

验证准确率超过了训练准确率,这可能是由于验证集相对较小。值得注意的是,项目的主要目的是展示新分割模型的潜在应用,而不是构建一个高性能的硬币估计网络。因此,对这些观察结果的深入分析将不会进行。

第 3 步:硬币计数的流程

在训练了硬币估计网络后,图 3 中概述的所有工作流程步骤现已完成。现在,让我们构建一个从头到尾利用 Lang-SAM 和我们的自定义神经网络(NN)的流程,旨在计算图像中硬币的总价值。

我创建了一个名为coin_counter.ipynb的 Python 笔记本,该笔记本指导了计数步骤。就像我们在创建数据集的过程中一样,最初使用generate_coin_images函数为图像中的每个硬币生成掩膜。然后,这些掩膜会被逐一输入到硬币估计网络中。最后,将预测的硬币值相加,以找出图像中的总金额。

测试结果

图 3 中的硬币图像被输入到硬币计数流程中。下方的图像包含了估计的类别叠加。虽然有些估计不准确,但总体性能是可以接受的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如前所述,本项目的主要目标是展示一种可以接受文本输入的新分割模型的潜在应用,而不是构建一个高性能的硬币估计网络。

这是我的Github repo,你可以在其中找到本博客中使用的代码。

感谢阅读我的博客!

协整与相关性

原文:towardsdatascience.com/cointegration-vs-spurious-correlation-understand-the-difference-for-accurate-analysis-82727ad7cbc3

为什么相关性不等于因果关系

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Egor Howell

·发表于 数据科学前沿 ·6 分钟阅读·2023 年 7 月 17 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Wance Paleri 提供,来源于 Unsplash

背景

在时间序列分析中,了解一个序列是否影响另一个序列是有价值的。例如,对于商品交易者来说,知道 商品 A 的增加是否会导致 商品 B 的增加是很有用的。最初,这种关系是通过线性回归来测量的,但在 1980 年代,Clive GrangerPaul Newbold 证明了这种方法会产生错误结果,特别是对于 非平稳 时间序列。因此,他们提出了 协整 的概念,这为 Granger 赢得了诺贝尔奖。在这篇文章中,我想讨论协整的必要性和应用,以及为什么这是数据科学家应该理解的重要概念。

虚假相关

概述

在讨论协整之前,让我们先讨论一下它的必要性。历史上,统计学家和经济学家使用 线性回归 来确定不同时间序列之间的关系。然而,Granger 和 Newbold 表明这种方法是错误的,会导致所谓的 虚假相关

虚假相关指的是两个时间序列看起来相关,但实际上缺乏因果关系。这是经典的‘相关性不意味着因果性’声明。这是危险的,因为即使是统计测试也可能会表示存在一个因果关系

例子

下面的图示展示了虚假关系的一个例子:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由作者在 Python 中生成的图。

这里我们有两个时间序列A(t)B(t),分别以时间为函数(左)和彼此对比(右)进行绘制。从右侧的图中可以看出,回归线显示序列之间存在某种相关性。然而,通过查看左侧图,我们发现这种相关性是虚假的,因为***B(t)持续增加,而A(t)***则不规则波动。此外,两时间序列之间的平均距离也在增加。因此,它们可能是相关的,但没有因果关系的证据。

查看这里以获取更多虚假相关的例子。我最喜欢的是视频游戏销售与核能生产的例子!

原因

虚假相关发生的原因有几个:

  • 纯粹的运气、偶然或巧合。

  • 样本时间序列数据无法充分代表总体时间序列。

  • 两个时间序列 A B*,由一个未观测的第三个时间序列* C* 驱动。因此,* C 导致了 A B*,因此看起来像是* A 导致 B 或反之亦然

什么是协整?

概述

协整是一种技术,可以帮助我们区分两个时间序列是否存在长期关系,还是只是虚假相关。它不仅测量序列是否一起移动,而是专注于确定它们均值之间的差异是否保持一致。

理论

如果两个时间序列之间存在一种线性组合,使得结果序列的积分低于两个单独序列的积分,那么这两个时间序列被认为是协整的。这里的积分是指序列的平稳程度,而不是微积分。

例如,如果两个序列具有I(1) 积分顺序(非平稳),那么如果存在某种线性组合使结果序列为I(0)(平稳),则这两个时间序列被认为是协整的。

查看这里以获取有关积分顺序的更详细解释。

因此,如果我们有两个时间序列A(t)B(t),如果存在一个β 规模系数能生成一个平稳过程,则这两个时间序列被认为是协整的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由 LaTeX 生成的方程。

如果这是正确的,那么存在一种可能性,即***A(t)B(t)***确实有长期的因果关系。

如果你想了解更多关于平稳性的内容,可以查看我之前的帖子:

时间序列平稳性简单解释

对于时间序列建模中平稳性需求的一个简单直观的解释。

towardsdatascience.com

示例

下面绘制的是两个协整序列的示例:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图由作者在 Python 中生成。

注意序列间的均值距离保持一致。实际上,如果我们将B(t)乘以2, β=2, 得到的结果是:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图由作者在 Python 中生成。

这两个序列完全重叠!因此,我们可以说它们是协整的。

这是一个完美的玩具例子,实际上没有两个序列会完全重合。

协整测试:Engle-Granger 两步法

概述与理论

最常见的协整测试是Engle-Granger 测试。它测量两个序列的线性和的残差是否平稳。

例如,回到上述方程,假设线性组合A(t)B(t)导致一个平稳序列u(t)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

公式由 LaTeX 生成。

系数β可以通过对***A(t)B(t)***进行线性回归拟合来计算。这是标准的OLS过程:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

公式由 LaTeX 生成。

其中c是截距项。

我们可以通过进行统计测试来验证***u(t)***是否确实平稳。最常见的平稳性测试是单位根检验,它是Augmented Dickey-Fuller (ADF)测试。

假设是:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

公式由 LaTeX 生成。

示例

让我们通过一个简单的玩具例子来使这个理论更加具体:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图由作者在 Python 中生成。

首先,我们将对A(t)B(t)进行回归,以使用 OLS找到β:

GitHub Gist by author.

输出结果为:

LinregressResult(slope=1.000392464678179, intercept=0.31083202511773855, rvalue=0.9629500869656515, pvalue=4.162670194519794e-11, stderr=0.06795014678046259, intercept_stderr=0.9311636662243622)So our value is β=2.

现在使用β=1.0004, 我们可以计算两个序列的残差,并将这些残差进行 ADF 测试,以确定它们是否平稳:

GitHub Gist by author.

输出:

ADF Statistic:  -1.9502125507110546
P-Value:  0.30882039870947364
Critical Values:
 1%: -4.14
 5%: -3.15
 10%: -2.71

由于我们的 ADF 统计量大于 10% 置信区间,因此这两个系列不具备协整性

如果你想了解关于置信区间的内容,请查看我之前的文章:

## 置信区间简单解释

对置信区间的简要解释。

towardsdatascience.com

其他测试

Engle-Granger 检验的问题在于它仅测量两个时间序列之间的协整。然而,像Johansen 检验这样的检验用于确定多个时间序列之间的协整。

这里获取更多关于 Johansen 检验的信息。

总结与进一步思考

协整是时间序列分析中的一个重要工具,它使数据科学家能够区分系列之间真实的长期因果关系与虚假的相关性。这是一个有用的概念,特别是对那些在金融和交易公司工作的数据科学家来说,确实值得深入了解。

本博客中使用的完整代码可以在我的 GitHub 上找到:

[## Medium-Articles/Time Series/Time Series Tools/cointegration.py 在主分支 · egorhowell/Medium-Articles

我在 Medium 博客/文章中使用的代码。通过创建一个帐户来贡献于 egorhowell/Medium-Articles 的开发…

github.com](https://github.com/egorhowell/Medium-Articles/blob/main/Time%20Series/Time%20Series%20Tools/cointegration.py?source=post_page-----82727ad7cbc3--------------------------------)

参考文献及进一步阅读

另一个话题!

我有一个免费的通讯,Dishing the Data,在其中我每周分享成为更好数据科学家的技巧。没有“空洞”的内容或“点击诱饵”,只有来自实践数据科学家的纯粹可操作的见解。

[## 数据分析 | Egor Howell | Substack

如何成为更好的数据科学家。点击阅读由 Egor Howell 编写的 Dishing The Data,这是一个 Substack 出版物…

newsletter.egorhowell.com

与我联系!

在 Raspberry Pi 上使用 Apache Airflow 收集数据

原文:towardsdatascience.com/collecting-data-with-apache-airflow-on-a-raspberry-pi-0ac3f72e377f

一个 Raspberry Pi 就能满足你的需求

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Dmitrii Eliuseev

·发表在 Towards Data Science ·8 分钟阅读·2023 年 10 月 21 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Raspberry Pi Zero(2021 型号),图片来源 维基百科

通常,我们需要在一定时间内收集一些数据。这些数据可以来自物联网传感器、社交网络的统计数据或其他来源。举个例子,YouTube Data API 允许我们获取任何频道当前的观看次数和订阅者数量,但分析和历史数据仅对频道所有者可用。因此,如果我们想要获取这些频道的每周或每月总结数据,我们需要自己收集这些数据。对于物联网传感器,可能根本没有 API,我们也需要自己收集和保存数据。在这篇文章中,我将展示如何在 Raspberry Pi 上配置 Apache Airflow,这样可以在长时间内运行任务,而无需依赖任何云服务提供商。

显然,如果你在一家大公司工作,你可能不需要 Raspberry Pi。在这种情况下,如果你需要额外的云实例,只需为你的 MLOps 部门创建一个 Jira 工单即可 😉 但对于一个小项目或低预算的创业公司,它可能是一个有趣的解决方案。

让我们看看它是如何工作的。

Raspberry Pi

那么 Raspberry Pi 到底是什么?对于那些在过去 10 年里从未对硬件感兴趣的读者(第一款 Raspberry Pi 模型是在 2012 年推出的),我可以简要地解释一下,这是一款运行完整 Linux 系统的单板计算机。通常,Raspberry Pi 配备有 1GHz 的 2–4 核 ARM CPU 和 1–8 MB 的 RAM。它小巧、便宜且安静;没有风扇,也没有磁盘驱动器(操作系统从 Micro SD 卡运行)。Raspberry Pi 只需要一个标准的 USB 电源;它可以通过 Wi-Fi 或以太网连接到网络,并可以在几个月甚至几年内运行各种任务。

对于我的数据科学小项目,我想在 2 周内收集 YouTube 频道统计信息。对于一个每天只需 30-60 秒的任务,无服务器架构可以是一个完美的解决方案,我们可以使用类似Google Cloud Function的服务。但 Google 的每个教程都以“启用项目计费”开始。Google 提供了免费首信用和免费配额,但我不想再有另一个监控花费和请求量的麻烦,因此决定使用Raspberry Pi。结果发现 Raspberry Pi 是一个出色的数据科学工具,用于收集数据。这台$50 的单板计算机仅消耗 2W 的功率;它体积小,安静,可以放在任何地方。最后但同样重要的是,我家里已经有了一台 Raspberry Pi,因此我的成本几乎为零。我只需将电源插入插座,云计算问题就解决了 😉

市场上有不同的 Raspberry Pi 型号,在撰写本文时,Raspberry Pi 4 和 Raspberry Pi 5 是最强大的。但对于不需要大量请求或“重”后处理的任务,最早的型号也能完成工作。

Apache Airflow

对于我的小项目,我有一个包含 3000 个 YouTube 频道的列表,而 YouTube 数据 API 限制为每天 10000 个请求。因此,我决定每天收集数据两次。如果我们想要每天运行 Python 代码两次,可以使用 CRON 作业或仅在主应用循环中添加time.sleep(12*60*60)。但一种更有效和有趣的方法是使用Apache Airflow。Apache Airflow 是一个专业的工作流管理平台,它也是开源的,且免费使用。

使用pip命令在 Raspberry Pi 上安装 Airflow 很简单(这里,我使用了 Apache Airflow 2.7.1 和 Python 3.9):

sudo pip3 install "apache-airflow==2.7.1" --constraint "https://raw.githubusercontent.com/apache/airflow/constraints-2.7.1/constraints-3.9.txt"

一般不建议使用sudo与 pip,但在我的 Raspberry Pi 上,当我将 airflow 作为服务启动时(服务以 root 身份运行),找不到 Python airflow 库,使用sudo pip3是最简单的解决方法。

安装 Apache Airflow 后,我们需要初始化它并创建一个用户:

cd ~
mkdir airflow && cd airflow
export AIRFLOW_HOME=/home/pi/airflow
airflow db init
airflow users create --role Admin --username airflow --password airflow --email admin --firstname admin --lastname admin
mkdir dags

现在,Airflow 已安装,但我希望它在启动后自动作为服务运行。

首先,我创建了一个/etc/systemd/system/airflow-webserver.service文件,将 Apache Airflow 网络服务器作为服务运行:

[Unit]
Description=Airflow webserver daemon
After=network.target postgresql.service mysql.service redis.service rabbitmq-server.service
Wants=postgresql.service mysql.service redis.service rabbitmq-server.service

[Service]
EnvironmentFile=/home/pi/airflow/env
User=pi
Group=pi
Type=simple
ExecStart=/bin/bash -c 'airflow webserver --pid /home/pi/airflow/webserver.pid'
Restart=on-failure
RestartSec=5s
PrivateTmp=true

[Install]
WantedBy=multi-user.target

以相同的方式,我为 Airflow 调度器创建了一个/etc/systemd/system/airflow-scheduler.service文件:

[Unit]
Description=Airflow scheduler daemon
After=network.target postgresql.service mysql.service redis.service rabbitmq-server.service
Wants=postgresql.service mysql.service redis.service rabbitmq-server.service

[Service]
EnvironmentFile=/home/pi/airflow/env
User=pi
Group=pi
Type=simple
ExecStart=/usr/bin/bash -c 'airflow scheduler'
Restart=always
RestartSec=5s

[Install]
WantedBy=multi-user.target

我们还需要一个/home/pi/airflow/env文件:

AIRFLOW_CONFIG=/home/pi/airflow/airflow.cfg
AIRFLOW_HOME=/home/pi/airflow/

现在,我们可以启动新服务,Apache Airflow 已准备好使用:

sudo systemctl daemon-reload
sudo systemctl enable airflow-webserver.service
sudo systemctl enable airflow-scheduler.service
sudo systemctl start airflow-webserver.service
sudo systemctl start airflow-scheduler.service

如果一切操作正确,我们可以登录到 Apache Airflow 网页面(凭据为“airflow”,“airflow”):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Apache Airflow 登录页面,图像由作者提供

Apache Airflow DAG

DAG(有向无环图)是 Apache Airflow 的核心概念。将不同的任务组合成一个图形可以让我们组织相当复杂的数据处理管道。DAG 本身是以 Python 文件的形式创建的,必须放置在 Apache Airflow 的“dags”文件夹中(该路径在“airflow.cfg”文件的 dags_folder 参数中指定)。在启动时或按下“刷新”按钮后,Apache Airflow 会导入这些 Python 文件,并从中获取所有所需的信息。

在我的例子中,我创建了一个位于 get_statistics.py 文件中的 process_channels 方法:

from pyyoutube import Api

def process_channels(requests_limit: int,
                     data_path: str):
    """ Get data for YouTube channels and save it in CSV file """
    ...

(获取 YouTube 数据本身超出了本文的范围;它可以是我们想要定期运行的任何方法)

用于在 Apache Airflow 中运行我们代码的 DAG 文件非常简单:

from airflow import DAG
from airflow.decorators import task
from airflow.models import Variable
from datetime import datetime, timedelta

data_path = "/home/pi/airflow/data/"

default_args={
        "depends_on_past": False,
        "email": [],
        "email_on_failure": False,
        "email_on_retry": False,
        "retries": 1,
        "retry_delay": timedelta(minutes=60),
}

def create_dag():
    """ Create a DAG object """
    return DAG(
        "dag_youtube",
        default_args=default_args,
        description="YouTube Retreive",
        schedule_interval=timedelta(hours=12),
        start_date=datetime(2021, 1, 1),
        catchup=False,
        tags=["youtube"]
    )

@task(task_id="collect_channels_stats_gr1")
def get_channels_stats_gr1():
    import get_statistics as gs
    limit = int(Variable.get("RequestLimit"))
    ret = gs.process_channels(limit, data_path)
    return f"GR1: {ret} channels saved"

@task(task_id="collect_channels_stats_gr2")
def get_channels_stats_gr2():
    import get_statistics as gs
    limit = int(Variable.get("RequestLimit"))
    ret = gs.process_channels(limit, data_path)
    return f"GR2: {ret} channels saved"

@task(task_id="collect_channels_stats_gr3")
def get_channels_stats_gr3():
    import get_statistics as gs
    limit = int(Variable.get("RequestLimit"))
    ret = gs.process_channels(limit, data_path)
    return f"GR3: {ret} channels saved"

# Create the DAG
with create_dag() as dag:    
    get_channels_stats_gr1()
    get_channels_stats_gr2()
    get_channels_stats_gr3()

如我们所见,我在这里创建了 create_dag 方法,其中最重要的部分是 schedule_interval 参数,它等于 12 小时。总的来说,我的 DAG 中有 3 个任务;它们由三个几乎相同的 get_channels_stats_gr1..3 方法表示。每个任务都是隔离的,将由 Apache Airflow 单独执行。我还创建了一个变量 RequestLimit。YouTube API 每天的请求限制为 10,000 次,在调试过程中,将此参数设置为较低的值是有意义的。稍后,可以通过使用 Apache Airflow 的“变量”控制面板随时更改此值。

运行 DAG

我们的任务已经准备好了。我们可以按“刷新”按钮,一个新的 DAG 将出现在列表中,并按照我们编程的时间表执行。如我们所见,安装 Apache Airflow 和创建 DAG 并不是火箭科学,但仍然需要一些努力。这是为了什么?我们能否仅仅在 CRON 作业中添加一行呢?即使是像这样的简单任务,Apache Airflow 也提供了大量功能。

  • 我们可以看到任务状态、完成和失败任务的数量、下次运行的时间以及其他参数:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Apache Airflow DAGs 列表,图片来源于作者

  • 如果任务失败,可以轻松点击它,查看发生了什么以及事件发生的时间:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Apache Airflow 图形视图,图片来源于作者

  • 我甚至可以点击失败的任务,查看其崩溃日志。在我的例子中,崩溃发生在检索 YouTube 频道数据时。一个频道可能已经被所有者删除或禁用,因此不再有数据可用:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Apache Airflow 崩溃日志,图片来源于作者

  • 我可以看到一个日历,详细记录了之前和未来任务的日志:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Apache Airflow 日历视图,图片来源于作者

  • 我还可以看到一个持续时间日志,这可以提供有关任务执行时间的一些见解:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Apache Airflow 持续时间日志,图片来源于作者

因此,与添加简单的 CRON 作业相比,使用 Apache Airflow 在功能上要好得多。最后但同样重要的是,掌握 Airflow 也是一个在行业中常常需要的不错技能 😉

结论

在这篇文章中,我安装并配置了 Apache Airflow,并成功在 Raspberry Pi 上运行它。Apache Airflow 是一个专业的工作流管理平台;根据 6sense.com,它拥有 29% 的市场份额,被 Ubisoft、SEB 或 Hitachi 等大型公司使用。但正如我们所见,即使在“nano”规模上,Apache Airflow 也可以成功地在像 Raspberry Pi 这样的微型计算机上使用。

对于有兴趣在 Raspberry Pi 上进行数据科学相关项目的读者,欢迎阅读我的其他 TDS 文章:

  • MEMS 传感器数据的探索性分析

  • 在 Raspberry Pi 上的 YOLO 目标检测

如果你喜欢这个故事,可以随时 订阅 Medium,你将收到我的新文章发布通知,并能完全访问其他作者的成千上万的故事。如果你想获取本文及我下一篇文章的完整源代码,可以访问我的 Patreon 页面

感谢阅读。

大学篮球的 NET 排名解析

原文:towardsdatascience.com/college-basketballs-net-rankings-explained-25faa0ce71ed?source=collection_archive---------2-----------------------#2023-03-08

数据科学如何驱动“疯狂三月”

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Giovanni Malloy

·

关注 发表于 Towards Data Science ·12 分钟阅读·2023 年 3 月 8 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Jacob Rice 提供,来源于 Unsplash

如果你是大学篮球迷,你一定会迫不及待地等待即将到来的“疯狂三月”。如果你是大学篮球新手,“疯狂三月”是指 NCAA 锦标赛,旨在为男子篮球一级联赛选出冠军。不论你是新晋粉丝还是资深观众,数据科学在你体验比赛的方式上正发挥着比以往更大的作用。参赛队伍的选择很大程度上依赖于一种数据驱动的算法——NET 排名。

作为数据科学家或机器学习工程师,了解该领域如何影响不同的行业,包括体育和娱乐是非常重要的。虽然大学篮球在这一不断增长的趋势中起步较晚,但 NET 排名是一个很好的例子,说明我们如何设计算法会影响结果并激励行为。如果你从事体育分析工作,理解 NET 排名是绝对必要的,但无论你所处的行业如何,大学篮球世界为利用数据科学改进产品和增长收入提供了一个重要的案例研究。

疯狂三月简要介绍

对于那些从未听说过疯狂三月的人,这个博客需要一些额外的背景信息:疯狂三月是一个 68 支球队参加的男子大学篮球锦标赛,每年从三月中旬持续到四月初。锦标赛的获胜者被冠以国家冠军的头衔。锦标赛开始时,有四场叫做“首场四强”的附加赛。在这四场比赛之后,剩下的 64 支球队被分为四个区域,每个区域包含 16 支球队,排名从 1 到 16。每个区域的冠军进入被称为“最终四强”的半决赛。

在整个赛季中,讨论的主要内容往往围绕疯狂三月展开。该锦标赛广受关注,通常是朋友间或在拉斯维加斯下注的绝佳借口。在 68 支参赛球队中,有 31 支是会议冠军,37 支获得了“外卡”资格 [3]。关于这些球队如何在锦标赛中组织的研究和讨论被称为“赛程分析”。然而,赛程分析更多的是艺术而非科学。决定谁获得“外卡”资格是一个持续争论的话题。这正是 NET 排名发挥作用的地方。

NET 排名介绍

早在 2018 年,NCAA 首次发布了一种新的排名系统,称为 NCAA 评估工具或 NET [1]。该排名系统是与 Google Cloud 专业服务合作的,旨在提供一个数据驱动的指标来衡量给定大学篮球队的质量。当排名首次发布时,它依赖于五个不同的指标:球队价值指数、净效率、胜率、调整后的胜率和得分差 [2]。然而,从那时起,排名已被调整为仅包括球队价值指数和净效率 [1]。

关于这是否是确定球队质量的最佳系统,体育记者和篮球迷之间确实存在争议。尽管对 NET 排名有各种不同的看法,但 NCAA 选择委员会将其作为决策的基础,用于确定哪些球队获得“外卡”资格以及如何在一个区域内分配排名(这些排名称为种子)。所有这些决策都会影响锦标赛的结果。因此,你可以开始看到数据科学如何构成疯狂三月的基础。

计算 NET 排名

NET 排名由数据科学驱动。NCAA 在 2018 年推特上发布了这张图表来解释这一指标:

正如你所见,团队价值指数是游戏结果、对手和地点的函数。计算团队价值指数的算法没有公开,因此是一个黑箱,但我们可以确定的是,团队价值指数的重要组成部分与对手的质量相关。NET 排名将对手质量细分为四个象限,分别命名为 Quad 1、Quad 2、Quad 3 和 Quad 4。根据[4],象限的定义如下:

  • Quad 1: “主场对阵 NET 排名在 1–30 之间的对手,中立场对阵 NET 排名在 1–50 之间的对手,客场对阵 NET 排名在 1–75 之间的对手” [4]

  • Quad 2: “主场对阵 NET 排名在 31–75 之间的对手,中立场对阵 NET 排名在 51–100 之间的对手,客场对阵 NET 排名在 76–135 之间的对手” [4]

  • Quad 3: “主场对阵 NET 排名在 76–160 之间的对手,中立场对阵 NET 排名在 101–200 之间的对手,客场对阵 NET 排名在 135–240 之间的对手” [4]

  • Quad 4: “主场对阵 NET 排名在 161–363 之间的对手,中立场对阵 NET 排名在 201–363 之间的对手,客场对阵 NET 排名在 241–363 之间的对手” [4]

Quad 系统固有地捕捉了对手实力和比赛地点的特征。因此,无论团队价值指数的结果如何,选拔委员会在分配“外卡”名额和锦标赛种子时,都极为重视 Quad 1 的胜利和 Quad 4 的失利。

另一方面,净效率是极为透明的。净效率是进攻效率和防守效率的函数 [2]。进攻效率的计算公式如下:

O = PF/(FGA — OREB+TO+.475*FTA)

其中 O 为进攻效率,PF 为得分(总得分),FGA 为投篮尝试次数(投篮数量),OREB 为进攻篮板,TO 为失误,FTA 为罚球尝试次数 [2]。

防守效率计算公式如下:

D = PA/(Opp_FGA — Opp_OREB+Opp_TO+.475*Opp_FTA)

其中 D 为防守效率,PA 为被得分,Opp_FGA 为对手的投篮尝试次数,Opp_OREB 为对手的进攻篮板,Opp_TO 为对手的失误,Opp_FTA 为对手的罚球尝试次数 [2]。

净效率就是进攻效率和防守效率之间的差值,即 NE = O — D [2]。净效率是一个密集的指标,从整体上反映了球队相对于对手的表现。

示例 NET 球队表

那么,这一切如何汇聚到 NCAA 选拔委员会呢?答案并不完全明确。显然,他们将能访问 NET 排名。此外,他们还将获得每支球队的 NET 表格报告。每支球队的 NET 表格被分成几个部分。表格的顶部包含 NET 排名、球队记录信息、赛程强度、对手的平均 NET 排名、其他基于结果和预测的排名,以及按对手象限和比赛地点细分的胜负记录。表格的下半部分是逐场比赛的球队表现分析,按对手 NET 排名/象限分为几个部分。我鼓励你查看一些示例 NET 团队表格,例如我下面提供的[5]。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由NET — NCAA 男子大学篮球的细节报告和团队表格 | WarrenNolan.com提供,已获许可。

虽然这不是最美观的数据可视化,但信息量很大,紧凑地集中在一个空间里。在团队表格中,Quad 1 和 Quad 2 的比赛进一步分为上半部分和下半部分。此外,注意到非会议比赛用蓝色突出显示,并在上述指标中加以标注。损失用红色突出显示,以便轻松指出糟糕的(Quad 4)损失或出色的(Quad 1)胜利。如你所见,象限系统在数据呈现中发挥了关键作用。

NET 排名的局限性

我知道有许多篮球迷对 NET 排名系统持批评态度。没有任何模型是完美的,NET 也不例外。不过,我会尝试从数据科学家的角度(结合大学篮球迷的视角)突出一些我看到的 NET 排名系统的局限性。

我看到的 NET 排名系统最大的局限性是没有考虑最近的表现[1]。虽然整个赛季的一致性是有价值和值得称赞的,但在赛季中的最佳状态也有其重要性。无论是体能、化学反应还是信心,一切都需要完美对接,才能在三月疯狂期间取得成功。在篮球术语中,这些被称为“无形因素”。这些因素不容易衡量(虽然有人尝试过),但它们会随时间变化并影响结果。在计量经济学术语中,这就是模型固有的“异质性”。

另一个我将其归类为限制的 NET 排名的好奇之处是其初次发布的延迟。NET 排名每天更新,但直到 12 月初才更新——在大多数球队打完 5 到 10 场比赛后。我认为这可能意味着 NET 排名有一个高度不确定的初始化状态。如果我们能看到赛季开始前 NET 排名的初始状态,我认为我们可以获得一些关于算法如何工作的非常有价值的见解。它是否完全是幼稚的,还是有某种迁移学习的元素,来自于以往的赛季或其他投票的季前排名?

为了汇总最终的 NET 排名,我假设有某种方式将团队价值指数转换为数值,并与净效率结合,计算出团队质量的加权指标。我承认 NET 排名很可能确实是启发式或其他非 AI 算法。当然,Net Efficiency 统计数据的计算方式表明 NCAA 可能会接受启发式方法。此外,我在将数据科学洞察提供给非技术领域(如健康政策)的经验表明,有时候少即是多。更易于理解的模型有时对决策者更具吸引力。

尽管如此,我的第三个也是最后一个限制依赖于假设这是一个监督学习算法。如果 NET 排名源于一个监督学习算法,那么我想知道训练数据可能来自哪里。基准真相是什么?准确性如何衡量?究竟是什么真正区分了第 232 队和第 233 队?即使在将同一队伍与其自身逐年比较时,你也可能在比较大相径庭的阵容。在像均方根误差这样的误差指标中很难找到意义。

假设基础算法

那么,NET 排名系统是如何形成的呢?也许我们应该尝试重新创建它?我们确实知道一些确定的事情:

  1. 前黄金标准的大学篮球排名统计模型,RPI 排名系统,是一个优雅但简单的启发式算法。像 NCAA 这样的机构通常不以创新著称,我怀疑大学篮球界是否希望它的皇冠赛事由不可解释的 AI 算法驱动。因此,我的最佳猜测是,机器学习的应用非常有限,如果有的话。回到我之前提到的第三个限制,一个监督学习方法可能会更多麻烦而不值得。

  2. 从某种意义上说,NET 排名是递归的。一个团队的 NET 排名依赖于其对手的 NET 排名,而这些对手的 NET 排名又依赖于其他对手的 NET 排名,依此类推。NET 排名可能由一种贝叶斯方法驱动,其中对每个团队有一个初始的朴素分布,并在每场比赛后更新该分布。

  3. Google Cloud 专业服务参与其中。这可能是认知偏见或巧妙营销的一个很好的例子,但我希望相信 Google 所涉及的都是最前沿的方法。虽然不一定真实,但与 Google 合作使 NCAA 获得了巨大的计算资源和开发超越传统体育分析领域的方法的能力。即使算法是可解释的,也许结构是复杂的,甚至可能是反直觉的。

  4. 历史上的 NET 排名很难找到。经过大约一个小时的网络搜索,很难找到每天发布 NET 排名的来源。这让我对我在第 3 点中的假设产生了怀疑。也许,该算法足够简单,可以通过访问一个赛季的数据和 NET 排名来轻松逆向工程。也许,我们可以拟合一个简单的线性回归来为每支球队生成一个分数,而 NET 排名则是这些分数的排序列表。

鉴于产生最终 NET 排名的方法有很多可能性,我认为最可能的情况是 NCAA 使用了集成学习,比如投票。这意味着他们可能会采用多种方法来生成一个基于团队价值指数和净效率的 NET 排名。然后,他们将这些方法的结果结合起来,制定出每周发布的最终 NET 排名。

追随金钱

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Giorgio Trovato 提供,来源于 Unsplash

尽管试图推测生成 NET 排名的可能方法是一项有趣的练习,但我认为结果并不是 NET 排名最重要的结果。当然,NCAA 希望识别出最佳的球队,使三月疯狂尽可能竞争激烈和有价值。因此,排名需要看起来合理,并可能与 AP 和教练投票紧密对齐。

然而, NET 排名为大学篮球的商业运作提供了其他排名系统显式没有的一个重要功能:NET 排名激励高质量的非会议比赛

这一点不容忽视。这是数据力量的另一面。通过公布 NET 排名的组成部分,NCAA 公开宣示了所有球队应当达到的指标。从商业战略的角度来看,团队价值指数(Team Value Index)非常有创意。像净效率指标(Net Efficiency metric)一样,它旨在以易于理解的方式量化大学篮球队的质量。然而,与净效率指标不同的是,团队价值指数推动体育总监、教练和所有参与排赛的人员确保非会议赛程充满了四分之一(Quad 1)比赛。与会议赛程相比,非会议赛程更加灵活和多样化。虽然非会议竞争的重要性在 NET 排名推出之前就已经在增加,但团队价值指数和 NET 排名中的四分之一系统的强调使这一重要性得到了正式化,并奖励了赛程更艰难的球队。

在一个流媒体变得越来越重要的体育媒体和娱乐环境中,内容为王。NCAA 篮球赛季中能产生的高质量(四分之一(Quad 1))比赛越多,大学篮球的媒体内容价值就越高。随着更多流媒体服务开始涉足大学体育,赛季中改善的比赛意味着更多的球迷。更多的球迷意味着在常规赛和季后赛期间都会有更多的兴趣。更多的兴趣意味着整个赛季的收入增加,以及对已经非常盈利的三月疯狂的收入大幅提升。

如果你怀疑 NET 排名是否旨在激励更高质量的非会议比赛,请回顾一下 NET 球队表的图像。非会议赛程的强度和记录在顶部有自己的行,而非会议比赛以明亮的青色突出显示。我不能保证这是选拔委员会看到的版本,但这仍然支持了我的观点。

结论

如果你像我一样,到这篇博客结束时,你可能会有更多的问题而不是答案。NET 排名的组成部分非常简单,但将这些组成部分整合成一个连贯排名的算法却笼罩在神秘之中。可能有许多方法和模型可以用来生成 NET 排名,但无论方法是什么,它都由数据支持。

数据正在从两个方面推动三月疯狂(March Madness)。NET 排名向选拔委员会提供了如何构建锦标赛的建议,同时明确激励球队提高其非会议赛程的强度。总体而言,我认为 NET 排名对篮球运动和锦标赛是有益的。它们有助于减少选择和排名球队时的偏见,并提高常规赛期间的运动质量。因此,无论你是从未错过过第一次四强赛(First Four)还是从未听说过最终四强赛(Final Four),现在你知道数据科学如何在其中发挥作用了。

参考文献

大学篮球词典:51 个术语定义 | NCAA.com

大学篮球 NET 排名解析 | NCAA.com

前四场 NCAA 比赛的解析 | NCAA.com

大学篮球 NET 排名解析:Quad 1 胜利如何影响 NCAA 比赛球队 | Sporting News

NET — NCAA 男子篮球球队的 Nitty Gritty 报告 | WarrenNolan.com

对我的内容感兴趣?请考虑在 Medium 上关注我。

在 Twitter 上关注我:@malloy_giovanni

你认为 NET 排名背后使用了什么算法?你是否更喜欢其他系统?请在下方评论中分享你的想法或经历,保持讨论的热度!

大学橄榄球会议重组 — 聚类

原文:towardsdatascience.com/college-football-conference-realignment-clustering-6ca16840ed3d

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Giovanni Malloy

·发表于 Towards Data Science ·阅读时间 12 分钟·2023 年 8 月 7 日

欢迎来到本系列的第三部分,关于会议重组!这是我们将开始使用数据集来指导重组决策的博客文章。常有人抱怨会议重组破坏了传统的对抗关系和大学橄榄球的地区特色。确实,大学体育往往具有地区性,这甚至体现在会议名称中:太平洋 12,大西洋海岸,东南和大东会议等等。当我们包括 FCS 时,有些会议名称更为具体:俄亥俄谷会议。当然,FBS 中的地区会议时代早已过去。最近几天,Pac 12 似乎也可能成为过去的遗物。

本系列分为四个部分(完整的动机见第一部分):

  1. 大学橄榄球会议重组 — Python 中的探索性数据分析

  2. 大学橄榄球会议重组 — 回归分析

  3. 大学橄榄球会议重组 — 聚类

  4. 大学橄榄球会议重组 — node2vec

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Gene Gallin 提供,来源于 Unsplash

希望系列中的每一部分都能为你提供对大学橄榄球这一心爱的游戏未来的新视角。如果你没有阅读第一部分或第二部分,简要概述是我创建了自己从网络来源汇总的数据集。这些数据包括每个 FBS 项目的基本信息、所有大学橄榄球竞争对手的非规范性近似体育场规模历史表现AP 前 25 名投票的出现频率、学校是否为AAUR1机构(在 Big Ten 和 Pac 12 中具有历史重要性)、NFL 选秀选手数量、2017–2019 年的项目收入数据,以及关于大学橄榄球球迷基础规模的最新估计。在第一部分中,我们发现有几个特征与球迷基础规模有很强的相关性,因此在第二部分中,我们开发了一个线性回归和随机森林回归模型来预测球迷基础规模。

聚类

我撰写这篇文章的动机如下:今天的会议建立在传统的核心上。你可以将它们看作是一个新的计算机硬盘驱动器。在区域会议中有条不紊地组织。然而,多年来,就像我们在硬盘驱动器上创建、操作和删除文件一样,大学橄榄球界也出现了新会议(最近是 AAC),旧会议崩溃(Big East 橄榄球),以及曾经的中西部会议从纽约扩展到洛杉矶。最有趣的案例研究是西部运动会议。下面是一个来自维基百科的会员图表,WAC 已经多次自力更生。我会为另一天保留深入探讨的文章。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

维基百科有这些令人着迷的会员图表,而 WAC 是其中最疯狂的之一。

目前,我想知道如果我们重新安排大学橄榄球会议的磁盘驱动器会发生什么。如果我们从零开始构建会议呢?如果我们去掉传统和权利的授予,2022 年学校将如何分组?回答这个问题的最佳方法在于一种叫做聚类的无监督机器学习方法。无监督机器学习意味着我们将向模型提供未标记的数据,希望能够引发隐藏的模式。具体来说,聚类的目标是将相似的观察结果分组在一起。这些组可能在经过彻底的探索性数据分析后还是显而易见或不明显的。

K 均值聚类

最广泛实施的聚类算法之一是 k-means 聚类。k-means 的基本思想描述得很好 这里。本质上,用户定义所需的聚类数 k。算法随机分配 k 个质心,并使用欧几里得距离找到离每个项目最近的质心。然后,该项目被视为该质心簇的成员。接着,质心被移动到分配给该质心的项目的平均位置。在 scikit-learn 包中,这个过程有一个用户定义的迭代次数。

特征工程

现在你已经了解了我们将如何处理这个问题,开始编码吧。首先,我们将导入数据集并删除不需要的列。记住,我们希望从模型中隐藏像当前和过去的会议这样的标签,以便重新开始。

import numpy as np
import pandas as pd
cfb_info_df = pd.read_csv(r'.\FBS_Football_Team_Info.csv', encoding = 'unicode_escape')
clustering_data_df = cfb_info_df.drop(['Team','Nickname', 'City', 'Current_conference', 'Former_conferences', 'First_played', 'Joined_FBS'], axis = 1)

谁不喜欢州内竞争呢?从铁碗战到床垫战再到旧木桶战,这些比赛在整个年度的社区聚会上都是重要的吹嘘权利。所以,让我们保留这个特征,但使用 pandas 将其转换为独热编码,就像我们在本系列第二部分中做的那样。(我们的数据也包括现有对手的独热编码)。

clustering_data_df = pd.get_dummies(clustering_data_df,prefix = 'is_state', columns = ['State'])

对于这次分析,我们不需要担心训练集和测试集的划分。因此,我们可以一次性对所有数值特征应用最小-最大缩放。

from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
clustering_data_df['Latitude'] = scaler.fit_transform(clustering_data_df[['Latitude']])
clustering_data_df['Longitude'] = scaler.fit_transform(clustering_data_df[['Longitude']])
clustering_data_df['Enrollment'] = scaler.fit_transform(clustering_data_df[['Enrollment']])
clustering_data_df['years_playing'] = scaler.fit_transform(clustering_data_df[['years_playing']])
clustering_data_df['years_playing_FBS'] = scaler.fit_transform(clustering_data_df[['years_playing_FBS']])
clustering_data_df['Stadium_capacity'] = scaler.fit_transform(clustering_data_df[['Stadium_capacity']])
clustering_data_df['total_draft_picks_2000_to_2020'] = scaler.fit_transform(clustering_data_df[['total_draft_picks_2000_to_2020']])
clustering_data_df['first_rd_draft_picks_2000_to_2020'] = scaler.fit_transform(clustering_data_df[['first_rd_draft_picks_2000_to_2020']])
clustering_data_df['number_1_draft_picks_2000_to_2020'] = scaler.fit_transform(clustering_data_df[['number_1_draft_picks_2000_to_2020']])
clustering_data_df['wsj_college_football_revenue_2019'] = scaler.fit_transform(clustering_data_df[['wsj_college_football_revenue_2019']])
clustering_data_df['wsj_college_football_value_2018'] = scaler.fit_transform(clustering_data_df[['wsj_college_football_value_2018']])
clustering_data_df['wsj_college_football_value_2017'] = scaler.fit_transform(clustering_data_df[['wsj_college_football_value_2017']])
clustering_data_df['tj_altimore_fan_base_size_millions'] = scaler.fit_transform(clustering_data_df[['tj_altimore_fan_base_size_millions']])
clustering_data_df['bowl_games_played'] = scaler.fit_transform(clustering_data_df[['bowl_games_played']])
clustering_data_df['bowl_game_win_pct'] = scaler.fit_transform(clustering_data_df[['bowl_game_win_pct']])
clustering_data_df['historical_win_pct'] = scaler.fit_transform(clustering_data_df[['historical_win_pct']])
clustering_data_df['total_games_played'] = scaler.fit_transform(clustering_data_df[['total_games_played']])

聚类实施——Power 5 对阵 Group of 5

我们再次依赖 scikit-learn 来实现一个简单的 k-means 聚类函数。我们可以在大学橄榄球中做的最基本的划分是 Power 5 对阵 Group of 5 队伍,所以我们从这里开始。这意味着我们希望将数据分成 2 个簇。模型不会知道这两个簇是 Power 5 和 Group of 5 队伍。它只会尝试找到一种将 133 支 FBS 队伍分成两组的方法。鉴于 Power 5 和 Group of 5 队伍在收入和粉丝基础规模上的差异,我的初步假设是这应该相对容易。

from sklearn.cluster import KMeans
# Implement K means clustering
kmeans_conf_p5_v_g5 = KMeans(n_clusters=2, random_state=0).fit(clustering_data_df)

我手动将模型的输出与 FBS 球队的真实分布进行了比较。结果显示,我们正确识别了 69 支 Power 5 球队中的 59 支和 64 支 Group of 5 球队中的 63 支。

import plotly.express as px

#Manually compare to P5 v G5 conferences 2025
num_tp = len(list_p5) - 1
num_fp = 1 # Tulane
num_tn = len(list_g5) - 10
num_fn = 10 #Baylor, BYU, Cincinnati, Houston, Oregon State, TCU, Texas Tech, UCF, Wake Forest, Washington State
fig = px.imshow([[num_tn, num_fn],
                 [num_fp, num_tp]], text_auto=True,
               labels=dict(x="True P5 v G5", y="Clustering P5 v G5"),
                x=['Group of 5 Team', 'Power 5 Team'],
                y=['Group of 5 Team', 'Power 5 Team'])
fig.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2-means 聚类很好地区分了 Power 5 和 Group of 5 球队。

这 10 个错误标记的 Power 5 球队包括七支 Big 12 球队(贝勒、BYU、辛辛那提、休斯顿、TCU、德州科技和 UCF)、两支 Pac 12 球队(俄勒冈州立和华盛顿州立)以及一支 ACC 球队(维克森林)。鉴于数据基于 BYU、辛辛那提、休斯顿和 UCF 的首个 Big 12 赛季之前的收入,因此它们与当前的 Group of 5 球队聚集在一起也就不足为奇了。贝勒、TCU 和德州科技位于一个足球忠诚度竞争激烈的州,而孤星州的休闲球迷常常倾向于德州农工或德州。对于那些关注 Pac 12 未来变化的人来说,许多人预测俄勒冈州立和华盛顿州立可能会被 Big Ten 和/或 Big 12 在任何实际调整中排除在外。

唯一一个有 Power 5 信誉却被排除在外的球队是杜兰。这个赛季是杜兰本千年以来第一次被排名,因此他们没有被选中加入 Power 5 会议也不足为奇。然而,30 年来,他们曾是 SEC 会议的成员。

创建 10 个新会议

现在,是时候将一切整合在一起了。假设大学橄榄球现在保持 10 个会议。这意味着我们将实施 10-means 聚类,以建议基于数据驱动的调整所产生的会议。希望通过在形成新会议时加入竞争和地理信息,除了收入和球迷基础大小外,我们可以同时考虑金钱和传统。

kmeans_10_conf = KMeans(n_clusters=10, random_state=0).fit(clustering_data_df)

labels_10_conf = kmeans_10_conf.labels_

标签将告诉我们哪个球队在哪个会议中。查看这些聚类时,你会注意到一些聚类非常大(20+),而其他聚类只有四支球队。根据NCAA 章程,会议必须至少有八支球队。尽管这篇博文的全部内容都是挑战现有会议结构,但我们仍希望将结果限制在这一规则内。

幸运的是,微软和 RPI 的研究人员开发了一种约束 k-means 算法,通过将其建模为最小成本流优化问题。有一个可以在 Python 中实现这种约束 k-means 算法。它的实现与 scikit-learn 包一样简单。然而,我确实需要通过 pip 安装 ortools 9.3.10497。

除了将我们最低的会议规模设置为八个,我们还将最大会议规模设置为二十个。这是一个随机模型,所以如果你更改随机状态,你会得到一组新的会议。

import ortools
import ortools.graph.pywrapgraph
from k_means_constrained import KMeansConstrained #needs pip install --user ortools==9.3.10497
clf = KMeansConstrained(n_clusters=10,size_min=8,size_max=20,random_state=0)
kmeans_Constrained_10_conf = clf.fit_predict(clustering_data_df)

数据驱动的 FBS 会议

现在是大揭晓的时刻。我冒昧地为每个集群命名了它们的新会议名称。

  • 西南部:阿肯色州大学、贝勒大学、路易斯安那州立大学、赖斯大学、TCU、德克萨斯大学、德克萨斯农工大学、德克萨斯科技大学。除了路易斯安那州立大学,这些球队曾经在 1990 年代前互相比赛,这个联盟被称为西南会议。现在,它回来了!

  • 阳光美国:阿克伦大学、阿巴拉契亚州立大学、阿肯色州立大学、博灵格林州立大学、夏洛特大学、海岸卡罗来纳大学、乔治亚南方大学、杰克逊维尔州立大学、詹姆斯·麦迪逊大学、肯特州立大学、自由大学、路易斯安那州立大学-蒙罗分校、中田纳西大学、北德克萨斯大学、老国王大学、萨姆休斯顿州立大学、南阿拉巴马大学、德克萨斯州立大学、特洛伊大学、西肯塔基大学。就像阳光联盟扩展成一个超级联盟,并说服了一些俄亥俄州球队加入。

  • 大 8:爱荷华大学、密歇根大学、明尼苏达大学、内布拉斯加大学、俄亥俄州立大学、俄克拉荷马大学、佩恩州立大学、威斯康星大学。这是大十足球的核心,加上了来自另一个充满农场并喜爱足球的中西部州的强队:俄克拉荷马大学。

  • 国家运动:亚利桑那州立大学、波士顿学院、辛辛那提大学、堪萨斯州立大学、肯塔基大学、路易斯安那州立大学、路易斯维尔大学、密西西比州立大学、北卡罗来纳州立大学、新墨西哥大学、俄亥俄大学、俄克拉荷马州立大学、俄勒冈州立大学、南卡罗来纳大学、南密西西比大学、锡拉丘兹大学、坦普尔大学、德克萨斯大学-厄尔帕索、华盛顿州立大学、西弗吉尼亚大学。这个会议在所有四个时区都有存在,这目前是像大 12 这样的会议所羡慕的。这个会议有来自当前十个会议中的九个的成员(没有大十学校),它包含了每个人和一切。

  • SEC:阿拉巴马大学、奥本大学、克莱姆森大学、佛罗里达大学、乔治亚大学、乔治亚理工学院、密西西比大学、田纳西大学。除了克莱姆森大学,这些学校都是今天 SEC 会议的创始成员,所以它们保留了这个名字。

  • 篮球天才:亚利桑那大学、布法罗大学、科罗拉多大学、杜克大学、伊利诺伊大学、印第安纳大学、爱荷华州立大学、堪萨斯大学、密歇根州立大学、密苏里大学、北卡罗来纳大学、西北大学、匹兹堡大学、普渡大学、罗格斯大学、杜兰大学、犹他大学、范德比尔特大学、维吉尼亚大学、华盛顿大学。我称这个为篮球学校会议,因为它包括传统的强队亚利桑那大学、杜克大学、堪萨斯大学和 UNC,以及大十篮球学校普渡大学和印第安纳大学。这个会议与国家运动会议的真正区别在于所有这些学校都是 AAU 成员,因此有了"天才"这个副标题。

  • MAC+:陆军、博尔州立、BYU、中密歇根、东卡罗来纳、东密歇根、路易斯安那理工、马歇尔、孟菲斯、迈阿密(OH)、海军、新墨西哥州立、北伊利诺伊、SMU、托莱多、塔尔萨、UMass、犹他州立、维克森林、西密歇根。似乎每个人都在给他们的名字加上“+”,那么为什么不在以 MAC 队伍为核心的扩展会议中也加上呢?这是三个具有巨大地理多样性的会议中的最后一个。我认为可以说这是相对于国家体育会议的低收入、小球迷基础的等价物。

  • Fun Belt:FIU、佛罗里达大西洋、佛罗里达州立、乔治亚州立、休斯顿、迈阿密(FL)、内华达、南佛罗里达、UAB、UCF、UConn、UNLV、UTSA。这个会议的联系在于其相对接近赤道。我想 UConn 也因为雪鸟球迷的加入而参与其中。有人提到 SEC 或 Big Ten 可能会招揽佛罗里达州立和迈阿密。在这种情况下,我们的模型倾向于将它们排除在外。

  • Mountain West:空军、博伊西州立、科罗拉多州立、弗雷斯诺州立、夏威夷、圣地亚哥州立、圣荷西州立、怀俄明。这一会议包含了 Mountain West 会议中的八支现有队伍。这是当对手、地理位置和收入数据全部对齐时的一个极佳示例。Mountain West 始终在赛场上提供优质的橄榄球产品。自 1999 年成立以来,九支不同的队伍赢得了会议冠军,这证明了赛场上的长期平衡。

  • Paclantic 8:加州、马里兰、诺特丹、俄勒冈、斯坦福、UCLA、USC、弗吉尼亚理工。这些队伍代表了西海岸最大的几个名字,结合了一个共同的对手诺特丹。除了诺特丹外,Paclantic 8 的所有学校都是 AAU 成员。

主成分分析

我们已经有了新的会议及其名称,但这些会议有多不同呢?一种方法是比较不同特征的会议级分布。我们在本博客系列的第一部分做了很多这种探索性数据分析,所以让我们改为使用主成分分析(PCA)来减少这些特征的维度。PCA 将减少数据的维度,这有助于可视化。

首先,我们将新的会议名称添加到数据集中。

# Initialize new column to define our newly assigned conference
cfb_info_df['k_means_conf'] = 'Southwest'

#for loop to add the conference name for each team
for i in range(len(cfb_info_df['Team'])):
    if cfb_info_df['Team'].iloc[i] in cluster_1:
        cfb_info_df['k_means_conf'][i] = 'Sun USA'
    elif cfb_info_df['Team'].iloc[i] in cluster_2:
        cfb_info_df['k_means_conf'][i] = 'Big 8'
    elif cfb_info_df['Team'].iloc[i] in cluster_3:
        cfb_info_df['k_means_conf'][i] = 'National Athletic'
    elif cfb_info_df['Team'].iloc[i] in cluster_4:
        cfb_info_df['k_means_conf'][i] = 'SEC'
    elif cfb_info_df['Team'].iloc[i] in cluster_5:
        cfb_info_df['k_means_conf'][i] = 'Basketball Brainiacs'
    elif cfb_info_df['Team'].iloc[i] in cluster_6:
        cfb_info_df['k_means_conf'][i] = 'MAC+'
    elif cfb_info_df['Team'].iloc[i] in cluster_7:
        cfb_info_df['k_means_conf'][i] = 'Fun Belt'
    elif cfb_info_df['Team'].iloc[i] in cluster_8:
        cfb_info_df['k_means_conf'][i] = 'Mountain West'
    elif cfb_info_df['Team'].iloc[i] in cluster_9:
        cfb_info_df['k_means_conf'][i] = 'Paclantic 8'

现在,我们可以使用 scikit-learn 来无缝计算两个主成分。这将减少我们的维度,使我们能够生成散点图并可视化队伍的相似性。我们将结果添加到一个数据框中,并用新的会议名称标记,以便继续绘制。

from sklearn.decomposition import PCA

# Set the n_components=2
principal = PCA(n_components=2)
principal.fit(clustering_data_df)
pca_clustering_data = principal.transform(clustering_data_df)

# Create data frame for plot
pca_clustering_data_df = pd.DataFrame(pca_clustering_data, columns = ['PCA_1', 'PCA_2'])
pca_clustering_data_df['k_means_conference'] = cfb_info_df['k_means_conf']

使用 plotly,我们将绘制所有队伍的散点图。

import plotly.express as px

fig = px.scatter(pca_clustering_data_df, x="PCA_1", y="PCA_2", color="k_means_conference",
                labels=dict(PCA_1="PCA Dimension 1", 
                            PCA_2="PCA Dimension 2", 
                            k_means_conference="10-means Conference"))
fig.show()

结果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

主成分分析将聚类数据的维度减少到两个。这样我们可以直观地检查各个会议之间的差异。

通过视觉检查,很容易看出 Sun USA、Fun Belt 和 MAC+、Mountain West 紧密对齐且彼此密切相关。像 Big 8 和 SEC 这样高收入的会议则分布得更加分散。

我们从会议团队名称中感受到地理分布,但我们可以在地图上绘制这些会议,以查看我们在多大程度上保留了这项运动的区域历史:

import plotly.graph_objects as go

fig = go.Figure(data=go.Scattergeo(
    lon = cfb_info_df['Longitude'],
    lat = cfb_info_df['Latitude'],
    text = cfb_info_df['Team'],
    mode = 'markers',
    marker = dict(color = cfb_info_df['map_color'])))

fig.update_layout(title = 'Conference Membership',
        geo_scope='usa')
fig.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

会议会员地图展示了许多地理上分散的会议。

结果是一个混合体。有些会议确实是区域性的:西南、SEC、Big 8 和 Mountain West,而其他则拥抱更具全国性的分布。也许这就是大学足球的命运。

感谢阅读!一如既往,请在下方评论你的想法。我知道这是一个思维实验,所以请告诉我你的反应。你的团队最终去了哪里?你会更倾向于这些联盟而不是今天的会议吗?

对我的内容感兴趣吗?请考虑在 Medium 上关注我

在 Twitter 上关注我:@malloy_giovanni

大学橄榄球联盟重组——Python 中的探索性数据分析

原文:towardsdatascience.com/college-football-conference-realignment-exploratory-data-analysis-in-python-6f4a74037572

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Giovanni Malloy

·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 8 月 5 日

这是我最喜欢的时节:秋天,意味着大学橄榄球的季节。我一直喜欢大学体育。成长过程中,我生活在一个 Big Ten/SEC 家庭和一个 Big East(现在的 ACC)城市,这意味着从 8 月的第一次开球到 4 月的最后一秒钟,电视屏幕上充满了大学体育。最近,数据分析已经主导了这两种运动,但由于现在是橄榄球季节,我们就从这里开始。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 David Ireland 提供,发布在 Unsplash

过去两个休赛季,大学体育界一直充斥着 NIL、转会门户和联盟重组的新闻。我认为,大多数球迷的情绪可以通过 Dr. Pepper 的“Chaos Comes to Fansville”广告来体现。我开始注意到,每次关于联盟重组的对话,特别是充满了猜测和直觉的推动。然而,普遍存在一种信念,认为某个伟大的大学橄榄球奥兹正在 crunch 数据,以决定哪个球队值得加入哪个联盟。我仍然没有机会见到幕后的那个人,所以在那之前,我想尝试提出一种基于数据的联盟重组方案。

这是一个四部分的博客,希望能作为学习一些新数据科学工具的有趣方式:

  1. 大学橄榄球联盟重组——Python 中的探索性数据分析

  2. 大学橄榄球联盟重组——回归分析

  3. 大学橄榄球会议重组 — 聚类

  4. 大学橄榄球会议重组 — node2vec

我在此帖子开头声明,进行探索性数据分析有很多方法,所以我只会介绍一些与会议重组相关的方法。

数据

我花时间构建了自己的数据集,使用了我从网络上汇总的各种来源。这些数据包括 每个 FBS 项目的基本信息、所有 大学橄榄球对抗赛 的非规范近似、体育场大小历史表现AP 前 25 名投票的出现频率、学校是否为 AAUR1 机构(对加入 Big Ten 和 Pac 12 历史上非常重要)、NFL 选秀情况 从 2017 年至 2019 年的 项目收入数据,以及关于大学橄榄球粉丝基础的 最近估算

寻找纬度和经度

我们的探索性数据分析的第一步是将我们拥有的每支球队的城市和州数据转换为纬度和经度。在 Python 中使用 geopy 包 很容易做到这一点。首先,我导入依赖项,并加载一个包含每支球队城市和州的 csv 文件。

# Import dependencies
import pandas as pd
import numpy as np
from geopy.geocoders import Nominatim
# Read csv data with column 'City' and column 'State'
city_list_df = pd.read_csv(r'.\FBS_Football_Cities.csv', encoding = 'unicode_escape')

接下来是代码的核心部分。我运行一个 for 循环来收集每个城市的所有纬度和经度。

# Lists to track latitude and longitude of each city
lat_list = []
long_list = []
# for each city in the dataframe, get the latitude and longitude and # add them to the lists
index = 0
for city in city_list_df['City']:
    city_name = str(city)+', '+str(city_list_df['State'][index])

    # Two cities needed some manual cleaning
    if index == 39:
        city_name = 'Urbana, Illinois'
    elif index == 92:
        city_name = 'San Jose, California'

    print(city_name)
    print(index)

    # calling the Nominatim tool
    loc = Nominatim(user_agent="GetLoc")

    # entering the location name
    getLoc = loc.geocode(city_name)

    # add the latitude and longitude to their respective lists
    lat_list.append(getLoc.latitude)
    long_list.append(getLoc.longitude)

    index = index + 1

最后,我将这些数据合并到一个数据框中,并输出为 csv 文件。

lat_long_df = pd.DataFrame(lat_list, columns=['latitude'])
lat_long_df['longitude'] = long_list
lat_long_df.to_csv(r'.\cfb_lat_long.csv')

会议地理中心

现在我们有了每支球队的纬度和经度数据,我们可以使用相同的软件包找到每个会议的地理中心。你可以把地理中心看作是会议冠军赛的最佳中立场地。我整理的数据将球队分配到 2025 赛季的各自会议(例如,UCLA 和 USC 在 Big Ten)。

# Read csv of all data
cfb_info_df = pd.read_csv(r'.\FBS_Football_Team_Info.csv', encoding = 'unicode_escape')
# Track conference name, lat., long., and city name 
conf_name_list = []
conf_lat_list = []
conf_long_list = []
conf_city_list = []
# for each conference in the data set calculate the mean  latitude and mean longitude
for conf in np.unique(cfb_info_df['Current_conference_2025']):
    conf_latitude = np.mean(cfb_info_df[cfb_info_df['Current_conference_2025'] == conf]['Latitude'])
    conf_longitude = np.mean(cfb_info_df[cfb_info_df['Current_conference_2025'] == conf]['Longitude'])

    # calling the Nominatim tool
    loc = Nominatim(user_agent="GetLoc")

    # entering the lat. and long. to return the city name
    getCity = loc.reverse(str(conf_latitude)+', '+str(conf_longitude))

    #Update lists
    conf_name_list.append(conf)
    conf_lat_list.append(conf_latitude)
    conf_long_list.append(conf_longitude)
    conf_city_list.append(getCity)

    print(f'Conference: {conf}, Centroid City: {getCity} ({conf_latitude}, {conf_longitude})')

#Create data frame by conference
conf_center_df = pd.DataFrame(conf_name_list, columns=['conference'])
conf_center_df['latitude'] = conf_lat_list
conf_center_df['longitude'] = conf_long_list
conf_center_df['city'] = conf_city_list
# Add a column for text to appear on our map
conf_center_df['text'] = conf_center_df['conference'] + ': ' + conf_center_df['city'].astype(str)

现在,我们可以使用 plotly 软件包来创建一个简单的交互式地图,并可视化大学橄榄球会议的新地理中心。

import plotly.graph_objects as go
fig = go.Figure(data=go.Scattergeo(
        lon = conf_center_df['longitude'],
        lat = conf_center_df['latitude'],
        text = conf_center_df['text'],
        mode = 'markers'))
fig.update_layout(title = 'Conference Geographic Centers<br>(Hover for conference names)',
        geo_scope='usa')
fig.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

每个会议(以及独立队)的地理中心图。

这些是最接近会议地理中心的小型和大型城市区域:

  • ACC: 格林斯伯勒,北卡罗来纳州(夏洛特,北卡罗来纳州)

  • American: 伯明翰,阿拉巴马州(孟菲斯,田纳西州)

  • Big 12: 费耶特维尔,阿肯色州(俄克拉荷马城,俄克拉荷马州)

  • Big Ten: 斯普林菲尔德,伊利诺伊州(圣路易斯,密苏里州)

  • C-USA: 杰克逊,密西西比州(孟菲斯,田纳西州)

  • MAC: 托莱多,俄亥俄州(底特律,密歇根州)

  • Mountain West: 拉斯维加斯,内华达州

  • Pac-12: 里诺,内华达州(盐湖城,犹他州)

  • SEC: 孟菲斯,田纳西州

  • Sun Belt: 伯明翰,阿拉巴马州(亚特兰大,乔治亚州)

  • Independents: 斯克兰顿,宾夕法尼亚州(纽约,纽约州)

这些结果似乎相当合理,尽管 Independents 实际上不是一个会议。南方是大学橄榄球世界的中心。孟菲斯市正计划建设一个新体育场,也许正好赶上成为 American、C-USA 或 SEC 冠军赛的潜在主办城市。

摘要统计

了解任何数据集的最佳方法之一是生成数字特征的摘要统计。Pandas 提供了一个名为 describe() 的内置方法来获取摘要统计。以下是一个关于入学的示例:

cfb_info_df['Enrollment'].describe()

这一行语句输出:

count      133.000000
mean     29337.541353
std      13780.834495
min       3297.000000
25%      21200.000000
50%      28500.000000
75%      38500.000000
max      69400.000000
Name: Enrollment, dtype: float64

从摘要统计中,我们可以看到均值和中位数相似,变异系数(标准差/均值)小于 0.5,意味着我们可能有一个大致对称的分布,但我们可以通过生成直方图来直观检查这一点,从而按会议划分入学人数。

import plotly.express as px
fig = px.histogram(cfb_info_df, x="Enrollment", color="Current_conference_2025", marginal="box", hover_data=cfb_info_df.columns)
fig.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

直方图展示了按会议划分的入学分布。边际分布如上所示。

从直方图中,我们可以直观地看到大多数大学的入学人数在 20,000 到 35,000 学生之间。Big Ten 的中位数入学人数超过 45,000,而 Sun Belt 包含了中位数入学人数不到 20,000 的最小学校。ACC 拥有所有 Power 5 会议中入学人数最少的学校。

相关性

相关性是衡量两个特征之间关系的指标。相关性范围从 -1 到 1,其中 1 表示完全正相关,-1 表示完全负相关,0 表示没有关系。相关性不应与因果关系混淆,因为相关性仅仅是观察到的两个特征之间关系的度量。Pandas 再次提供了一个方法来轻松计算两列之间的相关性。

import seaborn as sns
# Pull out numeric columns from data
numeric_columns = ['Enrollment','years_playing','Stadium_capacity','total_draft_picks_2000_to_2020',
                  'wsj_college_football_revenue_2019', 'tj_altimore_fan_base_size_millions', 'bowl_game_win_pct',
                 'historical_win_pct', 'p_AP_Top_25_2001_to_2021']
# Generate Correlation Matrix using only the numeric columns
correlation_matrix = cfb_info_df.loc[:,numeric_columns].corr()
# Print the correlation matrix
#print(correlation_matrix)
# Plot the correlation matrix using seaborn
# Set plot size
sns.set(rc={"figure.figsize":(11, 10)})
# Show the plot
sns.heatmap(correlation_matrix, annot=True)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

相关性矩阵显示每个特征与自身之间的完全正相关关系。我们还看到体育场容量、粉丝基础大小、2019 年收入和 2001 至 2021 年间 AP 前 25 名出现周数之间的相关性很高。

结果相关矩阵是理解哪些特征可能相互关联的一个很好的可视化工具。例如,体育场容量、2019 年收入、粉丝基础规模和历史上进入 AP 前 25 名排名的概率都是高度相关的。有趣的是,这些特征与历史胜率的相关性只是中等水平,与碗赛胜率的相关性几乎不存在。一个解释可能是赛程强度在我们的数据中并没有体现出来,一个在 Power 5 联盟中取得 8 胜 4 负的球队可能会比一个在 Group of 5 联盟中取得 8 胜 4 负的球队要好得多。此外,我们可以直接直观地理解相关性并不等于因果关系,因为建造一个更大的体育场并不能保证拥有更多的粉丝基础。

另一个有趣的发现是,球队活跃的年限与任何成功指标的相关性并不强。当然,长期的橄榄球历史可能会给粉丝基础带来长期的痛苦。例如,印第安纳大学最近成为第一个达到历史 700 场失利的 FBS 球队。

最后,我们可以放大我们的最新发现,并使用 plotly 制作一个简单而强大的可视化图。

import plotly.express as px
fig = px.scatter(cfb_info_df, x="Stadium_capacity", y="p_AP_Top_25_2001_to_2021", color="Current_conference_2025",
                 size="tj_altimore_fan_base_size_millions",
                labels=dict(Stadium_capacity="Stadium Capacity", 
                            p_AP_Top_25_2001_to_2021="Percent of Weeks in AP Top 25", 
                            Current_conference_2025="Conference (2025)", 
                            tj_altimore_fan_base_size_millions = "Fan Base Size"))
fig.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

该图展示了 x 轴上的体育场容量与 y 轴上的 AP 排名成功。较大的点代表估计的更大粉丝基础。

该图直观地展示了 x 轴上的体育场容量与 y 轴上的 AP 排名成功之间的强相关性。散点图上每个点的大小表示粉丝基础的估计规模。该图表明,我们可能成功地使用回归模型来估计 Tony Altimore 的粉丝基础规模分析的结果。我将在我的会议重新调整博客的第二部分中进一步讨论这个问题。

感谢阅读!请在下面评论你的想法。

对我的内容感兴趣吗?请考虑 在 Medium 上关注我

在 Twitter 上支持一下: @malloy_giovanni

大学橄榄球联盟重组——node2vec

原文:towardsdatascience.com/college-football-conference-realignment-node2vec-ba2e931bb1c

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Giovanni Malloy

·发布在 Towards Data Science ·阅读时间 16 分钟·2023 年 8 月 8 日

你已经来到了这篇四部分博客的最后一部分。在这篇博客的第三部分中,我们尝试探索基于聚类的联盟世界,其中相似的团队可以共享联盟。在本博客中,我们将从电视和媒体网络的角度进行分析。我们将专注于创建一系列为电视定制的精彩对决:每周想象一下Camping World Kickoff Game。换句话说,如果 ESPN 或 FOX 可以根据自己的喜好(以及股东的喜好)调整联盟,那么大学橄榄球的格局将会是什么样的。 在许多方面,这是一种比前面博客更现实的方法。我们的想法是计算大学橄榄球中每场可能比赛的预期收益,贪婪地填充赛程以最大化收益,创建一个“梦想”赛季,基于选择的对决定义网络图,并根据图的结构创建联盟。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Jacob Rice 提供,来自 Unsplash

本系列分为四个部分(详细动机见第一部分):

  1. 大学橄榄球联盟重组——Python 中的探索性数据分析

  2. 大学橄榄球联盟重组——回归分析

  3. 大学橄榄球联盟重组——聚类

  4. 大学橄榄球联盟重组——node2vec

希望系列的每一部分能为你提供对受人喜爱的大学橄榄球未来的新视角。对于那些没有阅读第一部分或第二部分的人,快速概述是我创建了一个从网络来源编制的数据集。这些数据包括每个 FBS 项目的基本信息、所有大学橄榄球对抗赛的非标准近似体育场规模历史表现AP 前 25 名投票频率、学校是否为AAUR1机构(对 Big Ten 和 Pac 12 的会员资格历史上很重要)、NFL 选秀球员数量2017–2019 年的项目收入数据关于大学橄榄球粉丝基础的最新估计。在第一部分,我们发现有几个特征与粉丝基础规模强相关,因此在第二部分,我们开发了线性回归和随机森林回归模型来预测粉丝基础规模。在第三部分,我们使用受限的 k-means 聚类建议了 10 个新的联盟。其中一些是传统区域对手的网络,而其他则是混合的不匹配的超级联盟。这种方法将如何与我们利润最大化的方法相比?

特征工程

鉴于模型方法的复杂性,我们首先从数据开始。这次,我们不需要做太多的处理:

import numpy as np
import pandas as pd

cfb_info_df = pd.read_csv(r'.\FBS_Football_Team_Info.csv', encoding = 'unicode_escape')

现在,我们将设置一些用户指定的输入来帮助我们计算特定比赛的预期收益。我们知道前 25 名比赛和对抗赛是精彩的电视节目,因此我们为每场比赛中的每支前 25 名球队分配了一个额外的乘数(每队 1.25 倍)。对于两支前 25 名球队的比赛,这相当于略高于 1.5 倍。对抗赛的额外乘数为 1.5 倍。我们还定义了每个赛季的联盟比赛数量,以帮助按联盟计算收益。目前,许多联盟每赛季进行 9 场联盟比赛。

multiplier_per_top_25_team = 0.25
multiplier_rivalry_game = 0.5

conference_games_per_team = 9

现有联盟的预期收益

现在,我们准备将预期收益模型投入实际应用。对于每个现有的联盟,我们可以计算每场可能的联盟比赛的收益。首先,我们可以创建每个联盟中所有球队的列表:

acc_teams = list(cfb_info_df['Team'][np.where(cfb_info_df['Current_conference_2025'] == 'ACC')[0]])
american_teams = list(cfb_info_df['Team'][np.where(cfb_info_df['Current_conference_2025'] == 'American')[0]])
big_12_teams = list(cfb_info_df['Team'][np.where(cfb_info_df['Current_conference_2025'] == 'Big 12')[0]])
big_ten_teams = list(cfb_info_df['Team'][np.where(cfb_info_df['Current_conference_2025'] == 'Big Ten')[0]])
cusa_teams = list(cfb_info_df['Team'][np.where(cfb_info_df['Current_conference_2025'] == 'C-USA')[0]])
independent_teams = list(cfb_info_df['Team'][np.where(cfb_info_df['Current_conference_2025'] == 'Independent')[0]])
mac_teams = list(cfb_info_df['Team'][np.where(cfb_info_df['Current_conference_2025'] == 'MAC')[0]])
mountain_west_teams = list(cfb_info_df['Team'][np.where(cfb_info_df['Current_conference_2025'] == 'Mountain West')[0]])
pac_12_teams = list(cfb_info_df['Team'][np.where(cfb_info_df['Current_conference_2025'] == 'Pac-12')[0]])
sec_teams = list(cfb_info_df['Team'][np.where(cfb_info_df['Current_conference_2025'] == 'SEC')[0]])
sun_belt_teams = list(cfb_info_df['Team'][np.where(cfb_info_df['Current_conference_2025'] == 'Sun Belt')[0]])

这不是绝对必要的,但我认为它有助于消除之后的混淆。接下来,我们可以开始按会议进行分析。为了本博客的目的,我们以 ACC 为例。我们的策略是循环遍历每支球队及其可能的对手,以获得一个可能的会议比赛数据框。对于每场比赛,我们将假设预期回报是每支球队球迷基础的大小、其中一个、两个或没有球队进入 AP top 25,以及比赛是否为对抗赛的函数。因此,在循环过程中我们会跟踪这些因素。在这个例子中,我们跟踪对抗赛的方式是通过从对抗赛边列表创建一个网络。

import networkx as nx

rivalry_edge_list = pd.read_csv(r'.\cfb_rivalry_edge_list.csv', encoding = 'unicode_escape')

G = nx.Graph()

node_df = pd.read_csv(r'.\cfb_node_list.csv', encoding = 'unicode_escape')

for team in np.array(node_df['Team']):
    G.add_node(team)

for index in range(0, len(rivalry_edge_list['i'])):
    G.add_edge(rivalry_edge_list['i'][index], rivalry_edge_list['j'][index], attr = rivalry_edge_list['weight'][index])

现在,我们准备好开始循环:

curr_conf_teams = acc_teams

team_i_name = []
team_j_name = []
game_is_rivalry = []
team_i_prob_top_25 = []
team_j_prob_top_25 = []
team_i_fanbase = []
team_j_fanbase = []

for i in range(len(curr_conf_teams)):
    for j in range((i+1), len(curr_conf_teams)):
        team_i_name.append(curr_conf_teams[i])
        team_j_name.append(curr_conf_teams[j])

        if (curr_conf_teams[i] in list(G.neighbors(curr_conf_teams[j]))) | (curr_conf_teams[j] in list(G.neighbors(curr_conf_teams[i]))):
            game_is_rivalry.append(1)
        else:
            game_is_rivalry.append(0)

        team_i_prob_top_25.append(float(cfb_info_df['p_AP_Top_25_2001_to_2021'][np.where(cfb_info_df['Team'] == curr_conf_teams[i])[0]]))
        team_j_prob_top_25.append(float(cfb_info_df['p_AP_Top_25_2001_to_2021'][np.where(cfb_info_df['Team'] == curr_conf_teams[j])[0]]))

        team_i_fanbase.append(float(cfb_info_df['tj_altimore_fan_base_size_millions'][np.where(cfb_info_df['Team'] == curr_conf_teams[i])[0]]))
        team_j_fanbase.append(float(cfb_info_df['tj_altimore_fan_base_size_millions'][np.where(cfb_info_df['Team'] == curr_conf_teams[j])[0]]))

然后,我们使用 pandas 创建一个包含所有会议对阵信息的数据框。我们知道 AP Top 25 每周都会变化,因此我们假设进入 AP Top 25 的概率是一个球队在 2001–2021 年间出现在 AP Top 25 调查中的频率。对于每场比赛,有三种情况:两个 top 25 球队、一支 top 25 球队或零支 top 25 球队。我们计算每种情况的概率。然后,对于这些情况中的每一种,我们计算预期回报为 (1+muliplier_top_25)^(# teams in top 25) * (1 + (multiplier_rivalry_game * is_rivalry_game) * (两个球队球迷基础的总和)。最后,我们通过将比赛情况的概率与该比赛情况的回报相乘来获得比赛的预期回报。

acc_game_df = pd.DataFrame(team_i_name, columns = ['team_i'])
acc_game_df['team_j'] = team_j_name
acc_game_df['is_rivalry_game'] = game_is_rivalry
acc_game_df['p_top_25_team_i'] = team_i_prob_top_25
acc_game_df['p_top_25_team_j'] = team_j_prob_top_25
acc_game_df['fanbase_millions_team_i'] = team_i_fanbase
acc_game_df['fanbase_millions_team_j'] = team_j_fanbase

acc_game_df['p_2_top_25_teams'] = acc_game_df['p_top_25_team_i'] * acc_game_df['p_top_25_team_j']
acc_game_df['p_0_top_25_teams'] = (1 - acc_game_df['p_top_25_team_i']) * (1 - acc_game_df['p_top_25_team_j'])
acc_game_df['p_1_top_25_teams'] = 1 - acc_game_df['p_0_top_25_teams'] - acc_game_df['p_2_top_25_teams']

acc_game_df['payoff_2_top_25_teams'] = ((1 + multiplier_per_top_25_team) ** 2) * (1 + (multiplier_rivalry_game * acc_game_df['is_rivalry_game'])) * (acc_game_df['fanbase_millions_team_i'] + acc_game_df['fanbase_millions_team_j'])
acc_game_df['payoff_0_top_25_teams'] = (1 + (multiplier_rivalry_game * acc_game_df['is_rivalry_game'])) * (acc_game_df['fanbase_millions_team_i'] + acc_game_df['fanbase_millions_team_j'])
acc_game_df['payoff_1_top_25_teams'] = (1 + multiplier_per_top_25_team) * (1 + (multiplier_rivalry_game * acc_game_df['is_rivalry_game'])) * (acc_game_df['fanbase_millions_team_i'] + acc_game_df['fanbase_millions_team_j'])

acc_game_df['expected_payoff_game'] = (acc_game_df['p_2_top_25_teams'] * acc_game_df['payoff_2_top_25_teams']) + (acc_game_df['p_1_top_25_teams'] * acc_game_df['payoff_1_top_25_teams']) + (acc_game_df['p_0_top_25_teams'] * acc_game_df['payoff_0_top_25_teams'])

结果是所有可能的会议对阵的预期回报分布,我们可以使用 plotly 直方图绘制出来:

import plotly.express as px

fig = px.histogram(acc_game_df, x="expected_payoff_game", 
                   title='ACC Conference Games',
                   labels={'expected_payoff_game':'Expected Payoff per Game'})
fig.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

大多数 ACC 比赛的预期回报在 0 到 7 之间。有一些异常值的回报非常高。

我们可以看到,通常情况下,比赛的预期回报在 0 到 7 之间。像 Florida State — Miami、Clemson — Florida State 和 Virginia Tech — Miami 这样的比赛是回报高于 10 的异常值。这些比赛作为持续的黄金时段对决是合理的,这验证了我们的模型。

我们可以对每个会议重复这一过程,并通过箱形图轻松比较各个会议的分布。在这里,我们将使用 seaborn 和 matplotlib 来创建箱形图。

from matplotlib import pyplot as plt
import seaborn as sns

combined_payoff_df = pd.DataFrame({'ACC': acc_game_df['expected_payoff_game'],
                                   'American': american_game_df['expected_payoff_game'],
                                   'Big 12': big_12_game_df['expected_payoff_game'],
                                   'B1G': big_ten_game_df['expected_payoff_game'],
                                   'C-USA': cusa_game_df['expected_payoff_game'],
                                   'MAC': mac_game_df['expected_payoff_game'],
                                   'Mountain West': mountain_west_game_df['expected_payoff_game'],
                                   'Pac-12': pac_12_game_df['expected_payoff_game'],
                                   'SEC': sec_game_df['expected_payoff_game'],
                                   'Sun Belt': sun_belt_game_df['expected_payoff_game']})

plt.figure(figsize=(16, 6))
sns.set_style('white')
sns.boxplot(data=combined_payoff_df)
sns.despine()
plt.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在经济回报方面,我们可以看到 Big Ten 和 SEC 明显占优势,而其他 Power 5 会议紧密分布,Mountain West 会议则在 Group of 5 会议中处于领先地位。

在这里,我们可以数学上确认体育媒体世界自最新一轮重组以来一直声称的:大十和 SEC 正在巩固权力和价值。这对于诸如“大游戏”这样的会议的精彩对决尤其如此:俄亥俄州立大学——密歇根大学。它确实名副其实。我们看到 ACC、大 12 和 Pac-12 之间存在相对的平衡,也看到山西西部会议是 Group of 5 会议中的明确首选。在下面的表格中,我们可以详细了解按会议划分的回报情况。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

表格比较了每场比赛和每队每会议的预期回报。

正如我们之前所指出的,大十和 SEC 主导了比赛,但 SEC 的方差几乎是大十的一半。另一个有趣的结果是,大 12 的每队回报大约是 Pac-12 的一半。那么,为什么这两个联盟最近似乎在不断重组争吵呢?这可能与 Pac-12 剩余的宝贵资产:俄勒冈州和华盛顿的未来不确定性有关。另一方面,大 12 的球队并不担心被挖角。

贪婪调度

现在,我们对我们的模型已经足够自信,可以计算每场比赛的预期回报,我们可以计算 FBS 中所有 8,778 种可能的对决的预期回报。

fbs_teams = list(cfb_info_df['Team'])

curr_conf_teams = fbs_teams

team_i_name = []
team_j_name = []
game_is_rivalry = []
team_i_prob_top_25 = []
team_j_prob_top_25 = []
team_i_fanbase = []
team_j_fanbase = []

for i in range(len(curr_conf_teams)):
    for j in range((i+1), len(curr_conf_teams)):
        team_i_name.append(curr_conf_teams[i])
        team_j_name.append(curr_conf_teams[j])

        if (curr_conf_teams[i] in list(G.neighbors(curr_conf_teams[j]))) | (curr_conf_teams[j] in list(G.neighbors(curr_conf_teams[i]))):
            game_is_rivalry.append(1)
        else:
            game_is_rivalry.append(0)

        team_i_prob_top_25.append(float(cfb_info_df['p_AP_Top_25_2001_to_2021'][np.where(cfb_info_df['Team'] == curr_conf_teams[i])[0]]))
        team_j_prob_top_25.append(float(cfb_info_df['p_AP_Top_25_2001_to_2021'][np.where(cfb_info_df['Team'] == curr_conf_teams[j])[0]]))

        team_i_fanbase.append(float(cfb_info_df['tj_altimore_fan_base_size_millions'][np.where(cfb_info_df['Team'] == curr_conf_teams[i])[0]]))
        team_j_fanbase.append(float(cfb_info_df['tj_altimore_fan_base_size_millions'][np.where(cfb_info_df['Team'] == curr_conf_teams[j])[0]]))

fbs_game_df = pd.DataFrame(team_i_name, columns = ['team_i'])
fbs_game_df['team_j'] = team_j_name
fbs_game_df['is_rivalry_game'] = game_is_rivalry
fbs_game_df['p_top_25_team_i'] = team_i_prob_top_25
fbs_game_df['p_top_25_team_j'] = team_j_prob_top_25
fbs_game_df['fanbase_millions_team_i'] = team_i_fanbase
fbs_game_df['fanbase_millions_team_j'] = team_j_fanbase

fbs_game_df['p_2_top_25_teams'] = fbs_game_df['p_top_25_team_i'] * fbs_game_df['p_top_25_team_j']
fbs_game_df['p_0_top_25_teams'] = (1 - fbs_game_df['p_top_25_team_i']) * (1 - fbs_game_df['p_top_25_team_j'])
fbs_game_df['p_1_top_25_teams'] = 1 - fbs_game_df['p_0_top_25_teams'] - fbs_game_df['p_2_top_25_teams']

fbs_game_df['payoff_2_top_25_teams'] = ((1 + multiplier_per_top_25_team) ** 2) * (1 + (multiplier_rivalry_game * fbs_game_df['is_rivalry_game'])) * (fbs_game_df['fanbase_millions_team_i'] + fbs_game_df['fanbase_millions_team_j'])
fbs_game_df['payoff_0_top_25_teams'] = (1 + (multiplier_rivalry_game * fbs_game_df['is_rivalry_game'])) * (fbs_game_df['fanbase_millions_team_i'] + fbs_game_df['fanbase_millions_team_j'])
fbs_game_df['payoff_1_top_25_teams'] = (1 + multiplier_per_top_25_team) * (1 + (multiplier_rivalry_game * fbs_game_df['is_rivalry_game'])) * (fbs_game_df['fanbase_millions_team_i'] + fbs_game_df['fanbase_millions_team_j'])

fbs_game_df['expected_payoff_game'] = (fbs_game_df['p_2_top_25_teams'] * fbs_game_df['payoff_2_top_25_teams']) + (fbs_game_df['p_1_top_25_teams'] * fbs_game_df['payoff_1_top_25_teams']) + (fbs_game_df['p_0_top_25_teams'] * fbs_game_df['payoff_0_top_25_teams'])

结果分布如下:

fig = px.histogram(fbs_game_df, x="expected_payoff_game", 
                   title='All Possible NCAA FBS Games',
                   labels={'expected_payoff_game':'Expected Payoff per Game'})
fig.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

整个 FBS 中每场比赛的预期回报分布严重右偏。在近 9,000 种可能的对决中,大多数回报较低。

直方图揭示了一个预期结果:有一些比赛的回报非常高,而大多数则是低回报。分布是右偏的,游戏的中位数预期回报大约为 2,平均预期回报大约为 3。虽然预期回报数字本身难以解释,但定性结果无疑是启发性的。会议重组的动机在于最大化每队的会议预期回报(这通常,但并非总是,与最大化每场比赛的预期回报相一致)。

虽然我们都希望每年能看到 8,778 场大学橄榄球比赛,但每支球队的赛程是有限的。通常,球队会打 12 场常规赛(除非他们在夏威夷打比赛,那他们可以打 13 场),但为了我们的目的,假设每支球队每赛季可以打 15 场比赛,以增加一些额外的灵活性,方便扩展季后赛。我们可以创建一个数据框来跟踪我们新安排的比赛:

num_games_per_school = 15

schedule_df = pd.DataFrame(cfb_info_df['Team'])
schedule_df['games_scheduled'] = 0

for i in range(num_games_per_school):
    col_name = 'game_' + str(i+1)
    schedule_df[col_name] = ''

我们将安排视为一个背包问题,我们将使用贪婪算法来解决。实质上,我们将尽可能多地将最高回报率的比赛添加到赛程中,前提是该比赛的两支球队的赛程中仍有空余。结果应该是媒体公司所期望的最佳赛季。他们将尽可能多地让最顶尖的球队对战。当然,这种策略忽略了任何现有的会议边界,这正是重点:我们将首先生成赛程,然后使用这些赛程来确定会议。以下是生成我们主 FBS 赛程的代码:

#Sort all games in descending order
sorted_fbs_game_df = fbs_game_df.sort_values(by = 'expected_payoff_game', ascending = False)
sorted_fbs_game_df['is_game_scheduled'] = 0

# For each game, pick it if the teams have space on their schedule
for index in range(len(sorted_fbs_game_df['expected_payoff_game'])):

    #Get index of team in schedule df
    team_i_sched_id = np.where(schedule_df['Team'] == sorted_fbs_game_df['team_i'].iloc[index])[0][0]
    team_j_sched_id = np.where(schedule_df['Team'] == sorted_fbs_game_df['team_j'].iloc[index])[0][0]

    #Check that both teams have room in the schedule
    if (schedule_df['games_scheduled'].iloc[team_i_sched_id] < num_games_per_school) and (schedule_df['games_scheduled'].iloc[team_j_sched_id] < num_games_per_school):

        #Find num games scheduled
        num_games_i = schedule_df['games_scheduled'].iloc[team_i_sched_id]
        num_games_j = schedule_df['games_scheduled'].iloc[team_j_sched_id]

        #Add team j to team i's schedule
        team_i_name = sorted_fbs_game_df['team_i'].iloc[index]
        team_j_name = sorted_fbs_game_df['team_j'].iloc[index]

        schedule_df['game_'+str(num_games_i + 1)].iloc[team_i_sched_id] = team_j_name

        #Add team i to team j's schedule
        schedule_df['game_'+str(num_games_j + 1)].iloc[team_j_sched_id] = team_i_name

        #Increment games scheduled
        schedule_df['games_scheduled'].iloc[team_i_sched_id] = schedule_df['games_scheduled'].iloc[team_i_sched_id] + 1
        schedule_df['games_scheduled'].iloc[team_j_sched_id] = schedule_df['games_scheduled'].iloc[team_j_sched_id] + 1

        #Mark game as scheduled in sorted games df
        sorted_fbs_game_df['is_game_scheduled'].iloc[index] = 1

    if index % 100 == 0:
        print(str(round((index+1)/len(sorted_fbs_game_df['expected_payoff_game']), 2)*100)+'% complete')

最高回报率的比赛不被安排?俄亥俄州立大学对阵佛罗里达州立大学,预计回报率为 19.84。最低回报率的比赛?杰克逊维尔州立大学对阵萨姆休斯顿州立大学,预计回报率为 0.08。我们可以创建一个直方图来比较我们贪婪选择的比赛的预计回报率与所有可能比赛的预计回报率:

fig = px.histogram(chosen_games_df, x="expected_payoff_game", 
                   title='Chosen NCAA FBS Games',
                   labels={'expected_payoff_game':'Expected Payoff per Game'})
fig.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

大多数低回报率的比赛没有被选择,而高回报率的比赛被选择了。

根据直方图,我们可以看到,我们的新会议模型保留了最高回报率的比赛,并大幅减少了最低回报率的比赛,相较于可能的 FBS 比赛。

网络模型

要从主赛程转换到会议,我们需要理解现在球队之间的关系。我们将采用的方法是使用networkx将我们的主 FBS 赛程转换为无向图。(如果你对图论不熟悉,我建议你从我多年前开始的地方入手:维基百科。)每支球队将成为一个顶点(或节点),每场比赛将成为图中的一条边。我们还将为图添加边权重,其中每条边的权重是连接该边的两支球队之间比赛的预计回报率。让我们创建网络:

#Get dataframe of only chosen games
chosen_games_df = sorted_fbs_game_df.iloc[np.where(sorted_fbs_game_df['is_game_scheduled'] == 1)]

#Create graph
chosen_G = nx.from_pandas_edgelist(chosen_games_df, source='team_i', target='team_j')

#Add edge weights to graph
for c in range(len(chosen_games_df['team_i'])):
    chosen_G[chosen_games_df['team_i'].iloc[c]][chosen_games_df['team_j'].iloc[c]]['weight'] = chosen_games_df['expected_payoff_game'].iloc[c]

我们可以通过使用 draw_circular()来可视化地确定这种方法是否适用于创建会议。我们可以直观地检查模型是否看起来像一个小世界。这是一个好兆头,因为我们期望同一会议的球队彼此对战。在我们的网络中,这意味着大量的共同邻居。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

主 FBS 赛程显示出小世界特性。

node2vec

将我们的大学橄榄球球队网络转化为一组联盟的关键在于一种名为node2vec的算法,该算法由 Aditya Grover 和 Jure Leskovec 提出。node2vec 算法的简短总结是,它使用偏置随机游走过程来生成图中节点的低维表示。它的灵感来自于那些处理自然语言处理的人员所熟悉的 word2vec 算法。然而,node2vec 并不是词的 skip gram 模型,而是图中的偏置随机游走。稍后,我会尝试做一个博客帖子,比较这两者。如果我们在 Python 中使用了 SNAP 包来创建网络,那里有一个内置的node2vec函数。然而,我们将使用与 networkx 兼容的实现。我必须承认,在线文档不像我在博客系列中使用的大多数其他包那样详尽,因此我会尽量详细解释。

from node2vec import Node2Vec

#Precompute probabilities and generate walks
node2vec = Node2Vec(chosen_G, dimensions = 64, walk_length = 30, num_walks = 50, workers = 1, seed = 0)

#Embed nodes
model = node2vec.fit(window = 10, min_count = 1, batch_words = 4)

在上面的代码中,我们导入了 networkx 的 node2vec 包,首先生成随机游走。维度输入是每个节点的数组输出长度,游走长度是每次随机游走包含的转移数量,游走数量是从每个节点开始的随机游走数量。workers 输入用于并行执行,但对于 Windows 机器,必须设置为 1。然后,我们运行 node2vec 模型以获取嵌入。我们创建了一个仅包含嵌入的数据框和一个包含球队名称的数据框。

# Data frame of embeddings only
node2vec_df_clean = pd.DataFrame(model.wv.vectors)

#Data frame of embeddings and team name
node2vec_df = pd.DataFrame(model.wv.vectors)
node2vec_df['Team'] = model.wv.key_to_index.keys()

聚类

和之前一样,我们将使用 k-means 聚类来创建我们的联盟。然而,这次,聚类将使用 node2vec 嵌入来创建。通过这种方式,网络结构中具有相似位置的球队被分组到一起形成联盟。鉴于网络基于贪婪调度,我们应该看到那些经常一起比赛的球队被聚集到新的联盟中。由于我在这一系列的第三部分中已经详细描述了 k-means 聚类,所以现在我将跳过解释。相反,这里是实现 8 个重新调整联盟的聚类的代码。这次,我们的联盟数量将是总球队数量和每支球队每赛季比赛数量的函数。

from sklearn.cluster import KMeans

num_conferences = int(len(node2vec_df['Team']) / num_games_per_school)
kmeans = KMeans(n_clusters = num_conferences, random_state=0).fit(node2vec_df_clean)
kmeans_conferences = kmeans.labels_
node2vec_df['Conference'] = kmeans_conferences

数据驱动的 FBS 联盟

现在,揭晓时刻到了!如果媒体高管能够随心所欲地展示每年大学橄榄球中最有回报的比赛,球队将如何重新划分为新的联盟?这种方法既受到数据的驱动,也受到金钱的推动,因此这是我们迄今为止最强有力的推荐。

  • ACC+:雪城大学、迈阿密(佛罗里达)、佛罗里达州立大学、内布拉斯加大学、克莱姆森大学、北卡罗来纳大学、南卡罗来纳大学、密苏里大学、阿肯色大学、爱荷华大学、弗吉尼亚理工大学、华盛顿大学、奥尔·密斯大学、西弗吉尼亚大学、加州大学洛杉矶分校、肯塔基大学、马里兰大学、亚利桑那州立大学、乔治亚理工学院。我忍不住在这个博客中用一个“+”来命名一个会议。这太时髦了,这个会议看起来像是 ACC 加上一些其他的。在这种情况下,“+”包括了中美洲和太平洋沿岸的存在。这对那些无法在超级 20 中获利的媒体高管来说真是一个真正的喜悦。

  • Mountain South:马歇尔大学、特洛伊大学、路易斯安那大学、UTEP 大学、阿巴拉契亚州立大学、路易斯安那理工大学、佛罗里达大西洋大学、圣荷西州立大学、UTSA 大学、阿肯色州立大学、南卫理公会大学、UNLV 大学、阿克伦大学、布法罗大学、犹他州立大学、FIU 大学、北德克萨斯大学、路易斯安那-门罗大学。这些球队都来自当前的山地西部会议或美国南部,除了布法罗和阿克伦这两个明显的例外。这些球队有可能打乱一个高期望的赛季(参见阿巴拉契亚州立大学对密歇根大学)。

  • Tuesday Group:新墨西哥州立大学、南阿拉巴马大学、乔治亚州立大学、鲍灵格林大学、中密歇根大学、德克萨斯州立大学、鲍尔州立大学、肯特州立大学、杰克逊维尔州立大学、东密歇根大学、詹姆斯·麦迪逊大学、萨姆·休斯顿州立大学。部分以温和的共和党核心小组命名,部分因为这个会议最有可能在 ESPNU 的热门周二夜晚时段比赛。

  • Power Group:海军学院、杜克大学、弗雷斯诺州立大学、西北大学、维吉尼亚大学、波士顿学院、印第安纳大学、中佛罗里达大学、爱荷华州立大学、贝勒大学、华盛顿州立大学、范德比大学、圣地亚哥州立大学、堪萨斯大学、陆军学院、俄勒冈州立大学。这个会议包括了在五大会议中粉丝基础较低的球队和五小会议中粉丝基础较高的球队。

  • Super 20:俄亥俄州立大学、诺特丹大学、德克萨斯大学、佛罗里达大学、密歇根大学、宾夕法尼亚州立大学、阿拉巴马大学、俄勒冈大学、路易斯安那州立大学、乔治亚大学、威斯康星大学、南加州大学、俄克拉荷马大学、奥本大学、德克萨斯农工大学、密歇根州立大学、田纳西大学、德克萨斯理工大学、伊利诺伊大学、TCU 大学。这是一个会让 ESPN、福克斯、CBS 和 NBC 争夺的会议。大名鼎鼎的球队意味着大粉丝基础和丰厚的回报。这是一个媒体高管的梦想,实际上如果 Big Ten 和 SEC 的最佳球队希望联合起来并加入一些其他球队,也并非不可能。

  • Pan-American Conference 17 (Pac-17):匹兹堡大学、明尼苏达大学、NC 州立大学、俄克拉荷马州立大学、普渡大学、斯坦福大学、亚利桑那大学、路易斯维尔大学、博伊西州立大学、犹他大学、密西西比州立大学、BYU 大学、加州大学伯克利分校、堪萨斯州立大学、康涅狄格大学、科罗拉多大学、拉格斯大学。这是我们新调整的世界中最大的会议之一。它从康涅狄格州延伸到加利福尼亚州,拥有许多粉丝基础较小的稳固球队。预测是高质量的足球,但不总是回报最高的足球。

  • 中西部: UAB、中田纳西州立大学、西密歇根大学、自由大学、塔尔萨大学、迈阿密(OH)、莱斯大学、托莱多大学、北伊利诺伊大学、夏洛特大学、旧统治大学、俄亥俄大学、海岸卡罗莱纳大学、杜兰大学、UMass、乔治亚南方大学、西肯塔基大学。这个会议结合了来自目前五大组别的各支球队。大多数这些球队位于国家的内陆地区。因此,提出了这个名字。

  • AAMWC: 休斯顿、孟菲斯、新墨西哥、怀俄明、夏威夷、东卡罗来纳、内华达、南密西西比、科罗拉多州、南佛罗里达、辛辛那提、空军、天普、维克森林大学。这个会议是当前美国竞技会议和山西会议的完美融合。这些球队过去都表现非常出色,但在他们的赛季表现上却缺乏一致性。

node2vec 算法旨在将相似的节点分组,但我们仍然应该验证每支球队的会议比赛数量在各会议之间是否稳定。

num_teams_per_conf = np.zeros(num_conferences)

for i in range(num_conferences):
    print(node2vec_df['Team'][np.where(node2vec_df['Conference'] == i)[0]])
    num_teams_per_conf[i] = len(node2vec_df['Team'][np.where(node2vec_df['Conference'] == i)[0]])

num_conference_games_per_conf = np.zeros(num_conferences)
num_non_conference_games_per_conf = np.zeros(num_conferences)

for e in chosen_G.edges():
    conference_i = node2vec_df['Conference'][np.where(node2vec_df['Team'] == e[0])[0][0]]
    conference_j = node2vec_df['Conference'][np.where(node2vec_df['Team'] == e[1])[0][0]]

    if conference_i == conference_j:
        num_conference_games_per_conf[conference_i] = num_conference_games_per_conf[conference_i] + 2
    else:
        num_non_conference_games_per_conf[conference_i] = num_non_conference_games_per_conf[conference_i] + 1
        num_non_conference_games_per_conf[conference_j] = num_non_conference_games_per_conf[conference_j] + 1

plot_df = pd.DataFrame(num_conference_games_per_conf, columns = ['num_conf_games'])
plot_df['num_non_conf_games'] = num_non_conference_games_per_conf
plot_df['Conference'] = ['ACC+', 'Mountain South', 'Tuesday Group', 'Power Group', 'Super 20', 'Pac-17', 'Middle West', 'AAMWC']
plot_df['Percent Conference Games'] = plot_df['num_conf_games']/(plot_df['num_conf_games']+plot_df['num_non_conf_games'])
plot_df['Average Number Conference Games'] = num_games_per_school*(plot_df['num_conf_games']/(plot_df['num_conf_games']+plot_df['num_non_conf_games']))

import plotly.express as px

fig = px.bar(plot_df, x='Conference', y='Average Number Conference Games', height=400)
fig.show()

上述代码生成了新调整的 FBS 世界中每所学校会议比赛平均数量的对比。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

所有会议的每支球队平均进行 10.1 到 11.3 场会议比赛。

所有新的会议平均每年进行 10 到 11 场会议比赛,总赛程为 15 场。这种会议之间的一致性验证了会议之间赛程的稳定性。

至此,我们结束了这个博客系列。我们学习了如何探索数据,使用监督和无监督机器学习,并实施决策模型以创建理想的大学橄榄球会议。也许这个博客中介绍的方法已经在幕后用于指导 FBS 中的会议重新调整。如果没有,那它们应该被使用。我们钟爱的比赛正进入一个由金钱驱动的新纪元。这个框架提出了一种由经济驱动的方法,同时融入了传统的竞争,这使得大学橄榄球与众不同。如果有一天我们看到会议按照我在这里呈现的方式对齐,你会在这里第一个听到。直到那时,我会继续享受我的周六,观看并等待。

感谢阅读!一如既往,请在下方评论你的想法。我知道这是一个思维实验,所以让我知道你的反应。你的球队最终在哪儿?你是否更喜欢这些联盟而不是今天的会议?

在 Twitter 上表达一些爱意: @malloy_giovanni

对我的内容感兴趣?请考虑 在 Medium 上关注我

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值