杜克大学数据科学笔记-全-
杜克大学数据科学笔记(全)
大规模数据科学(第1课/共3课)📊:激发胃口:政治案例

概述

在本节课中,我们将通过几个真实的政治案例,初步了解数据科学的核心活动与价值。这些案例将展示数据科学如何从简单方法中提取强大洞察,以及如何通过有效的数据处理和呈现来影响现实世界。
案例一:2012年美国总统选举预测
上一节我们概述了课程目标,本节中我们来看看第一个经典案例:2012年美国总统选举。
这是一张选举人团地图,每个州根据赢得选举人票的候选人着色,数字代表该州拥有的选举人票数。当时,这张地图之所以引发广泛讨论,是因为Nate Silver及其博客“538”在选举前完美预测了这张地图的结果。
媒体讨论常将Nate Silver描绘成天才,并强调他使用了复杂的数学方法。但有趣的是,Nate Silver本人第一个指出,他用于预测的方法实际上相当简单。
以下是他在当时博客文章中的一系列引述:
- 第一段引述来自10月26日:“这背后的逻辑应该非常简单。奥巴马先生在俄亥俄州和其他州的民调中保持领先,这足以让他赢得270张选举人票。”
- 几天后,他的表述变得更加直白:“我们提出的论点极其简单,如下:奥巴马在俄亥俄州领先。这不是魔术。”
- 选举结束后(11月10日),在他被证明正确并做出完美预测后,他在解释最初为何创建“538”博客时写道:“竞争对手设定的门槛低得诱人。一个人只需对政治竞选活动中真正具有预测能力的东西进行一些相当基础的研究,就能看起来像个天才。”
在这个案例中,真正具有预测能力的是各州民调数据本身的聚合。历史上,聚合的州民调数据在预测大选结果方面表现相当不错。他确实在量化不确定性和呈现结果方面做了一些复杂的工作,特别是创建了许多精美的交互式可视化图表,以向公众传达这些想法。
这正是我想强调的一点:在某些情况下,得到答案可能是容易的部分;而解释结果,并通过可视化等方式说服他人相信结果,往往是困难的部分。这也是我们将在整个课程中反复提及的主题之一。
总结:简单方法 + 足够多的优质数据,在许多情况下胜过更复杂的方法。这是我们将要回归的另一个主题。
案例二:奥巴马竞选团队的数据驱动系统
在转向其他话题之前,另一个与竞选相关的案例是奥巴马竞选团队使用的数据驱动地面战系统。
该系统能够针对特定类别的用户进行精准信息推送。他们建立并维护了一个非常庞大(海量)的选民数据库,并利用它来为非常具体的群体设计高度定制化的信息。
例如,一位住在俄亥俄州小镇、有两个孩子的母亲,她在推特上谈论环境问题,在Facebook页面上提到有机蔬菜,在2008年投过票,并在奥巴马网站上注册过但从未捐款。这样,她可能会收到米歇尔·奥巴马发来的信息,重点强调巴拉克·奥巴马的环境政策。
为了设计这些信息,你需要对各种可能有效或无效的方案进行临时的假设检验,并且需要以交互速度对数据进行切片和切块分析。
这引出了我们将要回归的另一个主题:对这种临时的、交互式分析的需求。用于此目的的系统也很有趣。这是一个SQL数据库,一个名为Vertica的非常快速的数据库。我们希望在课程后期讨论一下Vertica的特殊之处。它是一个SQL数据库,在数据科学语境中,SQL有时名声不佳,被视为可能无法用于分析、在当今时代已不合时宜的旧事物。
但请不要相信这种说法。在许多情况下,SQL都有一席之地。在这个案例中,他们确实使用了Hadoop来进行非实时的批量数据生成。但对于需要快速思考的数据查询,他们使用了这个Vertica数据库。我们将在后续的课程中再次讨论这些系统。
总结

本节课中,我们一起通过2012年大选预测和奥巴马竞选团队的数据运营两个政治案例,初步领略了数据科学的实践。我们了解到,优质数据结合简单方法往往能产生强大效果,数据的解释与呈现是关键挑战,而临时的、交互式的数据分析在现代数据驱动决策中至关重要。同时,我们也看到传统工具如SQL在特定场景下依然发挥着核心作用。这些主题将在后续课程中深入展开。
大规模数据科学(第1课)🌪️:激发胃口 - 极端天气数据分析

在本节课中,我们将通过分析飓风“桑迪”的案例,了解数据科学如何实时处理与可视化公开数据,以应对极端天气事件。我们将学习数据获取、重利用和可视化的基本流程。
大约在飓风“桑迪”登陆的同时,一个引人注目的现象是,针对这场风暴的实时数据可视化开始涌现。
以下是人们利用数据进行实时分析的两个例子。
- 有人利用推特数据,分析并绘制了停电区域的地图。
- 约瑟夫·鲁威尔从本地气象站获取了公开数据。
上一节我们看到了数据应用的例子,本节中我们来看看约瑟夫的具体做法。
他仅使用了两个不同气象站(大西洋城和费城)的数据,并进行了简单的绘图。
这张图显示了几日内的气压变化。你可以看到巨大的气压骤降对应着风暴过境,同时也能观察到风暴在大西洋城和费城之间的时间差。由于大西洋城的气压下降更显著,其风暴强度可能也更高。


所以,这里有几点值得注意:一是从网络获取数据并近乎实时地(或至少在短时间内)进行重利用以生成可视化图表,然后将其发布回网络。我认为这非常具有数据科学活动的特征。
在这个特定例子中,虽然不一定是大数据集,但重利用为其他目的收集的数据是我们将反复遇到的主题。此外,我们也再次看到了这种分析的临时性特点。
接下来,我们看看同一组数据的另一个变量。
以下是风速数据图。大西洋城的风速峰值达到40(绿色线),确实更为剧烈。图中的灰色部分表示误差范围。
所以,这又是对同一组数据的另一种变体分析。


本节课中,我们一起学习了如何利用公开的实时数据(如气象站数据)进行简单的可视化分析,以理解极端天气事件。我们看到了数据重利用和临时性分析在数据科学中的实际应用,这为后续处理更复杂、更大规模的数据集奠定了基础。
课程1:大规模数据科学 - 激发胃口:数字人文 📚

在本节课中,我们将通过一个具体的“数字人文”研究案例,来了解如何利用大规模数据和相对简单的方法,揭示文化趋势的变迁。这个案例分析了20世纪书籍中情感词汇的使用变化,其方法直观易懂,非常适合初学者理解数据科学的核心工作流程。
研究概述:20世纪书籍中的情感表达
上一节我们提到了数据科学的广泛应用,本节中我们来看看一个来自“数字人文”领域的生动例子。这项研究的标题是“20世纪书籍中的情感表达”。研究者感兴趣的核心问题是:我们集体文学作品中选用的词汇是否随时间发生了变化?这种变化最终是否能告诉我们一些关于文化或文明的信息?
这项科学探究本身非常引人入胜。但最引人注目的是,他们所采用的方法相当直接。即使没有深厚的技术、统计学或语言学背景,你也可以自己尝试完成类似的分析。
研究方法解析
以下是他们完成这项分析所采取的具体步骤。
第一步:数据获取与数字化
第一步看似艰巨:将20世纪所有书籍数字化。这对个人而言几乎不可能完成。但幸运的是,谷歌已经为我们完成了这项工作,并将数据公开在了一个可访问的网址上。谷歌不仅扫描了书籍,还进行了字符识别,并生成了“n-gram”数据集。
这些数据集本质上是结构化的数据表格。其中,每一行包含一个n-gram、对应的年份以及该n-gram在该年份出现的频次计数。数据已经被处理成易于使用的形式。
核心概念解释:
- n-gram:指文本中连续的n个项(如单词)。在本研究中:
- 1-gram:指单个单词,例如 “yesterday”。
- 5-gram:指连续的五个单词,例如 “analysis is often described as”。
在这项具体研究中,作者只使用了1-grams(即单个单词)进行分析。
第二步:情感评分映射
接下来,他们从这些1-gram中选取了一个子集,并为每个单词赋予了一个“情绪得分”。你可以想象,某些单词带有特定的情感色彩,与快乐、悲伤或恐惧等相关联。其同义词也可能具有相似的情感关联。
这项分析听起来不简单,确实如此。但同样,这项工作也已经有了现成的资源。网络上有一个名为WordNet的数据库,它已经完成了这类情感分析。因此,论文作者可以直接利用谷歌的数字化书籍数据(已分解为n-gram)和WordNet的情感得分。
第三步:核心计算与分析
然后,他们进行了核心计算。其数学表达式可能初看令人生畏,但实际上非常简单。
核心计算公式:
他们首先计算某个特定情感类别(如“快乐”)的得分。对于每一年:
-
计数与归一化:对属于该情感类别的每个单词,计算其在当年书籍中出现的次数,然后除以单词“the”在当年出现的次数。公式可表示为:
(特定情感单词的出现次数) / (单词“the”的出现次数)
进行这种归一化是为了消除书籍总量、数字化程度等因素的影响。之所以选择单词“the”而非总词数作为分母,是因为“the”更能代表散文性文本的比例,从而避免因书籍中图表说明、技术术语、公式等非散文内容增加而扭曲结果。 -
求和与平均:将所有属于该情感类别的单词的上述比值相加,然后除以该情感类别中的单词总数,得到该年份此类情感的平均强度。
-
标准化(Z-Score):最后,为了便于比较不同情感随时间的变化趋势,他们对每年的平均强度值进行了标准化处理。即减去所有年份的均值,再除以所有年份的标准差。这在高中学过的统计学中很常见,目的是使数据符合标准正态分布。
Z = (某年值 - 历年均值) / 历年标准差
计算资源说明:
这些数据集虽然庞大,但并非不可处理。它们的大小足以装入如今大多数笔记本电脑的内存中。因此,这是一个计算量显著但并非超级计算级别的任务。如果你有兴趣,完全可以在一个周末内尝试完成。


研究结果展示
我认为这些结果非常具有说服力。以下是论文中的部分发现:




上图展示了“快乐”词汇得分减去“悲伤”词汇得分后的Z值变化。可以观察到二战后出现了一个明显的低谷,这是论文中强调的一点。此外,在90年代后期,这个值似乎开始回升。




上图则显示了“情感词汇”总分减去“随机词汇”总分后的趋势,呈现出一个明显的随时间下降的斜率。这似乎论证了我们随时间推移,正在使用越来越少的情感词汇。不过,图中红色的“恐惧”词汇线自1980年代以来有所上升,其原因可以想象。
这项分析非常有趣,它是一项重要的研究,而完成它仅仅利用了他人已准备好的数据集。

数据科学的深远影响
关于这个案例,我还想指出另一点。下图是这篇论文所引用的一些其他研究的标题列表:

这些标题令我印象深刻:“利用数百万数字化书籍进行文化定量分析”、“量化语言的进化动力学”、“词汇使用频率预测印欧语言历史中的词汇演变速率”、“歌词与语言标记”等等。这说明了语言学、人类学、历史学、文化研究等领域,正在凭借数据驱动的方法转变为“硬科学”。从某种意义上说,所有科学都在成为数据科学。
因此,数据科学家在这个领域拥有巨大的力量。现在正是成为“数据极客”的好时代。
数据科学的影响也延伸到了新闻业。例如,当维基解密材料公布时,你不可能自己泡一壶咖啡然后逐页翻阅所有打印出来的材料。你会编写算法来进行分析,比如词汇使用分析、寻找邮件链和对话等。新闻业本身正在成为一种计算事业,一个数据科学问题,或者至少它非常适用于数据科学技术。
所以,作为一名数据科学家,世界是你的舞台。
本节课中我们一起学习了一个精彩的“数字人文”案例。我们看到了如何通过获取现成的大规模数据集(谷歌图书n-gram)、利用已有的知识库(WordNet情感词典),并应用基础的统计计算(计数、归一化、标准化),来揭示长达一个世纪的文化情感变迁趋势。这个例子生动地说明了数据科学方法的强大与可及性,即使初学者也能理解其核心逻辑。在接下来的课程中,我们将继续探讨更多数据科学的应用实例。
📚 大规模数据科学(第1课)| 文献计量学:从引文网络到科学发现

在本节课中,我们将通过一个具体的例子——文献计量学,来探索如何利用图(Graph)这种数据结构进行大规模数据分析。我们将学习如何评估科学论文的重要性,以及如何通过分析引文网络来发现新兴的科学领域。
🔍 评估论文的重要性
上一节我们提到了图数据结构的广泛应用。本节中,我们来看看如何利用图来解决一个实际问题:如何判断一篇科学论文相对于其他论文的重要性?
一种直观的方法是统计论文被引用的次数。例如,在下图中,蓝色圆圈标记的论文被其他四篇论文引用,而红色圆圈标记的论文只被引用了一次。因此,蓝色论文似乎比红色论文更具影响力。

然而,如果我们观察更长的时间(图中用深蓝色表示),情况可能发生变化。中间那篇论文可能会被更多后续论文引用。最终,我们可能会得出结论:这篇中间论文的影响力更大,因为它不仅直接影响了其他论文,还通过其引用的论文产生了间接影响。
那么,我们如何在两种解释之间做出决定呢?这个问题与判断网页相对重要性的问题非常相似。
⚖️ PageRank算法:从网页到论文
解决上述问题的一种方法是借鉴谷歌提出的PageRank算法。该算法的核心思想是:一个网页(或一篇论文)的重要性,取决于链接到(或引用)它的其他网页(或论文)的重要性。
以下是PageRank算法的基本思路:
- 每个节点(网页或论文)初始被赋予相同的权重。
- 每个节点将自己的权重平均分配给所有它指向(或引用)的节点。
- 每个节点将所有指向它的节点传递来的权重相加,作为自己的新权重。
- 重复步骤2和3,直到所有权重值收敛(即变化非常小)。
最终,每个节点收敛后的权重值就代表了其相对重要性。这个过程可以用以下公式化的方式描述:
公式:
新权重 = 所有指向本节点的权重之和
这个算法不仅适用于网页排名,也适用于任何具有链接或引用关系的图结构数据,例如我们的科学论文引文网络。
🧩 超越重要性:发现科学新领域
除了评估重要性,对引文网络进行数据分析还能揭示更深层次的模式。使用相同的数据集,卡尔·伯格斯特龙和马丁·罗斯威尔创建了另一种可视化。


这次我们暂时忽略重要性问题,只关注引文网络图本身。一种分析是判断重要性,另一种分析则是进行聚类。
聚类算法能够将相似的文档(论文)自动分组。研究人员将这些聚类映射到相应的科学领域。例如,一个包含《柳叶刀》和其他医学期刊的聚类可以 confidently 被判定为“医学”领域;另一个聚类则可能代表“分子与细胞生物学”。
将这种聚类结果按时间线排列后,一个惊人的发现出现了:一部分分子与细胞生物学领域和神经学领域的论文开始融合,形成了一个全新的科学领域——神经科学。
仅仅通过对这个图进行此类数据分析(即数据科学),我们就能揭示新兴科学领域的诞生。这个发现非常引人注目。
📊 文献计量学:研究科学本身
基于同一数据集,研究者们还进行了许多其他类型的分析。这种通过研究科学文献本身来得出推论的整体领域,被称为 文献计量学。

📝 本节课总结
在本节课中,我们一起学习了:
- 问题引入:如何评估科学论文的重要性,并发现这与网页排名问题类似。
- 核心算法:引入了PageRank算法,它通过考虑“被重要节点引用”来计算图中节点的重要性。
- 扩展应用:对同一引文网络进行聚类分析,可以自动识别和跟踪科学领域的演变,甚至能发现像神经科学这样的新兴交叉领域。
- 领域概览:这类研究构成了一个名为文献计量学的元科学领域,即用数据科学的方法来研究科学活动本身。

通过这个案例,我们看到了如何将抽象的图算法(如PageRank和聚类)应用于具体的、大规模的真实世界数据,从而获得有价值的洞察。
大规模数据科学(第1课/共3课)🍽️🎵🏥:激发胃口:食品、音乐与公共卫生

在本节课中,我们将通过三个具体案例,探索数据科学在食品搭配、音乐流派演变和公共卫生预测等非传统领域的应用。我们将看到数据驱动方法如何揭示隐藏的模式,同时理解数据本身的局限性。
上一节我们讨论了数据科学的核心概念,本节中我们来看看数据科学如何应用于一些意想不到的领域。
🍽️ 食品搭配的数据驱动分析
你或许认为食品搭配是一门艺术,难以用数据科学分析。然而,一项发表在权威期刊上的研究应用了数据驱动技术来分析食物配对。
以下是他们的研究方法:
- 构建食材关系图:如果两种食材在某道菜谱中同时出现,就在它们之间绘制一条边。这形成了一个由顶点(食材)和边(共现关系)构成的图。
- 分析图结构:研究者分析了这个大型图表的社区结构,寻找其中的聚类。
- 对比传统认知:他们检查这些数据聚类是否与已知的烹饪流派或搭配方法相符。
研究结果显示,在某些情况下数据聚类与传统认知一致,而在另一些情况下则揭示了此前未被明确认识、但隐藏在数据中的搭配模式。这种数据驱动的方法,利用网络上大量的菜谱数据,将以往基于经验的“民间智慧”置于了更量化的基础之上。这是一个有趣且非常规的数据科学应用案例。
🎵 音乐流派的演变分析
接下来,我们看看数据科学如何用于分析文化趋势,例如音乐流派的演变。
这个例子来自Last.fm博客。他们分析了歌曲上的标签数据,用于研究音乐流派随时间推移的兴衰。
以下是他们的分析思路:
- 利用标签数据:他们使用了歌曲上用户添加的标签(如“朋克”、“摇滚”)。
- 追踪流行度变化:通过分析特定时期这些标签的流行程度,来推断流派的兴起。
- 可视化演变过程:例如,图表显示“后朋克”(红色)在“朋克”(紫色)之后出现。你也能看到“摇滚”随时间兴起,并在近期略有衰退。
这个案例再次体现了数据再利用的主题。这些标签数据最初只是为了帮助用户搜索和发现相似音乐而收集的,现在却被重新用于推断整个音乐流派的演变历程。
🏥 公共卫生预测与数据偏差
最后,我们探讨一个更严肃的领域:利用数据科学进行公共卫生预测,并认识其中的挑战。
一个著名的例子是谷歌流感趋势。谷歌通过分析搜索日志中特定词汇的出现频率,试图预测流感爆发的严重程度和范围。
他们的工作原理基于一个假设:当人们患上流感时,会更频繁地搜索流感症状相关的词汇。通过监测这些词汇搜索量的激增,可以预测流感爆发的到来。
最初,这项服务表现得比美国疾病控制与预防中心的传统监测方法更早、更准确。谷歌因此发布了论文并提供了交互式可视化工具。
然而,近年来的科学回顾显示,谷歌流感趋势在最近一次预测中严重高估了流感季的规模。
研究者认为原因在于数据偏差:由于媒体对当年流感季的高度关注,导致人们更多地搜索流感相关词汇。这种搜索行为可能源于对症状的担忧、想了解更多信息、搜索相关新闻,或是担心自己的孩子,而并非完全源于实际患病率的上升。这种由媒体关注引发的二阶效应,最终导致了有偏差的数据和错误的预测结论。
这个案例的启示是:将搜索引擎的数据重新用于其他领域的预测固然强大,但必须谨慎对待这些数据中可能存在的偏差,并仔细审视由此得出的结论。


本节课中,我们一起学习了数据科学在食品网络分析、音乐流派追踪和公共卫生预测三个领域的应用。我们看到,通过构建关系图、分析时间序列数据和再利用现有数据,可以揭示出有趣的模式。同时,谷歌流感趋势的案例也提醒我们,必须警惕数据中的偏差,并审慎地解释分析结果。数据科学是一把强大的工具,但其有效性和可靠性高度依赖于对数据本身特性的深刻理解。
大规模数据科学(第1课/共3课)🚀:激发胃口:公共卫生、地震与法律案例

在本节课中,我们将通过几个具体案例,探讨数据科学在不同领域的应用与挑战。这些案例涵盖了公共卫生监测、地震预测的法律责任以及数据再利用等核心主题,帮助我们理解数据科学实践的广度与深度。
公共卫生:利用网络搜索数据监测药物副作用 💊
上一节我们看到了数据在公共卫生领域的初步应用,本节中我们来看看一个更具体的科学分析案例。
微软研究院的一些研究人员进行了一项分析网络搜索流量的研究。这项研究旨在发现与特定药物相关的副作用。
以下是该研究的关键发现图示与分析:

该图表展示了对约一百万用户(在获得许可的情况下)的网络搜索流量进行长期监测的结果。具体分析如下:
- 药物A的关联性:当用户搜索药物A(图中绿色部分)时,大约有5% 的时间他们也搜索了与高血糖症状相关的词汇。
- 药物B的关联性:对于药物B,这个比例约为4%。
- 背景基线:在一般情况下(平均案例),用户搜索高血糖症状的比例接近0%。
- 联合用药的发现:如果用户同时搜索了这两种药物,那么他们搜索高血糖症状的几率上升到了10%。
这项研究的惊人之处在于,高血糖并非这两种药物的已知副作用。然而,网络搜索数据强烈暗示了这种关联性,这种模式很难用巧合来解释。研究人员在论文中对此论点进行了更深入的阐述。
这个案例是数据再利用的典型例子。研究团队利用了一个为其他目的收集的大型数据集——网络搜索日志,并从中得出了关于药物安全的新推论。关于数据再利用,我想强调的就是以上两点,但这无疑是一个非常有启发性的案例。
地震预测:数据科学中的法律责任与预测不确定性 🌋
接下来,我们从一个不同的角度探讨预测问题,这个案例更侧重于预测行为本身及其后果,而不仅仅是数据。
回顾去年十月,有六名意大利地震学家因未能预测2009年4月发生的6.3级地震而被判过失杀人罪。当时当地居民对地震活动感到担忧,而研究人员被认为在评估风险时表现得过于 reassuring(让人安心)。
我想通过这个案例指出的核心点是责任问题。科学界对此事件感到震惊,我个人以及几乎所有能想到的人也是如此。让研究人员为未能预测一件已被公认且证实为无法预测的事情负责,这令人难以想象。全球没有任何地震学家会认为地震是可以预测的。
然而,法院的判决在某种程度上是因为研究人员给出了“错误的答案”。这固然糟糕,但它确实引出了一个重要议题:当你做出一个预测时,无论有意与否,你都在为其赋予一定的可信度权重。因此,理解你对该预测的信心程度,是数据科学实践中至关重要的一环。
本章核心主题总结 📚
本节课中我们一起学习了几个关键的数据科学应用案例,并从中提炼出了一些反复出现的主题。
以下是我们在案例中观察到的主要主题:
- 图分析:我们看到了几个图分析的应用实例。
- 数据库的实用性:在奥巴马竞选案例中,我们看到了数据库如何发挥作用。
- 可视化与沟通:我们看到了大量关于结果可视化和解读、传达这些结果的例子。
- 数据规模:我们看到了一些使用非常大的数据集的例子,也看到了使用非常小的数据集的例子。并非所有事情都关乎“大数据”。
- 交互式分析:我们讨论了即席的、交互式的分析。它不仅仅是速度更快,而且本质不同,因此支持这种分析模式非常重要。
- 数据再利用:我们讨论了数据的再利用,即使用他人为其他目的收集的数据,来推断其他事情。这是本课中一个非常常见的主题。
在接下来的几个部分中,我们将讨论本课程的组织结构,以及我们在创建课程材料时做出的一些设计决策。


大规模数据科学(第1课/共3课) - P7:数据科学的特征 📊

在本节课中,我们将探讨“数据科学”这一术语的具体含义。我们将通过多位专家的观点,了解数据科学的定义、所需技能以及从业者的特征。课程将涵盖数据科学的核心构成、实践技能以及它在当今技术领域的重要性。
数据科学的定义与重要性


上一节我们介绍了课程概述,本节中我们来看看数据科学的具体定义。
《财富》杂志将数据科学称为科技领域的热门新职业。谷歌首席经济学家哈尔·瓦里安在2009年《纽约时报》的一篇文章中提出,统计学将成为“最性感的职业”。他将其描述为一种能力:获取数据、理解数据、处理数据、从中提取价值并进行沟通,并认为这种能力在未来将极其重要。
另一位在该领域有深入思考和著述的专家是Metamarkets公司的CEO迈克·德里斯科尔。他提出了一种通俗的观点:数据科学的实践是“红牛驱动的黑客精神”与“浓缩咖啡启发的统计学”的融合。
他还将数据科学比喻为“数据的土木工程”。从业者既需要实践知识,也需要理论理解。这种实用主义与理论之间的平衡,是我们后续会反复提及的主题。
数据科学的构成:德鲁·康威的维恩图
理解了基本定义后,我们来看看数据科学的具体构成。
另一个你应该熟悉的观点是几年前德鲁·康威提出的维恩图。他的核心观点是,数据科学可能是三个不同领域的混合。
以下是这三个领域:
- 黑客技能:指编程专业知识。
- 数学与统计知识:指学术层面的数理统计理论。
- 实质专业知识:指对数据的深度投入和领域知识。
典型的IT商店通常为他人构建数据分析工具,但自身不一定进行深度分析。相比之下,数据科学家可能参与工具构建,但也会深入数据并亲自进行分析。这是我对德鲁图中“实质专业知识”的一种解读。
康威还填充了这些领域之间的交集:
- 将统计知识应用于特定领域,可能构成了传统研究。
- 深厚的理论加上实用的编程能力,使你成为机器学习专家。
- 他称中间区域为“危险区”:你对领域知识和黑客技能略知一二,但不足以将分析建立在正确的理论基础上。我的同事常开玩笑说“计算机科学家不懂误差棒”,指的可能就是这种情况。
数据科学家做什么?
明确了数据科学的构成,接下来我们看看数据科学家具体从事哪些工作。
EMC公司(收购了Greenplum公司并大力推动数据科学计划)提出,数据科学家需要在数据中寻找真理的“金块”,然后向业务领导者解释它。我很赞同“向业务领导者解释”这一点,之前也提到过,沟通结果是这个过程中至关重要的一环,而不仅仅是得出结果。
数据科学家DJ·帕特尔提出了另一个有趣的观点:数据科学家往往是硬科学家(例如拥有物理学背景),他们具备强大的数学背景和计算技能,来自一个生存依赖于从数据中获取最大价值的学科。他们习惯于“拷问数据”,以榨取其最后一滴价值。DJ本人拥有应用数学背景,因此可能从这个角度出发。我认为参与数据科学的人群可能非常广泛,不一定非得来自硬科学,但这个观点值得考虑。
迈克·德里斯科尔还谈到了“数据极客的三项性感技能”。
以下是这三项技能:
- 统计学
- 数据清洗:这个词听起来有点古怪,我们稍后会详细讨论。你会看到很多这样的通俗语言,我也会谈谈我认为这反映了数据科学领域的何种现状。它指的是高效地解析数据、从网络抓取数据、在不同文件格式间转换,并且不被处理大规模异构数据集时遇到的“摩擦”所困扰。数据科学家就是能在这种环境中游刃有余、灵活工作的人,即使数据并不十分“干净”。
- 可视化:指通过可视化沟通结果的能力。
总结

本节课中,我们一起学习了“数据科学”的多维度定义。我们了解到,数据科学是黑客技能、数学统计知识与实质领域专长的结合体。它要求从业者不仅能从数据中挖掘价值,还必须具备出色的沟通能力,尤其是向非技术背景的决策者解释复杂发现。核心技能包括统计学、数据处理(清洗)和数据可视化。数据科学家通常需要在实用主义与理论深度之间找到平衡,并能够在不完美、大规模的数据环境中灵活工作。
大规模数据科学(大数据操作,第1课/共3课) - P8:数据科学的特征(续)🔍

在本节课中,我们将继续探讨数据科学的特征,了解不同专家对数据科学范畴的定义,并深入理解“数据产品”这一核心概念。
上一节我们介绍了数据科学的一些基本特征,本节中我们来看看其他几位专家如何定义数据科学。
雪城大学教授杰弗里·斯坦顿参与了早期的数据科学项目,他将其定义为一个新兴的工作领域,关注大规模信息集合的收集、准备、分析、可视化、管理和保存。他的观点中一个有趣之处是包含了“保存”这个词。虽然准备、分析和可视化是数据科学中常见的三个支柱,也是本课程所强调的,但杰弗里更进一步,谈到了数据的长期保存问题。这源于他的图书馆学与信息研究背景,该领域非常关注数据的策展。虽然本课程不会过多强调这一点,但它无疑是完整数据生命周期的一部分。
该领域的另一位思想家、Bitly首席科学家希拉里·梅森认为,数据科学家是能够获取、清洗、探索、建模和解释数据的人,融合了黑客技能、统计学和机器学习。这与康威博士的维恩图观点一致。她进一步指出,数据科学家不仅擅长处理数据,还能将数据本身视为一等产品。这意味着能够组织数据,并产出可供他人使用的成果。
拥有一个高质量的数据资源或数据资产,让他人能用它来回答问题,是数据科学的产出之一。她还提到了“清洗”,即我们上一张幻灯片看到的“数据整理”。你会看到诸如“数据柔术”这样的术语,人们会说他们想雇用的数据科学家非常擅长数据柔术。在接下来的部分,我会谈谈我对这些术语的理解。
我们之前提到过,数据科学中反复出现的任务类型主要有三种。
以下是数据科学工作流程中的三个核心阶段:
- 准备阶段:为运行某种模型做准备,包括数据清洗和整理。
- 分析阶段:实际运行模型,进行统计分析。
- 沟通阶段:向决策者解释和传达分析结果。
这里我们再次看到了数据整理的重要性。
我喜欢的另一个观点是,数据科学本质上是关于创造数据产品,这些产品可能供自己使用,也可能供他人使用。
那么,什么是数据产品?以下是几种主要类型:
数据驱动的应用程序
例如,拼写检查器不仅仅是一段代码或软件,它的功能依赖于一个单词词典,实际上还有一个拼写错误单词的词典。类似地,机器翻译能将法语句子自动翻译成阿拉伯语,这不仅依赖于巧妙的算法,更依赖于庞大的法语和阿拉伯语文本语料库。事实上,本课程中会贯穿一个观点(虽然现在不展开讲):大型数据集、大型信息语料库往往胜过精巧的技术。
交互式可视化
我们在谷歌流感趋势应用中看到了这类例子。网络上还有一套与全球疾病负担相关的可视化工具(此处不展示),由华盛顿大学的健康指标与评估研究所制作。我喜欢这个例子,因为它展示了他们进行了大量研究,但产出不仅仅是研究论文(尽管他们也写了很多论文),还包括这些允许用户探索数据的交互式可视化。这体现了产出不仅仅是答案或论文,而是可供他人使用的数据产品这一理念。
在线数据库
另一种数据产品可能是某种在线数据库,他人可以查询并回答自己的问题。它不一定包含可视化组件,但投入到创建这些数据库中的工作,我认为也是数据科学的一部分。这涵盖了企业数据仓库的大量软件和努力,也包括商业智能工作。在后续部分,我会尝试区分这两者。这也包括了像斯隆数字巡天这样的项目。简单来说,这是一个天文学数据库,他们从天空图像中提取天体,将所有天体存入数据库并在网上提供。这个资源的创建对整个天文学领域产生了极其强大的影响。
再次总结,图中红色部分强调:数据科学不仅仅是构建数据产品,更是构建数据产品,而非仅仅一次性回答问题。数据产品是数字资产,能够赋能他人以新的方式使用数据。它们可以帮助传达结果(例如内特·西尔弗构建的地图),也可以赋能他人进行自己的分析(例如通过数据仓库或可视化工具)。

本节课中我们一起学习了不同专家对数据科学范畴的补充定义,深入探讨了数据科学工作流程的三个核心阶段,并重点理解了“数据产品”这一核心概念及其主要类型。数据科学不仅是分析数据得出洞见,更是创造能够持续产生价值的可复用资产。
杜克大学《大规模数据科学》第1课:数据科学与其他领域的区别 🔍

在本节课中,我们将探讨“数据科学”这一术语与商业智能、统计学、数据库管理、可视化及机器学习等相关领域的核心区别。理解这些差异有助于我们明确数据科学的独特定位和工作范畴。
商业智能与数据科学
上一节我们明确了课程目标,本节中我们来看看数据科学与商业智能的区别。
商业智能系统通常与两个核心概念相关联:数据仓库和仪表板/报告。这些组件需要大量的前期设计和构建工作,因此在需求变化时缺乏足够的适应性。为商业智能设计的软件栈,可能并不适合那些以需求频繁变更为常态的数据科学问题。
另一个关键区别在于角色定位。商业智能工程师通常不直接使用自己的数据产品进行分析和商业决策,他们主要为他人构建决策工具。而数据科学家则需要同时承担数据产品构建和数据分析决策的双重任务。
统计学与数据科学
统计学方法是数据科学家日常工作的核心。然而,统计学家的一个典型假设是:他们遇到的任何数据集都能放入单台机器的主内存中。这是因为统计学诞生于数据稀缺的时代,其目标是从稀疏、收集成本高昂且通常很小的数据集中提取尽可能多的信息。
例如,如果世界上只有20名患有某种特定疾病的患者,你无法轻易找到更多样本,因此需要新的数学方法来从现有数据中“榨取”信息。但随着我们从“数据贫乏”转向“数据丰富”的时代,挑战也从需要新数学方法提取信息,转变为需要新工程技术来处理非常庞大的数据集。
不过,两者构建的模型和方法在许多情况下是相同的。
数据库专家与数据科学
数据库程序员和管理员为数据科学任务带来了许多重要技能。但他们的工作通常聚焦于特定的数据模型,尤其是关系型数据模型(即行和列)。
如果你的数据源是视频、音频、文本,甚至是图结构(节点和边,我们后续会讨论),那么关系型数据库可能并非合适的工具。甚至那些超越特定数据库系统的概念,也可能不完全适用。我们将在课程中探讨何时何地关系型模型是不合适的。
可视化专家与数据科学
可视化专家同样贡献了许多关键技能。但和统计学家类似,他们历史上较少关注分布在数百台机器上的海量规模数据。
机器学习与数据科学
机器学习或许是和数据科学最接近的领域。但我们需要明确一点:在一个数据科学项目中,选择正确的模型、算法或机器学习技术并应用运行,所花费的时间只占很小一部分。
以下是数据科学家花费更多时间的环节:
- 数据准备:获取和整理原始数据。
- 数据操作:转换数据以适应分析需求。
- 数据清洗:处理缺失值、异常值和不一致。
- 数据整理:将数据重组为更易用的格式。
对于这些环节,机器学习技术本身并不特别相关,这些工作更接近于数据库专家的领域。
课程定位与设计空间
市面上有许多可被视作数据科学的课程,有些在名称中使用了“数据科学”,有些是历史悠久的相关领域课程。我想花点时间描述一下定义这些课程的几个维度,并说明我们这门课程在设计空间中所选择的具体定位。

本节课中,我们一起学习了数据科学如何区别于商业智能、统计学、数据库管理、可视化和机器学习。数据科学是一个交叉领域,它融合了这些学科的技能,但特别强调在大规模数据环境下,解决从数据工程到分析决策的端到端问题。理解这些区别,能帮助我们更好地把握数据科学家的核心工作与独特价值。
📊 课程1:数据科学的四个维度

在本节课中,我们将学习如何从多个维度理解数据科学这一领域。通过分析其广度、深度、规模与目标受众,我们可以更清晰地把握数据科学的核心要求与学习路径。
作为引言,请看来自Wey Data公司首席技术官Aaron Kimball的一段话。他表示,他担心数据科学家这个角色可能类似于90年代神话般的“网站管理员”。当时,网络公司知道他们需要在90年代中期接入互联网,但不知如何操作,于是他们说:“我们雇一个网站管理员,问题就解决了。”网站管理员需要为网站撰写所有内容、进行设计并管理用户体验、编写代码将网站与后端订单履行系统连接、实际构建页面并设计导航、进行必要的日志记录以确保网站持续运行并保持较高的可用性、设计用于存储通过网站提供的数据的模式等等。显然,期望一个人完成所有这些任务是不现实的。因此,互联网战略演变为需要一个更广泛的团队。
类似的情况可能正在数据科学领域发生。但对我而言,“数据科学”这个术语意味着:如果你是一名数据库管理员,且技能仅局限于关系数据库,那么当前趋势要求你学习更多关于非结构化数据和统计建模的知识。如果你是一名统计学家,你将需要学习处理无法装入内存的数据。如果你是一名习惯于直接构建系统和处理文件的软件工程师,你将需要学习一些统计建模知识,并学会如何向管理者传达你的结果;你需要处理这些数据集并实际使用它们来做出决策。如果你是一名接受过基于数据做出决策培训的业务分析师,你将需要开始更多地理解算法及其权衡,尤其是在大规模场景下。
这主要有两个原因。一是成本会根据你选择的技术而发生巨大变化,我们稍后会讨论云计算的发展以及这些算法的变化。你可能能够获得答案,但其成本可能与五年前大不相同。另一个原因是,随着我们越来越多地依赖算法进行“线控”业务(即我们越来越信任算法为我们做出某些决策),这些算法变成了不透明的黑盒。如果你不了解黑盒内部发生了什么,你很可能会误解结果。因此,不再安全的是简单地将信任完全交给某个算法,或者交给运行这些算法的员工。你可能需要自己理解并内化选择不同模型时的权衡。
以下是我喜欢用来描述这些不同课程的维度。
第一个维度是广度。我将广度分为工具与抽象。一门精深的课程可能倾向于侧重抽象,即希望传授超越任何特定实现的概念。然而,学生感兴趣的是能够获得使用工具的实践经验,这些工具他们明天在工作中就能用上。因此,这两者之间始终存在张力。例如,Hadoop(我们将讨论它)是名为MapReduce的抽象概念的一种实现,而MapReduce抽象概念本身当然超越了其在Hadoop中的特定实现。正如我将在下一部分提到的,我希望尽可能侧重抽象,但确保有作业能提供大家感兴趣的实践技能。
接下来是深度维度。我所说的深度是指数据的结构性操作与数据的统计性操作之间的区别。你可以将关系代数视为一种结构性操作的形式体系,而线性代数或许是进行数据统计操作的形式体系。在这里,我试图取得平衡,但实际上我更倾向于结构方面,我将在下一部分为这个立场进行辩护。
你可以思考的第三个维度是规模。规模的一端是数据可以装入单台机器的内存,另一端我称之为云端,意味着可能需要数百台机器来处理。我倾向于侧重云端,原因如前所述:不再能安全地假设数据能装入内存,并且只培训人们处理那种规模的数据是不够的。当你开始转向两台机器(更不用说一百台)时,整个世界都变了,如果不让你接触这种变化,将无法使你成为一名高效的数据科学家。
我使用的最后一个维度是目标受众,即面向黑客还是更面向分析师。这里的“黑客”指你已经拥有丰富的编程经验,并希望完善自己在某些数学领域的技能;而“分析师”则指更多是试图增加一些技术深度的技术决策者。在这里,我实际上喜欢取得平衡。我不希望本课程完全假设你是一名经验丰富的开发者,但我们也不能完全忽略编程。因此,我们将尝试在这两者之间取得平衡。
以下是我们为本课程做出的选择:我们倾向于侧重抽象,我们倾向于结构,我们明确侧重大规模,并且我说我们会取得平衡,但实际上我们更倾向于分析师一方。我们考虑到课堂上将会有不一定具有丰富编程经验的分析师。我已经收到很多人的邮件提问,他们说:“嘿,我平时不怎么做编程,我能从这门课中学到东西吗?”我认为答案是肯定的,尽管会有一些编程内容,所以请做好准备。

本节课中,我们一起学习了从广度、深度、规模与目标受众四个维度来剖析数据科学领域。我们了解到,成为一名高效的数据科学家需要在工具与抽象、结构与统计、单机与云端、编程实践与业务分析之间取得平衡。本课程的设计正是基于这些权衡,旨在为不同背景的学习者提供有价值的入门指导。
大规模数据科学(第1课/共3课) - P11:工具与抽象 🛠️➡️🧠

在本节课中,我们将探讨数据科学中的一个核心维度:工具与抽象。我们将通过回顾数据库技术的发展历程,来理解为什么掌握抽象概念比仅仅学习特定工具更为重要。
上一节我们介绍了课程的四个维度,本节中我们将重点讨论第一个维度:工具与抽象。
工具与抽象的维度
这个维度强调,我们应该关注基础概念,而非具体的工具。虽然许多学习者渴望获得实践经验,但理解底层原理至关重要。为了说明这一点,让我们回顾一下数据库领域的一个典型发展故事。
一个反复上演的故事:从关系型数据库到NoSQL
在2004年之前,数据库市场由三大商业关系型数据库供应商和一些开源方案(如MySQL和PostgreSQL)主导。
2004年发生了一个重大事件:Jeff Dean及其同事发表了关于Google MapReduce 的论文。MapReduce允许处理超大规模数据集,并重新定义了数据库的功能集。它专注于横向扩展并行性,而舍弃了传统数据库的其他功能。
这对许多人来说非常振奋,因为他们既无需处理不需要的数据库功能,也无需支付高昂的许可费用。MapReduce似乎是一个完美的解决方案。
几年后,基于该论文思想的Hadoop 开源实现出现了。
然而,有趣的是,Hadoop生态系统中最早期且最成功的项目之一,是一个名为 Pig 的系统。Pig本质上是Hadoop上的一个关系代数编程环境。关系代数是关系数据库的“秘密武器”。因此,在这个非关系型系统之上,早期就出现了对关系式编程的需求。
此外,还出现了其他竞争项目,如Dryad 和 DryadLINQ,它们也为大规模并行数据处理应用提供了面向关系代数的编程环境。
随后,人们甚至直接将 SQL 语言移植到了Hadoop之上。接着,Hadoop也开始引入索引、模式等传统数据库才有的更复杂功能。如今,事务处理也成为了NoSQL系统中一个非常重要的议题。
MapReduce的持久贡献
这并不是说MapReduce毫无用处。它实际上做出了一些重要的持久性贡献,主要有三点:
以下是MapReduce带来的三个关键理念:
- 强调容错性:其核心思想是,当你在数千台计算机上运行任务时,短时间内出现故障的概率极高。传统数据库通常无需担心此问题,因为它们假设查询通常很快,且并非运行在如此大规模的集群上。MapReduce强调在查询处理过程中的容错,确保工作不会因单点故障而全部丢失。
- “读时模式”:传统数据库要求数据必须符合预先定义的模式,否则无法处理。但很多数据(尤其是海量数据)并没有现成的模式。MapReduce的理念允许你在读取数据时再应用或推断模式,而不是在加载数据之前强制要求。
- 用户定义函数的更好实现:虽然大多数数据库都支持用户定义函数,允许在SQL查询之外执行自定义代码,但编写和维护这些函数的体验并不好。MapReduce论证了可以直接为Java程序员等提供他们熟悉的编程环境(如Java),让他们能够编写可扩展的系统,而无需被迫使用数据库提供的复杂用户定义函数接口。
为什么抽象比工具更重要?
那么,我整体的观点是什么?如果我们过于关注工具,我们得到的只是某个时间点上重要工具的“快照”。我们会看到数据库的某些功能在流行度上起起落落,但它们在思考大规模系统时具有持久的价值。
同样,在关系型数据库与NoSQL系统的争论中,我们可能会迷失方向,看不清什么是真正新颖的。因此,在本课程中,我们将尽可能聚焦于这些抽象概念。


本节课中,我们一起学习了“工具与抽象”这一维度。通过回顾数据库技术从关系型到MapReduce再到NoSQL的演进,我们理解了掌握底层抽象概念(如容错性、读时模式、并行计算范式)的重要性,这远比追逐特定工具有更长远的价值。下一节,我们将继续探讨课程的其他维度。
大规模数据科学(第1课/共3课)📊:桌面规模与云端规模

在本节课中,我们将探讨数据科学的核心抽象概念,并重点分析处理“桌面规模”数据与“云端规模”数据在思维方式与技术栈上的根本区别。
数据科学的抽象概念 🤔
上一节我们提到了关注抽象概念的重要性。那么,数据科学的抽象概念究竟是什么?
目前业界对此尚无明确定论。一个迹象是,人们常使用“数据柔术”、“数据整理”、“数据清洗”等模糊词汇来描述数据科学家的核心技能。这实际上意味着,我们尚未清晰地定义所谈论的内容。
如果必须列举数据科学的潜在抽象概念,可能有以下候选方案:
- 万物皆矩阵:这种观点认为,一切数据操作都可归结为矩阵运算和线性代数表达式。这是一个有效的视角。
- 万物皆关系:这种观点认为,数据处理主要是关系代数的各种变体,可能辅以用户自定义函数。即使你经常使用数据库,也可能不熟悉关系代数,但我们后续会讨论。我认为这是一个有潜力的、用于处理数据的通用抽象。
- 万物皆对象:对于熟悉面向对象编程的人而言,世界可以通过对象、消息传递和方法调用来建模。这也是一种候选方案。
- 万物皆文件:这种观点认为,一切数据都存储在文件中,我们通过编写脚本(例如Perl或R)来处理这些文件。系统管理员可能更倾向于这种视图。
- 万物皆数据框:这是R语言的核心数据模型,即数据框及对其操作的函数。
在以上所有候选方案中,我认为前两种(矩阵和关系)最值得我们关注。原因是它们最专注于我们处理数据(尤其是大规模数据)时要执行的任务本质。其他抽象则更侧重于代码组织或操作系统层面。

桌面规模与云端规模 ☁️ vs 💻
现在,让我们来探讨桌面规模与云端规模的区别。主张“桌面规模”的观点认为,数据科学的核心是函数、统计和操作技术,因此可以将大规模数据处理归入另一门课程或类别,而只专注于数学和函数。
然而,在数据科学课程中忽略规模问题是一个错误。原因在于,规模是整个技术类别的根本限制。以R语言为例,在其基本用法中,你需要将整个文件读入单台机器的内存,然后对其调用函数。如果你的数据无法放入单台机器的内存,你就无计可施。
当然,你可以通过使用索引来限制需要访问的数据,或者尝试利用现代计算机多核特性进行并行处理。但自己费心实现这些技巧,忽略了这些技术早已在其他系统中被深入理解并实现的事实。
因此,认知其他系统的能力,并灵活利用这些已具备可扩展性的系统来编写应用程序,是数据科学的一项关键技能。
本幻灯片中的一个例子(虽已过时,但能说明问题)是grep工具。grep用于在文件中搜索特定模式,但它是线性扫描文件,逐行检查。对于超大规模数据集,这种线性扫描不再可行,必须采用更智能的搜索方式。
核心观点是:大规模数据不仅仅是“更大”,它是“不同”的。 它要求以不同的方式思考技术,并要求一套不同的技术栈。在数据科学课程中忽视这一点是错误的。
总结 📝
本节课我们一起学习了数据科学的几种潜在抽象概念,并重点比较了处理桌面规模数据与云端规模数据的根本差异。我们认识到,面对大规模数据时,不能仅仅依赖单机内存计算,而需要了解和利用那些原生支持分布式、可扩展的技术栈与思维方式。这是成为一名有效的数据科学家必须掌握的维度。
课程1:大规模数据科学(大数据操作) - 黑客与分析师 🧑💻 vs. 👨💼

在本节课中,我们将探讨数据科学领域中的一个重要维度:参与者所需的技能背景,即“黑客”与“分析师”的区别。我们将理解为何数据科学活动需要不同类型的人才,以及技术如何帮助降低参与门槛。
上一节我们讨论了数据科学的其他维度,本节中我们来看看参与者技能背景的差异。

这个最终的维度是关于黑客与分析师的对比。这里的意思是,参与这门数据科学课程以及一般的数据科学活动,是否需要深厚的编程能力?答案是否定的。我认为我们至少需要两种类型的人,实际上是一个广泛的人才谱系。这并非我的原创想法,麦肯锡全球研究所的一份经常被引用的报告也提到了这一点。
你会反复看到这段引述,但引用者往往只关注第一部分,该部分提到了需要14万到19万名具备深度分析技能的人才。然而,引述的第二部分指出,你还需要150万名经理和分析师,他们懂得如何利用大数据的分析来做出有效决策。
这意味着,在这个领域工作的将不仅仅是程序员。因此,我希望思考如何设计一门课程,能够同时吸引并启发这两类人。

上一部分我们提到了两类人才的需求,接下来我们看看为何这种界限正在模糊。

这是本部分的最后一张幻灯片。我认为存在“黑客与分析师”之分的另一个原因是,如今他们之间的界限正在变得模糊,而技术实际上可以在这方面提供帮助。在某些情况下,操作大型数据集甚至不需要计算机科学的博士学位或学士学位。
为了支持这一观点,我举一个我工作中的例子。我们曾致力于让数据库对生物学家等用户更易使用。下面是一个非常复杂的SQL查询,如果你仔细看,会发现它实际上是在对基因序列进行区间运算。
-- 示例:一个进行基因序列区间运算的复杂SQL查询
SELECT ... FROM ... WHERE ... -- (此处代表复杂的查询逻辑)
即使对专家而言,这也是一个相当难理解的查询。然而,这个查询是由一个完全不做任何编程的人编写的。她不写Python,不写Perl,也不写R,但她能够编写这些处理庞大数据集的SQL查询。
这个事实表明,如果你理解正在发生的事情,能够用一些抽象概念进行思考,并且足够了解你的问题,那么即使没有深厚的软件工程背景,你也可以参与操作大型数据集和进行数据科学的活动。这就是为什么我希望将指针向这个方向(分析师一侧)推动一些。
我可能把它放在中间。我并非只关注分析师,我只是想确保他们被包含在内。
本节课中,我们一起学习了数据科学中“黑客”与“分析师”角色的区别。我们了解到,数据科学需要具备深度分析技能的专业人士,也同样需要懂得利用分析结果进行决策的管理者和分析师。同时,现代技术正在降低数据操作的门槛,使得更多领域专家能够直接参与数据科学工作。下一节课,我们将探讨最后一个维度。
杜克大学《大规模数据科学》第1课:结构与统计 📊

在本节课中,我们将要学习数据科学中一个核心的权衡维度:“结构”与“统计”。我们将探讨为什么数据操作(处理数据结构和格式)在实际项目中往往比复杂的数学模型更为基础和重要。
上一节我们介绍了描述本课程设计理念的四个维度中的三个。本节中,我们来看看最后一个维度,即我所说的“结构”与“统计”。
这指的是数据操作与深层数学之间的相对重要性。你可以看到,我在这里将“指针”稍微偏向了左侧(即数据操作)。在接下来的几分钟里,我将尝试解释这样做的动机。
我们已经在第一段中看到了一个例子。当时我列举了一些数据科学的近期实例,其中之一是内特·西尔弗对2012年美国总统选举选举人团票数的预测。如果你还记得,这个预测本质上是通过计算每个州民意调查的平均值来实现的。它并不需要一个复杂的统计模型,却产生了巨大的影响。
以下引言很好地总结了这一点,它来自Weebi数据公司的亚伦·金博尔。他说:“80%的分析工作实际上只是求和与平均。”他的意思是,如果你能正确地完成这些求和与平均,并且能在任何规模、任何数据上做到这一点,那么你总是可以在此基础上构建更高级的技术。一切最终都可以归结为求和与平均。
我认为这解释了为什么关注数据操作如此重要。数据操作通常与表达求和和平均值的能力相关,例如,在数据库查询中就能做到这一点(我们将在接下来的几节课中讨论)。掌握这些,你就能解决80%的问题。
另一种看待这个问题的方式是,一个数据科学项目主要涉及三项任务。以下是这三项任务:
- 准备运行模型:包括收集、清理、整合、重构、转换、加载数据等。
- 运行统计模型:选择并执行实际的统计模型。
- 解释与传达结果:将结果可视化、沟通和解释。
我从与亚伦·金博尔的交谈中得知,80%的工作实际上都集中在第一步,即处理数据。这里列出的所有动词(收集、清理等)都是困难的部分。实际上,选择模型甚至运行模型在实践中往往不会让人彻夜难眠。
这里的玩笑是,另外的“80%”工作(暗示数据科学的工作量是正常的180%)在于解释结果,即结果的可视化、沟通和解释。
这就是为什么在本课程中,我想重点关注与这第一项任务相关的数据操作任务。
还有另一个原因支持这个重点。以下是看待这个问题的另一种角度,引用自道格·莱尼。这份大约12年前的文献首次提出了大数据的“3V”概念(体量、速度、多样性)。他写道:“有效数据管理的最大障碍将是不兼容的数据格式、非对齐的数据结构和不一致的数据语义。”数据库领域(也就是我的领域)称之为数据集成问题。他认为这是最困难的部分。他在2001年这样说,而我认为这在今天仍然成立。在“3V”的背景下,他指的是“多样性”比“体量”或“速度”更难处理。我们将在后续章节中详细解释这些“V”的含义。
好了,另一个小插曲是我们喜欢询问与我们合作的科学家(天文学家、海洋学家和生物学家)的问题。我们会非正式地问他们,他们花费多少时间在“处理数据”上,而不是在“做科学”上。我们让他们自己理解这两个引号的含义。对他们来说,“做科学”绝对包括选择统计方法或设计统计模型。而“处理数据”指的是所有其他杂事,比如格式转换等等。你认为最常见的答案是什么?你可以先猜一下。
他们甚至不假思索,给出的答案通常是90%。这个数字应该引起我们的深思。纳税人的钱通过联邦资助机构拨给博士后研究员,而她却要花费90%的时间去做她甚至不认为是科学的事情。因此,我认为作为数据科学家,专注于解决这个问题至关重要。
你可能会说,这只是科学领域的情况。那么商业领域呢?但正如我将在整个课程中试图阐明的,商业和科学领域正在发生的事情之间,一致性正日益增强。我们将在后续章节中更多地讨论这一点。
如果90%的问题在于处理数据,那么,天哪,我们确实应该在这方面投入大量精力。
好了,另一个论点延续了我展示的第一张幻灯片的内容,即“结构”——特别是数据操作平台和数据库——实际上在表达更高级的内容方面也能发挥很大作用。这不仅仅是“如果你有求和与平均,你就能表达任何东西”的问题。即使是相当先进的技术,人们也越来越有兴趣研究如何将它们融入数据库。
本节课总结


在本节课中,我们一起学习了数据科学中“结构”与“统计”的权衡。我们了解到,基础的数据操作(如求和、平均)和数据处理任务(收集、清理、整合)在实际项目中占据了绝大部分(约80%-90%)的工作量和重要性。复杂的统计模型固然重要,但构建模型的基础——干净、结构良好的数据——往往是更大的挑战和成功的关键。因此,本课程将重点关注数据操作和管理的技术与原理。
大规模数据科学(大数据操作,第1课/共3课) - P15:14_结构与统计(续) 📊➡️🗄️

在本节课中,我们将探讨传统统计工具在处理大规模数据集时面临的挑战,并了解数据库系统如何提供一种可扩展的替代方案。我们将通过一个具体的例子——在SQL中表达矩阵乘法——来展示数据库在处理特定计算任务时的潜力。
上一节我们讨论了数据结构和统计的关系,本节中我们来看看当数据规模超出单机内存容量时,传统统计工具面临的困境。

这张幻灯片引用了Christian Gr的观点。他指出,如果对比数据库和SAS、MATLAB、R或SPSS等统计软件包,会发现一个常见做法:用户经常需要将数据下载到他们偏爱的统计软件中使用。他们通常认为这是理所当然的,甚至是唯一能有效表达分析的方式。
大多数统计软件包的第一步操作是从磁盘读取数据并将其加载到内存中,然后开始调用函数进行处理。然而,越来越多的数据集根本无法装入单台机器的内存,尤其是在个人笔记本电脑上。
面对这种情况,通常有几个选择:
- 转向这些工具的某种高级集群版本(例如SPSS和MATLAB有此类版本,但通常价格昂贵)。
- 对数据进行采样,只处理能够装入内存的子集。这种做法非常普遍,几乎成了标准流程。
但关键在于,如果使用不同的工具包,这种限制并非必需。这里的论点是,如果你能使用数据库,并学会如何在数据库中执行你的任务,你就能免费获得可扩展性。
此外,这些统计工具包本身不一定具备并行处理的概念。如今购买的机器至少拥有四个核心,可能更多,如8个、12个甚至16个。能够利用所有这些核心来处理问题是你在选择一个工具包时会寻求的特性,而这是数据库可以自动完成的。虽然并非所有数据库都支持(例如你可能熟悉的MySQL和PostgreSQL通常不支持),但其他数据库可以,我们后续会详细讨论。
因此,如果你能使用数据库,就可以免费获得并行处理能力,以及超越主存大小的可扩展性。当然,这或许是一个很大的“如果”,我们也会讨论这一点。
现在,让我给你举一个例子,这实际上也是你们作业的一部分。问题是:你能用SQL表达矩阵乘法吗?如果可以,那么我认为,任何能用矩阵乘法表达的公式,或许都可以通过反复应用SQL来实现。答案是肯定的,而且最简单的版本相当直接。
如果你从未见过SQL,不用担心,我们稍后会详细讲解。如果你熟悉SQL,请耐心听我解释。假设你有两个矩阵A和B。
这里,每个矩阵的表示形式是一个关系,包含行ID(rid)、列ID(cid)和值(value)。如果你的矩阵是稠密的,这是一个非常低效的关系表示。让我解释一下原因,你也可以再思考一下。
一个隐式的表示只需要存储值本身。例如,一个5行6列的矩阵只需要30个值(5 * 6)。但在这里,你需要存储30个行ID、30个列ID和30个值,相当于将数据大小增加了约三倍。那么为什么要这样做呢?
实际上,很多现实中的矩阵是稀疏的。在稀疏矩阵中,并非所有单元格都有值,因此你不需要存储它们。这种显式存储行ID、列ID和值的表示方式,在这种情况下反而非常高效。事实上,稀疏矩阵求解器内部使用的正是这种表示。
因此,如果你有一个稀疏矩阵,并在数据库中以此形式表示它,那么表达矩阵乘法就不算太难。你需要做的是:对于矩阵A中的每个列号,找到矩阵B中对应的行号,然后对所有贡献值进行求和。稍后我会展示一个示意图。
实际上,我现在不打算深入太多细节,因为在布置相关作业时,我会详细讲解。现在,我希望你记住的关键点是:在数据库中表示矩阵听起来很不寻常,但这实际上并非一个糟糕的想法。通过阅读“Mad skills”论文的部分内容,你会明白原因。目前,你只需要知道这是可以做到的,并且不一定是个坏主意。


本节课中我们一起学习了传统统计软件在处理大数据集时的局限性,以及数据库系统如何通过其并行处理和磁盘存储能力提供可扩展的解决方案。我们还初步探讨了在关系数据库中表示和操作矩阵(即使是复杂的运算如矩阵乘法)的可能性,这为在数据库内执行更广泛的统计计算打开了思路。
课程1:大规模数据科学(大数据操作,第1课/共3课) - P16:科学的第四范式 🧪➡️💻

在本节课中,我们将探讨“数据科学”这一术语如何与其他科学领域相关联,并重点介绍“电子科学”的概念。我们将回顾科学探究方法的演变历程,并理解大规模数据分析如何成为现代科学研究的“第四范式”。
科学的四种探究范式
数千年来,科学探究一直是经验性的。科学家观察自然世界,或在实验室的受控环境中复现自然现象,并据此进行观察记录。
在过去的几百年里,科学界接受了理论模型作为一种有效的探究方法。新理论会启发新的实验,而理论本身则有助于解释从实验中获得的观测数据。


计算模拟的兴起
大约在过去的50年里,高性能计算催生了一种全新的科学探究方法。科学家可以在计算机中模拟那些无法直接观测、也无法在实验室复现的现象。即使理论模型变得过于复杂,无法用纸笔解析求解,也可以从初始条件开始运行模拟来获得结果。
这种方法被应用于恒星内部、板块运动、宇宙演化或物种灭绝对生态的影响等领域。
第四范式:数据密集型科学
至此,我们介绍了三种探究方法。但在过去十年左右,出现了可以被认为是第四种科学探究的方法。
这种方法是从仪器或模拟中获取海量数据集,然后使用新算法和新基础设施来探索这些数据集。电子科学的核心正是处理大规模且复杂的数据,其数据量之大,以至于必须依赖自动化或半自动化分析,无法直接人工检视。
以下是电子科学(或数据科学)所涉及的相关工具:
- 数据库
- 数据可视化
- 横向扩展计算
- NoSQL系统
- 机器学习技术
- 网络服务
从“先提问”到“先获取数据”
关于这第四范式,阅读清单中的一本书和一些文章有详细论述。这个故事有多种讲述方式,我倾向于从“提问”的角度来理解。
传统上,科学一直是关于向世界提问。数据采集活动(实验或实地研究)通常与非常具体的假设紧密相连。科学家先有问题,然后去收集数据。
然而,电子科学在一定程度上改变了这一模式。现在,科学家常常先大规模地下载数据,将世界“存入”计算机的某种表示形式中,然后通过查询这个数据库来检验假设。在某些情况下,数据的获取可以独立于任何特定假设。
数据爆炸与分析瓶颈
这种转变部分源于数据获取成本的急剧下降。例如,现代望远镜能以极高分辨率获取海量数据;生命科学领域的实验室自动化和高通量测序技术;海洋学中越来越便宜的传感器。
得益于摩尔定律和计算技术的进步,模拟运行的规模和分辨率越来越高,从而产生越来越庞大的数据。数据产生的速度已远远超过我们分析数据或提出相关问题的速度。
因此,发现、整合、分析数据并将结果传达给他人的成本,成为了新的瓶颈。这个故事听起来应该与我们一直在讨论的数据科学的核心挑战非常相似。

总结

本节课中,我们一起学习了科学探究方法的演变:从经验观察,到理论建模,再到计算模拟,最终发展到今天的数据密集型科学(第四范式)。我们理解了电子科学与数据科学在工具和挑战上的共通性,并认识到在数据爆炸的时代,如何高效地管理和分析海量数据已成为推动科学发现的关键。
大规模数据科学(大数据操作,第1课/共3课)🚀

概述
在本节课中,我们将要学习数据密集型科学(E-Science)的几个具体实例,并探讨其带来的挑战。这些实例展示了科学领域如何从数据匮乏转向数据丰富,以及由此催生的新技术和研究方向。我们还将看到,商业领域的大数据挑战与科学领域正变得越来越相似。
数据密集型科学实例
上一节我们介绍了数据密集型科学的背景,本节中我们来看看几个具体的领域实例,它们都面临着传统技术栈难以应对的挑战。

大型综合巡天望远镜(LSST)🔭
问题在于,同样的技术栈,甚至在某种程度上同样的方法,都难以应用于大型综合巡天望远镜这个案例。
这个望远镜产生如此多数据的原因,不仅在于其分辨率更高、能观测到天空更深的区域,还在于它会频繁地(每三天)返回天空的同一位置进行观测。这使得科学家能够研究随时间变化的天体,例如小行星、彗星或超新星等。

通过比较这些时间序列中的图像,科学家可以提出各种新的科学问题。
因此,无论是出于其科学目标、巨大的数据规模,还是数据采集细节的复杂性,之前的解决方案都已不再适用。这推动了一个全新研究领域的诞生,旨在研究支持此项目的数据管理技术和数据分析技术。
生命科学🧬
在生命科学领域,这些高通量测序仪在连续运行时,每天能够产生数TB的数据。从事此项工作的主要实验室,例如联合基因组研究所,同时运行着25到100台这样的机器,持续不断地产生海量数据。

这些机器处理各种样本,可能是单个生物体,甚至是来自环境的样本(其中并不存在某个特定的生物体,而是整个种群)。出于多种用途,这些设备能够输出大量数据。
海洋学🌊
在海洋学领域,美国国家科学基金会海洋观测计划(OOI)的区域尺度节点是一个由华盛顿大学领导的项目。海洋观测计划是一个多机构合作项目,其区域尺度节点部分由华盛顿大学负责运行。
该项目在海底铺设了长达1000公里的光纤电缆,连接了数千个化学、物理和生物仪器。
以下是连接的传感器类型:
- 数千个化学、生物和物理传感器。
- 包括来自海底的实时视频,用于监测火山活动。
同样,支持这项努力所需的数据集和数据基础设施非常庞大,这并非一个关系型数据库能简单应对,也因此推动了大量新的研究。
信息空间与网络🌐
在信息空间领域,有大量科学研究可以直接在网络本身上进行。仅就网络而言,一台计算机每秒可以从一个磁盘读取30到35兆字节的数据,这意味着仅仅读完整个网络的内容就需要大约四个月的时间。
总结一下,E-Science是关于数据分析的科学,即从海量数据中自动或半自动地提取知识。因此,寻找答案的主要工具是算法和技术,而非直接的人工检查,因为数据量太大,人力无法逐一审视。
大数据的三V特征
但挑战不仅仅在于数据量,正如我们将在下一部分讨论的。这也与商业领域正在发生的事情紧密相连,即“大数据”概念及其“三V”特征。我们下次会详细讨论,但在此先简要提及。
以下是“三V”的具体含义:
- 数据量(Volume):指数据的行数或字节数,即纯粹的规模。
- 多样性(Variety):可能指列数或维度数。例如在生命科学中,许多实验需要访问多个公共数据库、多个传感器、自己收集的数据以及同事的数据。将这些数据整合在一起的任务是一个重大瓶颈,即使数据本身的规模不一定特别大。这就是数据的复杂性。
- 速度(Velocity):正如我们在大型综合巡天望远镜案例中看到的,尽管规模本身已经巨大,但每两天收集40TB数据的事实意味着基础设施需要跟上这个速度。仅仅将数据从望远镜设施传输到数据分析设施就是一项工程挑战。
你还会看到其他“V”,例如真实性(Veracity)——我们能否真正信任这些数据?关于这些,我们下次再详谈。
总结与商业领域的关联
本节课中我们一起学习了数据密集型科学的多个实例及其核心挑战。
总结来说,科学正在经历一代人的转变:从数据匮乏(永远没有足够的数据)转向数据丰富(数据多到不知如何处理)。因此,数据分析已经取代数据采集,成为科学发现的新瓶颈。问题不在于出去获取数据的成本,而在于分析已有数据的成本。
但这与商业领域(可能也是许多同学的背景和兴趣所在)有什么关系呢?我们看到,商业领域正开始变得与科学领域长期以来的情况非常相似。
以下是商业与科学的趋同点:
- 企业正在积极地获取数据并无限期地保存,以备将来之用。
- 它们开始招聘拥有类似科学领域长期重视的技能组合的人才,特别是数学深度。
- 它们开始基于这些数据做出非常经验性的决策,总是希望用基于数据的清晰案例来支持每一个决定。
因此,我认为可以将科学中获得的经验教训应用于商业,反之亦然。因为科学在一个方面落后于商业,那就是对技术的采用。科学领域在IT基础设施上的投入比例远低于商业领域。所以,现在正是两个领域思想交叉融合的大好时机。


回到我展示的第一张幻灯片,E-Science(数据密集型科学)和数据科学本质上拥有共同的一切。因此,我们可能会在两者之间互换使用示例。
课程1:大规模数据科学(大数据操作,第1课/共3课) - 🧠 理解大数据与3V


在本节课中,我们将要学习“大数据”这一概念的核心内涵,特别是被称为“3V”的三个关键维度。掌握这些术语将帮助你在讨论大数据时,能够清晰、准确地表达自己的观点。
📊 大数据的3V概念
上一节我们提到了大数据,本节中我们来看看描述大数据时最常被提及的“3V”模型。这“3V”分别是:数据量(Volume)、处理速度(Velocity) 和 数据多样性(Variety)。
以下是这三个维度的详细解释:
-
数据量(Volume)
- 含义:指数据的规模大小。
- 衡量方式:可以用字节数、数据行数、对象数量等来表示。这本质上是数据的“垂直”维度,即数据的多少。
-
处理速度(Velocity)
- 含义:指数据处理的速度与数据活动需求之间的延迟关系。
- 核心:它关注的是数据产生的速度与需要被消费(处理)的速度之间的匹配度。在许多应用中,交互式的响应时间变得越来越重要。当处理速度成为瓶颈和挑战时,这个维度就变得尤为关键。
-
数据多样性(Variety)
- 含义:指数据来源和格式的多样性。
- 挑战:对于任何特定任务,需要整合的数据源类型越来越多。例如,你可能需要同时处理ASI文件、从网络下载的数据、从传统数据库提取的数据以及来自NoSQL系统的数据等。整合这些异构数据源是一个重大问题,往往会耗费大量时间。之前提到的研究人员将90%的时间花在“处理数据”上,其中大部分时间就消耗在应对数据多样性上。
在数据科学任务中,这三个维度都至关重要。
🌌 不同科学领域的3V挑战
为了更直观地理解3V,我们可以通过一个坐标系来观察不同科学领域面临的挑战。这个坐标系以数据量(字节数) 为Y轴,以数据源多样性(如单表中的列数、跨多表的列数或不同数据源的数量) 为X轴。
通过这个坐标系,我们可以将不同研究领域或问题映射出来:
- 天文学:主要挑战在于巨大的数据量(高Y轴)。例如,大型综合巡天望远镜等基础设施能产生数百PB的数据。然而,其数据源类型相对较少(低X轴),主要是望远镜、光谱图像和星系模拟。
- 海洋科学与生命科学:主要挑战在于极高的数据多样性(高X轴)。虽然其数据总量可能不及天文学,但用于采集数据的仪器类型繁多且不断增长。
- 例如在海洋科学中,数据来源包括:
- 滑翔机系统:可长时间在水下巡游,在四个维度(X, Y, Z, T)上采集数据。
- 自主水下航行器:用于短期任务,观测更精细尺度的现象。
- 海洋考察航次:使用温盐深仪等设备,在固定水平位置、不同深度和时间进行剖面测量。
- 数值模型:尺度范围很大,从整个北半球模型到特定海湾、河口模型,多样性很高(因此在图中用较大的圆圈表示)。
- 固定观测站:在固定位置部署传感器进行时间序列测量。
- 声学多普勒流速剖面仪:利用声波测量海水流速剖面。
- 卫星图像:测量海色、波浪破碎等现象。
- 例如在海洋科学中,数据来源包括:
图中每个领域图标的大小,代表了它在这个“数据量-多样性”空间中所占据的范围。

本节课中我们一起学习了描述大数据的核心框架——“3V”模型,即数据量(Volume)、处理速度(Velocity) 和 数据多样性(Variety)。我们还通过不同科学领域的例子,看到了这些维度如何在实际研究中构成不同的挑战。理解3V是进行大规模数据操作和分析的重要基础。
课程1:大规模数据科学 - 第1课:大数据定义 📊

在本节课中,我们将学习“大数据”这一核心概念的定义、历史背景及其关键特征。我们将探讨为何“大”是相对的,并理解数据管理中的实际挑战。

上图展示了数据在规模与多样性两个维度上的分布。蓝色区域可能代表高数据量但多样性相对较低的数据集,而绿色区域则代表其他组合。这个例子来自科学领域,但同样适用于企业问题。
很多时候,企业面临的问题可能是整合数百个电子表格。即使数据总量不大(例如少于10,000行),这个过程也可能耗时三周。相比之下,一个数百TB的数据仓库,其真正的挑战在于管理它所需的基础设施。
什么是大数据? 🤔
上一节我们看到了数据管理的复杂性,本节中我们来看看“大数据”这个术语更精确的含义。
加州大学伯克利分校的Mike Franklin提出了一个我认同的观点:大数据是相对的。它指的是任何管理成本高昂且难以从中提取价值的数据。因此,“大”并不取决于一个特定的数据量阈值(例如,PB级算大,TB级算小,GB级因为能放入机器内存就算很小)。关键在于你试图用数据做什么,以及你拥有什么样的资源和基础设施来处理问题。
从某种意义上说,“困难的数据”或许才是大数据的真正含义。重点不在于“大”,而在于其带来的挑战性。记住“大是相对的”这一点非常重要。
关于这一点,我想再补充一点。在许多大数据领域的演讲中,人们常常倾向于抛出最大的数字,例如“我们数据库里有60 PB数据”或“我们的仪器每晚收集40 TB数据”。单凭这些数字本身并不能传达真正的挑战是什么。如果你能通过简单方法立即将数据规模缩减到更小、更易管理的程度,那么这可能并不是一个巨大的挑战。所以,再次强调,“大”是相对的。
术语的历史 📜
前面我们讨论了“大数据”的现代定义,现在让我们追溯一下这个术语的历史。
我能找到的最早概念来自1989年的Eric Larson。他在《Harper's》杂志(后来收录到一本书中)写道:“大数据的守护者说他们是为了消费者的利益,但数据总有办法被用于最初意图之外的目的。”他的观点完全与技术无关,只是指出数据被收集用于一个目的,却被重新用于另一个目的——这是我在本课程最开始部分提到并会反复回归的一个主题。
我认为他这个观点是正确的。他真正的重点是关于消费者隐私数据开始被商品化,这在当时是绝对正确且相当有先见之明的,因为如今这已成为一个大问题。尤其令人印象深刻的是,这发生在互联网兴起之前,却已经预示了大数据中非常热门的问题,如伦理、隐私和敏感性等,我们稍后会讨论一些。但这并不完全是我们现今所说的大数据含义,因为它缺乏技术层面的讨论,没有涉及实际管理这些数据集的挑战。
3V模型:现代定义 🎯
了解了历史背景后,现在我们来看看当今更常用的大数据定义框架。
另一个参考点是近年来一些咨询公司提出的3V模型,但真正的源头是Gartner公司在2001年由Doug Laney撰写的一份报告。我们谈到了规模、速度和多样性,但让我给你机会看看这些引述。
以下是报告中关于三个维度的核心描述:
- 规模:他主要谈论企业对企业。考虑到2001年左右是互联网泡沫时期,人们试图弄清楚这个新技术时代、互联网除了向客户提供网页之外,还能带来什么,如何与供应链或供应商互动。报告中提到:“关于单笔交易,可能收集多达10倍的数据量。”这绝对正确,我们多次提到的“数据废气”正在导致收集的数据规模越来越大。
- 速度:报告指出“提高了交互点的速度”。这就是对交互性的需求,过去并不那么必需,但随着所有业务和交易速度的提升,处理它们的基础设施所面临的约束也增加了。
- 多样性:我非常喜欢这一点。报告预测(直到2003-2004年):“有效数据管理的最大障碍将是各种不兼容的数据格式、未对齐的数据结构和不一致的数据语义。”这说得太好了。他即使说这是直到2015年的情况,也 arguably 是正确的。这个问题至今仍未消失。

总结 📝

本节课中,我们一起学习了“大数据”的定义。我们明白了“大”是一个相对的概念,核心在于数据管理的难度和成本,而非绝对的数据量。我们回顾了该术语从1989年侧重于数据重用与隐私的早期概念,发展到2001年Gartner报告提出的现代3V模型——即规模、速度和多样性。理解这些维度有助于我们把握大数据带来的真正挑战,尤其是在数据格式、结构和语义的多样性方面,这仍然是当今数据管理的关键障碍。
大数据操作(第1课/共3课)📊:大数据来源

在本节课中,我们将学习“大数据”这一概念的历史背景、其核心挑战,以及大数据的主要来源。我们将探讨技术瓶颈如何推动大数据领域的发展,并了解数据是如何通过现代传感器和系统被大量收集的。
大数据的历史背景与挑战
上一节我们提到了“大数据”这一术语的演变。本节中,我们来看看其发展历程中的一个关键观点。
约翰·马西(John Masey)曾提出,大数据将成为下一代基础设施压力的主要来源。他认为,技术的真正推动力来自于我们感受到“痛点”的地方,而当时的瓶颈在于输入/输出(I/O)接口。
具体来说,磁盘容量正在飞速增长,但数据读取的延迟并未同步提升。公式可以简单表示为:
数据增长速率 >> 数据访问速率
这意味着,虽然我们可以廉价地存储海量数据(例如购买一块3TB的硬盘),但要从头到尾读取这块硬盘上的所有数据,可能需要数小时。因此,我们虽然能“保存”数据,却难以有效地“处理”数据,因为数据传输的“管道”太窄了。
约翰·马西的论点围绕着一个核心:大规模的数据及其处理将成为基础设施的应力点。尽管他认为“大数据”是一个通用术语,自己并非其首创者,但他早期的论述确实精准地预示了当今大数据处理的核心挑战。
大数据的当代意义与价值
上一节我们讨论了技术瓶颈,本节中我们来看看大数据在当今时代的核心价值。
大数据处理的必要性和从中提取信息的渴望,已成为所有科学领域乃至商业领域的关键主题。可以说,它是我们这个时代的核心科学议题之一。
其核心价值在于:从大数据中提取价值。无论是在科学研究还是商业决策中,关键问题都是如何“解锁”大数据中隐藏的信息。
大数据的三大来源
理解了大数据的重要性后,我们来看看这些海量数据究竟从何而来。大数据主要有三个来源:

以下是数据来源的具体分类:
-
客户数据废气
企业如今追踪的客户交互信息远多于过去。这不仅仅是记录订单,还包括监控点击流、广告点击率、广告位置的影响,以及订单在系统中流转的精确时间日志。本质上,企业与客户的每次互动细节都被记录下来,从而产生了用于研究的庞大数据集。 -
新型普及化传感器
新出现的、无处不在的传感器使我们能够获取以往无法看到的数据源。这不仅仅适用于科学研究,在商业中同样如此。 -
数据存储技术的进步
磁盘容量上升,每字节存储成本下降。这催生了一种“保存一切”的能力或观念,无论当前是否需要。因此,人们开始保留过去可能会丢弃的数据,并思考如何利用这些数据做出更好的预测和商业决策。
传感器数据来源实例
上一节我们概述了大数据来源,本节将通过具体例子,深入了解传感器如何创造新的数据源。
以下是几个新型传感器带来大数据潜力的例子:
- 汽车黑匣子:所有新车都将配备类似航空黑匣子的设备,用于事故调查。它们记录大量驾驶数据。保险公司也提供可自愿安装的设备,用于追踪车速和驾驶习惯以调整保费。这些数据最初为特定目的(如事故鉴定、风险评估)收集,但未来很可能被重新用于其他未知用途,这也引发了隐私方面的思考。
- 水感与电感设备:华盛顿大学的什韦蒂克·帕特尔教授团队开发了
Hydrosense和Electrosense设备。Hydrosense可夹在家庭主水管上,通过监测水压变化来识别并记录每个用水设备(如淋浴、冲马桶)的独特“签名”。Electrosense原理类似,通过分析电力信号负载来识别家中每个电器的能耗情况。这些传感器让以往难以获取的详细资源消耗数据变得可得。
这些例子体现了一个大数据的重要主题:我们正在收集新的信息源,而无需预先确知未来将如何利用它们。

课程总结
本节课中,我们一起学习了“大数据”概念的发展脉络,理解了数据访问速度滞后于存储容量增长这一核心瓶颈。我们探讨了大数据在当代科学与商业中提取核心价值的重要意义。最后,我们详细分析了大数据的三大来源:客户交互的全面记录、新型传感器带来的新数据可见性,以及存储成本下降催生的“保存一切”趋势,并通过汽车黑匣子和智能家居传感器等实例加深了理解。
📚 课程介绍与后勤安排(第1课)


在本节课中,我们将了解《大规模数据科学》课程的整体结构、学习目标、先修要求以及评估方式。课程旨在通过理论讲解与实践操作相结合的方式,帮助初学者建立对数据科学领域的广泛认识。
🎯 课程结构与设计理念
课程的组织方式是对重要趋势的引导性介绍,并结合特定主题的深入探讨。
课程包含一系列实践作业,旨在培养具体的技能与经验。这是课程中最重要的部分。
课程设计的挑战在于,既要广泛覆盖我们想要的主题,又要足够包容,避免为特定群体定制内容。因此,部分内容对某些学习者可能较难,而另一些学习者可能觉得某些方面比较常规。如果有人觉得全部内容都很常规,我会感到惊讶,因为那样的话你可能不会选择这门课。
📝 先修要求
课程的先修要求相对宽松,因为我们希望吸引广泛的学习者,但这些要求确实很重要。
以下是具体的先修知识:
- 编程经验:具备某种语言的编程经验至关重要。
- 统计学基础:需要了解大学基础或高中高级统计学术语。例如,当我提到线性回归时,你应该知道其含义。
- 数据解读能力:能够查看数据可视化图表并理解其传达的信息。
- 数据库概念:最具挑战性的一点是,需要对数据库和数据库概念有所了解。课程中的许多讨论将围绕数据库及其关系展开,因此了解其基本概念会很有帮助。
💻 作业安排
课程作业旨在提供多样化的实践体验。
以下是作业的具体内容:
- SQL作业:将涉及编写SQL。如果你从未写过SQL但了解一些数据库知识,你很可能可以完成这项作业。如果你是SQL专家,作业的某些部分可能仍然对你有启发。
- Python作业:有两项作业涉及编写Python。
- 可选的大数据处理作业:一项可选作业涉及使用亚马逊云服务处理大数据。这项作业可选的原因之一是学习者技能水平各异,另一个原因是你需要自费使用云资源(费用预计低于10美元)。即使你不完成这项作业,仍然可以获得课程的全部学分。
- Kaggle竞赛参与:另一项作业涉及在Kaggle.com上参与一个竞赛项目,你可以使用任何你喜欢的工具。这项作业不一定涉及编程,你完全可以使用Excel等图形化工具参与。
🧭 学习目标
本课程的核心目标是让学习者能够清晰地谈论数据科学的概念、工具、算法和技术全景。
这将成为你深入探索特定领域的跳板。例如,这不是一门机器学习课程,但你可以借此深入机器学习;这也不是一门数据库课程,但你可以借此深入学习数据库。
课程还旨在提供操作数据的实践经验,为没有编程经验的学习者打下基础,并为有经验的学习者提供特定的新体验。例如,第一项Python作业将涉及使用Twitter数据进行情感分析。如果你已经懂Python,学习Python本身可能不是这项作业的主要收获,但这可能是你第一次处理实时的Twitter数据流。
最终,我们希望你能在各种数据科学主题上达到“高级初学者”的水平。鉴于数据科学涵盖领域广泛,挑战在于如何超越浅层了解。我们认为课程设计得不错,但这需要由你来评判。
🔗 课程哲学与定位
需要强调的是,本课程的哲学在于:数据科学家所需的技能横跨多个不同领域,包括统计学、编程、数据库、分布式系统和可视化。
这些主题的传统组织方式是纵向的,并不利于获得数据科学的入门知识。为了在所有领域获得入门级知识,你通常需要学习七门不同领域的入门课程。
我们的目标是尝试揭示并简化这些不同领域之间的联系,而不是专注于它们各自的独特性。
完成本课程后,你不会成为统计学、机器学习、数据库或NoSQL方面的专家,也不会精通所有相关编程语言。
然而,你将使用所有这些工具,理解它们的基本概念,并应用其中的许多工具。
📊 课程评估方式
课程的评估将通过多种方式进行。
以下是具体的评估形式:
- 课堂小测验:讲座期间会有在线小测验(你已经看到了一些“手指练习”测验)。这些是完整在线作业的一部分。
- 作业评分:部分编程作业将自动评分。
- 同伴互评:不适合自动评分的作业将使用同伴评估工具。例如,你需要撰写一份关于Kaggle解决方案的描述,其他学生将评估其是否清晰易懂。
👨🏫 讲师背景
最后,介绍一下我的背景。
我拥有工业系统工程学士学位,曾在多家软件公司从事咨询工作数年。
之后我重返研究生院,获得了计算机科学博士学位,期间与海洋学家等研究人员进行了大量合作。
随后几年,我更加直接地与海洋学家合作,担任他们系统的数据架构师,工作内容主要是结合大规模海洋模型与观测网络。
目前我在华盛顿大学任职,是计算机科学与工程系的附属助理教授,并在数据科学与工程研究所领导一个可扩展数据分析小组。
✨ 总结

本节课中,我们一起学习了《大规模数据科学》课程的后勤安排与整体设计。我们了解了课程以广度介绍和深度探讨相结合的结构,明确了编程、统计和数据库基础的先修要求,并预览了包含SQL、Python、云服务和Kaggle竞赛在内的多样化实践作业。课程的目标是帮助你建立对数据科学领域的全景认识,成为连接不同技术领域的“高级初学者”,并为后续深入学习打下坚实的基础。
📘 课程名称:大规模数据科学(大数据操作,第1课/共3课有视频)|Data Science at Scale - P22:Twitter作业入门
在本节课中,我们将学习如何开始第一个作业。该作业要求你访问Twitter的程序化API来收集数据,进行情感分析,并分析结果。我们将从设置环境开始,逐步完成数据收集的初始步骤。
🛠️ 第一步:设置环境
上一节我们介绍了作业的整体目标,本节中我们来看看如何设置一个包含Python和Git的环境,以便获取起始代码。


你可以使用任何可访问的环境(Mac、Linux或Windows),也可以选择使用课程提供的虚拟机。本教程将使用虚拟机进行演示。
以下是设置虚拟机的步骤:
- 点击课程页面上的链接,下载OVA文件(约2.3GB)。
- 下载完成后,需要安装一个虚拟机播放器(如免费的VirtualBox)。
- 在VirtualBox中导入下载的OVA文件。
- 启动导入的虚拟机。
启动虚拟机后,大部分工作将在终端中进行。

📥 第二步:获取起始代码

环境准备就绪后,下一步是克隆包含起始代码的GitHub仓库。
在虚拟机终端中,使用以下命令克隆仓库。仓库URL可以在作业说明页面找到。


git clone [GitHub仓库URL]


克隆完成后,进入作业目录并确保代码是最新的。
cd data-science-course-materials
git pull
✅ 第三步:检查环境依赖
在开始编码前,需要确保系统已安装必要的工具和库。
首先,检查是否已安装Git和Python。
git --version
python --version
接下来,安装Python包管理工具pip(如果尚未安装),然后使用pip安装本作业所需的oauth2库。
# 安装pip(如果需要)
sudo apt-get install python-pip

# 使用pip安装oauth2库
pip install oauth2
如果环境已预装这些组件,相应步骤可以跳过。


🔑 第四步:连接Twitter API
现在,我们将设置Twitter应用程序,以获取访问API所需的密钥。
首先,在虚拟机的浏览器中访问 https://dev.twitter.com/apps,并使用你的Twitter账户登录(可以为此专门创建一个账户)。
以下是创建应用程序的步骤:

- 点击“Create New App”按钮。
- 填写应用程序详情:
- Name: 需要一个全局唯一的名称(例如
yourname_data_science_app)。 - Description: 填写描述,如“First assignment for data science”。
- Website: 可以填写一个占位URL,例如
http://www.example.com。
- Name: 需要一个全局唯一的名称(例如
- 同意条款并创建应用。
应用创建成功后,进入“Keys and Access Tokens”标签页。
你需要获取两组密钥:
- Consumer Key (API Key) 和 Consumer Secret (API Secret)
- Access Token 和 Access Token Secret(可能需要点击“Create my access token”按钮生成)

请妥善保存这四组密钥,下一步将用到它们。

📝 第五步:配置Python程序
我们已经获得了API密钥,现在需要将它们填入Python脚本中,以便程序能够认证并访问Twitter。
在作业目录中,找到并编辑 twitter_stream.py 文件。文件开头有四个变量需要填写:
# 在 twitter_stream.py 文件中找到并修改以下变量
api_key = "你的Consumer Key (API Key)"
api_secret = "你的Consumer Secret (API Secret)"
access_token_key = "你的Access Token"
access_token_secret = "你的Access Token Secret"

使用从Twitter开发者页面复制的密钥,分别替换上述四个变量的值。

保存文件后,你可以运行该脚本测试连接是否成功。
python twitter_stream.py

如果配置正确,终端将开始实时显示Twitter流数据。按 Ctrl+C 可以停止流。


💾 第六步:收集样本数据
测试成功后,下一步是运行脚本一段时间,以收集用于后续分析的样本推文数据。

根据作业要求,让脚本运行约3分钟,并将输出重定向(管道)到一个JSON文件中。

# 运行脚本并将输出保存到 output.json 文件
python twitter_stream.py > output.json
让命令运行足够的时间(约3分钟),然后按 Ctrl+C 终止。此时,当前目录下会生成一个包含推文数据的 output.json 文件。


(可选)你也可以修改 twitter_stream.py 文件,使用Twitter搜索API来收集包含特定关键词的推文,而不仅仅是样本流。

📤 第七步:准备作业提交
数据收集完成后,需要提取生成文件的前20行,作为作业第一部分提交。
使用以下命令提取 output.json 文件的前20行,并保存到另一个文件中。
head -n 20 output.json > problem1_submission.txt
现在,你可以打开 problem1_submission.txt 文件查看内容,并通过Coursera的作业提交界面提交该文件。
如果虚拟机内提交有困难(如显示问题),可以通过电子邮件、Dropbox或配置虚拟机共享文件夹等方式,将文件传输到主机再进行提交。


🎯 总结
本节课中我们一起学习了第一个Twitter数据科学作业的初始步骤。我们完成了从设置虚拟机环境、克隆代码、安装依赖、创建Twitter应用并获取API密钥、到配置Python脚本并成功收集推文样本数据的全过程。你现在已经准备好开始修改Python文件,以完成作业后续的情感分析等任务。

对于Python新手,这可能有些挑战,但请坚持并善用课程论坛寻求帮助。对于有经验的开发者,请确保不要过度复杂化问题,通常简单的解决方案就足够了。
数据科学大规模应用(第1课)📊:数据模型术语


在本节课中,我们将学习数据模型的核心概念。理解数据模型是理解数据库如何组织和操作数据的基础。我们将探讨数据模型的定义、组成部分,以及它与数据库的关系。
数据存储的逻辑视角
上一节我们提到了数据存储的技术层面。本节中,我们来看看数据存储的逻辑组织方式。
一个更偏向逻辑的视角是询问我们使用的数据模型是什么。数据模型不仅仅是磁盘或文件中的比特位,它描述了数据的逻辑组织方式。
例如,在你的个人电脑上,数据可能以分层嵌套的文件夹形式存储,这是一种类似树状的数据模型。另一种常见方式是行和列,这是本课程将重点讨论的表格形式。数据也可能以网格、非结构化形式或图的形式存在。
关键在于,每当思考数据时,都应考虑其应用的数据模型。
数据模型的三大组成部分
一个数据模型通常包含三个核心组成部分,理解它们对于掌握数据模型至关重要。
以下是数据模型的三个关键部分:
-
结构
- 定义了数据如何组织。例如,在表格模型中,结构是行和列;在图模型中,结构是节点和边。
-
约束
- 定义了哪些结构是合法的。例如,在表格模型中,一个常见的约束是所有行必须拥有完全相同的列数。另一个约束可能是某一列中的所有值必须是整数,或者必须在特定数值范围内(例如代表一年中的天数)。
-
操作
- 定义了可以对数据结构执行哪些操作。虽然有时被视为独立部分,但我认为操作是数据模型不可或缺的一环。它涵盖了这些结构所支持的实际功能。
数据模型示例
让我们通过具体例子来巩固对数据模型三大组成部分的理解。
以下是不同数据模型的示例:
-
表格模型
- 结构:行和列。
- 约束:所有行具有相同的列数;某一列的所有值类型相同。
- 操作:查找特定列等于特定值的所有行(例如,
last_name = 'Jordan')。
-
键值对模型(在NoSQL运动中流行)
- 结构:键值对。
- 约束:(可能)键是唯一的。
- 操作:给定一个键
X,返回其对应的值。
-
分层模型(如文件系统)
- 结构:父子节点(如文件夹和文件)。
- 约束:一个子节点不能有两个父节点(例如,一个文件不能同时位于两个文件夹中)。
- 操作:遍历目录树、打开/关闭文件。
-
原始文件
- 结构:字节序列。
- 约束:通常较少。
- 操作:获取下一个字节、移动到文件内另一个位置、打开/关闭文件。
总之,当你看到任何数据(尤其是在非易失性存储中),都可以思考它支持哪些操作、对结构有何约束,这能帮助你理解其背后的数据模型。
什么是数据库?
理解了数据模型后,我们自然要问:什么是数据库?
一个我认为相当充分的定义是:数据库是一个为支持高效检索而组织起来的信息集合。
这是一个非常通用的定义,它没有提及表格或关系。因此,当你想到数据库时,不必假设它一定是关系型数据库。谈论一个与关系无关的数据库是完全合理的。
但数据库也不仅仅是一堆数据。它的核心在于“为支持高效检索而组织”。
另一种观点来自Jim Gray在《第四范式》一书中的引述:
“当人们使用‘数据库’这个词时,根本上他们是在说数据应该是自描述的,并且应该有一个模式。这就是‘数据库’这个词的全部含义。”
这又回到了数据模型的概念。模式定义了结构和约束,而数据本身需要是自描述的,这意味着仅通过查看数据,你就能理解如何读取它。例如,对于一个表格数据文件,你应该能够通过检查得知它有哪些列、有多少行等信息。在数据库中,通常会有一个明确的目录或模式来实现这种自描述性。
总结

本节课中,我们一起学习了数据模型的核心术语。我们首先从逻辑视角探讨了数据存储,然后深入分析了数据模型的三大组成部分:结构、约束和操作。接着,我们通过表格、键值对、分层等模型示例巩固了这些概念。最后,我们探讨了数据库的定义,并将其与数据模型和自描述性的模式概念联系起来。理解这些基础术语是后续学习数据库和大数据操作的关键第一步。
大规模数据科学(大数据操作,第1课/共3课) - P24:从数据模型到数据库 🗄️

在本节课中,我们将探讨“数据库”这一核心概念,理解其本质、解决的问题以及在实际数据科学任务中如何评估和选择合适的数据存储方案。
数据库的另一种视角
上一节我们介绍了数据库通常被视为配备了模式(Schema)的自描述数据。本节中,我们来看看为什么我们需要数据库,它们解决了哪些实际问题。
以下是数据库可以帮助解决的四个主要问题:
- 数据共享:当多个用户需要访问同一份数据时,就需要某种基础设施或接口来管理并发访问。可以说,所有真正的数据库都提供了这种能力。
- 数据模型强制:无论是行列表(Rows and Columns)还是层次模型(Hierarchical),都需要有软件来强制执行这种结构。数据库可以做到这一点。数据模型不仅包括原始结构(如父子关系、行列),还包括更高级的约束,例如:
- 某个字段必须是1到5之间的数字。
- 某个字段必须是一周中的某一天。
- 某个字段必须是另一个表中已存在的客户ID。
这类约束在应用层很难强制执行,我们稍后会进一步讨论。
- 处理规模(Scale):当数据量超过一定规模,或者数据模型的实例数量变得非常庞大时,我们需要专门的算法来处理。自己编写所有遍历大型数据集的算法会成为瓶颈。数据库可以组织这些算法,并通过方便的机制(即复杂性隐藏接口)提供访问。数据库为大规模数据提供了一个复杂性隐藏的接口。
- 灵活性:你可能编写了特定软件以特定方式访问你的文件集。但一旦需要以你未预料到的方式访问数据,你就必须重写大量代码。数据库试图做的是,预测并支持一系列不同的数据访问和处理方式。
我在这里的讨论比较抽象,这是有意为之的。以上几点对于我们将要讨论的关系型数据库(Relational Databases)无疑是正确的,但我认为任何配得上“数据库”这一称谓的系统都应具备这些特性,其他类型的系统(如NoSQL、键值存储、图数据库)也是如此。因此,在思考NoSQL系统、键值存储、图数据库等时,我们也应牢记这些要点,这不仅仅是关系型数据库的事,范围更广。
评估数据存储层
一般来说,当我们审视这些不同的系统,思考数据存储层(这也是我们本次关于数据科学的对话以及本课程的起点)时,需要考虑以下几点:
- 数据在磁盘上是如何物理组织的?
- 某种特定的组织方式将高效支持哪些类型的查询和操作?哪些不支持?
一个直接的问题是(至少与第三点相关):更新数据或添加新数据困难吗?这是划分所有数据操作的一种快速方法:读操作(Reads)与写操作(Writes)。在许多情况下,你会发现,便于高效读取数据的组织方式,与便于高效写入数据的组织方式并不相同。这种权衡是数据库及其他大规模系统中许多设计挑战的核心。
当然还有其他类型的操作,例如:你想通过键(Key)查找,还是想通过其他字段查找?我们将在未来的章节中讨论这些。
然后,如果我遇到了未曾预料到的新查询,会发生什么?我需要完全重组数据吗?我需要编写一堆新代码吗?这有多困难?
这些就是你在为数据科学任务选择平台时将要使用的评估标准。以下是一些需要考虑的问题(可能不是全部):
- 如果大致上你的选择是一堆文件、一个现成的数据库、一个NoSQL系统或其他东西。

在评估各种方案的优缺点时,心中牢记这些问题将非常重要。
总结


本节课中,我们一起学习了数据库的核心价值。我们了解到,数据库不仅仅是存储数据的容器,它更是为了解决数据共享、模型与约束强制、大规模数据处理以及访问灵活性等关键问题而设计的系统。在评估数据存储方案时,我们需要从数据物理组织、读写效率、查询支持以及应对未来变化的灵活性等多个维度进行考量,为数据科学任务选择最合适的工具。
大规模数据科学(大数据操作,第1课/共3课) - P25:前关系型数据库 📚

在本节课中,我们将学习关系型数据库出现之前的数据组织方式。我们将探讨早期数据模型的设计思路、它们如何应对查询需求,以及这些设计存在的局限性。理解这些历史背景,有助于我们更好地评估现代NoSQL系统的设计权衡。

上一节我们讨论了数据模型,并用它们引出了数据库的广义概念。本节中,我们将以此为基础,具体探讨关系型数据库出现之前的几种数据组织方式。我们将使用一系列问题来分析不同的数据组织方法,并对照实际需求进行评估。这些分析最终将帮助我们理解关系模型的诞生动机。
分析数据系统的关键问题 🔍
在评估任何数据系统时,我们可以提出以下几个核心问题:
- 数据在磁盘上如何物理组织? 这关系到数据的存储效率。
- 系统高效支持哪些类型的查询? 这决定了系统的适用场景。
- 如何进行数据更新? 这影响了系统的可维护性和扩展性。
网络数据库(或文件系统)示例 📁
首先,我们来看一个被称为“网络数据库”的早期示例,它本质上更接近于直接使用文件系统。假设我们使用“零件与订单”模型。
数据物理组织
数据以记录形式存储在磁盘上。一个“订单记录”会包含一个地址字段,该字段物理指向与该订单关联的第一个“零件记录”。接着,这个零件记录又指向下一个零件,以此类推。订单记录中可能还有另一个字段,指向下订单的“客户记录”。
高效查询
如果我想查找某个订单关联的所有零件,效率很高。因为只要找到该订单记录,就可以顺着指针链遍历收集所有零件。
低效查询
然而,如果我想查找包含某个特定零件(例如垫圈)的所有订单,效率就很低。此时我必须扫描数据库中的每一个订单记录来寻找它。虽然可以通过添加“反向指针”等方法来缓解,但这增加了复杂性。
更新与扩展问题
这种基于文件的“原型数据库”模型在数据变更时面临挑战。例如,如果我想增加一个“账单客户”字段(区别于“收货客户”),我就在记录中增加了一个新字段,从而扩展了记录的长度。这可能导致该记录之后的所有数据都需要移动。更重要的是,所有遍历此结构的程序现在都需要知道这个新字段,它们都必须被重写以适应这个额外的数据。
多访问路径问题
此外,如果我们想支持不同的数据访问方式(例如,通过零件查找所有订单),最终可能不得不创建数据库的完整第二个副本。当更新数据时,我需要同时更新所有副本。可以想象,随着可能的访问路径增多,副本的数量和同步的复杂性会变得非常大。
分层数据库:部分解决方案 🌳
针对上述问题,出现了一种部分解决方案:分层数据库,其典型代表是IBM的IMS系统(至今仍有用户)。
核心思想
数据被组织成“段”。虽然逻辑模型仍然具有我们在网络模型中看到的层次化特征,但物理上数据被分割开了。例如,我将顶层访问路径设为“客户”,那么逻辑上,“订单”只位于“客户”之下,“零件”只位于“订单”之下。
改进之处
由于数据存在于独立的“段”中,我可以修改一个段(例如为订单增加字段),而无需破坏访问其他段的所有代码。更新也更容易,我可以将一个订单添加到存储在其他位置的段中,而不会影响所有其他结构。
遗留问题
然而,一个显著的缺点依然存在:应用程序开发者仍然需要理解这个层次结构才能找到任何数据。他们必须确切知道数据是如何组织的(例如,订单出现在客户之下)。因此,你仍然必须预先判断用户需要哪些访问方式,并为此进行设计。
数据独立性的萌芽
在这种模型上构建的软件层,能够在一定程度上将应用程序与底层数据结构的变更隔离开来,提供了一定的可靠性。例如,新增的字段只会在客户端真正需要时才会传递给它。这带来了某种程度的数据独立性,我们稍后会进一步讨论这个概念。

本节课中,我们一起学习了关系型数据库出现前的两种主要数据组织方式:网络(文件)模型和分层模型。我们分析了它们在数据物理组织、查询效率和更新维护方面的特点与局限。这些早期系统的经验,特别是对“数据独立性”和灵活访问路径的需求,直接推动了关系数据模型的诞生。下一节,我们将深入探讨关系模型如何解决这些问题。
大规模数据科学(大数据操作,第1课/共3课) - P26:关系型数据库的动机 🗄️

在本节课中,我们将探讨关系型数据库的核心动机与基本概念。我们将了解它为何被发明,其核心设计思想是什么,以及它如何通过一种统一的模型来组织数据。
关系型数据库的核心理念
上一节我们讨论了层次型和网状数据库。本节中,我们来看看关系型数据库的设计哲学。
关系型数据库管理系统被发明出来,是为了让你能够以多种方式使用同一组数据。这包括在数据库构建之初、以及最初编写应用程序时未曾预见到的方式。
需要强调的是,这才是关系型数据库的关键思想,并非SQL语言,也并非你可能会联想到的某些具体实现细节。它的核心在于:以某种方式组织数据,以支持未预见的数据访问方法和查询方式,并使应用程序免受数据组织方式变更的影响。
什么是关系型数据库?
那么,什么是关系型数据库呢?在最简单的层面上,一切皆是关系(Relation),这与表(Table)是同义词。所有数据都组织成行和列。
以下是一个需要明确的重要细节:
- 表中的每一行都拥有完全相同的列,即列的数量相同。
- 同时,每一列的数据类型也必须一致。如果某一列在某一行是整数类型,那么在所有行中,该列都必须是整数类型。
关系模型的含义
这种“一切皆是表”的模型带来一个重要的结果:你不再拥有指针,即不再有物理地址。你拥有的只有表。
因此,不同数据项之间的关系是隐式的。让我们切换到“课程与学生”这个领域来举例说明。
假设我们有两个表:
学生表:包含学生记录。选课表:记录哪个学生(通过ID)选了哪门课程(通过课程ID)。
在关系模型中,选课表和学生表之间没有物理指针。它们之间唯一的联系是:两个表中某一列具有相同的值(例如,学生ID)。
直观上,这听起来可能对性能非常不利。例如,如果我想查找与某门特定课程相关的所有学生姓名,在找到课程记录后,我需要在选课表中查找所有匹配该课程ID的行,然后再去学生表中查找对应的学生姓名,而不是像层次型数据库那样直接导航过去。
但是,这种设计具有对称性。如果我想反方向查询——查找某个学生选修的所有课程,我可以使用完全相同的机制:先在学生表找到该学生ID,然后在选课表中查找所有匹配该学生ID的行。虽然可能需要付出查询的代价,但两个方向的查询机制是相同的。
此外,所有数据只存储一次。这与层次型数据库在大多数情况下能达到的效果一致,但网状数据库则无法保证这一点。我们不会让数据的多个副本四处散落。
设计哲学总结
用一句俏皮话来总结其哲学:正如19世纪的名言“上帝创造了整数,其余都是人类的杰作”,在数据库领域,“科德创造了关系,其余都是我们的杰作”。这里指的是埃德加·科德(Edgar Codd),他撰写了第一篇关系型数据库论文,并因其工作获得了图灵奖(计算机科学领域的诺贝尔奖)。
所以,关于关系数据模型,要记住的第一件事就是:一切皆是表(关系)。

本节课中,我们一起学习了关系型数据库的起源和基本设计思想。我们了解到,其核心动机是支持灵活、未预见的查询方式,并通过“一切皆是表”的模型,使用共享列值来隐式地建立数据间的关系,从而实现了数据的独立性和一致性。
课程名称:大规模数据科学(大数据操作,第1课/共3课) - P27:关系型数据库的关键思想 🗄️

概述
在本节课中,我们将学习关系型数据库的核心思想。我们将探讨其历史背景、关键设计原则,以及支撑其强大功能的代数结构。理解这些概念对于掌握现代数据管理至关重要。
关系型数据库的历史背景与核心动机
上一节我们介绍了早期数据模型的局限性。在关系型数据库出现之前,如果数据的结构发生显著变化,例如在层次模型中更改父子关系,或在网络或面向文件的模型中做任何调整,应用程序通常需要重写才能适应这些变化。
早期关系型数据库正是为了解决这个问题而诞生的。尽管它们最初可能存在缺陷且速度较慢,但它们只需要编写之前约5%的代码量。这是一个巨大的进步。
以下引文来自Ted Codd关于数据库的原始论文,它延续了上一节中Kurt Monash引文的观点,强调了关系型数据库的核心动机:


当数据的内部表示发生变化时,甚至当外部表示的某些方面发生变化时,终端用户的活动和大多数应用程序应保持不受影响。
我想强调这一点,因为数据独立性是关系型数据库的关键思想,而不是SQL或特定实现的其他功能。这个核心思想在原始论文的摘要中就已明确提出。
我如此强调这个观点,是因为这个思想在今天和当时一样重要。
关键思想一:物理数据独立性
接下来,我们来看看与关系型数据库相关的其他关键思想,无论它们是否出现在原始论文中。
一个关键思想是:操作表格数据的程序展现出一种代数结构。我们可以利用这种结构来推理和操作逻辑模型,而无需考虑任何物理数据表示。
这意味着,如果你从表格的角度思考,并考虑表格支持的操作,你就可以理解程序的含义,甚至知道如何优化它,而无需关心数据在磁盘上的实际组织方式。这是极其强大的。
这里的核心思想同样是物理数据独立性。我们将在下一节讨论逻辑数据独立性的含义。
你编写的用于操作数据的程序不再需要直接操作文件和追踪指针。相反,你可以通过高级语言(如SQL,但重点不在于必须是SQL)来访问数据。关键在于,你操作的是称为“表”的逻辑结构。
请记住术语“物理数据独立性”,并理解它意味着:你编写的操作数据的程序比没有这种关系模型时更加健壮。
关键思想二:关系代数
另一个关键思想是我提到的关系代数。我们稍后会详细讨论这些具体的操作符。
从高层次看,对表的一个操作是选择出满足某些条件的行。另一个操作是投影,即忽略你不感兴趣的列。对于两个表,还有一个操作是连接:对于第一个表中的每条记录,在另一个表中找到对应的记录。
以下是核心操作:
- 选择 (Select/σ):根据条件筛选行。
σ_{condition}(Table)
- 投影 (Project/π):选择特定的列。
π_{column1, column2, ...}(Table)
- 连接 (Join/⋈):基于关联条件合并两个表。
Table1 ⋈_{Table1.key = Table2.key} Table2
此外,你还可以定义其他操作,如聚合,以及源自集合论的各种集合操作,如并集 (Union)、差集 (Difference) 和笛卡尔积 (Cross Product) 等。
如果你用这些操作来表达你的查询,其含义会非常清晰。从软件工程的角度看,这允许数据库设计者专注于高效地实现这些操作。
如果是在教室里,我通常会问两个问题:有多少人听说过关系代数?又有多少人使用过数据库?通常,使用过数据库的人数非常多,而听说过关系代数的人数则相对较少。
我希望通过本课程能改变这一状况,将两者等同起来。如果你理解数据库,我希望你也能理解关系代数,反之亦然。


总结
本节课中,我们一起学习了关系型数据库的核心思想。我们回顾了其解决数据依赖问题的历史动机,深入探讨了数据独立性(尤其是物理数据独立性)这一根本原则,并介绍了支撑关系数据库查询的关系代数(包括选择、投影、连接等基本操作)。理解这些基础概念是掌握后续更复杂数据操作技术的关键。
大规模数据科学(大数据操作,第1课/共3课) - P28:代数优化概述 🧮

在本节课中,我们将要学习代数优化的基本概念。这是一种数据库系统用于高效处理查询的核心技术,其思想与我们中学代数课上学到的简化表达式原理相通。
概述
代数优化是数据库查询处理中的一项关键技术。它通过应用一系列代数规则来重写和简化查询表达式,从而找到执行成本最低的查询计划。其核心思想与我们简化数学表达式(如 (z*2 + z*3 + 0) / 1)的过程类似:通过消除冗余操作(如加0、除以1)和应用分配律等规则,将复杂表达式转化为更高效的形式。
上一节我们介绍了查询处理的基本流程,本节中我们来看看数据库系统如何利用代数规则来优化查询执行。
代数优化的核心思想
想象一个数学表达式:(z*2 + z*3 + 0) / 1。当 z = 4 时,最直接的求值方法是按顺序计算:
4 * 2 = 84 * 3 = 128 + 12 = 2020 + 0 = 2020 / 1 = 20
这需要5次操作。然而,我们可以运用代数知识进行简化:
- 消除恒等操作:加0和除以1不影响结果,可以忽略。表达式简化为
z*2 + z*3。 - 应用分配律:
z*2 + z*3 = z*(2+3)。 - 计算常数:
2+3 = 5。最终表达式简化为z*5。
现在只需一次乘法即可得到结果 20。这种符号推理过程就是代数优化的精髓:在计算前,先利用规则对表达式本身进行变换。
对于计算机处理整数,这种符号推理的成本可能高于直接计算。但当操作对象是TB级别的数据表时,操作的顺序和数量将极大影响性能。执行不必要的连接(Join)或选择(Select)操作可能导致系统无法承受的开销。因此,代数优化对于大数据处理至关重要。
关系代数与查询优化
所有关系型数据库在处理SQL查询时,都会进行这种代数优化。过程如下:
- 将SQL查询翻译成关系代数表达式(包含选择
σ、投影π、连接⋈等操作)。 - 应用一系列代数重写规则(类似于分配律、结合律)对表达式进行变换。
- 从多个等价的表达式中,估算每个的执行成本,并选择成本最低的一个作为最终执行计划。
以下是关系代数表达式及其不同执行顺序的例子:
考虑一个涉及对同一关系 R 进行多次选择和连接的查询。其代数表达式可能如下:
( σ_a(R) ⋈ σ_b(R) ) ⋈ σ_c(R)
这表示先执行第一个连接,再与第三个结果连接。
另一种等价但执行顺序不同的计划是:
σ_a(R) ⋈ ( σ_b(R) ⋈ σ_c(R) )
这表示先执行后两个连接,再与第一个结果连接。
甚至存在一种理论上等价但效率极低的计划(例如先计算所有表的笛卡尔积),数据库优化器会识别并避免选择此类计划。
数据库优化器的任务就是评估这些不同但等价的执行计划,并选择预计成本最低的一个。
代数的封闭性
“代数”一词在此并非比喻,它严格遵循数学中代数的定义,核心特性是封闭性。这意味着:
- 每个作用于关系(表)的操作,其结果仍然是一个关系(表)。
- 因此,操作可以无限组合串联,始终得到关系。
这类似于整数上的运算:两个整数相加或相乘,结果仍是整数(尽管除法可能产生实数,这引入了多类代数,但封闭性的思想是相通的)。这种封闭性保证了我们在优化时,始终在同一个系统(关系集合)内进行变换。
代数优化的价值与挑战
代数优化是关系数据库查询处理的“魔法”,它之所以有效,是因为关系模型为数据操作提供了形式化的数学定义。优化器可以基于这些明确的语义进行推理和变换。
然而,当我们放松这种形式化模型,允许在数据上执行任意代码(例如在MapReduce等框架中编写自定义处理逻辑)时,系统就失去了自动进行代数优化的能力。优化工作的负担完全转移到了程序员身上,他们必须手动设计出高效的算法,这无疑增加了复杂性和出错的风险。
总结
本节课中我们一起学习了代数优化的核心概念。我们了解到:
- 代数优化通过重写查询表达式来减少不必要的计算,特别适用于大数据场景。
- 其原理类似于简化数学表达式,运用了消除恒等操作、分配律等规则。
- 关系数据库将SQL查询转换为关系代数表达式,并通过基于成本的优化从多个等价计划中选取最优解。
- 关系代数的封闭性是这一系列操作能够成立的基础。
- 形式化模型是自动优化的前提,而在更灵活的编程范式中(如MapReduce),优化责任则由系统转移给了程序员。


理解代数优化有助于我们认识到数据库高性能查询背后的机制,以及在设计大规模数据处理管道时需要考虑的效率因素。
大规模数据科学(大数据操作,第1课/共3课) - P29:关系代数概述 📚

在本节课中,我们将要学习关系代数的核心概念。关系代数是理解数据库操作,特别是数据清洗、重构和操作的基础。它提供了一种独立于物理数据存储细节的方式来思考和描述数据操作任务。
上一节我们介绍了关系数据库的历史及其解决物理数据独立性的初衷。本节中,我们来看看实现这一目标的关键“秘方”——关系代数。
关系代数运算符概览 🧮
关系代数包含一系列用于操作表格(关系)的运算符。以下是其主要组成部分:
- 集合运算:这些是传统集合论运算(如并集、交集、差集)在关系(表)上的扩展。
- 核心三大运算符:这是关系代数的基石,包括:
- 选择 (Selection)
- 投影 (Projection)
- 连接 (Join)
- 扩展关系代数运算符:这些运算符处理关系中的重复元组或引入排序等概念,使其更贴近实际应用(如SQL)的需求。主要包括:
- 消除重复的运算符
- 分组操作
- 排序操作
需要理解的是,纯粹基于集合论的关系代数模型不包含顺序概念。而扩展关系代数为了实用性(例如,在SQL中排序结果)引入了这些操作。从理论证明的角度看,纯粹的关系代数更易于处理,但两者在实际应用中的区别并不特别重要。
核心要点:当提到“关系代数”时,你首先应该想到的是集合运算加上选择、投影和连接这三大核心操作。
总结 📝

本节课中我们一起学习了关系代数的基本框架。我们了解到,关系代数通过提供一套形式化的表格操作符,使得我们能够独立于底层物理存储细节来思考和描述数据操作。其核心在于集合运算以及选择、投影和连接这三大操作。理解这些概念是掌握后续数据操作技术,包括SQL语言的基础。
大规模数据科学(第1课) - P30:关系代数运算符:并集、差集、选择 🧮

在本节课中,我们将要学习关系代数中的三个基础运算符:并集、差集和选择。理解这些运算符是掌握数据操作和查询构建的关键。我们会从集合与包的概念差异开始,然后逐一探讨每个运算符的定义、语义及其在SQL中的表达方式。

集合与包的概念 🔄
上一节我们介绍了关系代数的基本概念,本节中我们来看看一个重要的底层区别:集合与包。
集合是一个不包含重复元素的对象集合。包则是一个允许包含重复元素的对象集合。

例如,在集合 {a, b, c} 中,元素 a 不会重复出现。而在包中,{a, a, b, c} 是合法的,元素 a 可以出现多次。
这种合法性差异定义了集合语义与包语义。关系代数可以基于这两种语义来定义。扩展关系代数的概念源于需要处理包以及其他操作(如排序)的需求。
一个实用的经验法则是:在学术论文中,除非明确说明,否则通常默认采用集合语义。而在实际的商业数据库实现中,则通常默认采用包语义。理解这一区别在实践中非常重要。
并集运算符 ∪
在理解了集合与包的区别后,我们来看看第一个运算符:并集。
并集运算符将两个关系合并。在关系代数中,关系被视为元组的集合,因此并集的定义是标准的集合合并。
在关系代数符号中,并集写作: R ∪ S
在SQL中,对应的关键字是 UNION。此时集合与包语义的区别就显现出来了:
- 标准的
UNION会消除重复元组,遵循集合语义。 - 如果想保留所有重复项(包语义),则需要使用
UNION ALL。
以下是具体示例:
假设有两个关系 R1 和 R2。
- R1 包含元组:(A1, B1), (A2, B1)
- R2 包含元组:(A1, B1), (A3, B4)
那么:
R1 UNION R2的结果是三个元组:(A1, B1), (A2, B1), (A3, B4)。重复的(A1, B1)只出现一次。R1 UNION ALL R2的结果是四个元组:(A1, B1), (A2, B1), (A1, B1), (A3, B4)。所有元组都被保留。
差集运算符 −
接下来,我们探讨差集运算符。
差集运算符找出存在于第一个关系但不存在于第二个关系中的所有元组。其定义同样源于集合论。
在关系代数符号中,差集写作: R − S
在SQL中,对应的关键字是 EXCEPT(在某些数据库中也用 MINUS)。
继续使用上面的例子:
R1 − R2的结果是:(A2, B1)。- 因为
(A1, B1)也出现在 R2 中,所以被移除。 (A3, B4)不在 R1 中,因此从一开始就不在考虑范围内。差集的目标是获取所有在 R1 中但不在 R2 中的元组。

交集运算符 ∩ 及其表达方式
现在,我们来看看交集运算符。虽然交集也是一个基本的集合操作,但在关系代数中,它并非必须作为原始运算符存在,因为它可以通过其他运算符表达。

交集运算符找出同时存在于两个关系中的元组。符号为: R ∩ S

交集可以通过差集来重新表达:
R ∩ S = R − (R − S)
这个表达式的逻辑是:
(R − S)得到所有只在 R 中的元组。R − (R − S)从 R 中移除所有“只在 R 中”的元组,剩下的就是既在 R 中又在 S 中的元组,即交集。
我们稍后也会看到,交集同样可以通过连接运算符来表达。
选择运算符 σ
最后,我们来学习最常用的运算符之一:选择。
选择运算符用于筛选出满足特定条件的元组。它不会改变关系的结构(列),只会减少行数。
在关系代数符号中,选择写作: σ_c(R)
其中 c 代表选择条件。
在SQL中,这直接对应 WHERE 子句。
选择条件 c 可以是一个布尔表达式,以下是其组成部分:
- 基本比较:可以使用等于(
=)、大于(>)、小于(<)、大于等于(>=)、小于等于(<=)等操作符。- 示例:
salary > 40000
- 示例:
- 复杂布尔表达式:条件可以是任意复杂的布尔表达式,使用
AND、OR、NOT进行连接。- 示例:
salary > 40000 AND name = ‘Smith’
- 示例:
- 任意函数:条件甚至可以是一个返回布尔值的用户自定义函数,这提供了极大的灵活性。
让我们看一个具体例子。假设有一个“员工”表,包含姓名、部门和薪水。
对“员工”表执行选择操作 σ_(salary>40000)(Employee),将过滤掉薪水小于等于40000的元组(例如员工“John”),结果表只包含薪水高于40000的员工记录。输入一个表,输出也是一个具有相同列的表,只是行数减少了。
总结 📝

本节课中我们一起学习了关系代数的三个核心运算符:
- 并集:合并两个关系,需注意
UNION(去重)与UNION ALL(保留所有)在集合与包语义下的区别。 - 差集:找出属于第一个关系但不属于第二个关系的所有元组。
- 选择:根据指定条件过滤关系中的元组,是进行数据筛选的基础操作。

我们还了解到,交集可以通过差集运算推导得出。理解这些运算符的集合论基础及其在SQL中的实现,是构建复杂查询和进行高效数据操作的基石。
大规模数据科学(大数据操作,第1课/共3课)📊:关系代数运算符:投影与笛卡尔积

在本节课中,我们将学习关系代数中的两个核心运算符:投影与笛卡尔积。我们将探讨它们的基本概念、在不同语义下的行为差异,以及它们在现代数据科学应用中的实际意义。
投影运算符 📤
投影运算符用于从关系(表)中消除列。这是一个需要特别注意集合语义与包语义差异的运算符。
在集合语义下,投影不仅会移除所有未明确列出的列,还会自动移除结果中可能出现的所有重复元组。而在包语义下,重复的元组会被保留。这种差异直接影响了数据库查询的效率和结果。
以下是投影运算符的一个示例:假设我们只想从员工表中获取“姓名”和“薪资”两列。
-- 投影操作示例:选择姓名和薪资列
π_{姓名, 薪资}(员工表)

原始员工表可能包含三列:SSN(社会保险号)、姓名和薪资。


应用投影后,我们移除了SSN列。结果中可能出现多个同名但薪资不同的元组(例如,三个名为John的员工有不同的薪资)。

- 包语义会接受并保留这三个重复的“John”元组。
- 集合语义则会自动移除重复项,只保留一个“John”元组(通常保留第一个,但集合本身无序)。
那么,哪种方式更高效?移除重复项是一个昂贵的操作。因此,直接保留重复项(包语义)通常效率更高。事实上,这正是商业数据库系统默认采用包语义的关键动机之一。在许多实际应用中,用户可能不关心重复数据,或者根据领域知识知道剩余列的组合本身是唯一的,因此可以容忍重复。为了形式化理论的纯粹性而强制去重,并非总是必要。
上一节我们介绍了用于筛选列的投影运算符,本节中我们来看看一个能组合两个表的运算符。
笛卡尔积运算符 ✖️
笛卡尔积是一个在传统关系数据库应用中不常被提及,但值得了解的运算符。原因有二:第一,它是思考表操作时一个有用的推理工具;第二,它在分析和数据科学应用中正变得越来越常见。
笛卡尔积的定义是:对于关系R1中的每一个元组,与关系R2中的每一个元组进行组合,生成输出中的一个新元组。
其输出结果的大小是:
|R1| × |R2|
为了让你理解它为何越来越常用,这里有一个例子:你经常需要找到所有满足某种相似性条件的对象对。虽然针对特定问题可能有更高效的技巧,但在实践中,生成所有可能的配对,然后应用某个函数来判断它们的相似性,这种“暴力”方法经常被使用。
例如,如果你想比较两张图像的相似性(比如判断两张人脸是否属于同一个人),并且你有一段代码可以完成这个比较,同时你拥有两个包含大量图像的表或集合。那么,生成所有可能的图像对并应用比较函数,始终是一种可行的实现方式。因此,这类笛卡尔积操作正日益频繁地出现。

总结 📝
本节课中我们一起学习了关系代数中的两个重要运算符:
- 投影:用于选择表中的特定列,需注意其在集合语义(自动去重)和包语义(保留重复)下的不同行为,后者因效率更高而广泛应用于商业数据库。
- 笛卡尔积:用于生成两个表中所有元组的组合对,其结果规模是两表大小的乘积。尽管在传统事务处理中不常用,但在需要成对比较或组合的数据科学和 analytics 任务中正变得越来越重要。

理解这两个运算符是掌握更复杂数据操作和查询的基础。
大规模数据科学(大数据操作,第1课/共3课) - P32:关系代数运算符:笛卡尔积(续)、连接 🧮

在本节课中,我们将继续学习关系代数中的核心运算符。上一节我们介绍了笛卡尔积的基本概念,本节中我们将深入探讨笛卡尔积的具体示例,并重点介绍一个极其重要且常用的运算符——连接(Join)。我们将通过简单的例子和清晰的解释,帮助你理解这些操作是如何工作的。


笛卡尔积示例

上一节我们介绍了笛卡尔积,本节中我们来看看一个具体的例子。
想象你有一个名为 employee 的表,它包含以下两列。同时,还有一个名为 dependent 的表,也包含两列。
如果我们对 employee 表和 dependent 表执行笛卡尔积操作,结果将是所有员工记录与所有家属记录的所有可能组合。


因此,我们可以预知输出结果中将包含四条记录。
以下是 employee 表和 dependent 表进行笛卡尔积运算后的结果。你可以检查发现,John 出现了两次,对应 dependent 表中的每一个实例,其他记录同理。
连接运算符介绍
现在,让我们来讨论连接(Join)操作。或许我应该先放一张专门介绍连接操作的幻灯片。
实际上,我们讨论的连接,是你最常遇到的那种连接类型。事实上,如果我们不加限定地提到“连接”,通常指的就是等值连接(Equijoin)。
等值连接是一种在特定相等条件下进行的连接操作。
那么,连接操作具体做什么呢?连接操作是说:对于关系 R1 中的每一条记录,在关系 R2 中寻找满足某个条件的对应记录。
通常,特别是对于熟悉数据库的同学来说,你会从等值连接的角度来思考。例如,对于每门课程,找出选修了该课程的学生ID。因此,基于主键和外键的连接(如果你不介意这些术语)就是等值连接的实例。

它们不一定是等值连接,但我想强调一点:在本课程中,我们不会过多讨论数据库模式设计。我更感兴趣的是教授关系代数及其应用,而不是从一开始就教你如何设计数据库。原因在于,正如我之前提到的,你通常没有精心设计的模式可用。你没有时间去构建一个,或者一开始就没有现成的模式给你。甚至可能因为只需要回答几个问题就转向其他任务,而没有太大必要去设计一个完整的模式。因此,没有机会去分摊开发模式的初始成本。
如果你使用过数据库,你所做的大多数连接操作都是沿着这些被称为外键的预定义关系进行的。但即使没有这些预定义关系,你仍然可以应用连接操作。

连接在SQL中的两种写法

这里我想指出一个语法上的注意事项。在SQL中,你可以用两种不同的方式编写连接查询。

有时你会看到这样的写法:SELECT * FROM R1 JOIN R2 ON <连接条件>。

而其他时候,你可能会看到:SELECT * FROM R1, R2 WHERE <条件>。
如果我机械地将后一种写法翻译成关系代数执行计划(我们稍后会讨论如何更机械地做到这一点),它实际上是在说:首先构建 R1 和 R2 的笛卡尔积,然后过滤这个笛卡尔积,使其满足指定的条件。
而前一种写法则是说:不,不要那样做,实际上应该使用连接运算符,而不是先生成笛卡尔积。
但是,数据库的查询优化器并没有那么笨。它们足够聪明,能够识别出即使在后一种写法中,正确的执行方式也是表达为一个连接操作。因此,在实践中,这两种编写同一查询的不同方式没有区别。
事实上,粗略地讲,这是两个语法不同但语义相同的SQL查询。优化器并不关心你如何编写SQL,它都会对查询进行优化。它会将查询转换为关系代数执行计划,并操作该计划以找到评估该查询的最佳方式。
我说“粗略地讲”,是因为存在查询提示(query hints)等方法,你可以用来告诉优化器你希望如何评估查询。我们不会讨论这些,因为它们很少重要。另外,虽然存在优化器无法识别为等价的、功能相同的不同查询,从而导致不同执行计划的情况,但这并不常见。在我们这个例子中,这完全无关紧要。
关于使用 JOIN 关键字与在 WHERE 子句中指定条件,如果它们出现,我倾向于使用前一种方式编写查询。
连接操作的本质
以上就是连接运算符最常见和最简单的实例。事实上,我觉得我在这里用左右结构举例可能有点不妥。这实际上是一个很好的例子:这种写法是SQL中等价于使用 JOIN 关键字的写法。
而这种写法,在关系代数中,等价于先进行笛卡尔积再进行选择操作的SQL写法。实际上,它们是相同的。
在代数中,代数作为一种形式体系,只要你能表达出操作,是否推导出新运算符并不那么重要。然而,由于存在大量优秀的工作和算法专门用于高效实现连接操作,因此它理应作为一个特定的运算符存在。当我们讨论高效的连接算法时,我们并不希望写成“先笛卡尔积再选择”的形式。
总结

本节课中我们一起学习了关系代数中两个重要的运算符。我们首先通过一个具体例子回顾了笛卡尔积的操作和结果。然后,我们重点介绍了连接运算符,特别是最常见的等值连接,解释了它的基本逻辑和实际应用场景。我们还探讨了在SQL中编写连接查询的两种等效语法,并理解了数据库优化器会如何处理它们。最后,我们明确了连接操作在关系代数中的核心地位,它不仅仅是笛卡尔积加选择的组合,而是拥有独立高效实现算法的关键操作。理解这些基础运算符是掌握大规模数据操作的重要一步。
课程名称:大规模数据科学(大数据操作,第1课/共3课) - 第33节:关系代数运算符:外连接 🧩

概述
在本节课中,我们将要学习关系代数中的一个重要运算符——外连接。我们将了解外连接的基本概念、它与普通连接的区别,以及它在实际应用中的用途。
外连接的概念
上一节我们介绍了连接操作,本节中我们来看看外连接。外连接是一种特殊的连接操作,它确保结果中包含指定一侧(或两侧)表中的所有元组,即使在另一侧没有匹配的元组。
外连接的核心思想是:保留所有来自左侧(或右侧,或两侧)的元组。如果另一侧存在匹配的元组,则像普通连接一样输出;如果不存在匹配,则用 NULL 值填充缺失的列。
外连接有三种主要变体:
- 左外连接:保留左侧表的所有元组。
- 右外连接:保留右侧表的所有元组。
- 全外连接:保留两侧表的所有元组。
在SQL中,左外连接和右外连接较为常用。全外连接在形式上较难表达,因为它看起来像笛卡尔积。右外连接在实践中很少需要,因为通常可以通过调整操作顺序来实现相同效果。
外连接的示例
为了理解外连接如何工作,我们来看一个具体的例子。假设我们有两个表:匿名病人 表和 匿名工作 表。
以下是这两个表的示例数据:

如果我们在这两个表之间执行外连接操作,需要明确连接的条件。虽然图中没有明确写出连接条件,但我们可以推断出来。
推断连接条件的方法是查看两个表共有的列。匿名病人 表有 年龄 和 邮编 列,匿名工作 表也有 年龄 和 邮编 列。因此,连接条件很可能是基于这两列进行匹配。

连接条件可以描述为:对于 匿名病人 表中的每一个元组,在 匿名工作 表中寻找一个对应的元组,使得两者的 年龄 和 邮编 值都相等。

外连接与内连接的区别
如果我们只进行普通的(内)连接,那么结果中只会包含在两个表中都能找到匹配的元组。

然而,当我们使用外连接时,情况就不同了。例如,在 匿名病人 表中有一个元组(年龄=33,邮编=98120),在 匿名工作 表中没有与之匹配的元组(没有年龄33且邮编98120的记录)。
如果执行内连接,这个不匹配的元组将从输出结果中消失。但如果执行的是外连接(例如左外连接),这个来自左侧表的元组仍然会被包含在结果中。对于右侧表没有匹配的列,系统会用 NULL 值进行填充。


SQL中的外连接
在SQL语言中,我们可以明确地写出外连接。就像可以写 JOIN 来表示连接一样,也可以写 LEFT OUTER JOIN 来表示左外连接,以匹配我们上面的例子。
需要指出的是,本课程作业中使用的数据库是 SQLite。SQLite 作为一个单用户数据库,具有将整个数据库存储为单个文件等优点,非常适合学习和完成作业。但它也有一些限制,其中之一就是无法表达某些类型的外连接,特别是全外连接。

外连接的实用性
外连接在形式上可能不那么优雅,但在实践中经常出现。对于用户(尤其是新手)手动编写查询时,外连接尤其有用。
许多时候,用户会发现当他们将一个表与另一个表连接时,自己表中的一些记录“消失”了,这常常令人感到意外。这是因为内连接只输出满足匹配条件的元组对,没有匹配的元组就不会出现在结果中。
外连接提供了一种方式,可以更符合SQL程序员(特别是初学者)的预期,确保重要的数据不会因为缺少匹配而被过滤掉。


总结
本节课中我们一起学习了关系代数中的外连接运算符。我们了解了外连接的核心是保留一侧或两侧的所有元组,并用 NULL 填充缺失值。我们通过示例比较了外连接与内连接的区别,并认识到外连接在保留所有相关数据、避免记录意外“消失”方面的实用价值。最后,我们还提到了在SQL(特别是SQLite)中实现外连接的语法和限制。
大规模数据科学(大数据操作,第1课/共3课) - P34:关系代数运算符:θ连接 🧩

在本节课中,我们将要学习关系代数中的一个重要运算符——θ连接。我们将了解它的定义、它与等值连接的区别,并通过几个实际例子来理解其应用场景。
上一节我们介绍了等值连接,本节中我们来看看更通用的连接形式——θ连接。
θ连接的定义
更一般地,我们可以拥有一种称为θ连接的操作,这本质上仍然是一种连接,但其连接条件可以是任何你想要的谓词。它不限于等值条件,可以是大于、小于或任意函数等。
我之前提到的“邻近度测试”就是θ连接的一个例子,稍后我们将看到一个更详细的示例。

需要指出的是,等值连接本身是θ连接的一个特例,其中θ条件就是等值条件。

θ连接的应用示例

以下是一些θ连接的例子,用以说明它们在实践中出现的频率可能比你熟悉的要高。特别是对于有数据库经验的人来说,这些连接条件并不总是基于主键-外键关系。
示例一:查找学校五英里内的所有医院

这看起来不像是典型的关系代数或SQL查询,但它确实是。它就是一个连接,其连接条件是基于医院位置和学校位置的距离函数。
以下是该查询的SQL表示,它选取所有医院和学校的组合,然后筛选出医院位置与学校位置距离小于5英里的记录。这里假设存在一个能计算距离的函数。
SELECT H.name
FROM Hospitals H, Schools S
WHERE distance(H.location, S.location) < 5
这里的要点是,即使你没有看到等值条件,这仍然是一个连接操作。另一个要点是了解“θ连接”这个术语,它通常指代任意条件、更通用的连接情况。
示例二:查找页面加载后五秒内的所有用户点击
这个例子类似于之前的距离参数,但现在我们考虑的是时间维度,这是一个一维的、更容易定义的度量。

其逻辑是:计算点击时间与页面加载时间的绝对差值,并检查该差值是否小于5秒。这可能是网络分析人员常用的一个指标,用于快速找到用户所需内容。

SELECT ...
FROM Clicks C, PageLoads P
WHERE ABS(C.click_time - P.load_time) < 5
示例三:区间连接或范围连接
你可能会听到“区间连接”或“范围连接”,这类似于在一个表中有一个时间区间(开始时间和结束时间),并试图从另一个表中找到落在这个区间内的元组。我们稍后会看到一个这样的例子。
总结


本节课中我们一起学习了θ连接。我们了解到θ连接是连接操作的通用形式,其连接条件(θ)可以是任何谓词,而等值连接只是它的一个特例。我们通过查找附近医院、分析用户点击行为等实际例子,看到了θ连接在非等值条件(如距离、时间差、区间包含)下的应用。理解θ连接有助于我们认识到,许多复杂的数据关联问题都可以通过灵活的连接条件来解决。
课程名称:大规模数据科学(大数据操作,第1课/共3课) - P35:从SQL到关系代数 🧮

概述
在本节课中,我们将学习如何将SQL查询语句转换为关系代数表达式。我们将通过一个具体的例子,理解SQL查询的各个部分如何对应到关系代数的运算符,并探讨如何通过优化关系代数表达式来提高查询效率。
关系代数运算符回顾
上一节我们介绍了关系代数的基本运算符。本节中,我们来看看如何将这些运算符应用到实际的SQL查询转换中。
从SQL到关系代数的转换
我们通过一个具体的例子来学习转换过程。假设存在以下三个表的结构:
- product 表:主键为
PID。 - purchase 表:主键为
PID和CID的组合。 - customer 表:主键为
CID。
以下是需要转换的SQL查询语句:
SELECT DISTINCT P.name, Z.cname
FROM product X, purchase Y, customer Z
WHERE X.pid = Y.pid
AND Y.cid = Z.cid
AND Y.price > 100
AND Z.city = ‘Seattle’;
分析SQL查询结构
首先,我们分析SQL查询的构成。以下是关键步骤:
- 识别涉及的表:
FROM子句中包含了product、purchase和customer三个表。多表查询通常意味着需要进行连接操作。 - 识别连接条件:
WHERE子句中的X.pid = Y.pid和Y.cid = Z.cid是连接条件,它们将三个表关联起来。 - 识别过滤条件:
WHERE子句中的Y.price > 100和Z.city = ‘Seattle’是过滤条件,它们只涉及单个表的列,用于筛选数据。 - 理解查询语义:这个查询的目的是找出在西雅图的顾客购买的、价格超过100的产品的唯一产品名和顾客名对。
构建关系代数表达式
基于以上分析,我们可以构建出对应的关系代数表达式。其逻辑顺序如下:
- 首先,使用连接运算符将
product表和purchase表在PID相等的条件下连接起来。 - 接着,将上一步的结果与
customer表在CID相等的条件下进行第二次连接。 - 然后,使用选择运算符应用过滤条件:
price > 100且city = ‘Seattle’。 - 之后,使用投影运算符只保留我们需要的列:
P.name和Z.cname。 - 最后,使用去重运算符(对应SQL中的
DISTINCT关键字)消除结果中的重复元组。
这个表达式可以用一个树形结构来表示,清晰地展示了数据流的处理过程。
关系代数表达式的优化
我们之前讨论过代数优化。思考一下,上面构建的查询计划是最快的吗?查询成本通常取决于中间结果的大小,我们希望流经计划的元组尽可能少。
选择运算符能过滤数据,减少元组数量。一个基本的优化原则是:尽可能早地应用选择操作。
在上面的计划中,过滤条件 price > 100 和 city = ‘Seattle’ 是在所有连接完成之后才应用的。我们可以通过“选择下推”这一最常见的代数变换来优化它:
- 将
price > 100这个条件直接下推到purchase表连接之前。 - 将
city = ‘Seattle’这个条件直接下推到customer表连接之前。
这样,在连接操作发生前,每个表的数据量就已经被大幅减少了,从而降低了整个查询的成本。
理解这种优化非常重要。虽然在数据库实践中,查询优化器会自动完成这类工作,但当我们脱离数据库的舒适区(例如,在处理大规模数据或使用其他计算框架时),就需要手动考虑这些优化。关系代数为我们提供了一种形式和语言,来推理这类问题。

总结
本节课中,我们一起学习了如何将SQL查询转换为关系代数表达式。我们通过一个例子,逐步分析了SQL的 FROM、WHERE、SELECT DISTINCT 子句如何对应到关系代数的连接、选择、投影和去重运算符。此外,我们还探讨了通过“选择下推”来优化关系代数表达式的基本原理,这有助于我们理解查询执行的内部机制,并为后续处理更复杂的数据操作场景打下基础。
大规模数据科学(大数据操作,第1课/共3课) - P36:关系代数思维:逻辑查询计划 🧠

在本节课中,我们将学习如何将SQL查询语句(特别是包含GROUP BY和HAVING子句的查询)转换为关系代数中的逻辑查询计划。我们将理解每个SQL子句如何对应一个或多个关系代数操作符,并体会关系代数的“闭包”特性带来的优势。
上一节我们讨论了去重操作,它对应SQL中的DISTINCT关键字。本节中我们来看看分组和筛选操作。
我们之前没有深入讨论分组操作,稍后会看到一个例子。我现在不打算详细讲解关系代数的分组操作符,它会在后续讨论这些操作的实现时再次出现。分组操作对应SQL语句中的GROUP BY子句。我们也提到了排序,它对应ORDER BY子句。基本上,需要注意这些子句并理解它们意味着需要执行额外的操作。
以下是一个包含GROUP BY的例子,并且引入了另一个叫做HAVING的子句。
SELECT City, COUNT(*)
FROM Sales
GROUP BY City
HAVING SUM(Price) > 100;
这条语句的含义是:对于每一个唯一的城市,统计发生在那里的所有销售交易记录。这里我们只有一张表,所以没有连接操作。GROUP BY City明确指示按城市分组。
关于SQL有一点需要注意:即使没有明确写出GROUP BY,有时从上下文也能推断出分组意图。例如,COUNT(*)是一个聚合函数,而City列没有应用任何聚合函数,因此它必须是被分组的列。实际上,如果尝试不写GROUP BY子句,通常会得到一个错误,尽管从逻辑上系统可以推断出来。
对于每个唯一的城市,计算记录数量。这里还有一个额外的HAVING子句。它表示:我只想返回那些该城市总销售额大于100的记录。
现在,让我们从关系代数的角度来思考这个过程。
首先,我们从销售表(Sales table)开始。然后应用分组操作。分组操作有两类参数:分组列的集合(这里只有“City”一列)以及一个或多个聚合表达式。这里实际上有两个聚合表达式:一个是COUNT(*),出现在SELECT子句;另一个是SUM(Price),出现在HAVING子句中。但它们是在同一次分组操作中计算的。
需要认识到的是,在实际实现分组操作时,有机会在一次操作中计算尽可能多的聚合表达式,因为多次扫描数据是低效的。
那么,如何应用HAVING子句的筛选呢?这需要一个新的操作符吗?并不需要,因为它本质上就是一个选择(Selection)操作。总价(SUM(Price))现在就像一个普通的属性,我们可以像对底层表中的列一样,对它应用选择操作符。
这正体现了几个段落前讨论过的关系代数“闭包”特性的另一个优势:我们知道每个操作都返回表,因此我们可以对任何表应用相同的操作符集合。例如,我们不需要一个叫做“分组选择”的特殊选择操作符。我们稍后会在某些语言中看到这种模型被破坏的情况。
最后,我们投影出我们不需要的所有列,只保留城市和计数(COUNT(*))。
这里的要点是,你可以将每一个操作符的结果都视为一张临时表。虽然在物理上我们尽量避免,但在逻辑上,它就是一张表。


本节课中我们一起学习了如何将包含GROUP BY和HAVING的SQL查询分解为关系代数的逻辑操作序列。我们看到了分组操作如何同时计算多个聚合表达式,以及HAVING子句如何被简化为对分组后临时表的标准选择操作。这充分展示了关系代数作为查询处理基础模型的简洁性和一致性。
杜克大学《大规模数据科学》第1课 - P37:实用SQL:时间序列分箱 📊

在本节课中,我们将学习如何解读一个看似复杂的SQL查询语句。我们将通过一个具体的例子,理解如何将时间序列数据按固定时间窗口(例如5分钟)进行聚合(分箱),并分析查询中多层嵌套的逻辑结构。
上一节我们介绍了关系代数和SQL的基础,本节中我们来看看如何实际分析一个多层嵌套的SQL查询。


分析复杂SQL语句的方法
当面对一个可能看起来复杂的SQL语句时,关键在于寻找FROM子句。在本例中,FROM子句并未直接指定表名,而是包含了一个嵌套查询。这完全可行,因为关系代数具有闭包性:任何关系代数表达式(以及SQL语句)都操作于表并返回一个表。因此,我们可以像查询基表一样查询派生结果。
在这个例子中,我们查询的就是一个派生结果。
以下是理解嵌套查询的步骤:
- 定位数据源:我们继续向下查看
FROM子句,会发现又有一层嵌套查询。再深入一层,最终找到了基表tblESV。 - 理解数据:这些数据来自安装在海洋研究船底部的传感器,记录了多种变量(如荧光度、氧气、硝酸盐浓度)以及采集时刻的经纬度和时间戳。
- 查询目的:这个操作的核心是将测量数据聚合到5分钟的时间窗口中。
查询逻辑分解
现在,我们来逐层剖析这个查询是如何实现时间分箱的。
1. 最内层查询(红色部分)
这一层的主要任务是为每条记录添加一个常量列,作为分箱的宽度。
SELECT *, 5 AS binWidth FROM tblESV
代码中的 5 AS binWidth 为结果集中的每一行都添加了一个值为5的新列 binWidth。这为后续计算时间窗口提供了参数。
2. 中间层查询(蓝色部分)
这一层执行核心的分箱计算,即根据时间戳将每条记录归入对应的5分钟窗口。
SELECT
*,
FLOOR(EXTRACT(epoch FROM timestamp) / (binWidth * 60)) * (binWidth * 60) AS binId
FROM (...内层查询...)
这里使用了公式 binId = FLOOR(时间戳 / 窗口秒数) * 窗口秒数 来将时间戳向下取整到最近的5分钟边界。例如,6分32秒会被归入5分钟开始的窗口,11分29秒归入10分钟开始的窗口。* 确保了所有其他列(包括测量值和经纬度)都被传递到这一层。
3. 最外层查询(绿色部分)
这一层进行最终的聚合计算,并整理输出结果。
SELECT
binId,
AVG(lat) AS lat,
AVG(lon) AS lon,
AVG(fluorescence) AS fluorescence,
AVG(oxygen) AS oxygen,
AVG(nitrate) AS nitrate
FROM (...中层查询...)
GROUP BY binId
ORDER BY binId
我们使用 AS 重命名操作符为复杂的 binId 表达式赋予了一个简洁的名称。然后,查询通过 GROUP BY binId 对每个5分钟窗口内的所有记录进行分组,并计算经纬度及各测量变量的平均值(AVG)。最后,ORDER BY binId 确保结果按时间顺序输出,这可能符合某些应用的需求。
为何采用多层嵌套结构?
你可能会问,为什么不将所有逻辑合并成一个表达式?这类似于在命令式编程中进行抽象或重构,是软件工程思想的体现。这样做有两点好处:
- 逻辑分离:将不同的逻辑块(定义参数、计算分箱、执行聚合)分离,使查询结构更清晰,易于理解和维护。本例中通过颜色区分了这三个逻辑块。
- 提高可重用性:复杂的表达式(如计算
binId的公式)可以被封装和复用。更进一步,我们可以将中间结果保存为视图(VIEW),然后像查询普通表一样在外部查询中引用它,这能极大地简化最终查询语句的复杂度。

本节课中我们一起学习了如何解读一个利用多层嵌套查询实现时间序列分箱的复杂SQL语句。关键要点包括:利用关系代数的闭包性理解嵌套查询、通过分解逻辑块来分析查询步骤、以及了解使用嵌套或视图来组织复杂查询的软件工程实践。掌握这些方法将有助于你理解和编写更高效、清晰的数据分析SQL代码。
大规模数据科学(大数据操作,第1课/共3课) - P38:实用SQL:基因组区间分析 🧬

在本节课中,我们将学习如何分析一个复杂的SQL查询,即使你对其背后的数据领域(例如生物信息学)并不熟悉。我们将通过解构一个涉及基因组区间重叠分析的查询示例,来掌握分析复杂SQL语句结构的通用技能。
概述与查询结构
首先,我们来看一个SQL查询示例。分析任何SQL语句的第一步,通常是查看FROM子句,以了解涉及了哪些表。
以下是查询中我们看到的结构:涉及两个表,并使用INNER JOIN关键字进行显式连接。连接条件是某个ID字段与另一个ID字段的匹配。
上一节我们介绍了查询的基本结构,本节中我们来看看如何深入分析其逻辑。
即使你完全不了解数据的背景,通过分析SQL语句的结构来理解其意图也是一种有用的技能。在数据科学工作中,你可能会遇到这种情况:有人需要你预测下个月的平均销售额,但只告诉你“数据在某个数据库里”。这时,你可能需要自己分析数据库中的模式和现有查询。
因此,掌握分析复杂查询的技巧非常重要。
识别连接条件
表面上看,ON子句定义了连接条件。但如果我们查看WHERE子句,会发现一个要点。
被别名为X的表(hotspot_deserts)和被别名为W的表,在WHERE子句中还参与了额外的条件判断。
这些实际上也是连接条件。尽管INNER JOIN ... ON明确列出了部分条件,但任何涉及两个表属性的条件,本质上都是连接条件。
因此,整个ON和WHERE中的相关部分共同构成了一个复杂的连接条件。
理解CASE语句与函数抽象
现在,让我们回到查询顶部,这里有一段复杂的逻辑。
SQL中提供了一种称为CASE的语句,其作用与其他编程语言中的case语句类似,这并不难理解。具体来说,即使你忽略所有内部逻辑,也可以将这一整段CASE语句“折叠”起来,视作一个名为length_of_overlap的函数。
这个名称是查询作者为这个复杂表达式结果所起的别名。通过上下文线索(如SNP_region代表单核苷酸多态性,strain,BP代表碱基对,noncoding_regions),我们可以推断这个查询可能用于分析基因序列。即使你没有生物信息学背景也没关系,关键在于我们可以将length_of_overlap看作一个接收某些属性并返回结果的黑盒函数。这有助于我们看清查询底层的简洁性。
解析区间逻辑
接下来,让我们深入看看WHERE子句中那个复杂表达式具体在做什么。这是一个有趣的例子。
如果你将这些条件拆分成三部分,并且对数据来源有所了解,就能明白它的含义:

以下是三个区间包含关系的条件,用于判断两个基因组区间(范围)如何重叠:
- 条件A:
X.start_bp > W.start_bp AND X.end_bp < W.end_bp- 这意味着蓝色区间(X表)完全位于红色区间(W表)之内。
![]()

- 条件B:
X.start_bp < W.start_bp AND X.end_bp > W.end_bp- 这意味着红色区间(W表)完全位于蓝色区间(X表)之内。
![]()


- 条件C:
X.start_bp BETWEEN W.start_bp AND W.end_bp- 这意味着红色区间(W表)“跨越”了蓝色区间(X表)的起始点。
![]()

因此,他们直接在SQL中实现了区间逻辑。详细分析这个例子有几点启示:一是关于连接条件的观点,即使不明白具体逻辑,也能从结构上看出它只是一个连接条件;二是你实际上可以直接在SQL中执行某些类型的分析。
在数据库中进行分析的意义
这是一个相当不简单的操作。许多人,尤其是那些对数据库没有好感或听朋友说数据库不好用的人,可能会认为这在数据库中是不可能或不应该做的。
但事实并非如此。在数据库内进行分析不仅可能,而且通常是一个很自然的想法。将数据分析作为你的技能包的一部分是应该的。第一步不应该是“让我们把所有数据从数据库里拉出来,然后用命令式代码开始处理”。
仅凭这个例子,可能不足以说服你“把数据留在数据库里处理是个好主意”这个观点。在整个课程中,我可能会陆续提出一系列论据来支持这一点。同时,我需要说明,我并非将数据库鼓吹为数据科学的终极解决方案,但它确实扮演着重要的角色。
查询的简化视图
现在,我们可以将这个查询抽象化:
- 我们将
CASE语句折叠为函数length_of_overlap(...)。 - 我们将连接条件归结为两点:
- 必须在
CHR字段上匹配。 - 必须满足我们在上一张幻灯片中看到的那种区间重叠条件。
- 必须在
因此,这只是一个θ-连接的例子,其中对每一对元组应用了一个非平凡的函数 f(x, w) 作为连接条件。

总结

本节课中我们一起学习了如何解构和分析一个复杂的SQL查询。我们通过一个基因组区间分析的实例,掌握了几个关键技能:从FROM子句入手理解结构,识别显式和隐式的连接条件,将复杂的CASE逻辑抽象为函数以简化视图,以及解读其中具体的区间重叠逻辑。最重要的是,我们认识到直接在数据库中使用SQL进行此类非平凡的分析是可行且自然的,这应该是数据科学家工具箱中的一项重要技能。
大规模数据科学(大数据操作,第1课/共3课) - P39:用户自定义函数 (UDFs) 🧩

在本节课中,我们将要学习用户自定义函数(UDFs)。UDFs 允许你根据特定应用需求,在数据库中创建并注册自己的函数,从而扩展 SQL 的功能。我们将了解 UDFs 的三种主要类型及其使用场景。
我们已经在前面的例子中接触过用户自定义函数。例如,我们曾假设存在一个名为 overlaps 的函数和一个名为 length_of_overlap 的函数。这些函数并非数据库内置,而是为了满足特定应用逻辑而创建的。
实际上,作为用户,你可以编写这类函数,将其注册到数据库中,然后就可以在 SQL 语句中调用它们。你还可以为其他用户授予使用这些函数的权限。这样一来,数据库就成为了一个可被各处调用的用户自定义函数库。
UDFs 主要分为三种类型:标量函数、聚合函数和表函数。你可以通过它们在 SQL 语句中的使用方式来区分它们。
1. 标量函数 (Scalar Functions) 🔢
标量函数可以出现在任何允许表达式出现的地方。例如,在 SELECT 子句、WHERE 子句的条件比较中,甚至在连接条件里,只要属性可以出现的地方,标量函数就能出现。
它的行为类似于对单行数据进行操作并返回单个值的函数。
示例公式/代码表示:
-- 假设有一个标量函数 calculate_discount(price, rate)
SELECT product_name, calculate_discount(price, 0.1) AS discounted_price
FROM products;
2. 聚合函数 (Aggregate Functions) 📊
聚合函数只出现在 SELECT 子句中,并且总是与 GROUP BY 子句相关联。一个常见的、数据库可能未内置但用户常需要定义的聚合函数是字符串连接(concatenate)。
想象一下,你有一个包含标识符和单词的表,你想将所有相关的单词连接成一个长字符串。虽然许多编程语言可以轻松实现字符串连接,但 SQL 中可能没有内置此功能的聚合函数。
以下是其工作方式的抽象描述:你通过某个分组属性定义一组相关的字符串,然后使用自定义的聚合函数将它们全部连接起来。如果你有一个能连接两个字符串的基础函数,你就可以构建一个能连接多个字符串的用户自定义聚合函数。
示例公式/代码表示:
-- 假设有一个自定义聚合函数 string_agg_concat(words)
SELECT group_id, string_agg_concat(word) AS combined_text
FROM word_table
GROUP BY group_id;
3. 表函数 (Table Functions) 📑
表函数出现在 FROM 子句中,可以说是最复杂的一种。最常见的例子是用于生成整数序列的函数。
例如,如果你想将 5 到 10 的所有整数表示为一个表,一种方法是物理地在磁盘上创建表并插入这些整数。但这可能有些浪费资源,因为我们知道这个序列应该是什么,并不需要真正存储它。当序列范围很大时(例如从 1 到 100 万),物理存储的缺点会更明显。
因此,一个能够动态生成这些值的函数会非常有用。表函数就用于此类场景。
示例公式/代码表示:
-- 假设有一个表函数 generate_series(start, end)
SELECT * FROM generate_series(5, 10);
-- 这将返回一个包含 5, 6, 7, 8, 9, 10 的单列表

本节课中我们一起学习了用户自定义函数(UDFs)的三种类型:标量函数、聚合函数和表函数。它们极大地增强了 SQL 的表达能力,允许你将复杂的应用逻辑封装成可重用的数据库函数,从而简化查询并提高效率。理解并善用 UDFs 是进行高效、灵活大数据操作的重要技能。
大规模数据科学(大数据操作,第1课/共3课) - P40:用户自定义函数支持 📚

在本节课中,我们将探讨数据库中对用户自定义函数的支持情况。我们将了解哪些数据库提供了良好的支持,以及如何在不同环境中定义和使用这些函数。


概述
用户自定义函数允许开发者在数据库系统中扩展其功能。大多数主流数据库都提供了对此功能的支持,尽管存在一些例外。
用户自定义函数的广泛支持
几乎所有数据库都提供了对用户自定义函数的全面支持。然而,一个显著的例外是 SQLite,它恰好是本课程作业中使用的数据库。因此,我鼓励大家去探索其他数据库在这方面的实现。
以下是支持用户自定义函数的主要数据库:
- PostgreSQL 和 Greenplum:Greenplum 是一个基于 PostgreSQL 代码库的商业并行数据库,它们对用户自定义函数提供了非常出色的支持。
- SQL Server、Oracle 和 IBM DB2:这些数据库同样提供了强大的支持。
- PostgreSQL 的独特优势:PostgreSQL 的接口特别清晰,其设计之初就将可扩展性作为核心理念。它最初是一个研究项目,主要目标就是证明可扩展数据库是一个好主意,允许用户添加自己的函数、类型和各种功能。例如,它对处理时间戳数据提供了很好的支持。
如何定义用户自定义函数
用户自定义函数可以用多种语言定义,包括纯 SQL。你可能会疑惑为什么用 SQL 定义函数是个好主意。简单来说,原因与在任何编程语言中定义函数的好处相同:抽象和复用。
此外,还有 SQL 的命令式语言扩展:
- Microsoft 的 T-SQL
- Oracle 的 PL/SQL
- PostgreSQL 的 PL/pgSQL
这些扩展增加了类似命令式编程的特性,例如:运行查询并将结果保存到变量中供后续使用,定义整数等基本类型的变量,以及通常还包括循环结构和条件判断等你在命令式语言中可能见到的所有功能。
这些扩展确实有用,如果你是数据库管理员或认识相关朋友,他们会非常熟悉这些语言。但我个人并不特别推崇使用它们,并非认为它们不好,而是觉得它们有时被过度使用了。有些任务其实不需要降级到编写命令式程序也能完成,这一点并不总是被认识到。
另一个我不喜欢使用它们的原因并非根本性的,只是因为用这些语言编写代码的体验有些痛苦。它们难以调试,因为运行时出错时缺乏良好的调试器支持,很难弄清楚发生了什么。
虽然当我看到 SQL 逻辑被推到应用层时我会抱怨很多,但如果替代方案是使用这些 SQL 的命令式扩展,我的抱怨会少一些。所以,不要把你的连接操作放在应用程序中,但你可以处理循环逻辑,这没问题。
扩展语言支持
在 Microsoft SQL Server 中,任何来自 .NET 的 CLR 语言都可以使用,通常需要用 C# 等语言编写。

对于 Python,PostgreSQL 有一个 R 语言扩展,我还没有太多机会尝试,但这很令人兴奋,因为它为那些希望将数据库与统计例程结合使用的人提供了一个更具吸引力的实现方式。
总结


本节课我们一起学习了用户自定义函数在数据库中的支持情况。我们了解到大多数主流数据库都支持此功能,并探讨了使用纯 SQL 或命令式扩展语言(如 T-SQL、PL/SQL)来定义函数的方法。关键在于理解用户自定义函数的核心价值在于代码抽象和逻辑复用。虽然命令式扩展功能强大,但也需权衡其复杂性和调试难度。最后,我们看到了像 .NET CLR 和 PostgreSQL 的 R 扩展等更广泛的编程语言集成可能性。
大规模数据科学(大数据操作,第1课/共3课) - P41:物理查询计划优化 🚀

在本节课中,我们将要学习数据库查询处理的最后一个关键环节:物理查询计划优化。我们将探讨在确定了逻辑操作顺序(即代数优化)之后,如何选择具体的执行算法来高效地实现这些操作。
上一节我们介绍了代数优化和声明式查询语言,它们让数据库系统能够自主决定如何执行查询。本节中,我们来看看如何将逻辑计划转化为可实际执行的物理计划。
逻辑计划与物理计划的区别
即使我们已经指定了操作的顺序(例如,先选择再连接),我们仍未指定实际评估查询所需的每一个细节。让我用一个例子来说明这一点。
这是一个我们上节课看过的查询的简化版本:对于每一个订单(Order),我们想找到所有属于该订单的对应商品(Item)。这直接翻译成的代数计划非常简单,就是两个表的连接(Join)。
你可能会认为工作已经完成了。然而,我们还需要指定如何执行这个连接操作。
连接操作的物理实现选项
以下是实现连接操作的两种主要物理算法。
选项一:嵌套循环连接
这种方法在高级伪代码中如下所示:
for each record I in Item:
for each record O in Order:
if I.order_id == O.order_id:
output (I, O)
这个算法检查Item表中的每一条记录与Order表中的每一条记录,如果它们在订单属性上匹配,则输出结果。其时间复杂度大致是O(n²)。
选项二:哈希连接
另一种方法如下:
# 构建阶段
hash_table = {}
for each record I in Item:
hash_table[I.order_id].append(I)
# 探测阶段
for each record O in Order:
for each I in hash_table.get(O.order_id, []):
output (I, O)
这种方法首先将Item表的所有记录插入一个哈希表(按连接键order_id)。然后,扫描Order表,并在哈希表中快速查找匹配的记录。如果哈希表查找是(平摊)常数时间,那么这个算法的时间复杂度接近O(n)。
算法选择:哪个更快?
我暗示了选项二(哈希连接)可能更快,但在实践中,情况可能并非总是如此。请思考一下,为什么在某些情况下,看似低效的嵌套循环连接反而可能更快?
答案与数据大小和内存有关。如果Item表非常小(可以完全放入内存),而Order表非常大,嵌套循环扫描小表对于大表的每一条记录可能开销不大。然而,如果Item表很大,构建哈希表可能消耗大量内存,甚至需要溢出到磁盘,这会显著降低哈希连接的速度。因此,查询优化器必须根据数据统计信息(如表的大小)来做出明智的选择。
实际数据库中的物理计划
你可以在实际数据库系统中访问和查看这些物理计划。例如,在Microsoft SQL Server(以及其他主流数据库)中,你可以使用EXPLAIN命令。
以下是一个针对Reuters数据集的查询示例(该数据集包含文档ID、词条和频率三列):
EXPLAIN
SELECT a.term, b.term
FROM Reuters a, Reuters b
WHERE a.doc = b.doc
AND a.term = ‘Parliament‘;
执行解释命令后,数据库会返回一个物理操作树。你可能会看到类似“Hash Match Inner Join”的操作,这正对应了我们之前讨论的哈希连接算法。
这个查询的目的是:找出所有与“Parliament”这个词出现在同一文档中的其他词条。
物理优化的重要性
物理查询计划优化是数据库性能的关键。它负责将高级的逻辑操作(如连接、选择)转化为具体的、考虑数据分布和系统资源(如内存)的低级算法步骤。优化器的目标是在众多可行的物理计划中,选择一个预估执行成本最低的计划。


本节课中我们一起学习了物理查询计划优化的核心概念。我们理解了逻辑计划与物理计划的区别,探讨了连接操作的两种基本物理实现算法(嵌套循环连接和哈希连接),并认识到查询优化器需要根据实际数据情况选择最合适的算法。最后,我们看到了如何在真实数据库中使用EXPLAIN命令来查看系统生成的物理执行计划。
大规模数据科学(大数据操作,第1课/共3课) - P42:优化:选择物理计划 🧠

在本节课中,我们将要学习数据库查询优化中的一个核心环节:物理计划的选择。我们将看到,对于同一个逻辑查询,数据库系统可能会根据数据的具体情况选择不同的物理执行算法,而程序员无需关心这些底层细节。
上一节我们介绍了逻辑查询计划,本节中我们来看看数据库系统如何将逻辑计划转化为具体的物理执行计划。
现在,请注意,当我解释这个查询时,我得到了一个不同的物理计划。逻辑计划看起来相同,它仍然包含扫描、扫描和连接操作,但计算连接所用的算法已经改变,现在它使用了一种叫做“嵌套循环”的方法。
这种嵌套循环算法完全对应了这里的伪代码。这就是它被称为嵌套循环的原因:一个外循环和一个内循环。两者完全一致。
因此,系统选择了这个嵌套循环计划。尽管我们曾论证这是一种O(n²)复杂度的算法,通常不会被频繁选择。那么,为什么在这个案例中它被选用了呢?
如果你仔细思考,这次连接操作的一侧只涉及那些包含“parliament”一词的文档记录。这是一个非常小的关系表。由于关系表非常小,这种嵌套循环算法可能非常高效,甚至比构建哈希表或其他数据结构的开销还要快。
这里的主要收获(与具体细节相对)是:不同的物理算法在不同情况下是合适的。得益于声明式语言和代数优化,程序员无需担心任何这些细节,也无需做出选择。
这是一个非常强大的理念。你只需表达查询意图,数据库会完成其余工作。
需要指出的是,这不仅限于SQL Server。在Postgres中,你也可以通过使用EXPLAIN命令生成这类代数计划。实际上,它们看起来更清晰。这里再次出现了哈希连接,它实际上向你展示了构建哈希表作为第一步,然后用第二步进行探测的过程。这里还有另一个我们未讨论的运算符,例如当你需要对某个分组的所有成员进行计数时。这里的哈希是基于分组ID的,你可以对其余部分应用聚合函数。不过,在未深入讨论的情况下,我不应给出如此高层次的概述,所以让我们先跳过这部分。
总之,代数计划确实存在。你可以直接通过使用关键字EXPLAIN来查看它。如果你使用数据库,我建议你经常这样做,以尝试理解正在发生的事情。
我想指出的另一点是,这很重要。下面的例子并非直接来自SQL,事实上也非来自商业数据库,而是来自我们研究小组的一些工作。但要点是相同的。
这些是针对完全相同的查询的不同物理计划。事实上,这里我正在进行并行处理,所以这里应用了多个处理器。当你从4个处理器增加到16个处理器时,执行时间有所下降,但下降幅度不如我们预期的那样大。实际上,它应该下降得相当多。
但关键是,这些计划中的每一个所花费的时间都非常不同。好吧,其中两个计划的时间差不多,但差异是相当重要的。因此,忽略这些优化机会,只坚持程序员指定的计划,将是一个巨大的错误。
以下是另一个说明,虽然有点难以直观理解,但让我尝试解释一下这里发生了什么。这是Hea等人在VLDB 2010会议上的一项非常出色的工作,围绕这项工作有一系列论文。他们试图可视化可能的查询计划空间。

这里的两个坐标轴都是针对同一个查询的,但查询的参数在变化。实际上,这涉及供应商账户余额和某种扩展价格参数。他们改变了查询中这些参数的值。想象一下,语法相同,都是SELECT * FROM something WHERE some condition = extended price AND some other condition = account balance,仅仅通过调整这两个“旋钮”,你就得到了由优化器选择的不同计划的丰富图谱。
在这个空间中,每种颜色代表一个不同的查询计划,即由优化器选择的一个不同的代数查询计划。我认为这里的要点是,数据库做出的决策非常复杂,而且必须如此。实际上,这些不同的计划确实很重要。虽然这里没有展示,但你可以证明计划的选择确实重要。数据库在寻找正确计划方面往往做得相当好。
我在上一张幻灯片中论证过,这实际上很重要,执行时间的差异可能相当显著。
将这种复杂性留给程序员处理,可能是一个巨大的效率损失来源。隐藏这种复杂性是一个巨大的、巨大的胜利。


本节课中,我们一起学习了数据库查询优化中物理计划选择的重要性。我们了解到:
- 同一个逻辑查询可以对应多个物理执行计划。
- 数据库优化器会根据数据特征(如数据量大小)自动选择高效的物理算法(例如,对小表使用嵌套循环,对大表使用哈希连接)。
- 程序员使用声明式语言(如SQL)时,无需关心底层执行细节,只需关注“要什么”,而不是“怎么做”。
- 我们可以使用
EXPLAIN等工具查看数据库生成的物理计划。 - 优化器的选择对查询性能有重大影响,将算法选择的复杂性从程序员身上抽象掉是数据库系统的一个核心优势。
大规模数据科学(大数据操作,第1课/共3课) - P43:声明式语言 🗣️

概述
在本节课中,我们将要学习关系型数据库中的一个核心概念:声明式语言。我们将了解它与之前讨论的代数优化的关系,并通过SQL查询示例来理解其“描述结果是什么,而非如何获取”的本质。
上一节我们介绍了代数优化,讨论了多个关系代数表达式在逻辑上等价,但执行顺序不同。数据库系统能够自动选择最优的执行顺序。
本节中,我们来看看声明式语言。声明式语言的核心思想是:你只需指定你想要的结果所满足的条件,而无需指定获取该结果的具体步骤或顺序。
什么是声明式语言?
声明式语言意味着你指定想要的答案,但不指定任何关于如何获取它的信息。
一个关系代数表达式实际上指定了操作顺序。正如上一张幻灯片所示,三个不同的表达式明确指示了每个操作的执行顺序。这意味着,如果你编写这样的表达式,你就是在指示计算机:“按照这个特定顺序执行”。
而声明式语言则说:“我们只描述结果必须满足的属性,然后让数据库自行找出执行这些操作的正确顺序。”
SQL查询示例一
以下是一个快速示例。假设你有两个表:
Order表,包含三列:order、date、account。Item表,包含两列:order、part。其中order列表示该物品应关联到哪个订单。
如果你想查询“找出今天的所有订单及其订购的物品”,你可能会编写如下查询:

SELECT *
FROM Order, Item
WHERE Order.order = Item.order
AND Order.date = today;

这个查询从 Order 表和 Item 表中选择所有列,但只保留满足以下条件的记录:
Order表中的order列与Item表中的order列匹配。Order表中的date列等于今天。
这只是对结果的条件表达,没有任何关于如何实际获取此答案的具体思路。自动发生的情况是,该查询被转换为类似于我们已经见过的关系代数表达式。你可以将其理解为:扫描 Item 表,扫描 Order 表,选择 date 等于今天的记录,然后执行连接操作,为 Order 中的每条记录找到 Item 中在 order 列上匹配的记录。
每次运行查询时都会发生这种情况。因此,SQL 描述的是 “做什么”,而不是 “如何做”。
SQL查询示例二
以下是另一个例子。我们有三个表:
Product(PID, name, price)Purchase(PID, CID)Customer(CID, name, city)
下划线表示构成表记录唯一性的列(主键)。这里,PID 使产品记录唯一,CID 使客户记录唯一,PID 和 CID 的组合使购买记录唯一。


以下是一个SQL查询:
SELECT DISTINCT P.name AS product_name, Z.name AS customer_name
FROM Product P, Purchase, Customer Z
WHERE P.PID = Purchase.PID
AND Purchase.CID = Z.CID
AND P.price > 100
AND Z.city = 'Seattle';
这个查询的意思是:
- 从这三个表中选择数据。
- 条件是:
Product表中的PID与Purchase表中的PID匹配,并且Purchase表中的CID与Customer表中的CID匹配。 - 同时,只选择价格大于100的产品,以及城市为西雅图的客户。
- 最后,返回唯一的(
DISTINCT)产品名称和客户名称组合。
用自然语言描述就是:找出所有唯一的产品和客户组合,其中客户位于西雅图,并且他们购买了价格超过100的产品。
我们很清楚我们想要什么,但如何获取它并不明确。这是一个相当复杂的查询,但SQL语句只声明了结果需要满足的条件。
总结

本节课中我们一起学习了声明式语言。我们了解到,在关系型数据库中,像SQL这样的声明式语言允许用户只描述查询结果的逻辑条件,而将具体的执行计划(包括操作顺序和优化)交给数据库管理系统自动处理。这是关系数据库的一个关键特性,它极大地简化了数据查询的复杂性,并使得系统能够通过代数优化等技术来提升查询效率。
大规模数据科学(大数据操作,第1课/共3课) - P44:声明式语言更多示例 📚

在本节课中,我们将通过更多示例深入探讨声明式查询语言,特别是如何将复杂的查询转换为关系代数表达式。我们将学习如何清晰地指定操作顺序,同时理解其中仍保留的物理实现细节。
关系代数转换示例
上一节我们介绍了声明式语言的基本概念,本节中我们来看看如何将具体的SQL查询转换为关系代数表达式。
我们有一个涉及产品、购买和客户三个表的查询。在关系代数中,这个查询被转换为以下操作序列。
首先,在底部我们有产品表和购买表。我们执行一个连接操作:对于产品表中的每一条记录,在购买表中找到对应的记录。接着,对上一步连接的结果,我们再执行一次连接操作:对于结果中的每一条记录,在客户表中找到对应的记录。
然后,我们进行过滤操作:筛选掉所有价格不大于100的记录,只保留价格大于100的记录;同时,只保留城市等于“西雅图”的记录。
接下来,我们进行投影操作。投影意味着只保留我们感兴趣的两列,舍弃所有其他列。
最后,我们得到最终答案。
这里有两个关键点:第一,操作的执行顺序现在被明确指定了;第二,许多物理层面的细节仍然是开放的。这是一个非常高层次的指示,只明确了操作顺序,但并未规定具体的实现方式。例如,我们不知道将如何具体执行连接操作,实际上存在多种方法。虽然我们指出“对于产品表中的每条记录,在购买表中查找对应记录”,但并未精确定义其含义。
RDF数据与复杂查询示例
现在,我们来看另一个涉及不同数据模型的例子。
这里我们只有一个名为R的关系(表),它包含三列:主语、谓语和宾语。当你处理RDF数据时,会看到这种模式。RDF是资源描述框架,是一种用于管理“关联数据”的语言、形式体系和软件栈。
在RDF中,一切都被编码为一组事实。例如,你可以编码一个事实:“本课程的讲师是Bill Howe”。其中,主语是“本课程”,谓语是“有讲师”,宾语是“Bill Howe”。人们使用这种形式体系作为一种非常通用的方式来编码来自任何来源的任何信息,我们可能在课程后期再次提及。
以下是一个稍复杂的查询示例。该查询使用了同一个关系的三个实例,并将它们全部连接起来。
查询的目标是寻找一个特定的模式序列:找到一个认识某人的人,而这个人持有一家公司的账户,并且该账户的主页是一个特定值。换句话说,我们寻找满足“认识 -> 持有账户 -> 账户主页”这个边序列的所有可能组合。
在关系代数中,这个查询被转换为以下形式:
- 首先进行选择操作,分别找出谓语等于“认识”、“持有账户”和“账户主页”的记录。
- 然后,通过一系列连接操作将这些结果组合起来,连接的条件是:第一个关系的宾语必须等于第二个关系的主语,第二个关系的宾语必须等于第三个关系的主语。
- 最后,通过投影操作提取出我们感兴趣的最终答案列。
这个例子可能有些复杂,但我想强调几个要点:第一,你可能会再次遇到RDF;第二,它演示了在单个查询中可以多次访问同一个关系;第三,它展示了即使复杂的查询也能被转换为关系代数表达式。
总结


本节课中,我们一起学习了声明式查询语言的更多转换示例。我们看到了如何将涉及多表连接和过滤的SQL查询,清晰地映射为关系代数中的操作序列(连接、选择、投影)。我们也接触了RDF数据模型,并通过一个复杂查询示例,了解到关系代数能够表达基于图模式的查找。关键在于,关系代数明确了操作的逻辑顺序,但将具体的物理实现细节留给了数据库系统。
课程1.3:大规模数据科学 - 视图:逻辑数据独立性 👁️

在本节课中,我们将要学习逻辑数据独立性的概念。这是数据库系统,特别是关系型数据库,提供的另一项强大功能。上一节我们介绍了物理数据独立性和代数优化,本节中我们来看看如何保护应用程序免受数据逻辑结构变化的影响。
逻辑数据独立性是指,即使数据库表的逻辑结构(例如增加一个列)发生变化,那些不依赖于这些变化的应用程序也无需重写。这与物理数据独立性(保护应用免受磁盘数据物理重组影响)相辅相成。
这种能力是通过视图这一概念实现的。所有关系型数据库都支持视图,但它在实践中有时未被充分利用。
什么是视图?
一个视图本质上就是一个被命名的查询。你编写一个查询,给它一个名字,并将其存储在数据库中。之后,你就可以像访问底层数据库中的物理表一样访问这个视图。
为什么可以这样做?这要归功于我们之前提到的代数封闭性。在关系代数中,每个查询都以表作为输入,经过操作后,输出也是一个表。因此,任何视图的结果本身就是一个表,我们可以在此基础上继续叠加其他查询。
为什么使用视图?
以下是使用视图的几个主要原因:
- 数据安全与权限控制:你可以为视图分配权限。例如,如果你只想让某个用户看到他账户相关的数据,可以创建一个只筛选出他账户数据的视图,然后授予他访问该视图的权限,而非底层原始表。
- 简化逻辑与隐藏复杂性:视图允许你根据用户的理解,以一种更合理的逻辑组织方式暴露数据。例如,即使你决定将数据重组到两个表中,要求程序员使用连接操作,你也可以简单地创建一个视图来隐藏这个连接操作,让所有人都能直接访问连接后的结果。
- 提供逻辑数据独立性:这是核心目标。无论你内部如何逻辑组织表格,你都可以向外部暴露一个不同的数据视角。这实现了数据管理员和实际数据使用者之间的分离。
视图如何工作?
你可能会想,通过视图访问数据会不会增加开销?这里有一个巧妙之处:由于代数封闭性,当用户对视图执行查询时,数据库会将用户的查询与定义视图的查询组合成一个整体,然后发送给数据库进行评估和优化。数据库并不关心这个查询是来自视图还是直接来自程序员,它会以完全相同的方式进行优化。因此,使用视图通常只有好处,没有额外的性能损失。
示例解析
假设我们有以下两个表的结构:
purchase 表 (假设包含列:pid, store, ...)
product 表 (假设包含列:pid, price, ...)
我们可以定义一个名为 store_price 的视图,它包含 store 和 price 两列。视图的定义如下:
CREATE VIEW store_price AS
SELECT purchase.store, product.price
FROM purchase, product
WHERE purchase.pid = product.pid;
这个视图的结果就像一个新表。正如之前所说,它向用户隐藏了表之间的连接操作以及可能复杂的列名。你可以为视图中的列起任何易于理解的名字。
逻辑数据独立性的核心思想 🧠
逻辑数据独立性的关键在于,它允许数据库管理员更改数据的逻辑结构(如添加列、拆分表),而通过视图访问数据的应用程序,只要其逻辑不依赖于这些更改的部分,就完全不受影响。这极大地提高了系统的可维护性和灵活性。

本节课中我们一起学习了逻辑数据独立性的概念及其实现工具——视图。我们了解了视图的定义、用途(包括权限控制、简化查询和提供数据独立性)以及其高效工作的原理。掌握视图的使用,是构建健壮、可维护的大型数据应用的重要一步。
大规模数据科学(大数据操作,第1课/共3课) - P46:索引 🗂️

在本节课中,我们将学习数据库中的两个重要概念:视图(Views)和索引(Indexes)。我们将了解它们如何帮助简化复杂查询并提升数据检索效率。
视图的使用与优势
上一节我们介绍了视图的基本概念,本节中我们来看看如何实际使用视图,并理解其带来的好处。
视图允许我们定义虚拟表,这些表基于其他表或视图的查询结果。使用视图可以简化复杂查询,并促进代码重用。
以下是一个使用视图的示例:
假设我们定义了一个名为 store_price 的视图,它包含了商店及其销售高价产品的信息。现在,我们想找出每位顾客访问过的所有“高端商店”(即销售过价格超过1000美元产品的商店)。
我们可以直接引用之前定义的 store_price 视图来简化这个查询,就像它是一个真实的表一样。查询逻辑是:对于每位顾客,找出他们访问过的、在 store_price 视图中存在的商店。
-- 假设 store_price 视图已定义
SELECT customer_name, store_id
FROM customer_visits
WHERE store_id IN (SELECT store_id FROM store_price WHERE price > 1000);
视图的评估机制
你可能会好奇数据库如何处理视图。关键在于,数据库优化器会将视图定义与引用它的查询“折叠”在一起,形成一个完整的查询计划进行优化。
这意味着,无论你嵌套了多少层视图,数据库最终都会将其优化为一个单一的、高效的执行计划。因此,使用视图是一种“免费”的抽象,它简化了程序员的工作,而不会带来性能损失。
在某些情况下,通过物化视图(Materialized Views),你甚至可以获得比手写完整查询更好的性能。物化视图会将结果缓存起来,但这属于更高级的数据库特定优化,在一般的数据科学场景中不常使用。
数据库的核心:索引 🔍
现在,让我们转向数据库的另一个核心思想:索引。虽然索引并非数据库独有,但数据库提供了一个极佳的平台来轻松创建、应用并自动利用索引。
索引的作用与优势
索引主要用于解决“大海捞针”式的问题,即从海量数据中快速查找个别或少量记录。数据库在这方面尤其擅长。
试想,如果你用Python或R等通用编程语言处理数据,并希望通过按客户姓氏排序来提升效率(这类似于构建索引),你必须重写大量代码来利用这个“索引”。但在数据库中,这个过程要简单得多。
如何创建与使用索引
在数据库中,创建索引非常简单。你只需要执行一条类似下面的语句:
CREATE INDEX idx_customer_last_name ON customers(last_name);
创建索引后,数据库优化器会自动识别其存在,并在合适的查询中决定使用它。例如,一个根据姓氏查找客户的查询会自动利用这个索引进行“索引扫描”,而不是低效的“全表扫描”。
你可以通过 EXPLAIN 命令查看查询执行计划,来验证优化器是否选择了使用索引。
数据库的自动内存管理与可扩展性
另一个关键点是数据库能有效利用内存层次结构。即使数据量远超内存容量,数据库也知道如何分块处理数据,确保查询最终能够完成(尽管速度可能受影响)。自己编写代码来实现这种功能会非常复杂,而数据库已经内置了这种能力。
因此,有效利用内存层次结构和索引,是数据库作为数据处理平台的强大之处。
总结
本节课中我们一起学习了:
- 视图的使用:视图可以简化复杂查询,通过引用视图就像引用表一样。数据库会将视图定义与查询合并优化,不会带来性能开销。
- 索引的核心价值:索引是快速检索数据的关键。在数据库中,索引易于创建(使用
CREATE INDEX语句),并能被查询优化器自动、智能地使用,从而高效解决“大海捞针”类查询。 - 平台优势:数据库作为一个平台,自动处理了复杂的内存管理和查询优化,让开发者能更专注于逻辑而非底层实现细节。

掌握视图和索引,将帮助你更高效地在大规模数据集上进行操作和分析。
大规模数据科学(第1课)📊:可扩展性的含义

在本节课中,我们将探讨数据科学中的一个核心概念——“可扩展性”。我们将了解其不同层面的含义,从单机处理到分布式计算,并简要介绍算法复杂度如何影响可扩展性。
可扩展性的操作定义
上一节我们提到了处理大规模数据的重要性。本节中,我们来看看“可扩展性”在操作层面上的具体含义。
过去,一种常见的理解是:算法必须能够处理无法完全装入单台机器主内存的数据。这意味着,即使只有一台机器,算法也需要能够分块从磁盘读取数据、进行处理,并可能分块写回磁盘。
以下是这种处理方式的关键点:
- 内存占用小:在任何时刻,算法在内存中只保留一小部分数据。
- 数据库的角色:数据库系统擅长这种“核外处理”,确保查询只要磁盘上有数据就能完成。
- 核外处理:
out of core processing指利用磁盘进行操作,而in core则指所有操作都在内存中完成。
从纵向扩展到横向扩展
然而,随着互联网应用在21世纪初的兴起,仅靠单台机器的核外处理变得不够。无论服务器多么强大,其磁盘I/O速度也难以满足海量并发请求。
因此,可扩展性的含义演变为:能够利用成千上万台廉价计算机共同解决同一个问题。这被称为 横向扩展。相比之下,为单台机器增加更大的内存和更多核心则被称为 纵向扩展。
算法复杂度的视角
从更精确的算法复杂度角度来看,可扩展性也有其定义。
传统上,如果一个算法处理 n 个数据项所需的操作数不超过 n^m(m是一个常数),则该算法被认为是“可扩展的”或“易处理的”。例如:
- 当 m=1 时,是 线性时间复杂度 算法。
- 当 m=2 时,是 二次时间复杂度 算法。
而非多项式时间算法(例如指数时间算法 k^n)则被认为是不可扩展的。但在实践中,即使是二次复杂度算法,对于超大数据集也可能变得不可行。
在现代分布式计算中,可扩展性要求算法复杂度能除以一个很大的常数 K(即 n^m / K),这里的 K 代表可以用于解决问题的计算机数量。算法必须能有效利用这些并行资源。
流式数据的挑战
最后,我们简要提及一个更极端的场景:流式数据处理。在某些情况下,即使是 n^m / K 的复杂度也可能不够。
对于以流形式高速涌入的数据(例如大型巡天望远镜每晚产生30TB数据),系统通常只能对每个数据项进行一次处理。因此,理想的算法复杂度应为 O(n log n)。
这里的 log n 因子通常意味着可以使用树形数据结构对处理过的数据进行索引。流式数据处理是一个重要的前沿领域,我们将在课程后续部分探讨相关技术。
总结
本节课我们一起学习了“可扩展性”的多层含义:
- 操作层面:从处理超出单机内存的数据开始。
- 架构层面:从单机纵向扩展转向多机横向扩展。
- 算法层面:从多项式时间复杂度,到需利用多机并行降低复杂度,再到流式处理中近乎线性的复杂度要求。

理解这些不同层面的可扩展性,是设计和实施大规模数据科学项目的基础。
大规模数据科学(大数据操作,第1课/共3课) - P48:算法复杂度概述 🧠

在本节课中,我们将学习算法复杂度的基本概念,并通过一个具体的例子来理解不同算法在处理数据时的效率差异。我们将探讨线性搜索和二分搜索两种方法,并分析它们的时间复杂度。
两种可扩展性的视角
“可扩展性”可能有两种不同的含义。在本节中,我们将通过一些例子和直观解释来探讨这个问题:我们能否利用多台计算机?是否存在“杀鸡用牛刀”的情况?
一个简单的示例问题
以下是一个经过简化的示例问题:我们想要找出所有匹配的DNA序列。一个DNA序列是由字母G、A、T和C组成的短字符串。给定一个目标短序列,我们需要找出所有与之完全相同的序列。
换句话说,就是找出所有与给定序列完全相等的序列。
如何解决这个问题?
想象一下,下图中的每一条黑线代表一个DNA序列。这条黑线对应一个序列,那条黑线对应另一个序列,其余黑线则代表其他序列。

请花一分钟思考一下,设计一个算法来找出匹配目标序列的序列。
方法一:线性搜索
在不对数据做任何假设的情况下,数据只是以列表形式提供。我们可以做的一件事是进行线性搜索。
我们将检查第一项,并将其与目标序列进行比较。如果相等,很好,我们找到了一个匹配项。如果不相等,我们则继续检查下一项。这一切都发生在时间 t=0。
然后我们移动到下一个序列。在时间 t=1,我们检查另一个序列并进行相等性比较。如果不匹配,我们继续移动。以此类推,直到时间 t=17,我们找到了一个匹配项。在这里,我用了“包含”而不是“相等”,可能改变了原意。但无论如何,我们找到了一个匹配项并将其输出。
那么,这需要多长时间?我们进行了多少次操作?在这个图示中,我们处理了40条记录,进行了40次比较。
因此,对于 n 条记录和 n 次比较,我们说算法复杂度是 O(n)。所以,对于这个简单的搜索和检索任务,这是一个线性时间算法。
能否做得更好?
问题是,我们能否做得更好?如果你有一些数据结构方面的经验或上过相关课程,你应该会想:是的,我们可以。
方法二:排序与二分搜索
一种更好的方法是先对序列进行排序。这有什么帮助呢?当然,我们仍然可以进行线性时间算法,逐一检查这些序列。但我们也可以做一些更聪明的事情:从中间开始搜索。
从中间开始,将我们的目标序列与在此处找到的序列进行比较。它们不相等,但我们可以看到找到的这个序列小于我们的目标序列。因此,我们知道目标序列在右侧,它一定在这个方向上的某个位置。
这样,我们就无需检查一半的数据(20条记录)。然后,跳转到剩余部分的中间再次比较。现在我们发现,我们“跳过”了——这个序列大于目标序列。同样,我们再次排除了一半的数据。
在第一步中,我们排除了一半数据;在这里,我们又排除了剩余部分的一半。现在我们知道目标序列在这个范围内。再次对半分割并比较,发现它小于目标序列,所以我们大致在目标附近来回调整。
让我试着画得更好一些,划掉那些部分,再划掉这些部分。现在我们知道它在某一侧。在下一步中,我们找到了一个匹配项。如果有多个相同的项,我们知道它们会相邻出现,因此如果需要,我们可以遍历记录,收集所有匹配项。
那么,这花了多长时间?这里我们仍然有40条记录,但我们只进行了4次比较。因此,对于 n 条记录,我们进行了 log n 次比较。我们遍历了这个隐含的二叉树,对排序后的数据进行了二分搜索。
所以,这个查找操作的时间复杂度是 O(log n)。当然,我们必须提前对数据进行排序。如果算上排序,那是一个 O(n log n) 的操作,我们这里不一定会讨论。但一旦你有了排序好的数据,查找就只需要 log n 次操作,这可能是更好的可扩展性。

总结

在本节课中,我们一起学习了算法复杂度的基本概念。我们通过一个查找匹配DNA序列的例子,对比了线性搜索(O(n))和基于排序的二分搜索(O(log n))两种方法。我们了解到,通过合理组织数据(如排序),可以显著提高算法效率,这对于处理大规模数据至关重要。理解算法复杂度是设计和选择高效、可扩展解决方案的基础。
大规模数据科学(大数据操作,第1课/共3课) - P49:数据并行算法概述 📊

在本节课中,我们将要学习数据并行算法的基本概念。我们将探讨如何通过并行处理来加速那些需要处理数据集中每个记录的任务,并与之前讨论的“大海捞针”式搜索任务进行对比。
关系数据库的优势 🗄️
上一节我们介绍了针对“大海捞针”式搜索任务的优化技巧。这个技巧非常有效,以至于已经被内置于许多系统中,尤其是关系数据库。
关系数据库擅长处理这类从大数据集中追踪小结果的问题。它们能够透明地提供一种旧式的可扩展性。这里所说的“旧式可扩展性”是指数据能装入主内存。正如我们多次提到的,无论主内存大小如何,你的查询总能完成。
此外,数据库是构建、使用和复用索引的绝佳平台。因此,关系数据库在“核外算法”和寻找对数时间算法这两种旧式可扩展性方面都表现出色。
索引可以轻松构建,并在适当时自动使用。在关系数据库中,你可以写一条简单的语句:
CREATE INDEX index_name ON table_name (column_name);
这条语句会根据指定列对记录进行排序。磁盘上的实际数据是否物理排序,取决于你所使用的具体系统。通常,这条语句不会移动物理记录,而是会构建一个辅助索引。无论如何,你都能利用这种对数时间的访问模式。
只需这一行代码,你就能创建索引并利用它。之后,任何需要且能受益于该索引的查询,优化器都会自动选择使用它(如果合适的话)。这比你手动重写代码以实现核外处理或利用索引要容易得多。
因此,当你将关系数据库与R或Python脚本进行比较时,仅仅通过将问题转化为SQL语句,你就免费获得了大量已经完成的算法工作。这不仅仅是SQL与表达能力更强的代码语言之间的对比,实际上你从中获得了巨大的好处。
引入新任务:序列修剪 ✂️
现在,让我们来看另一个任务,称为“序列修剪”。
我们被给予同一组DNA序列,但这次不是搜索一个特定的序列,而是要从每个序列中修剪掉最后几个碱基对。也就是说,我们将去掉一个后缀,并返回一个数据集,其中每个读取序列现在只是原读取序列的前缀。
在实践中,确实需要进行这种修剪。原因是测序仪的准确度在读取长度超过一定值后会急剧下降。因此,从每个读取序列中修剪掉最后几个碱基对是一种标准的预处理操作。
我们该如何完成这个任务呢?我们可以尝试使用与第一次搜索任务相同的技巧,即依次处理每个记录。

在时间0,我们可以修剪此后缀,只返回“TCCT”。在时间1,我们修剪这个后缀,依此类推。在时间17,我们会遇到以“GATTA”开头的老朋友。
然而,与搜索任务不同,这里没有索引能真正帮助我们。我们必须接触并操作每一个记录,从中提取前缀并移除后缀。因此,这个操作本质上是O(n) 复杂度的。不可能有低于O(n)的算法,你至少需要接触每一个记录。
并行处理的威力 ⚡
但是,我们能否做得更好呢?答案是肯定的。
处理第一个任务与处理最后一个任务完全独立,也与处理这个任务(实际上是指处理记录)完全独立。因此,虽然没有索引可用,但我们可以将数据集分解成多个部分,并独立处理每个部分。

想象一下,我们获取单个数据集,将其分解成这些数据块,并将每个数据块分配给不同的机器,或者更广义地说,分配给不同的处理器。

现在,在时间0,我们可以同时处理每个数据块中的一个序列。在时间1,我们同时处理每个数据块中的第二个序列,依此类推。
我们做了多少工作呢?我们仍然处理了全部40个记录,工作量相同。但花了多少时间呢?只花了7个时间单位(这里用“周期”表示,但更通用地说,因为我们有6个工作单元)。这里的复杂度是 O(n/k)。平均而言,对于n个项目,我们可以在n/k个时间步内完成工作。
总结 📝

本节课中我们一起学习了数据并行算法的核心思想。我们回顾了关系数据库通过索引和优化器,为“大海捞针”式搜索提供的强大且自动化的旧式可扩展性。接着,我们探讨了“序列修剪”这种需要处理每个记录的任务,并认识到虽然其复杂度下限为O(n),但通过将数据分块并分配给多个工作单元并行处理,我们可以将实际执行时间显著减少到O(n/k),其中k是并行度。这体现了数据并行在处理“全扫描”型任务时的巨大价值。
大规模数据科学(第1课) - 🧩 令人愉悦的并行算法

在本节课中,我们将要学习一种在大规模数据处理中反复出现的核心模式——令人愉悦的并行算法。我们将探讨其基本概念、典型应用场景,并理解其优势与局限性。
我们之前从算法复杂度的角度讨论了可扩展性,也从数据并行处理的角度讨论了可扩展性。数据并行处理是指将大型数据集分割成多个数据块,并在每个数据块上执行独立的计算。在我们观察的每一个任务中,都出现了同一种模式。
以下是几个典型的应用场景:
1. 基因序列修剪任务
我们被给予大量短读序列,需要修剪掉每个读序列的低质量后缀。这是因为测序仪在处理长读序列时表现不佳,序列越长,质量越差。这是基因组处理中的常见任务。我们将读序列分布在K台计算机上,然后对每个数据块完全独立地应用修剪函数。最终,我们得到了一个大型的、分布式的已修剪读序列集合。
2. 文件格式转换任务
例如,将TIF格式图像转换为PNG格式。在2008年,《纽约时报》需要回溯处理其整个印刷档案,就进行了此类处理。他们利用亚马逊网络服务在短时间内(例如一个周末)并行完成了这项工作。具体过程是:给定大量TIF图像,将图像分布在K台计算机上,应用将TIF转换为PNG的函数。该函数应用于每一项,且完全独立,计算机之间无需通信。最终,你得到一个大型的、分布式的已转换图像集合。
3. 大规模模拟任务
如果你需要运行数千次模拟,你可能会划分一个参数空间。对于输入条件在很大范围内的每一个不同值,你都想运行模拟以观察结果。这就是蒙特卡洛模拟的做法。将参数空间划分给K台计算机,针对这些参数运行模拟。同样,这些模拟完全并行且独立。最终,你得到一个大型的、分布式的模拟结果集合。
4. 查找文档中最常见单词
如果你想在一个包含数百万文档的大型集合中,为每个文档查找最常见的单词。你可以将文档分布在K台计算机上,提供一个函数来查找单个文档中最常见的单词。这些计算机并行且独立地计算。最终,你得到一个大型的、分布式的“文档ID - 单词”对列表。
这种结构一次又一次地反复出现。
上一节我们介绍了几个独立的并行计算任务。本节中我们来看看一个稍微更通用的程序。

例如,你不仅想找出最常见的单词,还想找出所有单词的频率。因此,输出不再只是一个单词及其频率,而是一组单词,每个单词都带有其频率。
对于一个单文档,函数是这样的:接收一个文本块,统计每个单词的出现次数,并输出整个词频集合。那么,如果你有数百万个文档,这个过程会是什么样呢?

将文档分布在K台计算机上,就像之前做的那样。现在的函数略有不同,它返回一组“单词-频率”对。最终,你得到一个大型的、分布式的词频集合列表。
这里存在一个相当简单的模式。程序员提供一个函数,该函数将:
- 一个读序列映射为一个已修剪的读序列。
- 一个TIF图像映射为一个PNG图像。
- 一组参数映射为一个模拟结果。
- 一个文档映射为其最常见的单词。
- 一个文档映射为一个词频直方图,等等。

因此,任何时候当你谈论跨大型数据集的独立分布式计算时,你都会遇到这种模式。所以,将这种模式抽象出来,让程序员更容易使用是合理的。他们只需要提供特定于应用程序的函数,而无需担心实际跨多台机器分配计算任务的机制。
然而,这些是令人愉悦的并行计算或易并行计算,它们之间实际上没有任何通信。因此,这种结构能做的事情是有限的。
为了说明这一点,想象一下,如果你想计算所有文档中的单词频率。这里的目标不再是某个特定文档中某个单词的出现频率,而是想找出该单词在整个语料库的所有文档中有多常见。

现在,我们想要做的仍然是提供“单词-计数”对。例如,单词“people”在所有三个文档中出现了78次,单词“government”出现了123次,依此类推。我需要聚合来自每个独立文档的所有信息,并且仍然希望以并行方式完成这个任务。
总结
本节课中我们一起学习了“令人愉悦的并行算法”的核心模式。我们了解到,这种模式适用于数据块间完全独立、无需通信的计算任务,例如数据清洗、格式转换、参数扫描和简单的文档分析。程序员只需定义应用于单个数据元素的函数,系统即可自动将其并行化处理。然而,这种模式的局限性在于它无法直接处理需要跨数据块聚合信息的任务,这引出了对更复杂并行模型的需求。
大规模数据科学(第1课)📊:更一般的分布式算法

在本节课中,我们将学习一种通用的分布式算法模式,用于处理海量文档并生成统一的词频统计结果。我们将从具体问题出发,逐步理解如何将计算任务分解、分发并最终汇总。
问题回顾与目标设定
上一节我们讨论了如何将数百万份文档分发到多台计算机上进行初步处理。本节中,我们来看看如何将分散在各处的中间结果汇总成一个全局的、统一的词频直方图。
你拥有数百万份文档,并将它们分发到了 K 台计算机上。你提供了一个函数,能将每份文档映射成一组(单词, 频率)对。但现在你面临一个新问题:你得到了一个分布在集群中许多计算机上的巨大数据集,你需要将所有信息整理合并,以得到最终所需的那个词频直方图。
你不想要一堆小直方图,你想要一个完整的大直方图。为了生成这个大直方图,你需要确保:对于任何一个单词(例如“Parliament”),它的所有出现记录都集中在同一台计算机上。这样,该计算机就拥有足够的“管辖权”来独立完成计数,无需再与其他计算机通信。
核心挑战:数据重分布
我们已将文档分发到 K 台计算机,并应用了映射函数,得到了一个分布式的单词频率列表。现在,假设我们有4台计算机来负责最终的计数汇总。
以下是实现数据重分布的关键步骤:
我们需要一个函数,能够将特定单词的所有出现记录发送到同一台指定的计算机。例如:
- 将所有红色单词(代表“Parliament”)的出现记录发送到计算机A。
- 将所有绿色单词(代表“government”)的出现记录发送到计算机B。
- 将所有蓝色单词(代表“people”)的出现记录发送到计算机C。
这个函数就是哈希函数。它根据单词本身的值,计算出一个目标计算机的ID(例如0, 1, 2, 3)。程序员通常无需自己编写这个哈希函数,因为它足够通用,可以被内置到系统中。
通信与最终计算
在映射阶段产出数据的每台计算机,可能需要将数据发送给所有参与最终汇总步骤的计算机。这就形成了一个大规模的、多对多的分布式通信步骤,数据像喷雾一样被分发到负责最终计算的计算机上。
此时,负责汇总的计算机便拥有了完整的“管辖权”。例如,负责“government”的计算机可以确信自己拥有该单词的所有出现记录,因此可以独立地完成计数并输出最终结果。
于是,我们得到了分布式的最终直方图。所有计算都是并行进行的,因此我们可以通过增加大量计算机来扩展系统。
通用模式:Map与Reduce
这里的核心在于,这种“对分布式数据应用函数 -> 使用哈希函数在网络中分发数据 -> 合并相同值”的模式频繁出现,以至于将其抽象到系统底层非常有用。这可以将程序员的任务简化为只编写两个简单的函数:
-
Map函数:处理单个数据项。在我们的例子中,它将文档映射为(单词, 频率)对。
# 伪代码示例 def map_function(document): word_counts = {} for word in document.split(): word_counts[word] = word_counts.get(word, 0) + 1 for word, count in word_counts.items(): emit(word, count) # 输出键值对 -
Reduce函数:对与特定键相关联的一组值进行聚合、组合或归约操作。在我们的例子中,它负责累加某个单词的所有计数。
# 伪代码示例 def reduce_function(word, list_of_counts): total_count = sum(list_of_counts) emit(word, total_count) # 输出最终结果
而中间那个Shuffle步骤(即基于哈希的数据重分发)是通用的,由系统自动提供。你无需考虑它,只需设计好你的Map和Reduce函数,让它们协同工作以产生你想要的结果。
公式表示核心思想:
最终结果 = Reduce( Shuffle( Map( 原始数据 ) ) )
这就是MapReduce系统和MapReduce编程模型的本质。我们将在接下来的课程中更详细地讨论它。
总结

本节课中,我们一起学习了如何从具体的词频统计问题,抽象出通用的MapReduce分布式算法模式。我们理解了通过Map函数进行本地处理,通过Shuffle阶段(基于哈希)将相同键的数据汇集,再通过Reduce函数进行全局聚合的核心流程。这种模式将复杂的分布式通信细节隐藏于系统底层,让程序员能够专注于业务逻辑(Map和Reduce函数)的编写,从而高效地处理大规模数据集。
大规模数据科学(大数据操作,第1课/共3课) - P52:MapReduce抽象 🧩

在本节课中,我们将深入探讨MapReduce编程模型的核心抽象概念。我们将通过经典的“词频统计”示例,详细解析MapReduce的工作流程、各个阶段的任务分配以及设计时需要考虑的权衡因素。
从并行处理到MapReduce
上一节我们介绍了并行处理,作为理解MapReduce的铺垫。我们最终得到了一个示意图。
在这个统计一组文档中词频的示例中——这是开始思考MapReduce编程的一个典型例子——每一根垂直的黑线代表一个文档。
我们将这些文档分割成较小的集合,并将每个集合发送到一台独立的机器上。然后,我们依次对每个文档应用我们的 map 函数。
如果你还记得,map函数接收单个文档,并产生一组 键值对。每个键值对包含一个单词,以及该单词在该文档中出现的次数。
当然,你可以想象这个过程的多种变体。
现在,同一个单词可能出现在多个文档中——可能在这台机器上出现一次,在那台机器上也出现一次,等等。因此,我们需要将它们全部汇集到一台机器上,以便进行统计。这正是 shuffle(混洗) 阶段所做的事情。
任务数量与资源分配
这里我画了四个不同的任务。看起来它每次处理一个组,但我想让你思考:我们会有多少个map任务?又会有多少个reduce任务?
- Map任务的数量:每个文档对应一个。我们必须为每个文档调用一次map函数。因此,map函数的调用次数等于文档的数量。
- Reduce任务的数量:这取决于map函数输出的分组数量。在这个例子中,每个在任意文档中出现的唯一单词对应一个分组。
从某种意义上说,我们解决这个问题所需的机器数量在map阶段是可以预测的,因为它对应于输入数据集的大小(我们假定已知)。但所需的reducer数量可能无法事先确定,因为它取决于map输出的规模。在这个例子中,我们或许可以推理,因为我们知道英语大概有多少单词,并且可以假设在足够大的数据集中,所有单词至少会出现一次。但一般来说,它依赖于map的输出,所以你无法真正预知。
我想指出的重点是:我们在这里决定将其画成四台不同的机器,但它可能是map阶段使用的同一批六台机器,也可能是一千台机器。没有什么能阻止你将所有单词的出现记录发送到一台机器上,让这台机器先处理绿色组,再处理红色组,然后处理蓝色组,依此类推。或者,因为机器有四个核心,你可以同时处理四组。
但这可能效率不高,因为它会做很多串行工作。另一个极端是,你可能认为我们需要数百万个任务,那就分配数十万台机器来处理,这样每台机器只做很少的工作。这可能说得通,但代价可能是启动所有这些机器、为它们准备工作所花费的开销。因此,这里存在一个需要权衡的决策,我们稍后会再讨论。
深入理解MapReduce抽象
我之所以详细阐述这一点,是希望你们开始用MapReduce的思维方式思考遇到的每一个问题:如果数据集极其庞大,一台机器无法处理,我们该如何将其分割成块?一个非常好的思考如何分割问题的方法,就是设想你会如何编写一个MapReduce程序来完成你想做的事情。
这仍然是同一个例子,只是用不同的方式绘制。这里的输入是文档ID,后跟一个值,这个值就是文档的完整文本。
map函数(为了清晰起见)产生一组东西,而不仅仅是一个。然后它们经过shuffle阶段,产生中间结果:例如“单词1”出现1次,“单词2”出现1次,“单词3”出现1次,等等。
在另一侧,我们得到的是“单词1”及其对应的一组所有出现次数记录:1, 1, 1, 1...。最后,reduce函数统计所有这些次数,并得出总共有25次出现的结论。
总结

本节课中,我们一起学习了MapReduce的核心抽象。我们通过词频统计的例子,详细剖析了map、shuffle和reduce三个阶段的分工与协作。我们了解到map任务的数量由输入数据决定,而reduce任务的数量则取决于map输出的中间键的多样性。同时,我们也初步探讨了在分布式计算中,任务粒度与资源开销之间的重要权衡。掌握这种“分而治之”的编程思维,是处理大规模数据集的关键第一步。
大规模数据科学(大数据操作,第1课/共3课) - P53:MapReduce数据模型 🗺️➡️🧮

在本节课中,我们将要学习MapReduce编程模型的核心概念,特别是其数据模型。我们将了解MapReduce如何通过简单的抽象来处理大规模数据,并理解其关键组成部分。
上一节我们介绍了并行计算的基本思想,本节中我们来看看MapReduce的具体数据模型是如何定义的。
MapReduce是2004年一篇论文中描述的编程模型。如今,当人们谈论MapReduce的流行时,有时会忽略该论文中提出的几个关键动机。我们稍后将讨论这两个好处。
需要认识到的一点是,MapReduce指的是这个抽象概念本身,它是2004年那篇论文作者赋予的名称。Hadoop是几年后出现的MapReduce的一个实现,最初由雅虎的一些人编写,后来成为一个由Apache管理、拥有众多贡献者的开源产品。
MapReduce的核心思想确实是这个编程模型。它当时也附带了一个系统,但编程模型能够表达许多不同的任务,并且有某种实现能自动将其转换为并行作业,这被证明是非常强大的。这是一种有吸引力的编写并行程序的方式,因为你实际上不必担心并行性。你只需编写一个串行的map函数和一个串行的reduce函数,并行性就会自动实现。
这更多是关于编程模型而非系统的证据是,你可以在其他上下文中看到MapReduce的实现。有人在GPU上实现了MapReduce,有共享内存多核机器上的MapReduce,有人在高性能计算平台上实现了MapReduce,甚至在手机群组上也有实现。这回到了本课程的一个动机,即我希望尽可能关注抽象而非工具。因此,我们讨论的是MapReduce编程模型,但我们会花较少时间在具体的Hadoop实现上,尽管你将有机会在一个可选作业中直接使用Hadoop。
那么,MapReduce的数据模型是什么?就是这样:键值对的集合。这里的“集合”指的是一个可能包含重复项的集合。我们之前见过,文档ID及其值构成一个键值对。有时在输入上我们会稍微宽松一些,不精确地指定键和值是什么。例如,如果只给你一条记录,你可以假设整个记录是键,或者一个文档。有时我们甚至可能没有明确的文档ID,但你可以假设URL或文件名等是键。
然而,对于映射器的输出,键和值之间的区别变得非常重要,因为这正是控制洗牌过程的关键,正如我们在示例中看到的那样。因此,这里的数据模型完全是关于键值对的。输入将是一组键值对,输出也将是一组键值对。关键在于,这组键值对可以任意变大,我们将能够处理这个集合,无论它变得多大。这里有一个隐含的假设,即键和值都是“小”的。“小”在这里不一定意味着非常非常小,只是它需要能放在一台机器上。如果值增长到TB级别,系统将无法工作,因为没有相应的支持。因此,一个能放在一台机器上的文档是可以的,一张能放在一台机器上的图像也是可以的。

本节课中我们一起学习了MapReduce数据模型的核心:键值对的集合。我们明确了MapReduce作为编程模型与Hadoop作为具体实现的区别,理解了键值对在控制数据分发(洗牌)中的关键作用,并认识了数据模型中对数据大小的隐含假设。下一节我们将深入探讨Map和Reduce函数的具体工作流程。
课程名称:大规模数据科学(大数据操作,第1课/共3课) - P54:Map与Reduce函数详解 🗺️➡️🧹

在本节课中,我们将要学习MapReduce编程模型中的两个核心函数:map函数和reduce函数。我们将详细解释它们各自的输入、输出以及在整个数据处理流程中的作用。
概述
MapReduce是一种用于处理大规模数据集的编程模型。它通过将复杂的计算任务分解为两个主要阶段——map(映射)阶段和reduce(归约)阶段——来简化并行处理。本节将深入探讨这两个函数的定义和工作原理。
Map阶段详解
上一节我们介绍了MapReduce的整体框架,本节中我们来看看map函数的具体细节。
map函数是MapReduce模型的第一阶段。它的输入是一个键值对,输出则是一组中间键值对。重要的是,一个输入可以产生多个输出,这在处理如文档分词等任务时非常有用。
以下是map函数的定义:
def map_function(in_key, in_value):
# 处理逻辑
# 输出一组 (intermediate_key, intermediate_value) 对
return [(intermediate_key1, intermediate_value1), (intermediate_key2, intermediate_value2), ...]
核心概念:
- 输入:一个输入键(
in_key)和一个输入值(in_value)。 - 输出:一个中间键值对的集合(通常称为“包”或列表)。例如,在词频统计中,输入一篇文档,输出可以是多个
(单词, 1)这样的键值对。
Reduce阶段详解
了解了map函数如何生成中间数据后,我们来看看reduce函数如何处理这些数据。
reduce函数接收由map阶段产生的、具有相同中间键的所有值。系统会自动将所有相同键的值分组,并传递给同一个reduce函数实例进行处理。
以下是reduce函数的定义:
def reduce_function(intermediate_key, list_of_values):
# 处理逻辑,例如对值进行求和、计数等操作
# 输出最终结果列表
return [final_value1, final_value2, ...]
核心概念:
- 输入:一个中间键(
intermediate_key)和与该键关联的所有值的列表(list_of_values)。 - 输出:一个最终值的列表。这些值可能来自任何
map任务实例,系统会自动完成分组。 - 实现细节:传递给
reduce函数的值集合,在具体实现上可能是一个列表、迭代器或其他可遍历对象,但这不影响其作为值集合的概念理解。
函数总结与术语起源
现在,我们已经分别了解了map和reduce函数。让我们将它们放在一起总结,并了解其术语的起源。
下图清晰地展示了两个函数的输入输出关系:

函数总结公式:
- Map:
(in_key, in_value) -> list((intermediate_key, intermediate_value)) - Reduce:
(intermediate_key, list(intermediate_value)) -> list(out_value)
术语起源:map和reduce这两个术语源于函数式编程社区。map意指将一个函数“映射”应用到集合的每个元素上(例如,将一系列TIFF图像文件转换为PNG格式),reduce则意指将集合中的元素通过某种操作“归约”为一个或一组汇总结果。MapReduce模型的设计灵感来源于此,尽管其具体含义并不完全等同。
代码示例与思考练习
理论需要结合实践来巩固。以下是针对我们讨论的示例(如词频统计)的可能实现代码。
建议你花点时间仔细阅读并思考这段代码,理解其每一步的作用。虽然我们已经通过示例讲解了很多,但亲自梳理一遍仍然非常有指导意义。

(此处可插入具体的伪代码或Python示例,例如分词和计数的map/reduce函数实现)
总结
本节课中我们一起学习了MapReduce模型的核心:map函数和reduce函数。
map函数负责将输入数据转换为中间键值对。reduce函数负责将具有相同键的中间值进行合并处理,产生最终输出。- 系统自动处理了中间数据的排序、分组和分发,使程序员能够专注于这两个核心的业务逻辑函数。

理解这两个函数是掌握MapReduce并行计算模型的基础。在接下来的课程中,我们将看到如何将它们应用于更复杂的实际场景。
大规模数据科学(第1课) - P55:MapReduce简单示例 🧮

在本节课中,我们将学习MapReduce编程模型,并通过一个具体的“词频统计”示例来理解其工作原理。我们将分析伪代码实现,并探讨如何优化其性能。
上一节我们介绍了MapReduce的抽象概念和一些图示示例。本节中,我们将深入查看实现词频统计应用的伪代码,并分析其具体执行过程。
以下是该MapReduce应用的伪代码,它实现了我们之前讨论的词频统计功能。请注意,这是无法直接执行的伪代码,仅用于描述逻辑。
Map函数:
function map(String key, String value):
// key: 文档名称
// value: 文档内容
for each word w in value:
EmitIntermediate(w, "1");
Reduce函数:
function reduce(String key, Iterator values):
// key: 一个单词
// values: 该单词对应的计数列表(均为“1”)
int result = 0;
for each v in values:
result += ParseInt(v);
Emit(key, AsString(result));
现在,我们来逐步解析这段代码的执行流程。
输入数据模型是键值对集合。在本例中,键是文档名称,值是文档内容。Map函数接收一个这样的键值对。
Map函数遍历文档内容中的每一个单词。对于每个单词w,它输出一个中间键值对,其中键是单词本身,值是字符串“1”。这表示该单词出现了一次。
神奇的Shuffle阶段随后接管工作。它将所有中间键值对按键(即单词)进行分组。拥有相同键的所有值会被收集到一起,形成一个列表。
例如,单词“history”在多个文档中出现后,Shuffle会生成类似这样的分组:键为“history”,值是一个包含多个“1”的迭代器。
Reduce函数接收一个分组。它初始化结果result为0,然后遍历该单词对应的所有值(即一堆“1”),将它们转换为整数并累加到result中。最后,它输出最终的键值对,键是单词,值是累加后的总计数(例如 ("history", 25))。
通过以上分析,我们已经清楚了基础MapReduce词频统计的实现。然而,当前的Map函数为每个单词都发射一个“1”,这会产生大量中间数据。
现在,请你思考一下:在不修改Reduce函数的前提下,如何改变Map函数来显著提升这个算法的计算效率?

本节课中,我们一起学习了MapReduce模型在词频统计任务上的具体实现。我们分析了Map和Reduce阶段的伪代码,并理解了数据从输入、映射、混洗到归约的完整流程。最后,我们提出了一个关于优化Map函数性能的思考题,为后续深入探讨性能调优奠定了基础。
大规模数据科学(大数据操作,第1课/共3课) - P56:MapReduce简单示例(续)🔍

在本节课中,我们将继续探讨MapReduce编程模型,重点分析如何通过优化Map和Reduce函数来提升算法性能。我们将通过一个具体的词频统计示例,学习如何减少网络传输的数据量,从而设计出更高效的MapReduce算法。
上一节我们介绍了MapReduce的基本概念和简单词频统计的实现。本节中,我们来看看如何通过优化Map阶段的输出来提升整体性能。
观察下图所示的初始实现,其核心问题在于Map函数为每个单词的每次出现都发射一个键值对。

这里需要关注的是,我们为特定单词的每一次出现都发射一个键值对。每一个这样的键值对都必须通过网络进行洗牌,然后发送给Reducer。
因此,如果我们在单个文档中看到单词“history”出现25次,我们将为该单词发射25个键值对,它们都将在洗牌阶段被分组到一起。
但是,我们在这个Map函数中可以访问整个文档。那么,为什么不预先统计这些单词的所有出现次数,并产生一个不同的键值对呢?
以下是优化思路:我们可以在Map阶段对每个文档内的单词进行本地聚合。
于是,我们改为声明:单词“history”在这个特定文档中出现了5次(或其它次数)。现在,对于单词“history”,从这个文档中只产生一个键值对,而不是五个不同的键值对。
总体而言,在应用于此问题的所有计算机上的所有文档中,这将带来显著的性能节省。
接下来,需要仔细检查以确保不必更改Reduce端的代码。希望这一点是清晰的,因为您是将总值累加到结果中。
在Reduce端,您不再是将数字1累加25次,而是累加更少的次数。例如,您将累加5 + 10 + 3 + 4 等等,最终得到25。因此,这个循环被评估的次数更少。
我之所以想详细讲解这个示例,是为了说明两点。
第一,是尝试以MapReduce的思维方式思考,考虑如何将一个问题转化为对多个数据块进行操作、发射键来定义分组,然后对这些分组进行操作。
第二,是说明您实际上可以通过仅仅修改Map和Reduce函数,对这些算法的性能进行大量控制。即使您不处理系统内部机制,只有这两个控制点,您实际上也可以得到非常不同的算法、非常不同的行为,以及产生不同数量的中间结果等等。
仅仅通过这两个函数,您就可以实现这些。因此,您不仅需要了解如何以最直接的方式在MapReduce中表达算法,还需要了解如何以合理高效的方式处理问题。
事实上,这个示例展示了您需要寻找的一个关键点:瓶颈通常(并非总是)在于通过网络传输的数据量。
因此,如果您能减少Mapper产生的输出量,特别是在键值对的数量方面,您往往能提高性能(同样,并非总是如此)。我们将在后续看到更多这方面的例子。


我将在此暂停。在下一节中,我们将探讨一个具有类似特征但略有不同的问题变体,以进一步巩固如何针对不同问题设计MapReduce算法。
本节课中我们一起学习了如何通过优化MapReduce的Map函数来减少中间数据的网络传输,从而提升作业性能。核心在于利用Map阶段对数据进行本地预聚合,减少发射的键值对数量。理解这种优化模式,对于设计高效的大规模数据处理算法至关重要。
大规模数据科学(大数据操作,第1课/共3课) - P57:MapReduce示例:单词长度直方图 📊

在本节课中,我们将学习如何运用MapReduce框架解决一个与单词计数类似但略有变化的问题:构建单词长度直方图。我们将了解如何统计文档集中不同长度单词的出现频率,并探讨当单个文档过大时,系统如何处理数据分块。
另一个MapReduce示例:单词长度直方图
上一节我们介绍了MapReduce的基本概念和单词计数的简单示例。本节中,我们来看看一个与之相似但目标不同的例子:分析单词长度分布。
假设我们想了解一组文档中单词长度的构成。我们不再统计每个单词的出现次数,而是希望知道不同长度单词的分布情况。例如,我们可能想知道有多少单词的长度超过10个字符。
我们可以将单词按长度分组,例如:
- 大单词:长度 ≥ 10个字母
- 中等单词:长度在5到9个字母之间
- 小单词:长度更短
当然,你也可以定义自己的分组方案,或者直接使用精确的长度数值进行统计,而不进行分组。
模式复用与算法设计
我希望你们已经发现,这个问题本质上是我们刚刚完成的单词计数问题的一个简单变体。这正是我想强调的一点:在设计MapReduce算法时,你会反复看到这些模式。一旦你掌握了基本方法,新的问题就不会显得截然不同,而是可以借鉴已有的模式。
接下来的内容将更详细地展示这些任务是如何被分解的。
处理大型文档:数据分块
在之前的例子中,我们默认每个文档都是一个小文件。但在现实中,你可能会遇到一个非常、非常大的文档。虽然由于各种原因,单个文本文档通常不会如此巨大,但请想象这些“文档”实际上是大型的单词数据集。
那么,如果一个文档大到无法被单个Map函数一次性处理,我们是否就束手无策了?MapReduce框架会因此崩溃吗?
答案是否定的。当你将大型文档加载到支持MapReduce的底层系统时(我们稍后会详细讨论这个底层系统及其“加载”的含义),文件系统会自动将数据集分割成多个数据块。
因此,我们可以假设这个文档因为过大而被分割成了多个块。例如,块1是文档的上半部分,块2是文档的下半部分。
如果一个文档小于预定的块大小,它就不会被分割。但大型文档会被自动拆分处理。
请注意:这种数据分块机制在之前的单词计数示例中同样可能发生,它并非单词长度统计所特有的功能。我们在此探讨它,是为了展示MapReduce框架处理大规模数据时的另一个重要特性。
本节总结
本节课中,我们一起学习了如何将MapReduce模式应用于构建单词长度直方图。我们认识到许多MapReduce问题共享相似的设计模式。此外,我们还了解了MapReduce底层系统如何通过自动将大型输入文件分割成块来处理超大规模的数据集,从而确保框架的可扩展性和健壮性。
大规模数据科学(大数据操作,第1课/共3课) - P58:MapReduce示例:倒排索引、连接 📊

在本节课中,我们将学习MapReduce编程模型的两个核心应用示例:词长统计与倒排索引构建。我们将通过具体的输入输出示例,理解Map函数和Reduce函数的设计思路,并体会MapReduce模式的通用性。
词长统计示例 📏
上一节我们介绍了经典的词频统计(Word Count)示例。本节中我们来看看一个它的简单变体:词长统计。这个任务的目标是统计文档中不同长度(或类别)单词的出现次数。

假设我们有一个简单的分类规则:根据单词长度将其归类为“黄色”、“红色”、“蓝色”或“粉色”词。输入数据是文档块,Map任务需要处理这些块。
以下是Map阶段的工作原理:
- Map任务读取一个文档块。
- 对于块中的每一个单词,计算其长度。
- 根据长度,使用一个条件判断(如
case语句)确定该单词对应的颜色类别。 - 为每个类别输出一个键值对,其中键是颜色(如“黄色”),值是初始计数
1。
其核心逻辑可以用伪代码描述:
def map(document_chunk):
for word in document_chunk:
length = len(word)
color = determine_color(length) # 例如,根据长度判断颜色
emit(color, 1)

如图所示,第一个文档块(Chunk 1)被分配给Map任务1,它处理后会生成一个关于“黄色”、“红色”、“蓝色”和“粉色”单词的初始计数直方图。第二个文档块(Chunk 2)进行同样的处理,生成另一组四个键值对。
在Shuffle阶段,所有相同颜色的键值对会被分组到一起。例如,所有键为“黄色”的键值对会汇集到同一个Reducer。
在Reduce阶段,Reducer的任务非常简单:对于每一个颜色键,将其对应的所有数值(都是 1)相加。例如,如果两个Map任务都输出了“黄色”,那么Reducer会收到两个 (“黄色”, [1, 1]),相加后得到最终结果 (“黄色”, 2)。

这个示例的结构与词频统计完全相同,只是修改了Map函数中的逻辑。值得注意的是,Reduce函数可以完全复用词频统计中的加法函数。这体现了MapReduce的一个特点:某些Reduce函数(如求和、计数)具有通用性,会在多种算法中反复使用。
倒排索引示例 🔍
理解了词长统计后,我们来看另一个重要的应用:构建倒排索引。这是搜索引擎等文本检索系统的核心步骤之一。
假设我们有一个文档(或推文)集合。通常,我们可以通过文档ID(如URL)高效地访问特定文档。而倒排索引则提供反向查找能力:给定一个单词,它能快速返回所有包含该单词的文档ID列表。
我们的输入数据是推文,其中键是推文ID,值是推文文本本身。期望的输出是:每个单词,以及包含该单词的所有推文ID的集合。
以下是构建倒排索引的MapReduce步骤:
-
Map阶段:对于每一条推文,遍历其中的每一个单词。对于每个单词,输出一个键值对,其中键是该单词,值是当前推文的ID。
- 这里可以进行一个优化:如果同一个单词在一条推文中出现多次,Map函数只需输出一次
(word, tweet_id)即可,因为索引只关心推文是否包含该单词,而不关心出现的次数。 - 伪代码如下:
def map(tweet_id, tweet_text): words = extract_unique_words(tweet_text) # 提取唯一单词 for word in words: emit(word, tweet_id)
- 这里可以进行一个优化:如果同一个单词在一条推文中出现多次,Map函数只需输出一次
-
Shuffle阶段:系统将所有相同单词的键值对分组。例如,所有键为“pancake”的
(“pancake”, tweet_id)会被送到同一个Reducer。 -
Reduce阶段:Reducer接收到一个单词(键)和对应的推文ID迭代器(值)。在这个特定任务中,Reduce函数几乎不需要做任何处理!因为Shuffle阶段完成的分组操作本身就已经产生了我们想要的结果——一个单词及其对应的推文ID列表。Reducer可以直接将这个列表作为最终值输出。
这种模式在MapReduce中也很常见:我们主要利用Map阶段进行数据转换,利用Shuffle阶段进行关键的分组,而Reduce阶段可能只是一个简单的“直通”操作,或者完全不需要。
这与传统关系型数据库的范式不同。在关系数据库中,一个单元格内通常不允许嵌套集合(这违反了第一范式)。但在MapReduce的键值对模型中,值可以是任意结构,包括列表或集合,这为处理复杂数据提供了极大的灵活性。

总结 📝
本节课中我们一起学习了两个MapReduce核心示例。
- 我们首先分析了词长统计,它是词频统计的变体,演示了如何通过修改Map函数来适应新的分类逻辑,同时复用通用的Reduce函数。
- 接着,我们深入探讨了倒排索引的构建过程。这个例子展示了MapReduce如何优雅地处理“分组”需求:Map阶段生成
(单词, 文档ID)对,Shuffle阶段自动按单词分组,而Reduce阶段有时只需输出已分组的列表即可。

这两个例子体现了MapReduce模型的强大与灵活:通过定义不同的Map和Reduce函数,可以解决多种大规模数据处理问题。下次课我们将探讨MapReduce在实现关系型连接(Join) 操作中的应用。
大规模数据科学(第1课)📊:关系型连接的Map阶段

在本节课中,我们将学习如何在大规模数据处理框架(如MapReduce)中实现关系型数据库中的“连接”操作。我们将从一个具体的例子出发,逐步拆解其实现逻辑,重点关注“Map”阶段的设计。
上一节我们介绍了MapReduce的基本概念,本节中我们来看看如何用它来处理一个典型的二元操作——关系连接。
连接操作回顾
首先,回顾一下关系型连接操作。我们有两个关系(即数据表),每个关系是一组元组的集合。连接操作的目的是找出一个关系中的每条记录,在另一个关系中的对应记录。
例如,我们想根据 SSN 字段等于 EmpSSN 字段来进行连接。这里需要明确指出连接的条件,因为两个字段名称并不相同(这不同于“自然连接”)。
连接的结果是以下三条记录:第一条记录与右边的两条记录匹配,另一条记录与右边的一条记录匹配。
现在,假设这两个关系的数据量都非常庞大。
MapReduce的挑战与技巧
Map阶段需要处理每一个元组。我们立刻遇到一个问题:连接是一个二元操作,它有左关系和右关系两个输入,目的是找出两边相互匹配的元组。然而,MapReduce本质上是一个一元操作,它处理的是单个数据集。
那么,如何在MapReduce中表达连接操作呢?初看似乎无法直接实现,但这里有一个技巧。
这个技巧是:我们可以将两个关系中的所有元组合并到一个庞大的数据集中,暂且称之为 tus。这就是我们将用MapReduce处理的数据集。
你可能会问,开头的 E 或 D 标签是做什么用的?这是我们附加在每个元组上的一个标签,用于标识它来自哪个原始关系(员工表 Employee 或部门分配表 Assigned Department)。我们稍后会看到它的用途。
在实践中,实现这个标签并不困难。例如,如果你处理的是分布式文件,员工表的数据块存放在一个目录下,部门分配表的数据块存放在另一个目录下,Map函数可以通过读取文件名或文件路径来判断元组属于哪个关系,并据此附加标签。
我们可以为此编写伪代码,这可能会是后续作业的一部分。
Map阶段的设计
那么,这个关系型连接的Map阶段具体是怎样的呢?
对于输入中的每一条记录,Map函数需要产生一个键值对,因为这是MapReduce的工作方式。
以下是关键设计点:
- 键:将是连接所依据的属性(即
SSN或EmpSSN)。 - 值:将是元组的其他所有部分(通常包含整个元组本身,虽然为了节省空间理论上可以去掉连接键,但通常不会这么做)。
因此,给定一个形如 (标签, 连接键, 其他字段...) 的元组,我们会产生一个键值对。例如,对于连接键为 777-77-7777 的元组,产生的键就是 777-77-7777,值就是整个原始元组。
需要再次强调的是,来自两个关系的所有元组都被合并到了同一个输入流中进行处理。
本节总结
本节课中我们一起学习了在MapReduce中实现关系连接的第一步——Map阶段。我们运用了两个核心技巧:
- 合并数据集:将需要连接的两个表的所有数据合并为一个输入。
- 键的设计:以连接属性作为Map输出的键,以便后续Reduce阶段能将相关记录汇集到一起。
通过为元组附加来源标签,我们为后续区分和处理数据做好了准备。下一节,我们将探讨Reduce阶段如何利用Map的输出完成最终的连接操作。


大规模数据科学(大数据操作,第1课/共3课) - P60:关系连接的Reduce阶段 🧩

在本节课中,我们将学习MapReduce框架中实现关系连接操作的Reduce阶段。我们将详细探讨在“魔法洗牌”阶段之后,数据如何被分组,以及Reduce函数如何利用这些分组数据来生成最终的连接结果。
上一节我们介绍了Map阶段如何为连接操作准备数据。本节中我们来看看在“魔法洗牌”阶段之后,Reduce阶段如何完成连接的计算。

在“魔法洗牌”阶段,所有具有相同键的记录会被集中到一起。因此,现在你会为每一个唯一的键调用一次Reduce函数。例如,会为键“9”调用一次,为键“7”调用一次。
与该键关联的值列表将包含所有共享此连接键的元组。这些元组来自哪个关系并不重要,它们都会出现在这个列表中。例如,对于某个键,我们可能得到一个来自employee关系的元组和一个来自department关系的元组;对于另一个键,可能得到一个来自employee的元组和两个来自department的元组。
此时,在单台计算机的上下文中,我们已经拥有了计算连接所需的所有数据。更重要的是,每一个Reduce函数的调用都可以在一台独立的机器上执行。这正是我们实现可扩展性的方式。
现在,如果我们要实现一个连接操作,程序需要编写的Reduce函数必须能够:接收这个键以及所有关联的元组,并生成连接后的元组。例如,连接结果中某些属性来自employee表,另一些属性来自department表。
以下是Reduce函数内部需要实现的操作逻辑:
如果你思考一下需要在这个Reduce函数中实现何种操作,你会发现:你得到了一组来自一个关系的元组,需要将它们与来自另一个关系的每一个可能的元组进行关联。因为根据定义,它们都共享相同的连接键,所以它们都能成功连接。
如果你需要将一个集合中的每个成员与另一个集合中的每个可能成员配对,这本质上是一个笛卡尔积操作。因此,我们再次看到关系代数在另一个上下文中出现。
我不想让你感到困惑,但总体而言,我们试图实现的是一个并行连接操作。而恰好,在单个Reduce函数内部,我们识别出这又是一个关系代数操作符——笛卡尔积。这更多是出于抽象层面的理解,而非算法本身的目的,但我想指出这一点。当你戴上关系代数的“有色眼镜”观察时,会发现这些操作符无处不在。
为了确保清晰,让我们再举一个例子。假设给你两个关系:order和line_item。
order表包含字段:order_id、account、date。
line_item表包含字段:order_id、item_id、quantity。
我们将基于order_id进行连接。
Map阶段是什么样的?键仍然是连接键,即order_id。值将是一个包含关系名和原始元组的配对。虽然具体的结构可能略有不同,但核心是所有信息都在这里:键必须是连接键,值是元组以及一个标识其来源关系的指示符。
这里有一个快速的问题:为什么需要这个关系指示符?
因为在Reduce阶段,如果我不知道哪些元组属于哪个关系,只是有一大捆元组,我将无法正确地生成连接后的元组。理论上,你可能可以通过模式推断来避免显式标记(例如,employee表可能先有一个字符串,然后是连接键;department表可能是连接键在前),但依赖这种信息有些危险。如果你连接的两个表具有完全相同的模式,就没有明显的方法来区分它们。因此,使用显式标记在这里更安全。
这就是Map阶段的过程:我们将所有记录汇集在一起,生成键值对。其中,键是来自对应关系的连接键(连接属性),值是被标记了关系名的整个元组。


本节课中我们一起学习了关系连接在MapReduce框架中的Reduce阶段。我们了解到,“魔法洗牌”将相同键的数据汇集,使每个Reduce任务能独立处理一组键及其关联的所有元组。Reduce函数的核心任务是在本地执行一个笛卡尔积操作,将来自不同关系但共享同一键的元组合并,最终生成连接结果。通过这种方式,连接操作被有效地并行化和规模化。
大规模数据科学(第1课)📊:简单社交网络分析 - 计算好友数

在本节课中,我们将学习如何使用MapReduce编程模型来解决一个简单的社交网络分析问题:计算社交网络中每个人的好友数量。我们将通过一个具体的例子,将问题分解为Map和Reduce两个阶段,并展示其与经典“词频统计”示例的相似性。
从连接操作到社交网络分析
上一节我们介绍了MapReduce如何执行连接(Join)操作。本节中,我们来看看一个不同的应用场景:社交网络分析。
假设我们有一个社交网络图,其中每条边代表一种“好友”关系(或在Twitter场景下为“关注”关系)。输入数据是一组边,每条边的语义是“用户A是用户B的好友”。为了简化,我们假设关系是对称的(即如果Jim是Sue的好友,那么Sue也是Jim的好友),这样我们可以避免有向图与无向图带来的混淆。
我们的任务非常明确:计算每个人的好友数量。期望的输出结果如下:
- Jim: 3
- Sue: 3
- Kai: 1
- Lyn: 2
- Joe: 1
这个任务的结构,应该让我们联想到之前已经完成过的某个经典示例。
Map阶段:生成键值对
以下是Map阶段需要执行的操作。对于输入数据中的每一条边(即每一条好友关系记录):
- 我们关注记录左侧的人名。
- 生成一个键值对,其中键(Key)是这个人名,值(Value)是数字
1,表示我们为这个人遇到了一个好友。
用伪代码描述这个过程:
def map(record):
# 假设 record 格式为 “person_a, person_b”
person_a = record.split(',')[0]
emit(person_a, 1)
经过Map阶段处理后,每条输入记录都被转换成了一个(人名, 1)的键值对。
Shuffle与Reduce阶段:汇总计数
接下来,通过MapReduce框架的Shuffle阶段,所有具有相同键(即同一个人名)的值会被分组到一起,传递给同一个Reducer。
以下是Reduce阶段需要执行的操作。对于每一个键(如“Jim”)及其对应的值列表(如[1, 1, 1]):
- Reducer的任务非常简单,只需将这个列表中的所有值(数字
1)相加。
用伪代码描述这个过程:
def reduce(key, list_of_values):
total_count = sum(list_of_values)
emit(key, total_count)
这个Reducer的逻辑与“词频统计”示例中的Reducer完全一致,都是执行求和操作。
与词频统计的类比
这个计算好友数的过程,本质上与经典的词频统计(Word Count)非常相似。
- 在词频统计中,输入是文档,Map阶段为每个单词生成
(单词, 1)。 - 在好友计数中,输入是边(好友关系),Map阶段为每条边左侧的人名生成
(人名, 1)。
之后的过程便完全相同:Shuffle阶段按键分组,Reduce阶段对值列表求和。这展示了MapReduce模型的核心优势——相同的聚合模式可以应用于多种看似不同的数据处理问题。
总结
本节课中,我们一起学习了如何运用MapReduce模型解决“计算社交网络好友数”的问题。我们首先明确了任务目标,然后将其分解为Map和Reduce两个核心阶段:Map阶段从每条关系记录中提取关键人物并标记为1,Reduce阶段则对同一人物的所有标记进行求和。通过这个过程,我们再次看到了MapReduce“分组聚合”模式的强大与通用性,它能够将复杂的数据处理任务简化为可并行执行的标准化操作。

在下一节中,我们将探讨一个更具挑战性的任务:如何在MapReduce中实现矩阵乘法。
大规模数据科学(大数据操作,第1课/共3课) - P62:矩阵乘法概述 🧮

在本节课中,我们将学习如何在MapReduce框架下实现矩阵乘法。我们将从回顾矩阵乘法的基本概念开始,然后介绍一种适用于稀疏矩阵的MapReduce算法。这个算法通过巧妙的数据分发和聚合,能够高效地处理大规模矩阵相乘的问题。
回顾矩阵乘法
上一节我们介绍了课程背景,本节中我们来看看矩阵乘法的基本规则。
矩阵乘法要求第一个矩阵的列数等于第二个矩阵的行数。例如,一个 2行4列 的矩阵乘以一个 4行2列 的矩阵,结果将是一个 2行2列 的矩阵。
结果矩阵中每个元素的计算方法如下:它是第一个矩阵的某一行与第二个矩阵的某一列对应元素的乘积之和。具体公式为:
C[i][j] = Σ (A[i][k] * B[k][j]),其中 k 从 1 到 M(M是A的列数/B的行数)。
例如,结果矩阵中第一行第一列的元素,是第一个矩阵的第一行与第二个矩阵的第一列进行点积运算的结果。
稀疏矩阵表示法
在进入MapReduce算法之前,我们需要理解输入数据的格式。为了高效处理可能包含大量零值的大规模矩阵,我们使用稀疏矩阵表示法。
在这种格式中,矩阵的每个非零元素由一个三元组表示:(行ID, 列ID, 值)。如果矩阵中某个位置的值为0,则直接省略该三元组,不存储在数据集中。这种方法可以显著节省存储和计算资源。
我们的MapReduce算法的输入,就是两个以这种三元组集合形式表示的稀疏矩阵。
MapReduce算法详解
现在,我们来看看如何利用MapReduce实现矩阵乘法。核心思想是将两个矩阵的数据重新组织,使所有需要相乘的元素在Reduce阶段汇聚到同一个键下。
以下是算法的两个主要阶段:
Map阶段
Map阶段的任务是读取稀疏矩阵的三元组,并为每个元素生成多个中间键值对,为后续的聚合相乘做准备。
对于矩阵A中的每个元素 (i, j, A_ij),我们需要为结果矩阵的每一列(即矩阵B的列数n)生成一个中间键值对。
对于矩阵B中的每个元素 (j, k, B_jk),我们需要为结果矩阵的每一行(即矩阵A的行数L)生成一个中间键值对。
具体操作如下:
-
处理矩阵A的元素
(i, j, A_ij):- 对于每一个
k(从1到n,n是矩阵B的列数),发射一个键值对。 - 键(Key):
(i, k)。这标识了结果矩阵C中C[i][k]的位置。 - 值(Value):
(‘A‘, j, A_ij)。值中需要包含来源标记‘A‘、列索引j和实际值A_ij。
- 对于每一个
-
处理矩阵B的元素
(j, k, B_jk):- 对于每一个
i(从1到L,L是矩阵A的行数),发射一个键值对。 - 键(Key):
(i, k)。同样标识了结果矩阵C中C[i][k]的位置。 - 值(Value):
(‘B‘, j, B_jk)。值中包含来源标记‘B‘、行索引j和实际值B_jk。
- 对于每一个
这个过程可以理解为:将矩阵A的每个值“广播”到结果矩阵对应行的所有列;将矩阵B的每个值“广播”到结果矩阵对应列的所有行。这样,所有对计算 C[i][k] 有贡献的 A[i][*] 和 B[*][k] 都会汇聚到同一个键 (i, k) 下。
Reduce阶段
Reduce阶段接收Map阶段输出的所有具有相同键 (i, k) 的键值对列表。这个列表里包含了所有标记为‘A‘和‘B‘的值及其对应的中间索引 j。
Reduce任务的目标是计算点积 C[i][k] = Σ (A[i][j] * B[j][k])。
以下是计算步骤:
- 将接收到的值列表按来源标记(‘A‘或‘B‘)和中间索引
j进行分组。 - 对于每个共同的
j值,找到对应的A[i][j]和B[j][k]。 - 将它们相乘。
- 将所有乘积结果相加,得到最终
C[i][k]的值。 - 最后,发射最终结果:键为
(i, k),值为计算得到的C_ik。
总结


本节课中我们一起学习了利用MapReduce进行矩阵乘法的算法。我们从基础的矩阵乘法规则出发,介绍了适用于大数据的稀疏矩阵表示法。重点在于MapReduce算法的设计:在Map阶段,通过将矩阵元素按特定规则复制并分发到以结果矩阵坐标 (i, k) 为键的中间对中;在Reduce阶段,将共享同一键的值聚合起来,执行点积运算,从而得到结果矩阵的每一个元素。这种方法能够有效地将大规模计算任务并行化,是处理大数据集上矩阵运算的经典范例。
大规模数据科学(大数据操作,第1课/共3课) - P63:矩阵乘法图解 🧮

在本节课中,我们将学习如何使用MapReduce框架进行矩阵乘法。我们将通过图解的方式,理解如何将矩阵乘法分解为可并行化的任务,并解释数据在映射器和归约器之间如何流动。
概述
矩阵乘法是许多科学计算和数据分析任务的核心。当矩阵规模非常大时,传统的单机计算方法会变得低效。MapReduce提供了一种将矩阵乘法并行化的方法。本节我们将图解这一过程,理解每个数据单元如何被处理和路由。
算法核心:一个归约器对应一个输出单元
首先需要认识到,在这个算法中,输出矩阵的每一个单元都将对应一个归约器。
例如,对于一个 2x3 的输出矩阵,我们将有六个归约器。当我们处理非常大的矩阵时(例如 10000 x 1000 的维度),这种设计的优势就体现出来了。
核心概念:一个归约器 = 输出矩阵的一个单元格
归约器需要什么数据?
接下来,思考每个归约器需要哪些数据来计算它的结果。
以输出矩阵中位置为 (1,1) 的归约器为例。它需要来自矩阵A第一行的所有值,以及来自矩阵B第一列的所有值。
公式表示:
C[1][1] = dot_product(A[1, :], B[:, 1])
同理,对于输出位置 (1,2) 的归约器,它需要矩阵A第一行和矩阵B第二列的数据。
公式表示:
C[1][2] = dot_product(A[1, :], B[:, 2])
数据分发面临的挑战
问题在于,输入数据并不是以完整的行或列的形式存储的,而是以一个个独立的单元格形式存在。
因此,我们必须确定每个输入值(如 a11)应该被发送到哪里。它需要被发送给所有可能需要它的归约器。
例如,值 a11(矩阵A第一行第一列)属于矩阵A的第一行。因此,所有需要矩阵A第一行数据的归约器(即输出矩阵第一行的所有归约器)都需要这个值。这意味着 a11 需要被发送到多个地方。
MapReduce的“复制”技巧
这正是MapReduce可以施展的一个巧妙技巧:映射器可以将一个单一的值复制并发送到多个目的地。
当我说“发送到多个地方”时,并不是指字面意义上通过网络发送多个数据包,而是指将一个值附加到多个键上,然后每个不同的键会被路由到不同的归约器。
代码逻辑示意:
# 对于矩阵A中的每个元素 A[i][j]
for k in range(num_columns_of_B):
emit(key=(i, k), value=('A', j, A[i][j]))
# 对于矩阵B中的每个元素 B[j][k]
for i in range(num_rows_of_A):
emit(key=(i, k), value=('B', j, B[j][k]))
通过这个技巧,我们可以安排矩阵乘法并行进行。虽然我们进行了一些数据复制,但这种复制是必要的,并且整个计算过程可以高度并行化。
归约阶段:计算点积
最后,每个归约器会收到所有它需要的、来自矩阵A某一行和矩阵B某一列的数据。归约器的任务就是计算这两个向量的点积,从而得到输出矩阵对应位置的值。
公式表示:
C[i][k] = sum_over_j( A[i][j] * B[j][k] )
总结


本节课我们一起学习了利用MapReduce进行矩阵乘法的图解过程。我们了解到,通过为输出矩阵的每个单元格分配一个归约器,并利用映射器将输入数据复制并路由到所有需要它的归约器,可以实现大规模矩阵乘法的并行计算。这种方法的核心在于巧妙的数据分发策略,它虽然引入了数据复制的开销,但换来了强大的横向扩展能力,非常适合处理超大规模的数据集。
大规模数据科学(第1课)📊:无共享计算架构

在本节课中,我们将学习大规模数据处理系统(如MapReduce)所部署的底层硬件架构。我们将重点了解“无共享”架构,并探讨为何它成为处理海量数据的首选方案。
系统架构类型概述
上一节我们提到了MapReduce的运行环境,本节中我们来看看支撑其运行的三种主要系统架构。理解这些架构有助于我们把握大规模计算的本质。
在以下示意图中,圆柱体代表磁盘,矩形代表内存,圆形代表处理器。

以下是三种主要的并行计算架构:
-
共享内存
- 所有处理器都能访问全部内存和所有磁盘。
- 这类似于你的笔记本电脑:如果它是四核(或如今常见的6核、12核)系统,使用的就是这种架构模型。
-
共享磁盘
- 多台独立的机器访问一个共享的文件系统。
- 这种设置本身很常见,但用于并行分析领域,则多见于商业数据库系统(如Oracle、IBM的某些系统)。
-
无共享
- 这是我们本节课的重点,也是MapReduce和大多数并行数据库的设计目标。
- 这意味着各台机器仅通过网络连接,没有共享内存,也没有共享磁盘。
为何选择无共享架构?
上一节我们介绍了三种架构,本节中我们来看看为何“无共享”架构在大规模数据处理中占据主导地位。
核心论点是:只有无共享架构能够扩展到成千上万台计算机甚至更多。因为共享内存或共享磁盘最终会成为瓶颈,限制你能连接到同一逻辑或物理设备的计算机数量。
因此,学习如何为这些庞大的无共享集群编程,正是MapReduce和并行数据库技术的核心。
- 共享内存机器可能最容易编程,但传统上被认为相当昂贵。不过需要指出,成本正在快速下降,购买一台拥有大量内存和多核心的强劲机器变得越来越可行。你的问题可能恰好能在一台这样的机器内解决。
大规模集群的上下文与挑战
了解了架构选择后,我们来看看MapReduce实际运行的环境以及随之而来的独特挑战。
我们讨论的上下文是:大量商用服务器通过高速商用网络连接。你可以想象一个数据中心:机柜里有多台服务器,数据中心里又有多个机柜,成千上万台计算机就是这样组织起来的。
我们寻求的是大规模并行计算能力,能够运行即使在数千或数万台服务器上也需要许多小时才能完成的任务。
在这种规模下,一个在较小规模场景中不常出现的问题变得至关重要:故障。如果你在数千台计算机上长时间运行一个作业,在此期间发生故障的概率几乎为100%。你的处理系统必须能够容忍这类故障,不能每次一有故障就回滚到起点重启,否则你将永远无法完成任何任务。
即使单个磁盘的平均故障间隔时间(MTBF)是一年,如果你有10,000台服务器(假设每台服务器有多个磁盘),你也会开始遇到大约每小时一次的故障率。你可以查阅平均故障间隔时间并实际计算,有几篇不错的论文讨论了这一点(我会尽量记得将其列入阅读材料)。

总结

本节课中我们一起学习了大规模数据处理系统的底层架构。我们了解了共享内存、共享磁盘和无共享三种架构,并重点探讨了为何无共享架构因其卓越的可扩展性而成为MapReduce等系统的设计基础。我们还认识到,在由成千上万台廉价商用硬件组成的大规模集群中,硬件故障是常态而非例外,因此系统必须具备容错能力。这正是像MapReduce这样的编程模型需要解决的核心问题之一。
大规模数据科学(大数据操作,第1课/共3课) - P65:MapReduce实现详解 🧩

在本节课中,我们将深入探讨MapReduce计算模型的具体实现机制,重点关注其如何在一个由普通硬件构成的分布式集群上可靠地处理大规模数据。我们将从底层分布式文件系统开始,逐步向上解析MapReduce作业的执行流程。
分布式文件系统(DFS)基础
上一节我们介绍了MapReduce的编程模型,本节中我们来看看支撑其运行的底层存储系统。MapReduce通常运行在分布式文件系统之上,例如HDFS(Hadoop分布式文件系统)。其核心设计目标是管理超大规模文件并容忍硬件故障。
以下是分布式文件系统的关键特性:
- 大文件分块:单个超大文件在上传到集群时,会被自动分割成固定大小的数据块(例如64MB)。这些数据块被分散存储在不同机器上。
- 数据块复制:每个数据块会被复制多份(例如3份),并存储在不同机器、甚至不同机架上。这样,即使某台机器或整个机架发生故障,数据依然可用。
- 主要实现:其概念对应DFS,而具体实现包括Google的GFS和开源的HDFS。
MapReduce架构与执行流程
理解了数据如何存储后,我们现在进入核心部分:MapReduce作业是如何在集群上调度和执行的。整个系统由一个主节点(Master)和众多工作节点(Worker)协同工作。
以下是MapReduce执行过程的关键步骤:
- 主节点(Master):负责协调整个作业。它掌握所有输入数据块的位置信息,并负责任务的分配与监控。
- Map阶段:主节点为输入文件的每一个数据块(Split)分配一个Map任务(Map Task)。一个Map任务会处理对应数据块中的所有记录,为每条记录调用一次用户定义的Map函数。Map任务运行在存储该数据块副本的机器上,以实现“计算向数据移动”。
- 中间输出:每个Map任务将其产生的中间键值对输出写入本地磁盘,而不是回写到HDFS。这些输出会根据Reduce任务的数量进行分区(Partition),确保相同键的数据进入同一个分区。
- Reduce阶段:主节点为每个分区启动一个Reduce任务(Reduce Task)。Reduce任务通过网络从各个Map任务的本地磁盘上“拉取”(Shuffle)属于自己分区的所有中间数据。
- Reduce计算:每个Reduce任务对拉取到的数据进行排序和分组,然后为每个唯一的键调用一次用户定义的Reduce函数,并将最终结果输出(通常写入HDFS)。
容错机制设计
MapReduce实现的核心优势在于其内置的容错能力,这使得它能在由廉价、不可靠硬件组成的大规模集群上稳定运行。
其容错设计基于以下原则:
- 中间结果持久化:Map任务将输出写入本地磁盘。这样,如果某个Reduce任务失败,只需重新执行该Reduce任务,并从Map任务的本地磁盘重新拉取数据即可,无需重新执行所有Map任务。
- 任务级重试:如果某个Map任务所在的机器彻底宕机,由于中间结果只存储在本地,该Map任务需要从头重新执行。但得益于数据块的冗余存储,主节点可以轻松地将该任务调度到存有同一数据块副本的其他机器上执行。这仅影响单个任务,而非整个作业。
- 主节点高可用:通常通过主节点元数据备份和快速恢复机制来应对主节点故障(图中未展开,但实际部署中会考虑)。
总结


本节课中我们一起学习了MapReduce的具体实现架构。我们从底层的分布式文件系统(HDFS)出发,了解了数据如何被分块和复制以实现可靠存储。接着,我们剖析了MapReduce作业的执行流程,明确了主节点、Map任务和Reduce任务各自的职责,以及关键的Shuffle阶段。最后,我们探讨了其核心的容错机制,理解了通过将中间结果写入本地磁盘和任务级重试,MapReduce能够优雅地处理集群中常见的节点故障问题。这套设计使得大规模数据批处理变得既高效又可靠。
杜克大学《大规模数据科学》第1课:MapReduce阶段详解 🗺️➡️🧹

在本节课中,我们将深入学习MapReduce的具体执行阶段。我们将从HDFS读取数据开始,一步步跟踪数据经过映射、组合、排序、归约,最终写回HDFS的完整流程。理解这些阶段对于掌握大数据处理的核心机制至关重要。
上一节我们介绍了MapReduce的编程模型,本节中我们来看看其更详细、更具体的执行阶段。
执行阶段概述
MapReduce作业的执行包含一系列精心设计的阶段,以确保大规模数据处理的可靠性和效率。
以下是MapReduce作业从开始到结束的详细步骤:
- 文件分片读取:作业从HDFS(Hadoop分布式文件系统)读取一个文件分片。HDFS是复制的,意味着每个数据块都有多个副本。
- 记录读取:一个称为“记录读取器”的代码单元将分片(或数据块)解析为单独的记录。
- 映射函数执行:程序员的映射函数在每个单独的记录上被调用。
- 组合阶段:这是一个我们尚未讨论的步骤,用于在数据发送前进行本地聚合,稍后会详细解释。
- 中间输出写入:这些阶段的输出被写入到该计算节点的本地存储中。
- 数据传输:本地存储中按键分区的“区域”通过网络被归约阶段拉取。
- 排序与合并:来自所有不同映射任务、对应相同键的区域被并行地排序和合并在一起。
- 归约函数执行:调用用户的归约函数以产生最终输出。
- 最终输出写入:该步骤的输出被写回HDFS,从而获得复制和持久化。
再次强调,如果在映射阶段出错,需要重新运行映射器;如果在归约阶段出错,可以从映射阶段的本地存储中拉取输出并重新运行归约器。负责协调整个作业的机制确保了容错性:因为HDFS数据是复制的,即使丢失了归约器或对应的映射器,也可以重新运行必要的部分,数据不会丢失。这保证了作业执行期间的容错能力。
深入理解组合器
为了理解为什么需要组合器,让我们回到最初讨论的词频统计例子。
在映射函数中,我们为每个单词生成一个键值对,例如 (word, 1)。如果一个单词(如“W1”)出现了两次,那么就会生成两个记录:(W1, 1) 和 (W1, 1),并将它们都发送到网络,以便在归约端进行排序、分组和处理。
这有些浪费网络资源。理想情况是将它们合并为单个记录 (W1, 2) 再发送,因为这样数据量更小。虽然你可以重写映射函数来实现这一点,但由于这是一个非常普遍的需求,MapReduce框架提供了称为“组合器”的功能。
组合器的作用是在将数据发送到归约端之前,识别具有相同键的键值对并将它们聚合在一起。这节省了网络流量。在许多情况下,组合器函数可以完全与归约器函数相同。要使组合器正常工作,所应用的函数必须是可结合和可交换的。
以下是包含组合器的伪代码示例:
# 映射函数
def map(doc_id, document_text):
for word in document_text.split():
emit(word, 1)
# 组合器函数(通常与归约器相同)
def combine(word, list_of_counts):
total = sum(list_of_counts)
emit(word, total)
# 归约函数
def reduce(word, list_of_counts):
total = sum(list_of_counts)
emit(word, total)
组合器的唯一目的是在数据通过网络传输之前应用聚合操作。
作业执行全景图
这是一个非常清晰的Hadoop作业执行总结图。
数据从HDFS开始,以数据块形式存储。输入分区被分配给各个映射任务。请注意,映射任务不是单次映射函数的调用,而是完整的处理单元;一个映射任务几乎肯定会产生多个键值对。
映射任务会产生按键着色的本地“区域”。这些区域通过网络被拉取到归约服务器。在此示例中,只有两个归约服务器。之前我们的例子显示所有蓝色键去一个地方,所有红色键去另一个地方,这是一种简化。实际上,如果你只有两个服务器,那么数百个可能的键必须映射到这两个服务器上,因此同一个服务器上肯定会收到混合的键。
逻辑上,你指定一个键,它通过哈希函数决定去往哪个归约器。但在那之前,数据必须被送到一台可能运行着许多归约任务或归约函数调用的机器上。在此图中,蓝色和红色的键最终到达同一台机器,绿色和橙色的键到达另一台机器。
随后,并行排序过程会将它们正确归类,把所有红色的放在一起,把所有蓝色的放在一起。然后,针对每一种颜色(即每一个键组),调用一次归约函数。归约函数被调用并产生输出分区,最终该输出被写回HDFS。


本节课中我们一起学习了MapReduce作业的详细执行阶段,包括从HDFS读取、映射、组合、排序、归约到写回HDFS的完整流程。我们还深入探讨了组合器的作用及其节省网络资源的原理,并通过全景图回顾了整个作业的生命周期。理解这些阶段是掌握大规模分布式数据处理的基础。
大规模数据科学(大数据操作,第1课/共3课)|第67讲:大规模数据系统的设计空间 🗺️

在本节课中,我们将探讨大规模数据处理系统的设计空间,了解MapReduce之外的其他系统类型,并分析它们在不同维度上的定位与特点。
概述:大规模系统的设计维度
上一节我们介绍了MapReduce的基本原理。本节中,我们来看看其他大规模数据处理系统,并从一个更宏观的视角理解它们所处的设计空间。
这个设计空间的划分是由微软Dryad系统的开发者Michael Isard提出的。他将大规模数据处理系统按照三个核心轴线进行分类。
设计空间的三条轴线
以下是划分大规模数据处理系统的三个关键维度:
- 延迟与吞吐量:一端是追求低延迟、快速交互响应的系统;另一端是追求高吞吐量、能同时在成千上万台计算机上运行大规模批处理作业的系统。
- 部署范围:一端是在私有数据中心内部运行的系统;另一端是能够跨互联网广泛扩展的系统。
- 架构模型:一端是数据并行模型;另一端是共享内存模型。
我们目前主要关注的领域位于高吞吐量、私有数据中心、数据并行这个象限,这也是MapReduce的典型定位。在后续章节中,我们也会探讨低延迟、小规模操作的象限,这类系统通常被称为NoSQL系统。
各类系统在设计空间中的位置
Michael Isard将传统的关系型数据库(尽管我这里的标注方式与他略有不同)定位在低延迟、共享内存的象限。我称之为“较旧的数据库”,是为了指出并非所有关系型数据库都只处在这个空间;事实上,许多现代并行数据库也是数据并行的。
例如,如果你熟悉的MySQL和PostgreSQL,它们可能更接近共享内存、共享磁盘的范畴。
- 高性能计算:位于私有数据中心、高吞吐量、共享内存象限。这是一种批处理作业提交系统,你将计算密集型任务提交给大型机器进行处理,最终返回结果。
- 网格计算:这个象限强调连接来自不同机构的计算机集群并让它们协同工作,因此它在“部署范围”轴线上被推向“跨互联网”的一端。
- 发展趋势:越来越多的系统正在向“跨互联网”这个维度扩展。例如,谷歌近期推出的Spanner系统就是一个很好的例子,它通过分布式哈希表等技术,在广域网范围内提供具有强一致性语义的服务。
MapReduce的兴起背景
现在,让我们回顾并总结上一讲的内容。大规模数据处理通常涉及处理海量数据并生成海量结果,因此需要动用数十万计的计算核心和成千上万台计算机。但使用这些资源必须足够简便。
并行数据库确实存在,我们也曾赞扬过其编程模型的优点。然而,它们通常价格昂贵,几乎无一例外,并且配置复杂。此外,许多并行数据库是否能真正扩展到数百或数千台机器,这一点并不完全明确。
MapReduce正是在这样的背景下应运而生,某种程度上是对上述情况的一种回应。它是一个更轻量级的框架,具备我们讨论过的自动并行化与分布式处理能力,并提供了容错性。此外,它还有一些或许相对次要但依然重要的特性,如I/O调度和状态监控。
MapReduce的核心思想是:剥离并行数据库提供的众多功能,只专注于并行处理本身,并额外赋予其容错的优势。这个设计恰好满足了人们的迫切需求。
这里我想提出一个观点:如果当时有一个开源的并行关系型数据库产品可用,MapReduce是否还会如此流行,这一点并不完全确定。因为当时所有的开源数据库都不是并行的,事实上,它们在查询处理时甚至是单线程的,一次只能由一个线程处理单个查询。当然,这只是一些推测,我后续会提供一些证据来支撑这个论点。
下一节预告
接下来,我打算深入探讨并行数据库的工作原理。我希望通过对比,展示它们与MapReduce之间的相似之处,同时也明确指出它们的差异所在。
总结


本节课中,我们一起学习了大规模数据处理系统的设计空间。我们了解了以三条轴线(延迟/吞吐量、部署范围、架构模型)划分的不同系统类型及其典型代表,并分析了MapReduce在其特定历史和技术背景下兴起的原因。这为我们后续比较不同大规模数据处理范式奠定了基础。
大规模数据科学(大数据操作,第1课/共3课) - P68:并行与分布式查询处理 🚀

在本节课中,我们将学习关系数据库查询处理的两个重要扩展概念:并行查询处理与分布式查询处理。我们将探讨它们的基本思想、工作原理、区别以及它们与MapReduce模型的关系。
概述
关系数据库的核心思想之一是关系代数,它允许我们构建查询计划。通常,我们使用SQL这种高级语言来编写查询,系统会自动将其转换为关系代数计划。
然而,在MapReduce范式中,这种自动转换的便利性被部分舍弃,以换取程序在数据处理上拥有更高的灵活性和控制力。
现在,我们希望评估这些查询,但要并行地进行。这就需要我们理解两个关键术语:分布式查询处理和并行查询处理。两者都旨在利用更多计算资源来执行同一查询,但它们的运作方式有所不同。
分布式查询处理
上一节我们提到了关系代数和SQL。本节中,我们来看看分布式查询处理。
在分布式查询中,我们的做法是:将一张大表分布到一个集群的多个节点上(正如我们之前讨论过的数据分区),然后将查询分解成多个独立的子任务,让每个任务在文件的一个分区上运行。
这听起来很像MapReduce,对吗?确实如此,但有一个关键区别:在典型的分布式查询系统中,所有子任务的结果最终都会被发送回一个主节点(Head Node) 来完成最终处理。
核心概念示例:
假设我们要对一个分布在多台机器上的超大文件执行计数操作(COUNT)。分布式查询系统(例如Microsoft SQL Server)足够智能,可以将查询分解并在多台机器上并行运行计数子任务。但是,这些子计数结果通常需要被发送回主节点进行汇总。
-- 分布式查询示例:计数操作可能被分解执行,但结果需汇总
SELECT COUNT(*) FROM large_table WHERE condition;
分布式查询的局限性:
虽然它获得了部分并行性,但并非所有操作都能并行完成。当需要将所有中间结果发送回单一节点进行最终处理时,就可能形成性能瓶颈。你可以将其理解为只有 Map阶段,而没有真正独立的 Reduce阶段。
并行查询处理
了解了分布式查询后,我们进一步探讨更彻底的并行化方法:并行查询处理。
在真正的并行查询中,关系代数中的每一个独立操作符(如连接JOIN、分组GROUP BY)本身都是以并行方式实现的。这意味着,当执行连接操作时,是在多个节点间并行进行的;执行分组操作时,也是在多个节点上并行完成的。
我们已经在MapReduce中见过如何实现关系连接,这与数据库内部的实际实现方式相去不远。如果我们知道如何实现连接(通常是最复杂的操作之一),那么其他操作也可以用类似的方式实现。
核心概念示例:
以Teradata系统为例(一家专注于高端企业级数据库的公司)。在这种并行数据库中,每一行数据在插入时,都会通过一个哈希函数被分配到集群中的某个特定服务器上。
# 概念性代码:通过哈希函数决定数据行归属的服务器
server_id = hash(row_key) % total_servers
因此,数据被自动、近乎随机地分区到整个集群中。当运行查询时,所有机器都会并行访问和处理自己持有的那部分数据。这和我们之前在MapReduce中实现关系连接的思想非常相似。
视图与分布式表
现在,让我们将并行处理的概念与数据库的另一个功能——视图——结合起来看。
如果你不记得视图是什么,它可以被理解为一个已命名的查询,之后可以像访问单个表一样访问它。
在分布式场景下,我们可以这样构建一个视图:假设sales(销售)表实际上是许多按月划分的小销售表的联合(UNION)。我们可以将每个月的销售表放在不同的磁盘甚至不同的服务器上。
创建分布式表的示例:
-- 概念性语句:创建基于多个分区表的视图
CREATE VIEW sales AS
SELECT * FROM sales_january
UNION ALL
SELECT * FROM sales_february
UNION ALL
SELECT * FROM sales_march;
-- ... 以此类推,每个子表可能位于不同的物理位置
查询sales表的用户无需关心数据实际上是分布存储的。系统会自动从一月、二月、三月等分区中收集结果并将其组合。这由系统自动完成,对用户透明。
然而,其处理模式可能仍属于分布式查询:数据读取可以并行,但最终处理可能仍汇集于一点。
总结
本节课中,我们一起学习了大规模数据查询处理的两种核心范式:
- 分布式查询处理:将大数据集分区存储,并行执行查询的子任务,但最终结果通常需要汇集到一个中心节点进行处理。它提供了部分并行性,但在最终聚合阶段可能存在瓶颈。
- 并行查询处理:通过哈希等方式将数据自动分布到集群,并在操作符级别实现全面并行(如并行连接、并行分组)。每个节点独立处理自己的数据分区,更接近MapReduce中“Map”与“Reduce”完全分布式的思想。

两者都是对传统关系数据库查询模型的扩展,旨在利用集群资源提升处理海量数据的性能。理解它们的区别有助于我们根据具体场景选择合适的数据处理架构。
大规模数据科学(大数据操作,第1课/共3课) - P69:Teradata示例:MapReduce扩展 🧩

在本节课中,我们将通过Teradata数据库的具体示例,来理解大规模并行处理(如MapReduce)与关系型数据库操作(如连接查询)在底层思想上的共通性。我们将看到,看似不同的系统如何采用相似的并行处理模式。
查询与执行计划 📋
首先,我们有一个涉及Orders(订单)和Line Items(订单明细)的查询。其执行计划是:先筛选一部分订单,然后将这些订单与明细项进行连接。
上一节我们介绍了查询的基本概念,本节中我们来看看这个查询在Teradata这样的并行数据库中是如何具体执行的。
并行处理单元:AMP
在Teradata的术语中,并行处理的基本单元被称为AMP。每个AMP包含数据的一个分片。数据分片的划分方式通常是基于哈希函数随机定义的。
以下是AMP并行处理查询的步骤:
- 所有AMP并行开始扫描各自持有的数据分片。
- 所有AMP并行应用筛选条件,过滤掉不需要的记录。
- 所有AMP并行基于连接键(此处应为
Order ID)进行哈希计算。
这个哈希操作会将数据重新分布(或称“洗牌”)到另一组AMP上,这组AMP将负责执行下一步操作。
与MapReduce的类比 🔄
这个过程看起来非常像一个MapReduce作业。
- 前面的扫描和筛选步骤,类似于Map函数。
- 基于连接键哈希并重新分布数据,是为后续操作做准备。
- 接下来的连接操作本身,则类似于Reduce函数。
对于另一个关系(Line Items表),会发生完全相同的过程:扫描数据,然后基于相同的连接键(Order ID)进行哈希。
这样,所有具有相同Order ID的订单记录和明细记录,最终都会被哈希到同一台机器(同一个AMP)上。这与我们之前看到的MapReduce连接示例完全一样。
完成连接操作 ✅
经过上述两个并行步骤后,数据已经根据连接键聚合。
例如:
- AMP 4 将拥有所有哈希值等于1的订单和明细。
- AMP 5 将拥有所有哈希值等于2的订单和明细。
- AMP 6 将拥有所有哈希值等于3的订单和明细。
此时,每个AMP都拥有了足够的信息,可以独立且并行地完成最终的连接操作,并产生结果。
抽象的价值与生态系统 🧠
关键在于,这种并行处理的机制在并行数据库和MapReduce中都已存在。在MapReduce中执行连接操作,本质上需要自己实现这套逻辑。
这一观察催生了许多工具,它们旨在为MapReduce提供标准化的高级操作接口,使用户无需每次都重写底层逻辑。
以下是几个重要的系统,它们都体现了关系代数的思想:
- Pig(来自Yahoo):提供了一套类SQL的语言,其操作符(如
JOIN、GROUP BY)可被识别为关系代数。它允许稍微复杂的数据嵌套。 - Hive:直接在Hadoop之上提供了SQL接口,比关系代数更进一步。
- Impala(来自Cloudera):一个较新的系统,为HDFS提供高性能的SQL查询,并使用了Hive的许多代码。
- Cascading:另一个系统,也清晰体现了关系代数。
- Dryad:虽然与MapReduce无直接关系,但动机相似,其内部也明显使用了关系代数。
- C-Store:一个研究型项目,同样基于关系代数。
总结 📝
本节课中,我们一起学习了Teradata如何执行一个并行连接查询,并揭示了其底层逻辑与MapReduce模式的深刻相似性。更重要的是,我们认识到关系代数是一套强大且可模块化的概念,它已经超越了传统数据库的边界,渗透到Hadoop等众多大数据处理系统中。

当你戴上“关系代数”的眼镜观察世界时,你会发现这种思想无处不在。因此,回过头深入理解这些基本操作,是非常有益的。
大规模数据科学(大数据操作,第1课/共3课) - P70:RDBMS与MapReduce特性对比 🆚

在本节课中,我们将深入探讨关系型数据库(RDBMS)与MapReduce框架之间的核心特性差异。我们将超越查询处理的表层,分析它们在数据模式、索引、容错性以及编程模型等方面的不同设计哲学与实现。
概述
上一节我们介绍了并行查询处理的基本策略在两者间的相似性。本节中,我们来看看关系型数据库所具备、而传统MapReduce所缺乏的一系列重要特性,并探讨现代大数据系统如何融合双方的优势。
关系型数据库的特性
以下是关系型数据库提供的一些关键特性,这些特性在早期MapReduce中并不直接具备。
数据模式(Schema)
关系型数据库要求在数据入库前定义并强制执行一个模式。任何不符合模式的数据都会被系统自动拒绝。这有助于保持数据清洁。其约束可表示为:
数据记录必须符合预定义的结构(例如:列名、数据类型、约束)
然而,在大数据场景下,数据往往“天生脏乱”,要求先清洗再处理通常不现实。这正是MapReduce的一个吸引力所在:它不强制要求预先定义模式。
但这并不意味着模式是个坏主意。即使在MapReduce中,模式也以隐式方式存在——它隐藏在应用程序代码里。例如,当你读取一条记录时,你假设第一个字段是整数,第二个是日期,第三个是字符串。这个模式是存在的,只是没有被“推入”系统底层。
近年来的大量实证研究表明,尽可能将模式下推到数据层是更优的做法。因此,我们看到Hive、Pig、Spark等系统都引入了自己的模式概念。
索引(Indexing)
索引是支持对数时间复杂度数据访问的重要结构。在原生MapReduce中,每次作业启动都会扫描输入中的所有记录,无法快速定位到特定记录,这被认为是低效的。
为了解决这个问题,人们为Hadoop生态系统添加了索引功能。例如,HBase(Google BigTable的开源实现)提供了对单个记录的快速访问,并与Hadoop兼容。这样,系统设计者就能同时获得MapReduce编程接口和索引能力的双重优势。
逻辑数据独立性与物化视图
逻辑数据独立性主要指视图的支持。传统Hadoop类系统对视图的支持不多,但预计未来会出现。物化视图是预计算并存储的视图,与运行时动态计算相对,在此不做深入讨论。
事务(Transactions)
关系型数据库非常擅长处理事务。但在MapReduce和NoSQL兴起的初期,事务特性曾被暂时搁置。如今,事务支持正在以新的形式回归大数据系统。需要注意的是,MapReduce提供的是极高的可扩展性(数千台机器)和容错性,这与基于事务的恢复机制不同。
MapReduce的核心优势
容错性(Fault Tolerance)
关系型数据库通过事务保证恢复后数据不丢失,但这不同于在长时间运行的查询过程中处理故障。其隐含假设是:查询不会运行那么久,故障无需特别处理。
在大数据时代,分析查询可能运行数小时甚至数天,并且是在成千上万台必然会发生故障的机器上运行。MapReduce的设计动机之一就是解决这种场景下的容错问题。现在,现代并行数据库也开始引入某种形式的容错机制。
编程模型的民主化
MapReduce一个真正强大的地方在于,它让广大的Java程序员变成了分布式系统程序员。一个普通的Java开发者突然之间就能高效地处理数百TB的数据,而无需深入学习分布式系统的复杂性。
相比之下,要使用传统数据库,你通常需要先成为一名数据库专家。这种降低分布式计算门槛的影响怎么强调都不过分。一个人就能完成过去需要一个庞大团队耗时数月才能完成的工作,这是意义重大的。
总结
本节课中,我们一起学习了关系型数据库与MapReduce在多方面的特性对比。我们看到,关系型数据库在模式、索引、事务等方面提供了严谨的保障,而MapReduce则在可扩展性、容错性以及编程简易性上具有突破性优势。

当前大数据领域的设计趋势是混合与匹配:人们不再非此即彼,而是从两者中挑选合适的特性进行融合。最初的设计空间主要由关系型数据库占据,MapReduce的出现重启了探索,如今我们正处在一个功能选择更为灵活、系统设计更为多元的时代。
课程1:RDBMS与Hadoop性能对比分析 🚀

在本节课中,我们将学习一篇2009年论文的核心内容,该论文直接对比了Hadoop与两种不同类型数据库的性能。我们将重点分析一个名为“Grep”的特定任务,并探讨不同系统在数据加载和查询执行阶段的表现差异。
上一节我们介绍了本次对比实验的背景,本节中我们来看看实验的具体设置。
实验对比了三个系统:
- Hadoop:一个基于MapReduce的分布式计算框架。
- Vertica:一个列式存储数据库。
- DBMS X:一个未具名的传统行式关系数据库。
虽然我们尚未深入探讨列式与行式数据库的区别,但为了理解本次实验,你可以将它们视为采用了不同底层技术的两种关系型数据库。
实验分析包含定性和定量两个方面。定性部分讨论了编程模型和系统搭建的便捷性。定量部分则针对特定类型的查询进行了性能测试。
以下是他们考虑的第一个任务:Grep任务。
该任务的目标是在一个包含100亿条记录(总计1TB数据)的数据集中,查找一个特定的3字节模式。每条记录大小为100字节。这个任务最初出现在2004年的MapReduce论文中,因此非常适合作为基准测试。
数据集被分布在25、50或100个节点上。
上一节我们了解了Grep任务,本节中我们来看看数据加载阶段的性能结果。
加载数据的结果如下图所示:

在Hadoop上加载数据很快,而在关系数据库(DBMS X)上加载则很慢。Vertica的加载速度也很快。
为什么Hadoop加载更快?
在Hadoop(HDFS)中加载数据主要涉及数据分区,没有太多额外工作。而将数据加载到数据库中时,需要将原始数据重新组织并转换为数据库的内部结构,这需要时间。如果还需要为数据建立索引,那么每次插入数据时都需要维护索引数据结构,这会使加载过程更慢。
关键结论:与传统关系数据库相比,Hadoop的数据加载时间通常更短,因为数据库需要做更多的预处理工作。
在25台机器的实验中,Hadoop的加载时间约为2500秒,而DBMS X的加载时间超过25000秒。随着服务器数量增加,时间有所减少。
数据加载完成后,我们进入查询执行阶段。现在来看看运行Grep任务查找数据的结果。
执行Grep任务的结果如下图所示:

在这个任务中,Hadoop的速度比数据库(DBMS X)慢。
为什么此时数据库更快?
Grep任务需要对所有数据进行全表扫描,无法利用索引进行加速。因此,理论上数据库并没有根本性的优势。然而,数据库更快的主要原因在于:
- 数据库得益于其结构化的内部数据表示。数据已经以一种紧凑的二进制格式存储,查询时无需像Hadoop那样从磁盘重新解析原始数据格式。
- 数据库在加载阶段付出了转换数据的代价,但在查询阶段获得了性能收益。
核心差异:数据库查询速度 = 利用加载阶段的结构化优势,而Hadoop查询速度 = 原始数据解析开销 + 全量处理。
即使在讨论索引之前,数据库因其内部数据表示的优势,在全扫描查询中也能表现得比Hadoop更好。

本节课中我们一起学习了Hadoop与关系数据库(RDBMS)在Grep任务上的性能对比。我们了解到,Hadoop在数据加载阶段具有优势,因为它无需进行复杂的数据转换和索引构建。然而,在需要全表扫描的查询执行阶段,关系数据库可能更快,因为它可以充分利用加载阶段创建的高效内部数据表示结构。这种“加载慢、查询快”与“加载快、查询慢”的权衡,是选择不同数据处理平台时需要考虑的重要因素。
课程名称:大规模数据科学(大数据操作,第1课/共3课) - P72:RDBMS与Hadoop:选择、聚合、连接 🗃️⚖️

在本节课中,我们将学习关系型数据库管理系统(RDBMS)与Hadoop在处理不同数据任务时的性能对比,具体包括选择、聚合和连接操作。我们将通过一个具体的实验案例,分析两者在不同场景下的表现差异及其背后的原因。
选择任务:索引的优势
上一节我们讨论了可扩展性,本节中我们来看看一个具体的选择任务。

在一个选择任务中,如果不需要扫描每一条记录,那么建立索引是可行的方案。正如我们在可扩展性部分讨论过的,索引可以极大地提升查询效率。
实验结果显示,Hadoop的处理时间远高于数据库,特别是远高于Vertica(一种列式数据库)的结果。原因在于,你可以在 pay_rank 属性上建立索引,并直接定位到你感兴趣的记录。
以下是选择任务的核心特点:
- 这类任务本质上是搜索和检索,并非Hadoop最初设计的主要目标。
- Hadoop的设计更侧重于分析型任务。
聚合任务:SQL的简洁与性能
现在,让我们考虑一个聚合任务。
实验数据集包含60万个文档(每个节点约6GB数据),以及另外两个数据集:151.55亿条用户访问记录和1800万条排名记录。这是一个典型的网络数据处理任务。
一个简单的聚合任务是:汇总特定子域名对应的所有广告收入。他们使用 substring 函数提取源IP地址的前6个字符(即子网掩码的前缀),并按此分组,然后累加广告收入。
需要指出的一点是,这个任务可以非常简洁地用SQL查询来表达,而不必编写大量的Java代码和MapReduce程序。
以下是实验结果和分析:
- 关系型数据库再次击败了Hadoop。
- 原因可能不那么直观,但本质上是因为数据库的内部表示方式更高效,无需进行数据解析。
连接任务:复杂查询的实现
在同一数据模式上,还存在一个连接任务。
该任务是:找出产生最多广告收入的源IP地址,并计算其平均页面排名。这涉及对数据进行多次复杂处理,例如计算平均页面排名、找到最大广告收入对应的源IP,然后再计算该IP的平均排名。
以下是两种技术的实现方式:
- 在SQL中,这需要一个相当复杂的SQL语句,可能涉及临时表的使用。
- 在MapReduce中,则需要将三个独立的MapReduce作业链接在一起。
我们不会过于详细地分析这个复杂的SQL语句,但请注意其中包含一个连接(JOIN)操作和一个分组(GROUP BY)操作。你可以看到涉及两个需要连接的表,然后进行分组操作,这是该任务的两个核心步骤。

第二个步骤是进行一次大规模排序(ORDER BY),以找出最顶部的记录。


总结

本节课中,我们一起学习了RDBMS与Hadoop在处理选择、聚合和连接三类任务时的性能表现。关键结论是:对于需要索引的快速检索任务,RDBMS优势明显;对于简单的聚合任务,SQL的表达简洁性和数据库内部优化使其更高效;对于复杂的多步骤连接与分析任务,两者实现方式不同,但都能完成,性能差异取决于具体实现和数据特性。理解这些差异有助于在实际工作中为不同的数据处理需求选择合适的技术工具。
大规模数据科学(大数据操作,第1课/共3课)🚀:NoSQL背景与路线图

在本节课中,我们将要学习NoSQL数据库的背景知识及其在大数据生态系统中的定位。虽然本课程的核心是数据分析,但理解NoSQL系统对于数据科学家至关重要,因为它影响着我们处理大规模数据的方式和平台选择。
为何需要关注NoSQL?🤔
上一节我们介绍了数据库和MapReduce在数据准备阶段的应用。本节中,我们来看看NoSQL系统。这些系统通常与构建大型、可扩展的Web应用相关联,而非数据分析。然而,数据科学家仍需了解NoSQL,原因如下:
首先,作为数据科学家,你处理的数据越来越多地存储在各类NoSQL系统中。其次,该领域的系统和术语深刻影响着人们处理大规模数据的思维方式。了解其发展脉络和主要趋势,有助于你做出明智的技术选型决策。例如,你可能会被要求为分析工作推荐合适的平台。
第三,我们在其他章节讨论过的核心概念,如关系代数、逻辑数据独立性、简单可扩展的分析以及索引等,在NoSQL领域同样适用。这为你提供了应用这些概念的另一个场景。最后,数据科学家有时也需要参与构建大规模系统,作为数据产品的一部分,而NoSQL系统可能是其中的组件。
课程进度回顾与定位📍

目前,我们仍处于数据准备的阶段。我们已探讨了如何使用数据库和MapReduce进行数据操作与整理。关键要点包括:关系代数的概念不仅限于SQL系统;物理与逻辑数据独立性反复出现;索引的重要性;以及MapReduce的基本操作和容错性等优势。
MapReduce的一个显著优势是允许开发者直接对原始数据进行编程处理,无需像传统数据库那样必须先定义模式并加载数据。这大大降低了入门门槛,单个开发者也能快速上手。
NoSQL的核心概念与分类🗺️
接下来,我们将深入NoSQL世界。理解其核心思想有助于把握不同系统的特点。
NoSQL系统主要围绕以下几个核心思想构建,它们通常为了追求扩展性、灵活性或性能而在某些方面放松了传统关系型数据库的约束:
以下是NoSQL系统的一些关键设计理念:
- 模式灵活性:数据模式可以动态变化,无需预先严格定义。
- 最终一致性:系统保证如果不再有新的更新,最终所有访问都将返回最后更新的值,这不同于关系型数据库的强一致性。
- 分区容错性:系统设计能在部分节点故障或网络分区时继续运行。
- 简单查询接口:通常提供比SQL更简单的API,有时以牺牲复杂查询能力为代价。
根据数据模型,NoSQL系统主要分为几类:
- 键值存储:最简单的模型,通过唯一键访问值。例如:
GET user:12345。 - 文档数据库:值通常是结构化文档(如JSON、XML)。例如,一个用户文档可能包含嵌套的地址信息。
- 列族存储:数据按列族组织,适合大规模分布式存储与分析。例如,HBase。
- 图数据库:专注于实体(节点)和关系(边),适合社交网络、推荐引擎等场景。
总结📚

本节课中,我们一起学习了NoSQL系统的背景、其对于数据科学家的价值,并回顾了我们在数据准备阶段已掌握的工具。我们明确了NoSQL的核心设计理念与主要分类。理解这些内容,将帮助你在面对大规模数据处理和存储的技术选型时,拥有更清晰的视野和判断依据。在接下来的课程中,我们将继续深入大数据操作的其他方面。
大规模数据科学(大数据操作,第1课/共3课) - P74:NoSQL综述 📚

在本节课中,我们将学习NoSQL系统的核心概念与特性。我们将通过一个特性对比表,系统地了解不同NoSQL系统的设计目标、功能差异以及它们与传统关系型数据库的区别。

我们尚未讨论的是NoSQL系统。因此,我想用这个表格来组织本次讨论的路线图。我在此尝试按特性列出该领域中的一系列相关系统。目前,这些系统大致按时间排序。这些特性是由我选择的,我认为它们很重要,但我觉得这不会有太大争议,也不认为这里明显遗漏了什么。
以下是这些特性的简要说明。
首先,一个主要特性是系统需要能够扩展到数千台机器,并且需要通过主键进行查找。这意味着可以通过某个键值或记录标识符来查找记录。
其次,另一个可能具备的特性是通过二级索引进行查找。这意味着可以通过非键值的属性进行查找。例如,在数据库中,你可以在任何属性上建立索引,优化器会利用它。
第三,我们将花大量时间讨论事务特性,因为它对于NoSQL系统获得其独特称谓或不同架构的动机至关重要。这里存在一些复杂性,我会在讲解过程中尝试解释。这不仅仅是“是”或“否”的问题,而是视具体情况而定。
接下来,这个字段是关于这些系统是否支持连接操作,但我已将其泛化为分析能力。我可能犯了将这两者几乎等同起来的错误。如果你能进行连接操作,那么你就可以进行各种分析;如果不能,那么所有这类工作都必须由客户端完成,系统只能检索数据。这真正表明了你能将多少计算推入系统,以及必须将多少数据带回客户端处理。
然后是完整性约束。我考虑过称之为“模式”,但我们会看到,有些系统确实有模式,但可能并不强制执行该模式。为了避免混淆,我称之为“完整性约束”。这可以看作是一种严格的模式。
接着是视图,我将其声明为与逻辑数据独立性同义。
最后,是否存在某种声明性语言或代数方法来对此系统进行编程,还是它只是一个低级的简单操作API?
这里有几个注意事项。首先,我完全没有在这个列表中包含任何并行数据库,尽管你完全有理由将它们包含进来。它们往往在这个表格中会有很多勾选标记。但由于重点是NoSQL系统,我将它们排除在外。
另一个注意事项是,表格中的各个单元格可能根据你对列的解释而存在争议。因此,这不一定意味着是硬性规定,但我同样不认为它们会引起巨大的争议。
通过审视这个表格,你可以得出的第一个结论是:关系数据库已经存在了很长时间,并且在几乎所有特性上都有勾选标记,具备所有这些功能,但它们从未被证明能够扩展到非常多台计算机。一切都停留在几十台机器的规模上。
那么,为什么它们不能扩展呢?我们在MapReduce部分讨论过读取性能及相关问题,并分析了一篇2009年论文中比较MapReduce与数据库优势和强度的实验结果。也许在读取性能上,有论点认为它们确实可以扩展。但有一个领域它们肯定没有,或者至少没有被证明能扩展到这种水平,那就是更新操作。不仅仅是读取和分析工作负载,还包括事务处理工作负载。


本节课中,我们一起学习了NoSQL系统的核心特性,包括可扩展性、索引类型、事务支持、分析能力、完整性约束、视图和查询接口。通过与传统关系型数据库的对比,我们理解了NoSQL系统在设计上的不同侧重点及其适用场景。
大规模数据科学(大数据操作,第1课/共3课) - P75:放宽一致性保证 🧩

在本节课中,我们将探讨在大规模分布式系统中,为何以及如何放宽传统数据库的强一致性保证,以换取更高的可用性和可扩展性。我们将从数据分片与复制的架构出发,分析强一致性模型在大型应用中的局限性,并介绍NoSQL系统为解决这些问题所采用的设计思路。
上一节我们介绍了数据分片与复制的基本架构,本节中我们来看看在这种架构下如何处理数据更新。
我们从一个与讨论MapReduce和可扩展性时相同的示意图开始。我们取一个大数据集,将其分解成多个数据块,并将这些块发送到不同的机器上。

在这里,我们将这个数据块复制到三台不同的机器上。这与我们在Hadoop文件系统中为容错所做的操作相同。这样做的目的是,如果一台机器宕机,我们仍然可以从另外两个副本中获取数据。我们对每一个数据块都执行此操作。
但这里我们需要满足两个要求:一是确保高可用性,以便在出现故障时数据仍然可访问;二是在此背景下支持数据更新,这与我们之前讨论的只读场景不同。因此,我们不仅关注读取性能或读取容错,还希望现在能对数据进行更改,并将这些更改传播到其他副本,在某些情况下,还要传播给其他关注这些变更的消费者。可能存在其他数据块引用了相同的信息,我将在下一张幻灯片中举例说明。
想象一个社交网络应用,用户更新自己的状态,而他们的好友可以看到这些状态更新。这里的正确操作是Sue更新她自己的状态。我们向她好友提出的问题是:会发生什么?谁看到新状态,谁看到旧状态?这个状态变更如何传播?
从数据库的角度来看,这个问题的答案是:要么所有人都必须看到新变更,要么没人看到。也就是说,事务要么提交,并且所有地方的数据副本都同时同步;要么不提交。此外,任何试图在新状态下读取该值的人,要么只能读取旧值,要么必须等待事务提交。这个等待时间可能相当长,甚至是任意的。死锁也可能发生,这就是我说“任意”的原因。
所以,这是数据库给出的答案:一切都是同步的,所有东西都必须更新,要么全有要么全无。
而NoSQL系统则提出了不同的观察。他们认为,对于真正的大型应用,我们根本无法承受为等待这种同步而花费任意长的时间。我的意思是,状态更新需要能够提交并响应,以便用户可以继续做其他事情。他们不能看着沙漏图标,而同步仍在进行。
进一步的观察是,也许这并不重要。我的意思是,如果Sue的朋友Joe看到了新状态,而Kai仍然看到旧状态,这真的那么重要吗?也许谁在乎呢?只要Kai最终能看到新状态,可能就足够好了。
这些观察建议在设计空间中向一个不同的领域移动,即高可扩展性、高可用性和一致性(应用一致性)的权衡领域。这类系统开始被视为一种反数据库,采取了与数据库截然不同的方法。因此,“NoSQL”这个术语开始流行。
实际上,“NoSQL”这个名字被固定下来有点不幸,因为它与SQL本身关系不大。它更多是与数据库的事务处理ACID特性相关,这与查询语言SQL并不完全相关。事务模型是关于读写序列的,与查询语言无关。但无论如何,这个名字已经固定下来了。
需要说明的是,“NoSQL”这个术语不仅暗示了不同的事务模型,也暗示了更弱的数据模型等等。我们稍后会讨论一点。但我希望这个观点能被理解,因为这是关键思想之一。


本节课中我们一起学习了传统强一致性模型在大规模分布式系统中的挑战,以及NoSQL系统通过放宽一致性保证来优先满足高可用性和可扩展性的核心理念。我们理解了在社交网络状态更新等场景中,“最终一致性”可能是一个更实用、更高效的设计选择。
大规模数据科学(大数据操作,第1课/共3课) - P76:两阶段提交与共识协议 📚

在本节课中,我们将学习分布式系统中如何确保数据一致性。具体来说,我们将探讨两阶段提交协议以及更高级的共识协议(如Paxos),了解它们如何解决多个副本间数据同步的问题,以及各自的优缺点。

数据库如何解决数据一致性问题?为何解决此问题耗时较长?一个名为两阶段提交的协议是此类同步处理场景中的标准解决方案。
其设计动机如下:假设你有一组副本或其他类型的从属节点(例如,所有需要看到状态更新的好友服务器)。如果你直接通知它们进行更新(例如,“我已更改状态,请更新你的内部状态以反映Sue的新状态”),可能会出现问题。部分节点可能成功,但其中一个可能失败。此时系统将处于不一致状态:失败的节点保留旧值,而其他节点已更新为新值。
那么,如何解决这个问题?答案是两阶段提交协议。
两阶段提交协议详解 🔄
上一节我们介绍了数据不一致的问题,本节中我们来看看两阶段提交协议如何通过两个阶段来确保所有节点要么全部提交更新,要么全部放弃。
第一阶段:准备阶段
协调者向所有从属节点发送“准备提交”消息。从属节点必须确保无论发生什么情况,当被要求提交时都能执行该事务。通常,这意味着将事务相关信息写入日志。即使断电,节点恢复后也能从日志中恢复。随后,从属节点回复“是,我已准备提交”。
第二阶段:提交或中止阶段
如果所有从属节点都回复准备就绪,协调者将发送“提交”消息。如果有任何节点失败,协调者则发送“中止”消息,各节点进行清理。
以下是该过程的示意图:
- 协调者发送“准备”消息。
- 所有从属节点预写日志并回复“准备就绪”。
- 协调者发送“提交”消息。
- 所有节点完成最终提交。
(示意图未展示失败情况,但本质上协调者需要监控并在出错时发送“中止”消息。)
两阶段提交的局限性与共识协议 🧩
尽管两阶段提交解决了基本问题,但它存在一些缺陷。主要问题是对协调者的依赖:如果协调者在错误时间点故障,系统可能陷入混乱。
为了在完全分布式的对等网络中确保事务的相互承诺,需要使用共识协议。其中最成功和流行的方法是Paxos算法(我们不会深入细节,但你在阅读NoSQL系统相关文献时会看到这个术语)。
简而言之,可以将两阶段提交理解为用于数据库本地集群的协议,而Paxos则是用于分布式对等网络的协议。
Paxos本质上是一种投票机制。各个服务器需要自行决定是否应该提交事务。其细节可能有些微妙,但考虑到任务的复杂性,其整体思想相对简单。
除了协调者单点故障问题,两阶段提交的另一个问题是耗时可能较长。如果从属节点响应不及时,协调者可能需要等待;如果多次失败并在应用层中止和重试事务,过程会变慢。当需要协调成千上万个从属节点时,该协议难以扩展。
其他并发控制机制 ⚙️
还有一些其他协议,我们不会深入讨论,但会在相关论文中看到,例如多版本并发控制。在该机制中,每次写入都会创建数据项的一个新版本。读取的合法性通过比较读取事务的时间戳与试图读取的版本的当前时间戳来确定。如果在你应该读取的时间点之后数据已被更新,在严格的并发控制下,你只能中止读取并告知数据已脏。但通过多版本并发控制,系统可以保留多个版本,并将读取重定向到之前正确的版本,从而避免中止某些事务。
然而,该机制仍然依赖一个协调者角色来管理时间戳。
完全分布式方案:Paxos 🗳️
在完全分布式方案中,是否继续推进或中止事务的决定,是通过对等节点间的投票机制做出的,这就是Paxos。Paxos非常成功且应用广泛,如果你花时间阅读课程阅读清单中的NoSQL论文,会看到它的提及。
Paxos减少了对中央协调者的依赖,但它仍然是同步的,仍然存在死锁的可能,并且根据发生的故障类型,达成共识可能需要一定时间,因此难以保证极高的性能或极低的延迟。

响应时间。

本节课总结
本节课中,我们一起学习了分布式系统中确保数据一致性的核心机制。我们深入探讨了两阶段提交协议的工作流程与局限,并介绍了更高级的Paxos共识协议如何通过投票机制在分布式对等网络中实现事务承诺。此外,我们还简要了解了多版本并发控制等其他方案。理解这些协议是掌握大规模数据系统协调与一致性的基础。
大规模数据科学(大数据操作,第1课/共3课) - P77:最终一致性 🧩

在本节课中,我们将学习分布式系统中的一个核心概念——最终一致性。我们将探讨其起源、基本含义、与强一致性的对比,以及它在不同NoSQL系统中的具体体现。
最终一致性的起源
上一节我们介绍了分布式系统的基本挑战,本节中我们来看看“最终一致性”这一术语的起源。
“最终一致性”这一术语最初并非主要在其实用性和允许系统扩展至极大规模的背景下定义。它更多源于一个论点:在分布式系统中,能够就如何处理冲突做出恰当决策的唯一合适参与者是应用程序本身。
这实际上是端到端原则的一个版本,该原则在网络领域可能已有所了解。这一术语由道格·特里在1995年的一篇论文中提出。他认为:
应用程序必须意识到,它们读取的可能是弱一致性的数据,其写入操作可能与其他用户和应用程序的操作冲突。应用程序必须参与冲突的检测和解决,这自然取决于应用程序的语义。
需要说明的是,我个人并不完全赞同这些断言。我认为,在系统能够处理时,由系统来负责处理可能更好。但介绍这些是为了让大家了解该术语的起源,而非近十年左右随着NoSQL系统流行而兴起的概念。
最终一致性的含义
了解了其起源后,我们来看看它的具体含义。
在没有更新操作的情况下,所有副本最终将收敛为相同的拷贝。这意味着,只要数据不再持续变动,当变更稳定下来后,所有节点最终都会看到相同的值。例如,你的所有朋友最终都会看到你的最新状态,而不会永久卡在旧状态上。
然而,在达到最终一致之前,应用程序(或你的某个朋友)具体看到哪个状态,高度依赖于你所构建应用程序的内部细节,因此难以预测。
正因如此,很难非常精确或形式化地推理“最终一致性”的含义,因为它极度依赖于那些本身就难以形式化的具体实现细节。
与强一致性的对比
上一节我们探讨了最终一致性的模糊性,本节中我们将其与另一种模型进行对比。
通常,我们可以将此与关系型数据库和Paxos等协议所保证的强一致性进行对比。强一致性系统可能面临死锁问题。可以证明,任何系统都无法在保证一致性的同时完全避免死锁。
因此,关系型数据库和Paxos协议放弃了活性属性,即它们可能允许死锁发生,以换取强一致性。虽然可以通过不同的设计决策使死锁情况变得罕见,但它们仍然可能发生。
而最终一致性模型则认为,我们无法承担等待这些协议运行完成的代价。此外,在某些应用场景下,强一致性可能并非必要。
NoSQL系统中的一致性支持
现在,我们来看一个总结了不同系统一致性支持的表格。我们将具体分析这些术语的含义。
以下是不同NoSQL系统在一致性支持方面的对比:
- 强一致性事务的支持范围:这指的是在多大范围内,系统能保证事务的强一致性(即所有变更要么全部生效,要么全部不生效)。
- 单个记录:意味着可以更新一条记录中的多个字段,并保证这些更改的原子性。
- 注意:此列表仅过滤了NoSQL系统。关系型数据库支持跨任意记录的事务。
- EC(最终一致):表示系统在记录级别提供最终一致性保证,而非强一致性事务。
- 实体组:这是基于Google Bigtable的Megastore系统引入的概念。它是一组相关的记录,系统保证在该组内的事务具有强一致性。这比仅支持单条记录更好,但不如支持数据库中任意记录。
- Spanner:这是Google最新的系统,它提供跨所有记录的真正强一致性。我们稍后会讨论他们为何做出这个选择。
总结

本节课中,我们一起学习了最终一致性。我们从其学术起源开始,理解了它的核心含义:在更新停止后,系统最终会达到一致状态,但在此过程中的状态难以预测。我们将其与追求原子性和隔离性的强一致性模型进行了对比,认识到最终一致性是以牺牲强一致性为代价来换取可用性和分区容忍性。最后,我们通过一个表格,看到了不同NoSQL系统(如Dynamo、Cassandra、Megastore、Spanner)在一致性支持范围上的权衡与演进,从仅支持单记录最终一致,到定义实体组,再到提供真正的全局强一致性。理解这些模型是设计可扩展分布式系统的关键基础。
大规模数据科学(第1课/共3课) - P78:CAP定理 🧩

在本节课中,我们将学习分布式系统中的一个核心概念——CAP定理。该定理由Eric Brewer在2000年提出,并由Lynch在2002年正式定义,它阐述了分布式系统在一致性、可用性和分区容错性三者之间必须做出的权衡。
概述
CAP定理定义了三个核心概念:一致性、可用性和分区容错性。该定理常被描述为:在分布式系统中,你只能同时满足这三个属性中的两个,必须牺牲第三个。然而,这种理解方式可能过于简化,定理的提出者Eric Brewer本人也指出这可能不是最准确的思考方式。
CAP定理详解
上一节我们概述了CAP定理的基本思想,本节中我们来详细看看这三个概念的具体含义以及它们之间的权衡关系。
核心概念定义
以下是CAP定理涉及的三个核心属性:
- 一致性:在分布式系统中,所有节点在同一时间看到的数据是相同的。任何数据更新操作完成后,所有后续的读取操作都将返回最新的值。这可以理解为系统状态的全局统一性。
- 可用性:系统提供的服务必须始终可用,对于每一个非故障节点的请求,系统必须在合理的时间内返回一个正常的响应(不保证是最新数据)。这可以理解为系统的持续服务能力。
- 分区容错性:系统能够容忍网络分区(即部分节点之间无法通信)的发生,并在分区发生后继续运行。这可以理解为系统对网络故障的容忍能力。
分区容错性的重要性
理解“分区”的含义是关键。在一个包含数百个节点、服务器彼此通信的大型分布式系统中,如果其中一部分节点与另一部分节点失去了通信连接,就发生了网络分区。
此时,系统面临一个选择:这两个被分割的片段能否独立地继续推进应用程序的工作,并在之后网络恢复时再进行同步?还是说,整个系统或部分节点必须停止工作,等待通信重新建立?
例如,在一个主从架构中,如果工作节点与主节点失去了联系,许多设计会要求工作节点在重新建立与主节点的连接之前无法进行任何有效工作,因为它们需要等待来自主节点的指令。在这种情况下,系统牺牲了可用性——这些节点变得不可访问或无法进行有效工作。
权衡的本质
另一方面,如果系统选择让各个分区在网络中断期间继续独立工作,那么不难证明,系统最终可能会进入一个不一致的状态。
网络两端的更新操作各自进行,当通信恢复时,可能会发现副本之间的数据出现了冲突。虽然系统之后可以尝试解决这些冲突,但在冲突解决之前,应用程序可能已经接触到了不一致的数据。因此,在这种选择下,系统牺牲了一致性。
关键在于,你无法同时获得全部三个属性。
传统数据库与NoSQL系统的选择
上一节我们理解了CAP定理的权衡本质,本节中我们来看看不同类型的数据库系统是如何做出选择的。
传统关系型数据库的选择
传统的数据库系统本质上假设网络分区不会发生。这是因为它们通常只在数十个节点的规模上运行,而非成千上万节点或全球范围的分布式系统。
这些节点通常位于同一个数据中心甚至同一个机房内,网络分区在当时并不是一个需要重点考虑的问题。因此,传统关系型数据库的设计目标是保证一致性和可用性,而牺牲了分区容错性(即假设分区不会发生)。
NoSQL系统的选择
NoSQL系统则必须认真对待分区问题。它们规模庞大,分布广泛。由于巨大的规模,各种复杂的故障(拜占庭故障)随时可能发生。
因此,许多NoSQL系统选择牺牲强一致性,以换取高可用性和分区容错性。这意味着在网络分区发生时,系统仍然可以提供服务并接受写入,但不同分区间的数据可能暂时不一致。
图形化理解与系统定位
我们可以用一个三角形来图形化地理解不同系统的定位。

以下是不同类型系统在CAP三角形中的大致位置:
- 关系型数据库:通常位于“一致性-可用性”这条边上。它们假设分区不会发生,因此致力于保证强一致性和高可用性。
- 强调分区容错性和一致性的系统:这类系统需要容忍分区,并保证某种程度的一致性(如最终一致性),但可能在分区期间牺牲部分可用性(例如,某些操作会被阻塞直到多数节点达成一致)。
- 强调分区容错性和可用性的系统:这类系统在网络分区期间保证服务始终可用,但允许数据出现暂时的不一致。许多NoSQL数据库(如Cassandra、DynamoDB在默认配置下)属于此类。
需要强调的是,这里讨论的“一致性”范围非常关键。像Spanner这样的新型全球分布式数据库,通过精妙的技术(如TrueTime API)试图在提供全球级事务的同时,也保证强一致性和高可用性,并容忍分区,但这通常需要付出更高的延迟和复杂性代价。
总结

本节课中,我们一起学习了CAP定理。我们了解到,在分布式系统的设计中,一致性、可用性和分区容错性构成了一个不可兼得的三角。传统数据库通常优先保证一致性和可用性,而假设网络环境可靠;现代大规模分布式NoSQL系统则为了应对不可避免的网络分区和追求高可用性,往往选择放宽对强一致性的要求。理解这一权衡是设计和选择适合业务场景的分布式数据系统的关键基础。
大规模数据科学(第1课) - P79:NoSQL系统类型 🗃️

在本节课中,我们将学习NoSQL系统的不同类型及其核心特征。我们将了解Rick在2010年关于可扩展SQL和NoSQL数据存储的论文中提出的分类法,并探讨NoSQL系统共有的关键特性。


NoSQL系统的分类 📊
上一节我们提到了NoSQL系统的多样性。本节中,我们来看看Rick在其论文中提出的一个具体分类法。他将流行的NoSQL系统实例归纳为三种主要类型。
以下是Rick定义的三种NoSQL数据存储类型:
- 键值存储:数据模型是一组键值对。系统通常不关心键的模式,可以是任意键,并且不暴露嵌套结构。值可以是任何复杂对象,但对系统而言只是一个不透明的“二进制大对象”。
- 文档存储:数据以文档形式存储,例如XML文档或我们在Twitter作业中看到的JSON对象。文档支持任意的嵌套结构,并且是可扩展的,可以随时添加新内容,没有自上而下强制执行的结构模式。
- 可扩展记录存储:可以将其视为类似于数据库记录,但允许为单个行添加新属性。存在某种用于不同目的的模式概念,特别是存在一些被一起操作的属性组(列族),但你可以为单行附加新属性,这在关系型数据库中通常是不允许的。
NoSQL系统的典型特征 ✅
了解了NoSQL的主要类型后,我们来看看这些系统通常具备哪些共同特征。需要承认的是,并没有一个关于NoSQL的正式定义,但这个术语往往被用于具备以下特性的系统。
以下是NoSQL系统常见的特征列表:
- 可扩展的简单操作吞吐量:能够将简单操作(如键查找、属性查找或少数记录的读写)的吞吐量扩展到许多服务器上。这与MapReduce和数据库中讨论的大型分析查询不同。
- 数据的复制与分区:能够跨多台服务器自动复制和分区数据,即将单个大型数据集分解成多个部分,而无需手动管理。你可能会看到“分片”和“水平分区”这两个术语,它们可以视为同义词。
- 简单的API:没有复杂的查询语言,操作主要是上述的简单操作。
- 弱于ACID的并发模型:提供比ACID事务更弱的并发一致性保证。我们将在下一张幻灯片中简要讨论ACID。
- 高效利用分布式资源:强调高效使用分布式索引和内存进行数据存储,侧重于最小化延迟,而不仅仅是吞吐量。
- 动态模式:通常能够以各种方式向数据记录添加新属性,这可以理解为“无模式”或“模式灵活”。
核心概念总结 📝

本节课中,我们一起学习了NoSQL系统的三种主要类型:键值存储、文档存储和可扩展记录存储。我们还探讨了NoSQL系统的一系列典型特征,包括可扩展的简单操作、数据自动分区、简单API、弱一致性模型、对分布式资源的高效利用以及灵活的模式。这些特征共同构成了“NoSQL”这一术语的常见语境,其核心可以概括为:无固定模式、无强事务保证(ACID)、无复杂查询语言,但具备高可扩展性。在接下来的课程中,我们将更深入地探讨其中的一些概念。
课程1:大规模数据科学 - 大规模数据操作 🚀

概述
在本节课中,我们将要学习大规模数据处理系统中的核心概念,特别是ACID原则及其在大数据环境下的演变。我们还将介绍三个对现代系统设计产生重大影响的早期系统:Memcached、Amazon Dynamo和Google BigTable。理解这些基础概念和系统架构,是掌握大规模数据操作的第一步。
ACID原则详解 🧱
上一节我们提到了事务处理的重要性,本节中我们来看看确保数据操作可靠性的经典框架——ACID原则。ACID是一个缩写,代表了原子性、一致性、隔离性和持久性这四个概念。其核心背景是,当我们在数据库等系统中修改记录时,一系列操作被捆绑成一个事务进行处理。
以下是ACID原则的四个组成部分:
- 原子性:整个事务必须要么完全成功,要么完全失败。不允许出现部分操作成功的情况。其核心思想可以表示为:
事务状态 ∈ {成功, 失败}。 - 一致性:这是最微妙的一个概念。任何写入数据库的数据都必须符合所有预定义的规则。这些规则可能来自数据库本身的完整性约束,也可能来自应用程序的业务逻辑。系统的目标是确保数据库始终从一个有效状态转换到另一个有效状态。
- 隔离性:在事务执行过程中,其他读写操作不能看到事务未完成的中间状态,它们只能看到事务开始前或完成后的最终状态。这个特性在实际系统中经常被以各种方式放宽限制,部分原因是实现严格隔离的成本很高,且并非总是绝对必要。
- 持久性:一旦系统报告事务成功提交,这个结果就必须是永久性的。这意味着数据必须被写入非易失性存储中,即使系统断电或崩溃,已提交的事务也不会丢失。
总的来说,ACID原则为传统数据库事务提供了可靠性的保障,其中“一致性”的概念相对灵活。

具有重大影响的系统 🌟
理解了保证数据一致性的经典模型后,我们来看看在大数据时代,为了追求可扩展性和性能,系统设计发生了哪些演变。本节将介绍三个具有开创性意义的系统,它们的许多思想被后续系统广泛借鉴。
以下是三个奠基性的系统:
- Memcached:这是一个非常简单的系统,其核心理念是将所有数据加载到内存中,并分布到多台机器上。这样,系统可以直接从内存服务读请求,而无需查询后端数据库。它的一个巨大优势是可以“安装”在现有的(无论是否可扩展的)数据库之上,并能显著提升读密集型工作负载的性能。该系统诞生于2003年,至今仍被广泛使用。
- Amazon Dynamo:亚马逊的Dynamo系统并未发明“最终一致性”的概念,但它有力地证明了,通过放宽对强一致性的要求,系统可以获得极大的可扩展性。在该模型中,数据副本不一定时刻保持最新,但系统保证所有更新最终都会被传播到需要的地方。其思想后来以云服务DynamoDB的形式发布。
- Google BigTable:谷歌的BigTable系统证明了面向记录的存储可以扩展到成千上万台机器。这是传统数据库当时未能充分展示的能力。我们将在后续课程中花更多时间探讨BigTable的细节。
总结
本节课中,我们一起学习了数据可靠性的基石——ACID原则,包括其原子性、一致性、隔离性和持久性的具体含义。接着,我们探讨了在大数据背景下,为了应对可扩展性挑战而诞生的三个关键系统:Memcached通过内存缓存加速读取,Amazon Dynamo通过引入最终一致性实现大规模扩展,而Google BigTable则展示了海量结构化数据存储的可能性。理解这些基础概念和系统架构,是进一步学习大规模数据操作技术的坚实基础。
大规模数据科学(大数据操作,第1课/共3课) - P81:Memcached与一致性哈希 🧠

在本节课中,我们将学习Memcached这一内存缓存服务,并深入探讨其采用的关键技术——一致性哈希。我们将了解哈希的基本概念、传统哈希方法在分布式系统中的局限性,以及一致性哈希如何优雅地解决服务器扩缩容时的数据迁移问题。
Memcached简介
Memcached是一种主要的内存缓存服务。它不提供数据持久化功能,其基础版本也不支持数据复制。这意味着每个缓存值通常只有一个副本。因此,如果某个节点发生故障,其上的数据就会丢失。但这对于缓存服务而言是可以接受的,因为它并不被假定为任何数据的“黄金”主副本。
尽管如此,社区已经开发了许多扩展来提供各种增强功能,例如Membase和Moxi。Memcached是一个相当成熟的系统,至今仍被广泛使用。它在此背景下采用的一个重要概念就是一致性哈希。
理解哈希
在深入一致性哈希之前,我们先来理解什么是哈希。哈希是一个在编程中非常基础且通用的概念。在我们当前的上下文中,我们试图将数据键(data keys)分配到一组不同的服务器上。
最简单的方法可能是采用轮询(round-robin)策略:第一个键分配给第一台服务器,第二个键分配给第二台服务器,依此类推。当服务器用尽后,再从头开始分配。这可以通过取模运算(modulus function)来实现。
公式表示:server_index = key % number_of_servers
每个数据键根据这个计算被放置在某台服务器上。这就是传统哈希的工作原理。
传统哈希的局限性
那么,这种方法有什么问题呢?想象一下,如果我们想向集群中添加更多的服务器,例如将服务器数量翻倍,会发生什么?
此时,每个现有数据键的位置都需要被重新计算。原本的计算是 key % n(n是旧服务器数量),现在需要变成 key % 2n(2n是新服务器数量)。这意味着几乎每一个数据项都需要被重新映射到新的服务器上。
每次添加服务器,你都必须移动系统中已经存在的所有数据,这在大型分布式系统中是灾难性的,会导致服务不可用。
一致性哈希的解决方案
因此,我们需要一种一致性哈希的方案。这里的“一致”意味着:当我将数据放置在某处后,即使我添加更多服务器,数据通常也会保持在原来的位置。有一个巧妙且易于理解的技巧可以实现这一点。
上一节我们介绍了传统哈希在服务器扩容时面临的挑战,本节中我们来看看一致性哈希是如何解决这个问题的。
一致性哈希的核心思想是,不再将键直接映射到服务器,而是将键和服务器都映射到一个共同的哈希环上。
以下是其工作原理的关键步骤:
- 构建哈希环:想象一个从0到某个大整数(比如2^32 - 1)的圆环。
- 放置服务器:使用一个哈希函数(例如
hash(server_ip))将每台服务器映射到这个环上的某个点。 - 放置数据键:同样使用一个哈希函数(例如
hash(data_key))将每个数据键映射到环上的某个点。 - 确定归属:为了找到某个键应该存储在哪台服务器上,从该键在环上的位置开始,顺时针查找,直到遇到第一台服务器。该键就存储在这台服务器上。
代码描述逻辑:
# 伪代码示例
def assign_key_to_server(key, server_ring):
key_hash = hash(key)
# 在环上找到第一个哈希值大于等于 key_hash 的服务器
for server in sorted(server_ring):
if server.hash_position >= key_hash:
return server
# 如果没找到,则返回环上的第一台服务器(环状结构)
return server_ring[0]
一致性哈希的优势
这种设计带来了巨大的好处:
- 最小化数据迁移:当添加一台新服务器(例如
Server D)到环上时,只有原本位于Server D与其在环上逆时针方向的前一台服务器(例如Server B)之间的那部分数据需要从Server B迁移到Server D。系统中其他大部分数据的位置保持不变。 - 应对服务器故障:类似地,当一台服务器下线时,只有原本存储在该服务器上的数据需要重新分配到环上的下一台服务器。影响范围被局部化。

本节课中我们一起学习了Memcached作为缓存服务的特点,以及一致性哈希这一关键算法。我们了解到,传统哈希在分布式系统扩缩容时会导致全局数据重分布,而一致性哈希通过引入哈希环的概念,将数据、服务器映射到同一个环上,并采用顺时针查找规则,极大地减少了服务器节点变化时需要移动的数据量,从而实现了高可扩展性和可用性。这是构建大规模、可扩展缓存和存储系统的基石之一。
大规模数据科学(大数据操作,第1课/共3课) - P82:一致性哈希(续) 🔄

在本节课中,我们将继续深入探讨一致性哈希算法。上一节我们介绍了其基本概念,本节中我们将详细讲解其工作原理、如何高效处理服务器增减,以及如何优化请求路由。一致性哈希是构建可扩展分布式系统的关键技术。
工作原理详解 🛠️
一致性哈希的第一个核心思想是,将服务器标识符映射到与数据键值相同的空间中。
我们应用一个哈希函数(此处暂不具体指定),将服务器 Server 1、Server 2 和 Server 3 分别映射到一个环形空间上的某个点。
这样,整个环形空间就被划分成了三个区域。
接下来,对于每一个数据键,我们也使用相同的哈希函数将其映射到这个环形空间上。例如,我们得到 Key 1、Key 2、Key 3、Key 4、Key 5、Key 6、Key 7 等。
现在,每个服务器负责管理环形空间上从它自身位置开始,到顺时针方向下一个服务器位置之前的所有数据键。例如,Server 1 负责其所在区域的所有数据键,Server 2 和 Server 3 同理。
处理服务器增减 ➕➖
这种设计的好处在于,当需要添加一个新服务器时,例如 Server 4,我们只需将其哈希到环形空间上的某个位置(例如 Server 3 和 Server 1 之间)。
根据规则,新服务器 Server 4 将负责其所在区域的所有数据键。这意味着原本由 Server 3 负责的、现在落入 Server 4 区域的那部分数据键,需要被迁移到 Server 4 上。
你只需要移动这一小部分数据,而不是重新分配所有数据。这极大地减少了因服务器增减而导致的数据迁移量。
支持数据副本 📝
为了支持数据副本(即将同一份数据存储在多个地方以提高可靠性),我们可以对同一个数据键进行多次哈希。
例如,如果你想将数据存储在两个不同的位置,可以计算数据键 D 的哈希值 H(D),然后将其存储到哈希环上对应的两个不同位置。如果需要三个副本,就存储到三个位置。
高效处理请求 🚀
在一致性哈希的架构中,键空间被划分到各个服务器上。当一个请求到达时(请求可能先到达一个被选举出的“领导者”服务器,或由系统自上而下指定,甚至是随机分配),需要被路由到正确的服务器。
以下是处理请求的两种方式:
朴素方式(低效)
服务器收到请求后,检查自己是否拥有请求的键。如果没有,就将请求转发给环上的下一个服务器。
这种方式效率低下,因为服务器数量可能非常多,每次读取请求都可能产生多次网络跳转,增加延迟。
优化方式(高效)
每个服务器除了知道自己的位置,还记忆环上其他一些特定服务器的位置。具体来说,每个服务器会记住距离自己位置为 +2、+4、+8、+16……(即2的幂次方)的服务器位置。
每个服务器都知道这些被记忆的服务器各自负责的键值范围。
因此,当服务器收到一个不属于自己负责范围的请求时,它可以将请求直接转发给负责的键值范围最接近目标键的那个服务器。
这种方式将请求路由的跳转次数控制在对数级别(O(log N)),即使服务器数量 N 很大,也能高效处理。
此外,当有新服务器加入时,每个服务器只需要更新对数数量的连接信息,整个系统的维护开销也保持在对数级别。

本节课中我们一起学习了一致性哈希算法的详细工作机制。我们了解了如何将服务器和数据映射到哈希环上,如何优雅地处理服务器的增加与移除,如何通过多哈希支持数据副本,以及如何使用对数跳转的策略来高效路由请求。这些特性使得一致性哈希成为构建大规模、可扩展分布式存储系统的基石。
课程名称:大规模数据科学(大数据操作,第1课/共3课) - P83:DynamoDB向量时钟 🕰️

概述
在本节课中,我们将学习亚马逊DynamoDB系统的核心机制,特别是其用于实现最终一致性的关键技术——向量时钟。我们将了解DynamoDB的设计目标、关键特性,并深入探讨向量时钟如何帮助检测数据冲突。
DynamoDB简介
亚马逊在2007年发表了一篇关于Dynamo的论文。几年后,它作为一项名为DynamoDB的云服务发布。
DynamoDB可以扩展到数千个节点。它基本上是一个键值存储,主要通过主键来查找数据,这与Memcached类似。
上一节我们介绍了DynamoDB的背景,本节中我们来看看它的一些关键特性。
以下是DynamoDB的一些关键特性:
- 它提供基于性能的服务水平协议。具体来说,它承诺在99.9%的请求中,响应时间在300毫秒以内。
- 它选择使用第99百分位数(99th percentile)来衡量性能,而不是平均值或中位数。这是因为如果使用平均值,可能会人为地惩罚重度用户。例如,只关注轻量级用户就很容易满足平均响应时间的要求。
Dynamo系统架构
Dynamo系统是一个分布式哈希表(DHT)。每个键值对会存储在多个位置以实现数据复制,复制因子为N。
具体存储位置从节点K开始,一直到节点K+N-1。它通过向量时钟来实现最终一致性,我们将在接下来的幻灯片中描述这一点。
另一个有趣的特点是,Dynamo在读取时处理潜在的写入冲突。写入操作永远不会失败。论文中引用的理由是,失败的写入操作会带来糟糕的用户体验。
例如,如果你正在一个社交网络网站上更新状态,系统却返回错误消息说“提交失败”,这比读到错误的数据更具破坏性。对于许多应用来说,冲突解决策略可以是“最近写入者获胜”,或者在某些情况下,应用可以控制解决逻辑,甚至可以让用户手动解决冲突。
向量时钟详解
向量时钟的目标是检测冲突,以便应用程序能够解决它们,而不是自动解决所有冲突。我们只是试图检测问题。
在这个方案中,每个数据项都与一个(服务器,时间戳)对的列表相关联,该列表指示了它的版本历史。
在下面的图表中,与这些项目相关的数字可能有点令人困惑,我们先忽略它们。假设这都是同一个数据项。
以下是向量时钟的工作原理:
- 一个写请求首先到达服务器Sx。服务器Sx附加向量时钟对
(Sx, t1)。这表示该数据项在时间戳t1由服务器Sx写入。 - 稍后,同一个服务器Sx处理另一个写请求,会创建向量时钟
(Sx, t1), (Sx, t2)。然而,(Sx, t2)完全包含了(Sx, t1)的信息(同一服务器,更晚的时间戳)。因此,我们不再需要关心(Sx, t1),可以将其垃圾回收,只保留(Sx, t2)。 - 接着,服务器Sy和服务器Sz独立地写入该数据项。向量时钟被扩展为
(Sx, t2), (Sy, t1), (Sz, t1)。 - 最后,一个读请求到达。它看到向量时钟中存在冲突,因为有两个不同服务器在相同时间戳(t1)上进行了写入。因此,系统知道这两个写操作是独立发生的,当前看到的值存在歧义。


总结
本节课中我们一起学习了亚马逊DynamoDB的核心设计。我们了解到它是一个高可用的分布式键值存储,通过多副本和向量时钟机制来保证最终一致性。向量时钟的核心作用在于记录数据项的版本历史,从而能够检测出并发写入导致的数据冲突,为应用程序层解决冲突提供了必要的信息。
大规模数据科学(大数据操作,第1课/共3课) - P84:向量时钟(续)⏰

在本节课中,我们将继续学习向量时钟的概念,并通过具体示例来理解如何判断数据版本之间是否存在冲突。我们还将简要介绍Dynamo系统中用于参数化一致性级别的R和W参数。
向量时钟冲突判断练习
上一节我们介绍了向量时钟的基本原理,本节中我们来看看如何运用它来判断数据版本之间的冲突关系。以下是几个向量时钟的示例,我们需要判断它们是否代表冲突。
示例一:
Sx:3, Sy:1Sx:3, Sz:1
是否存在冲突?
是的,存在冲突。因为在一条版本路径上,服务器Sy进行了一系列修改;而在另一条版本路径上,服务器Sz进行了一系列修改。这两个版本没有反映彼此的更改,说明它们之间没有通信。
示例二:
Sx:3Sx:5
是否存在冲突?
不,不存在冲突。因为它们由同一台服务器处理,只是时间戳不同。版本Sx:5完全包含了版本Sx:3的信息。
示例三:
Sx:3, Sy:6Sx:3, Sy:6, Sz:2
是否存在冲突?
不,不存在冲突。因为两个版本在Sx和Sy上的时间戳一致。第二个版本只是在第一个版本的基础上,在服务器Sz上多了一次更改。因此,第二个版本胜出。
示例四:
Sx:3, Sy:10Sx:3, Sy:6, Sz:1
是否存在冲突?
是的,存在冲突。版本Sy:10在时间上比Sy:6更晚。如果只有Sy的差异,我们可以直接选择Sy:10的版本。但是,由于第二个版本在服务器Sz上也有更改,并且两个版本都从它们最后一致的点(Sx:3)开始向前发展,我们无法确定如何解决这个分歧。
示例五:
Sx:3, Sy:10Sx:3, Sy:20, Sz:1
是否存在冲突?
不,不存在冲突。因为第二个版本在它们共有的所有服务器(Sx和Sy)上都具有更晚的时间戳(Sy:20 > Sy:10),它严格包含了第一个版本的信息。
一致性参数:R与W
除了向量时钟,Dynamo系统还讨论了一种参数化一致性级别的方法,这在相关论文和博客中时常出现。
其核心思想是定义两个参数:R和W。
- R 代表一次成功读取操作所需参与的最小节点数。
- W 代表一次成功写入操作所需参与的最小节点数。
在复制因子为N的系统中:
- 如果 R + W > N,则可以声称实现了强一致性。因为读写覆盖的节点集合必然存在交集,能确保读到最新写入的数据。
- 通常,为了获得更低的延迟,会设置 R + W <= N。这样就不需要在每次读写时都联系太多服务器。
公式表示:
N = 复制因子(数据副本总数)
R = 成功读取所需最小副本数
W = 成功写入所需最小副本数
一致性条件:当 R + W > N 时,系统可提供强一致性保证。
总结

本节课中我们一起学习了向量时钟的应用,通过多个例子练习了如何判断不同数据版本之间是否存在冲突。我们还简要了解了Dynamo系统中通过R和W参数来权衡一致性与延迟的机制。向量时钟和一致性哈希是在NoSQL及其他分布式系统中反复出现的重要概念。
课程名称:大规模数据科学(大数据操作,第1课/共3课) - P85:CouchDB概述 📄

概述
在本节课中,我们将要学习CouchDB,这是一个始于2005年并持续更新至今的流行文档导向数据库系统。我们将探讨其核心特性、数据模型、更新机制以及独特的“视图”概念。
CouchDB简介
上一节我们介绍了不同的数据存储系统,本节中我们来看看CouchDB。这是一个文档导向的存储系统,至今仍被广泛使用。
以下是CouchDB的一些关键特性:
- 扩展性:支持大规模数据存储。
- 索引:支持主键索引,并开始引入二级索引的需求。这意味着你不仅可以通过键(key)查找文档,还可以通过文档中的其他值进行查找。
- 事务:在单个文档内支持更好的事务性。这里的“记录”指的是你可以在一个文档对象(一组键值对)内更改多个值,而不仅仅是单个键值对。
- 分析:通过CouchDB特有的视图概念,提供一定的分析支持。你可以运行小的MapReduce脚本来从现有文档源计算或衍生出新值。
数据模型
CouchDB的数据模型是文档导向的。一个文档就是一组键值对。
例如,在一个博客应用中,一个文档可能代表一篇博客文章,其结构如下:
{
"subject": "大数据简介",
"author": "张三",
"date": "2023-10-27",
"tags": ["大数据", "教程", "CouchDB"],
"body": "这是博客文章的正文内容..."
}
subject、author、date、body都是键值对。tags是一个值列表,这体现了文档模型的嵌套能力。
需要注意的是,CouchDB中的所有数据都使用JSON格式表示,所有请求和响应也使用JSON。这就是为什么之前在Twitter作业中熟悉JSON很重要。
更新机制
现在,我们来看看CouchDB中更新是如何工作的。
在单个文档内,CouchDB提供完全的事务一致性。这意味着你可以“锁定”一个文档(逻辑上),并对其做任意修改。它实际上并不使用传统的锁机制,而是采用乐观并发控制。系统乐观地假设冲突不会经常发生。
其工作流程是:当你检出(check out)一个文档并进行修改后尝试提交时,如果在此期间已有其他人提交了对同一文档的修改,那么你的提交将会失败。在这种情况下,你只需获取最新的文档版本,重新应用你的修改即可。
然而,CouchDB不支持跨多个文档的事务。例如,你不能保证在更新自己状态文档的同时,能同步更新所有好友状态列表中的信息。你只能分两步进行,这可能导致短暂的数据不一致,但在许多应用场景中这是可以接受的。
视图概念
最后,我们来探讨CouchDB中一个独特且重要的概念:视图。
视图的概念在关系型数据库中也非常基础,它提供了逻辑数据独立性。在CouchDB中,视图是其支持数据分析功能的核心。通过编写MapReduce函数来定义视图,你可以从原始文档中聚合、筛选或计算新的数据表示形式,这为数据查询和分析提供了强大的灵活性。
总结

本节课中我们一起学习了CouchDB。我们了解了它是一个基于JSON的文档数据库,支持在单个文档内的事务(通过乐观并发控制),但不支持跨文档事务。其数据模型灵活,允许嵌套结构。此外,CouchDB通过视图和MapReduce提供了数据分析能力,这是其一个显著特点。理解这些核心概念有助于我们在需要灵活、文档型数据存储的场景中评估和使用CouchDB。
课程名称:大规模数据科学(大数据操作,第1课) - CouchDB视图 🗂️

概述
在本节课中,我们将要学习CouchDB中一个核心概念——视图。视图是CouchDB实现数据查询、建立二级索引以及进行计算(如聚合)的关键机制。我们将了解视图的规范结构、其实现方式(Map和Reduce函数),以及它在整个数据库系统中的作用。
视图规范 📄
上一节我们介绍了视图的概念,本节中我们来看看一个具体的视图规范是如何定义的。
视图规范本身是一个CouchDB文档,它包含一组键值对,是一种特殊的文档。其结构如下所示:
{
"_id": "_design/example",
"views": {
"all": { ... },
"by_last_name": { ... },
"total_purchases": { ... }
}
}
这个规范包含了三个视图:all、by_last_name 和 total_purchases。每个视图都将实现为一组键值对,本质上是一个字典。
视图的实现:Map与Reduce函数 ⚙️
理解了视图的规范结构后,我们来看看这些视图是如何被具体实现的。这里涉及到两个重要的概念:Map函数和Reduce函数。
这些函数使用JavaScript编写(因为CouchDB中一切皆是JavaScript),其形式通常是匿名函数。以下是每个视图的具体实现:
1. all 视图
all视图仅包含一个Map函数,没有Reduce函数。它的目的是获取所有类型为“customer”的文档。
function (doc) {
if (doc.type === 'customer') {
emit(null, doc);
}
}
- 函数逻辑:检查文档的
type字段是否为“customer”。如果是,则发射一个键值对。 - 键:
null(在此查询中我们不关心键)。 - 值:整个文档
doc。 - 作用:此视图用于获取所有客户文档。
2. by_last_name 视图
by_last_name视图也仅包含一个Map函数,用于建立以“last_name”为键的二级索引。
function (doc) {
if (doc.type === 'customer') {
emit(doc.last_name, doc);
}
}
- 函数逻辑:检查文档是否为客户,然后以客户的姓氏(
last_name)为键发射文档。 - 键:
doc.last_name。 - 值:整个客户文档
doc。 - 作用:此视图允许客户端通过姓氏高效地查询客户信息。CouchDB会主动物化这些视图,并将其存储在分布式的B树索引结构中,以支持快速查找。
3. total_purchases 视图
total_purchases视图则更进一步,它同时包含了Map函数和Reduce函数,用于执行简单的聚合计算。
以下是它的Map函数:
function (doc) {
if (doc.type === 'purchase') {
emit(doc.customer_id, doc.amount);
}
}
- 函数逻辑:检查文档类型是否为“purchase”(购买记录)。如果是,则以
customer_id为键,以购买amount为值发射键值对。
以下是它的Reduce函数:
function (keys, values) {
return sum(values);
}
- 函数逻辑:接收Map阶段输出的、属于同一个客户(相同
customer_id)的所有金额(amount),并将它们求和。 - 作用:此视图可以快速计算出每个客户的总购买金额。
视图的维护与一致性 🔄
CouchDB会在数据发生变化时维护和更新所有这些视图。这种更新遵循最终一致性语义。这意味着,在文档更新后,相关的视图索引可能会稍后才更新完成,但最终所有副本都会达到一致状态。
通过视图,我们实际上在一个概念下整合了多个重要功能:二级索引、逻辑数据独立性以及MapReduce计算。
关于连接(Joins)的澄清 💡
需要特别指出的是,虽然可以在CouchDB中使用MapReduce风格进行计算,但它无法直接执行类似关系型数据库中的表连接(Joins)操作。这是一个重要的限制,在设计数据模型和查询时需要特别注意。
总结
本节课中我们一起学习了CouchDB的视图机制。我们了解到:
- 视图规范是定义查询和索引的特殊文档。
- 视图通过Map函数和可选的Reduce函数(使用JavaScript编写)来实现。
- Map函数用于过滤和发射键值对,建立索引(如
by_last_name)或准备聚合数据(如total_purchases)。 - Reduce函数用于对Map阶段输出的值进行聚合计算(如求和)。
- 视图由CouchDB主动维护,遵循最终一致性。
- 视图提供了二级索引、逻辑数据独立性和计算能力,但不支持直接的连接操作。

掌握视图是有效使用CouchDB进行数据查询和分析的基础。
大规模数据科学(大数据操作,第1课/共3课) - P87:BigTable概述 🗃️

在本节课中,我们将学习Google BigTable系统。BigTable是Google在2006年提出的一种分布式存储系统,旨在为海量结构化数据提供快速、可靠的访问。它与MapReduce互补,解决了MapReduce无法进行低延迟、基于键值查找的问题。
BigTable简介
Rick在论文中提到的第三个有影响力的系统是来自Google的BigTable。这是一篇2006年的论文。
BigTable支持基于主索引的查找和基于二级索引的查找。其事务处理也主要针对单个记录级别的操作。BigTable本身不直接支持连接(Join)和分析(Analytics)操作。
在HBase(其开源实现)以及Google自身的实现中,BigTable的设计初衷是与MapReduce兼容。因此,你可以在存储在BigTable上的同一份数据上运行MapReduce作业,两者是互补的关系。
BigTable中有一些关于完整性约束或模式(Schema)的概念,我们稍后会讨论其实现方式。它没有视图(View)功能,也没有用于操作数据的特定查询语言或代数。所有操作都是NoSQL风格的,即针对单个记录和单元格的微操作。
BigTable的设计背景与数据模型
上一节我们介绍了BigTable的基本定位。本节中我们来看看它的设计背景和核心数据模型。
这篇论文发表在2006年的OSDI会议上,部分作者与MapReduce论文的作者重叠。它从一开始就被设计为MapReduce的补充。
回想一下,MapReduce主要缺失的功能之一(或者说几个功能)是无法通过索引快速查找数据,也无法进行低延迟的访问。例如,给定一个大数据集,如果你想找到所有与另一个数据集对应的记录,你无法执行连接操作。你只能处理整个数据集,无法直接定位到你想要的那部分特定记录。
BigTable则提供了基于键的快速查找功能。但你仍然可以将整个数据集视为一组键值记录,用MapReduce进行批量处理,这没有问题。
BigTable的数据模型是一个稀疏的、分布式的、持久化的、多维有序映射。这意味着,你可以通过提供行ID、列名和时间戳这三个参数,来访问BigTable中的任何一个单元格。此处的英文描述没有详细说明时间戳,它主要用于版本控制。在进行更新后,系统会保留同一单元格的过去版本。
因此,如果你提供了这三个参数,BigTable会快速返回一个字符串给你。
数据组织方式:行键与分片
了解了数据模型后,我们来看看数据是如何在集群中组织和分布的。
每一行数据都按照行键(即此处的行ID)进行字典序排序。在关系型数据库术语中,这类似于主键;在NoSQL框架中,这就是键。
连续的键值范围(例如一段连续的整数)会被分配给一个“Tablet”(分片)。这与我们之前讨论过的至少一个系统(例如并行数据库模型)的数据划分方式有所不同。
我们曾以Teradata为例。Teradata是如何分割数据的呢?它是通过哈希函数。每个独立的记录会根据一个哈希函数被发送到特定的服务器上,你可以大致将其理解为轮询分配。关键在于,在Teradata模型中,两个在键值空间上相邻的键(例如时间戳5:00和5:01)没有理由被分配到同一台服务器上。
但在BigTable中,相邻的键就在同一台服务器上。
这种设计有何利弊?
如果你通常需要一次性访问整个键值范围,那么这种设计非常有利。当你获取一个键时,可以近乎免费地获取相邻的其他键,因为它们都在同一个分片上。
然而,如果某个特定的键值范围比其他范围热门得多(再次以时间为例,最近的数据可能最受欢迎),那么所有请求都会涌向承载这个热门分片的服务器。而承载其他较旧时间分片的服务器则会闲置。
因此,Teradata选择哈希一切,这样每个请求都可能需要访问所有服务器,但这有利于可扩展性。所以,各有利弊。
在BigTable中,Tablet是数据分布和负载均衡的基本单位。当负载开始不平衡时(例如,如果你的键范围是1月到5月,而3月的数据量激增),系统会将这个分片拆分成多个更小的Tablet,并在服务器之间移动这些Tablet以实现负载均衡。
总结


本节课中,我们一起学习了Google BigTable系统。我们了解到BigTable是一种为海量数据设计的分布式存储系统,它使用基于行键的有序映射模型,通过Tablet进行数据分片和负载均衡。BigTable与MapReduce形成互补,提供了MapReduce所缺乏的低延迟、基于键的快速查找能力,同时保留了进行大规模批量处理的可能性。理解其数据组织方式(连续键值范围分配)及其与哈希分配方式的利弊,是掌握其设计思想的关键。
大规模数据科学(大数据操作,第1课/共3课) - P88:BigTable 实现详解 🗄️

在本节课中,我们将深入探讨 Google BigTable 的内部实现机制。BigTable 是一个用于管理海量结构化数据的分布式存储系统,理解其核心设计对于掌握大规模数据处理至关重要。
列族与访问控制
上一节我们介绍了 BigTable 的基本数据模型,本节中我们来看看其核心组织单元——列族。
在单个表中,可以存在被称为列族的分组。列名中包含族名作为限定符。列族是访问控制的基本单位,可以对一组列统一设置权限。
列族也是内存和磁盘存储计量的基本单位。它们通常在内存中作为一个单元进行分配,在磁盘上也作为一个单元进行移动。
需要指出的是,一个列族中的所有列通常是相同的数据类型。这一点有些特别,因为列族最初被描述为访问控制的基本单位,这暗示了逻辑上相关的数据(例如社会保险号和员工ID)会放在一起,而这些数据的数据类型可能并不相同。然而,要求同族同类型是出于技术原因,主要是为了便于压缩。例如,压缩一整组整数比压缩整数和字符串的混合数据要容易得多。这体现了设计上试图用单一结构满足多种需求的考量。
数据版本与键结构
每个数据单元都可以有多个版本,这是键值查找的第三部分。完整的键由行ID、列名和时间戳组成。每次写入新版本都会递增时间戳。
系统可以实施不同的版本保留策略,例如只保留最新的N个版本,或者只保留某个时间戳之后的版本。
Tablet 管理与元数据组织
接下来,我们看看 BigTable 如何管理数据分片(Tablet)。
Tablet 的管理由一个主服务器负责。主服务器将 Tablet 分配给 Tablet 服务器,由 Tablet 服务器处理来自 Tablet 的读写请求。客户端直接与 Tablet 服务器通信,而不是每次都经过主服务器,这有助于提高系统的可扩展性。
当一个 Tablet 变得过大时,系统会将其分裂并进行负载均衡。
以下是追踪 Tablet 位置的元数据组织方式:
- 元数据本身被组织在另一个特殊的 Tablet 中,称为根 Tablet。
- 根 Tablet 中的每条记录描述了一组位置记录,这些记录位于一个更大的元数据表中。
- 而这些元数据表中的每一个 Tablet,则给出了一个特定用户表的位置信息。
通过这种层次化的结构,系统可以有效地追踪所有数据的位置。论文中提到的 Chubby 是一个分布式锁服务,用于控制对这些资源的访问。
读写操作处理
现在,我们来看看系统如何处理读写操作。
内存中有一个表(MemTable),用于按顺序存储发生的更新。一个写操作会向这个驻留内存的表中添加一条记录,同时为了容错,该操作也会被写入日志。这样,如果服务器宕机,可以通过读取 Tablet 日志来重建状态。
读操作则通过读取实际的 SSTable 文件,并动态地应用 MemTable 中的更新流来提供服务。可以将其理解为:先获取基础值,然后应用一系列更新操作来得到真实值。
压缩与维护机制
当 MemTable 变得越来越大时,系统通过两种压缩事件来进行簿记管理。
第一种是次要压缩。当 MemTable 变大时,其内容会被写入一个新的 SSTable 文件,并与现有数据合并。
第二种是主要压缩。这个过程会获取所有的 SSTable 文件,并将它们重写为一个可能被分割成多个文件的大文件。同时,它会清理所有已发生的删除操作。在 BigTable 中,删除操作只是作为指令被追加,并不会立即物理删除数据,它们会在压缩过程中像垃圾回收一样被清理。
通过这种方式,系统可以在后台持续运行,保持较高的读取吞吐量。
性能优化技巧
BigTable 还采用了多种其他优化技巧。
客户端可以指定压缩方式,系统支持多种不同的压缩方法。布隆过滤器被用来加速存在性测试。例如,当给定行ID、列ID和时间戳来查找某个值时,布隆过滤器可以非常快速地判断该值不存在于系统中,从而避免不必要的磁盘访问。布隆过滤器数据结构非常巧妙,我们将在后续课程中详细讲解。
此外,还可以在列族之上定义局部性群组,这是另一层组织结构,用于将经常被一起访问的列族分组。
另一个关键技巧是确保 SSTable 这些磁盘数据块是不可变的。它们永远不会被直接写入,唯一的写入时机发生在主要压缩期间,届时整个结构会被重组。这意味着系统中唯一可写的数据结构就是 MemTable,从而使得维持数据一致性的并发控制机制保持相对简单。


本节课中我们一起学习了 Google BigTable 的核心实现机制。我们探讨了其以列族为基础的数据组织方式、包含时间戳的键结构、通过主服务器和 Tablet 服务器进行的分片管理、以及层次化的元数据组织。我们还详细分析了读写操作的处理流程,特别是 MemTable 和 SSTable 的协同工作方式,以及通过次要压缩和主要压缩进行的数据维护。最后,我们了解了一系列性能优化技巧,如布隆过滤器、局部性群组和 SSTable 的不可变性设计,这些共同构成了 BigTable 高效、可扩展存储系统的基石。
大规模数据科学(大数据操作,第1课/共3课) - P89:HBase与Megastore 🗄️

在本节课中,我们将要学习Google BigTable的两个重要衍生系统:开源项目HBase,以及Google为应对弱一致性模型挑战而设计的Megastore。我们将了解它们的设计理念、核心架构以及与MapReduce的兼容性。
HBase:BigTable的开源实现 🐘
上一节我们介绍了Google BigTable,它对业界产生了巨大影响。本节中我们来看看其最重要的开源实现之一:HBase。
正如MapReduce催生了Hadoop,BigTable的理念也被采纳并实现为一个开源项目,即HBase。HBase被设计为与Hadoop生态系统兼容。
在架构层面,HBase与BigTable的核心概念相似,但术语和部分结构存在差异。以下是HBase的关键层级结构:
- 表:数据的顶层容器。
- 区域:HBase引入的一个额外抽象层,表被水平分割为多个区域。
- 区域服务器:负责管理一个或多个区域。
- MemStore:内存中的写缓存。
- 存储文件:持久化到磁盘(HDFS)的数据文件。
HBase与MapReduce的兼容性体现在:每个Map函数通常会处理一个单独的“数据片”(在HBase语境下可对应一个区域),这与我们讨论MapReduce时提到的数据块处理方式是一一对应的。
混合系统下的挑战 ⚠️
现在,我们来看看一个开放性的问题。当我们混合使用这些系统时,可能会遇到一些挑战。
我们曾讨论过MapReduce中的“推测执行”机制:出于容错考虑,系统可能在两个不同的数据副本上启动相同的Map任务。这样,如果一个任务失败,另一个可以接替,无需完全从头开始。
然而,在HBase和BigTable所支持的数据被主动更新的环境中,情况变得不那么清晰。由于最终一致性,这两个处理相同数据片(Tablet)的任务可能无法瞬间就所有记录达成一致。它们最终会一致,但片内不同记录的状态在某一时刻可能不同。
当你混合使用这两种缺乏系统范围事务保证或强一致性属性的系统时,就可能遇到麻烦。我认为,包括Google设计的这些NoSQL系统在内,一个普遍的主题是:系统将部分责任转移给了应用程序和程序员,需要他们来处理和确保最终结果的正确性。
我们将在几分钟后回到这个话题。
Megastore:寻求更强的保证 🛡️
在BigTable问世几年后,Google的一批研究人员发表了一篇关于名为Megastore的系统的论文。我们不会深入细节,但需要了解其核心动机。
他们发现,正如我刚才指出的,这些松散的一致性模型会使应用程序编程复杂化。因此,他们希望为某些类型的安全更新提供更多的系统级支持。
与BigTable仅在单个记录内保证事务安全不同,Megastore通过引入“实体组”的概念扩展了事务支持。
以下是实体组的关键特性:
- 定义:一个实体组是一组倾向于共同访问、共同更新的记录的集合。
- 示例:例如,一篇博客文章及其所有评论。每篇文章和每条评论都是一个独立的记录(因为这是合理的记录存储方式,可以有不同的模式),但它们通常作为一个整体被处理。
- 事务范围:Megastore将事务支持扩展到了整个实体组,即一组相关的记录。
通过这种方式,Megastore在不需要全局同步(从而保持了可扩展性)的同时,允许你规避以下问题:在需要同时更新一个主记录及其所有关联子记录时,无法以任何安全的方式进行操作。

总结 📝

本节课中我们一起学习了HBase和Megastore。HBase作为BigTable的开源实现,继承了其核心思想并与Hadoop集成,但在架构细节上有所不同。同时,我们探讨了在弱一致性、支持更新的存储系统上运行像MapReduce这样的批处理框架时可能面临的挑战。最后,我们看到了Google如何通过Megastore系统,引入“实体组”的概念,在保持可扩展性的前提下,为相关联的记录集提供更强的事务性保证,以简化应用开发。
大规模数据科学(第1课/共3课) - P90:Spanner 🗄️

在本节课中,我们将学习谷歌开发的全球级分布式数据库系统——Spanner。我们将了解它诞生的背景、核心设计理念、数据模型特点,并探讨它如何尝试解决其前身BigTable的局限性。
概述
上一节我们介绍了BigTable等系统。本节中,我们来看看谷歌在2012年论文中提出的Spanner系统。Spanner旨在解决用户对BigTable的一些抱怨,特别是在复杂模式演变和需要跨地域强一致性场景下的使用困难。它代表了一种向更传统数据库特性(如事务和类SQL语言)回归的趋势,同时保持了大规模可扩展性。
Spanner出现的背景
尽管许多项目成功地使用了BigTable,但用户也持续提出了一些抱怨。
以下是用户反馈的主要问题:
- 某些具有复杂、不断演变的模式的应用,使用BigTable会感到困难。
- 那些在广域复制场景下需要强一致性的应用,BigTable难以满足。
因此,Spanner的设计者认为,与其让应用程序员始终围绕“缺乏事务”进行编码,不如提供系统级的事务支持。即使初期可能因过度使用事务导致性能瓶颈,这也比在应用层弥补事务缺失更好。
数据库社区或许会认为这是显而易见的道理。系统提供的事务支持总是更优的,原因在于:
- 在应用层实现事务逻辑是困难、易出错且昂贵的。
- 更重要的是,在应用层做这件事在根本上是错误的,因为应用程序不具备系统所拥有的全局视角。
Spanner的高层特点
现在,让我们看看Spanner在高层设计上的特点。
Spanner是一个行星级规模的数据库系统。以下是其关键特性:
- 大规模主键访问:支持通过其他属性进行访问。
- 真正的事务:提供真实的ACID事务,并且这次是全局范围的。
- 模式支持:提供某种模式概念,能防止不符合模式的数据写入。
- 逻辑数据独立性:具备逻辑数据独立性的概念,尽管论文中讨论不多。
- 类SQL语言:系统顶层有一个声明式的、类似SQL的语言。
关于连接(Join)操作是否被支持,文中未明确给出例子,但鉴于提到了SQL,推测很可能支持。同时,论文也指出其在复杂分析查询上性能不佳,但这可能是未来可以快速改进的方面。
Spanner的数据模型
接下来,我们深入了解一下Spanner的数据模型细节。其核心是“目录”的概念。
目录是一组具有共享前缀的连续键,可以粗略地类比为BigTable中的“片”。但Spanner引入了新的概念:多个逻辑表可以“交错”在一起。
如果你不熟悉这种语法,不必过于担心。简单来说,Spanner有一种类似关系数据库DDL的建表语言。例如,你可以用以下方式创建表:
CREATE TABLE Users {
user_id INT64,
name STRING
} PRIMARY KEY (user_id), DIRECTORY;
CREATE TABLE Albums {
user_id INT64,
album_id INT64,
title STRING
} PRIMARY KEY (user_id, album_id), INTERLEAVE IN PARENT Users;
这种“交错”设计的结果是,数据在物理存储上会像这样组织:一个用户记录后面紧跟着其所有的相册记录,然后是下一个用户及其相册。
我们可以看到,Spanner和其他许多现代系统一样,都在尝试实现这种嵌套的、层次化的数据结构。这与早在20世纪60年代就出现的数据模型动机相似:当你需要查询一个用户并立即获取其所有相册时,这种存储方式访问速度极快。
然而,关系模型最终取代了那些早期模型,原因可能在于性能并非总是第一优先级,很多时候“最小化开发者的麻烦”更为重要。因此,这种向大规模、可扩展的关系型数据库的渐进式回归是否成功,仍有待观察。但这并不意味着要全盘照搬旧数据库,它们是为不同的工作负载设计的,并且没有证据表明它们能扩展到Spanner所面对的级别。关键在于,我们不应抛弃过去数十年积累的所有知识。
总结

本节课中,我们一起学习了谷歌的Spanner系统。我们了解到它是对BigTable局限性的回应,旨在通过提供全局ACID事务、类SQL接口和更丰富的数据模型(如交错表),来构建一个兼具强一致性和全球规模可扩展性的数据库。Spanner的出现,标志着在大数据领域,系统设计正在重新审视并整合传统关系数据库中的经典概念与价值。
大规模数据科学(大数据操作,第1课/共3课) - P91:Spanner(续)与Google系统全景 🗺️

在本节课中,我们将继续深入了解Google的Spanner分布式数据库,并梳理Google内部一系列有影响力的大数据系统,理解它们的发展脉络、设计目标以及彼此之间的关系。
上一节我们介绍了Spanner的基本架构,本节中我们来看看其内部组件如何协同工作,以及它在Google庞大技术栈中的位置。
Spanner架构详解 🔧
Spanner的顶层是一个宇宙主节点。这是一个单例部署,全局可能只有一到两个这样的部署(例如一个用于测试,一个用于生产)。许多不同的应用程序会共享同一个Spanner部署。宇宙主节点主要负责存储各个区域的状态信息,它不与客户端直接交互。
放置驱动器负责为了负载均衡的目的,在几分钟的时间尺度上移动记录目录集。
在每个区域内部,存在以下组件:
- 区域主节点:负责将数据分配给Spanner服务器。
- 位置代理:知晓所有数据的位置,并将请求路由到相应的Spanner服务器。
- Spanner服务器:负责提供数据服务。
从区域层面看,其架构开始更类似于Bigtable,每个区域本质上是一个独立的Bigtable部署。
Spanner服务器与事务支持
Spanner服务器是实现完全一致性事务支持的关键所在。其核心逻辑如下:
- 当事务访问的数据跨越多个副本组时,系统使用两阶段提交协议。
// 伪代码示例:跨副本组事务if (transaction_spans_multiple_replica_groups) {execute_two_phase_commit();}
- 如果事务仅包含在单个副本组内,则系统会降级使用Paxos算法来处理写入操作。
// 伪代码示例:单副本组内事务else {execute_paxos_for_writes();}
此外,Spanner底层使用名为Colossus的存储系统,它是Google文件系统的继任者。需要明确的概念对应关系是:
- GFS 之于 MapReduce,正如 HDFS 之于 Hadoop。
Google大数据系统演进时间线 📅
了解了Spanner的细节后,让我们退一步,纵观Google的一系列大数据系统。理解它们的发展历程有助于厘清这些术语之间的关系。
以下是按时间顺序和影响力梳理的主要系统:
- MapReduce:2004年发表的论文,产生了巨大影响,专注于批处理分析。
- Bigtable:几年后出现,专注于低延迟的微观操作(如单行读写)。它与MapReduce设计上互补。
- 开源对应:MapReduce和Bigtable后来分别有了开源实现Hadoop和HBase。
- Megastore与Spanner:又几年后,两者接连出现。它们与Bigtable有直接的继承和发展关系(在论文和作者上有大量重叠)。
- Tenzing:一个基于MapReduce的SQL层系统,类似于Hive。
- Dremel:专为极低延迟的聚合查询设计,属于分析类系统,但不同于MapReduce的批处理模式。
- BigQuery:Dremel的服务化版本,可通过Web直接使用,是一个重要的大规模数据分析云服务。
- Pregel:该系统引入了迭代这一关键特性。许多数据科学和机器学习任务需要反复运行计算直至收敛,Pregel等系统正是为了弥补MapReduce在这方面的不足而设计的。
系统分类总结 🏷️
我们可以将这些系统大致分为几类:
- 低延迟微观更新系统:如Bigtable、Spanner,服务于在线事务处理。
- 批处理分析系统:如MapReduce、Tenzing,服务于离线数据分析。
- 支持迭代的分析系统:如Pregel,服务于机器学习、图计算等需要反复迭代的任务。

本节课中我们一起学习了Spanner内部组件如何实现分布式事务,并梳理了Google大数据技术栈的发展脉络与核心系统分类。理解这些基础系统及其设计哲学,是掌握大规模数据处理的关键。
📊 大规模数据科学(大数据操作,第1课/共3课)|基于MapReduce的系统

在本节课中,我们将学习基于MapReduce构建的系统及其发展历程。我们将探讨MapReduce如何催生出多种高层语言和NoSQL系统,并了解这些系统如何满足大规模数据处理的需求。
回到设计网格,我们可以突出那些基于MapReduce本身的系统。
2004年MapReduce论文发表,随后在2008年出现了构建在其之上的语言层,如Pig和Hive。
其中Hive使用SQL。Pig是一种类似关系代数的语言,我们将在后续章节详细讨论。
还有Tzing和Iala,它们也使用SQL。Tzing来自Google,Iala来自一家名为Cloudera的公司,该公司一直是MapReduce和Hadoop技术的积极推广者。
一个明显的趋势是,这些构建在MapReduce并行处理原语之上的声明式语言已经站稳脚跟。
过去可能有人反对这类语言,但现在情况已变。同时,企业普遍在SQL专业知识上投入了大量资源。
因此,即使他们被Hadoop的优势所吸引,也几乎都要求使用SQL。这可能是对过去SQL投资惯性的一种回应。
然而,对声明式语言的需求本身也是合理的,原因我们之前已经讨论过。
我们可以将这些系统放在时间线上。需要指出的是,2004年的论文与2008年这些系统之间有一段间隔。
但是,一旦Hadoop在雅虎开发出来并作为Apache开源项目发布,围绕它的扩展生态系统就迅速涌现,特别是增加了这些高层语言。
我认为,高层接口的需求从其迅速出现就可见一斑,并且这种发展并未停止,几年后出现了更多系统。
实际上,如果算上基于MapReduce扩展的研究项目,可能有数百个之多。这里列出的是其中一些最流行的。
接下来,我们来看看设计网格中的另一个子集:NoSQL系统。
虽然前几节都在讨论NoSQL,但我也将一些分析型系统包含在内,例如基于MapReduce的系统,以及Dremel、Spark和Shark等。
Dremel是来自Google的系统,是Google BigQuery查询即服务系统的后端。它非常出色,建议你了解一下。你可以上传数据,无论数据多大,都能以极低的延迟进行查询。
Spark和Shark来自伯克利的AMPLab,是伯克利数据分析栈(BDAS)的一部分。Spark本身不是一个MapReduce系统,而是构建在一个并行处理系统之上的语言层。Shark则是构建在Spark之上的SQL层。
Spark的几个显著特点是:它尽可能将所有数据加载到内存中处理,仅出于容错原因才会将数据写入磁盘,频率远低于MapReduce。它还支持迭代处理,这一点非常重要,我们将在课程后面再次讨论。而Shark,如前所述,是Spark之上的SQL层。
在这些NoSQL系统中,这个图表旨在指出曾出现过一次“寒武纪大爆发”。
最初有Memcached,它实际上只是一个缓存层,用于缓解后端一堆MySQL数据库的性能压力。它通过名称将数据存入内存进行查找,本质上是一种性能增强方案。
而彻底抛弃原有系统、用NoSQL系统替代的真正方法出现得稍晚一些。
我想指出的几点是:大约在这个时期,不同系统出现了爆发式增长,图表中的空白区域远非看起来那么空。我在这里只挑选了几个系统,但实际上,该领域新系统的涌现速度至今仍未显著放缓。
自2006年左右以来,设计空间被不断填充。我想指出的另一点是,这些非常流行的面向文档数据模型系统,如CouchDB和MongoDB,实际上已经存在相当长的时间了,它们分别于2005年和2007年发布。
有时它们看起来像是新系统,但它们已经具备了一定的成熟度。

本节课中,我们一起学习了基于MapReduce的系统生态。我们回顾了从MapReduce原语到高层声明式语言(如Hive、Pig)的发展,并探讨了NoSQL系统的兴起与多样化,包括像Spark这样支持内存计算和迭代处理的新架构。理解这些系统的演变和设计选择,是掌握大规模数据处理技术的基础。
大规模数据科学(大数据操作,第1课/共3课) - P93:重新引入连接 🔗

在本节课中,我们将探讨NoSQL系统中的一个核心话题:连接(Joins)。我们将分析NoSQL系统为何通常不支持连接操作,并讨论这种设计背后的权衡,以及为何在某些场景下重新引入“智能”的连接处理机制是有益的。
关于“连接与分析”的论点
上一节我们讨论了数据模型,本节中我们来看看NoSQL系统的一个显著特征。这个特征体现在我称之为 “连接与分析” 的列上。NoSQL系统的一个显著区别性特征在于,它们通常不支持任何形式的连接操作。这些系统的支持者会辩称,这是可以接受的,因为连接并非真正必要。
关于为何可以摆脱连接的论证大致如下:
- 数据共置替代连接:当连接两个表时,通常是一个记录对应另一个表中的一系列相关记录。如果我将所有这些相关记录都共置在父记录旁边,那么我就可以一次性访问所有数据,而无需实际计算连接。
历史权衡:性能与灵活性
这个思路听起来应该有些熟悉,因为它正是我们之前在讨论关系型数据库动机时提到的网络和层次数据模型的一部分。
如果你还记得,这种方法的缺点在于,你只有一种主导的数据访问路径。任何其他访问路径要么完全不被支持,要么效率低下。
如果你想为了支持不同的访问路径或因需求变化而重组数据,那么你的大量代码可能会崩溃,需要重写。
因此,这里存在一种张力:
- 性能:通过以相当刚性的方式组织数据来换取一点性能提升。
- 灵活性:通过无需在每次重组数据时重写代码来节省开发时间。
在70年代初,这个等式以某种方式达到了平衡。我认为,现在这个等式依然以同样的方式平衡着。
这并非是说当今的商业关系型数据库必然能满足所有人的需求。显然,由于各种原因,它们并不能。
但是,为了前期的设计决策(即一种主导的数据组织方式),而抛弃我们所知的、从关系数据模型中获得的灵活性,我也不确定这是正确的选择。这是我想阐述的第一个要点。
连接评估:没有唯一正确的方法
另一个要点是,即使支持连接操作,也不一定存在唯一正确的数据分解方式或连接评估方法。
这一点可能在之前提到过,但我想在NoSQL的背景下再次提出。
在一个典型的Web应用场景中,如果你想显示用户名为“Sue”的所有评论,这些评论关联到用户名为“Jim”的任何博客文章,有几种不同的方法可以实现。
以下是几种可能的查询策略:
- 正向查找:查找与“Jim”关联的所有博客文章,然后获取所有对应的评论,并从中筛选出“Sue”的评论。
- 反向查找:查找“Sue”的所有评论,然后针对每条评论,查找“Jim”的所有博客文章。
- 合并排序连接:独立筛选出“Sue”的所有评论和“Jim”的所有博客文章,按某种博客ID排序,然后从每个列表中提取数据进行匹配。这听起来可能有些奇特,但这正是你可能熟悉或不熟悉的排序合并连接。
即使这些方法都可用,也不清楚哪一种是最佳的。正确的选择取决于数据的具体细节,而这些细节作为应用程序员的你可能无法随时掌握,并且可能随时间变化。
因此,实际上只有系统本身才具备条件来做出这个决策。
所以,屈服于数据库构建或设计之初的初始设计决策的“暴政”是一个问题。另一个问题是,即使你有一定的灵活性,将选择正确数据访问方式的责任交给程序员,也是要求他们做出一个他们不具备条件去做的决定。
这些问题在关系型数据库中并不以同样的方式存在。
趋势:将“智能”重新注入系统
因此,我认为尝试将一些这种智能重新注入到这些NoSQL系统中是一个好主意,并且我们看到这种趋势正在发生。
总结

本节课中我们一起学习了:
- NoSQL系统通常不支持连接,其理念是通过数据共置来避免连接操作。
- 这实质上是网络/层次模型的回归,需要在查询性能和数据访问灵活性之间做出历史性的权衡。
- 即使实现连接,也存在多种评估策略(如正向查找、反向查找、排序合并连接),没有绝对最优解,最佳选择依赖于动态的数据状态。
- 将连接策略的选择权完全交给程序员或固化在初始设计中是低效的,数据库系统本身应具备优化选择的智能。
- 当前的一个积极趋势是,在NoSQL系统中重新引入智能的连接处理机制,以兼顾灵活性与效率。
大规模数据科学(大数据操作,第1课/共3课) - P94:NoSQL反驳 💬

在本节课中,我们将学习Mike Stonebraker在《ACM通讯》中对NoSQL运动提出的反驳观点。我们将重点探讨NoSQL社区提出的两个主要价值主张,并分析其在实际企业应用中的挑战。
上一节我们介绍了NoSQL的基本概念,本节中我们来看看Mike Stonebraker对其提出的具体反驳论点。
Mike Stonebraker指出了NoSQL社区提出的两个核心价值主张:
- 性能
- 灵活性
关于性能论点,我们不会花费太多时间,因为我们已经讨论过不同系统在事务性保证方面所做的权衡。Mike提出的另一部分论点更多涉及我们未深入探讨的数据库内部原理。
因此,让我们聚焦于灵活性论点。
Mike提出了一个观察,我可能也同意这个观察:谁是这些NoSQL系统的客户?
以下是关于客户群体的分析:
- 很多初创公司,尤其是网络初创公司,是NoSQL的主要用户。
- 在企业级市场中的渗透率则不完全相同,至少目前如此。
为什么会出现这种情况?
以下是几个可能的原因:
- 企业中的大多数应用是传统的OLTP(在线事务处理)系统。例如银行记录,在这些场景中,事务的正确性至关重要。这些系统处理的是结构化、有组织的数据。
- 企业边缘可能有一些其他应用,但它们被认为不那么重要。对于这些非核心应用,或许可以接受使用高风险系统,因为应用本身对高管的吸引力较小。
- 因此,真正的情况是:没有资产合规性、没有事务支持,等同于对系统没有太大兴趣。搞砸关键任务数据是不可接受的,但对于一些边缘应用,或许可以尝试使用NoSQL系统。
Mike提出的第二个观点是:依赖这些低级别的查询接口是一个很难推销的方案。
他提到了CODASYL,这是一种早期的数据操作语言,早于我们讨论的声明式语言。我们已经走过那条路,它很艰难。这正是更高级语言被发明的原因。我们在MapReduce(公认的分析工具,而非纯NoSQL)中也看到了这一点,这些高级接口的价值得到了体现。
Mike提出的第三个观点是:NoSQL意味着所有规则都不复存在。
在不同的部署之间没有任何同质性。在一个典型的企业中,可能有一万个数据库。由于这些数据库内部模式的异构性,整合数据已经足够困难。但至少你知道你总是在处理行和列,并且至少有一种操作它们的标准接口。
以下是使用NoSQL系统时面临的设计决策复杂性:
- 必须做出许多设计决策来将数据编码到这些NoSQL系统中。
- 例如,什么成为键,什么成为值?博客帖子是嵌套在评论下,还是评论嵌套在博客帖子下?
- 用户是维护自己的信息墙,还是消息出现在他们的首页上?是写消息的人保留访问权限,还是双方都保留?
- 所有这些关于嵌套层级等的不同设计决策,使集成和标准化变得复杂。
因此,这是一个难以推销的方案。
我想提出一个与此第三点相关的观点:天下没有免费的午餐。
复杂性要么存在于你用来建模数据的系统中,要么在某种程度上转移给了应用程序。但在某种意义上,模式总是存在的。应用程序的业务模型总会在某个地方被编码。
因此,将其集中放在数据系统中,而不是隐藏在访问数据系统的各个应用程序里,似乎是一个好主意。



本节课中我们一起学习了Mike Stonebraker对NoSQL的批判性观点。我们探讨了NoSQL在性能和灵活性上的主张,并分析了其在企业级应用,特别是在事务处理、接口复杂性和数据建模标准化方面面临的挑战。核心在于,数据系统的复杂性与业务逻辑的清晰分离是需要权衡的关键问题。
课程1:大规模数据科学 - 接近SQL的Pig 🐷

在本节课中,我们将学习Pig系统。Pig是构建在MapReduce之上的一个语言层,它更直接地体现了关系代数的思想。我们将探讨Pig是什么、为什么需要它,并通过一个具体例子来理解其优势。
什么是Pig?🤔
上一节我们介绍了课程概述,本节中我们来看看Pig的具体定义。
Pig是一个在Hadoop上执行程序的引擎。用户使用名为Pig Latin的语言编写Pig程序,该程序会生成一系列MapReduce作业来实现其功能。Pig是一个Apache开源项目。
为什么使用Pig?💡
如果你已经是MapReduce程序员,可能会问为何要使用Pig。本节将通过一个具体场景来说明其价值。
假设我们有两个文件:一个包含用户数据,另一个包含网站访问数据。我们的任务是找出特定年龄段的用户访问量最高的前5个网站。
如果用SQL思考,可以很容易地将此任务构思为一个查询。以下是一个描述此数据处理流程的图示:

该流程包括:加载用户数据、按年龄过滤、加载页面数据、按名称连接(join)、分组(group)、统计点击次数、按点击排序,最后取前5名。
如果用MapReduce实现此任务,需要约170行代码。根据相关论文中的例子,某人可能需要4小时来编写。虽然开发时间取决于个人背景和技能,但这仍然是一段可观的时间。
而用Pig Latin实现相同的程序,仅需9行代码。在同样案例中,只需约15分钟即可完成。并且,Pig Latin更贴近对此任务抽象描述。
Pig Latin程序示例 📝
上一节我们了解了Pig的优势,现在来看看具体的Pig Latin代码是什么样子。
以下是实现上述任务的Pig Latin程序示例:
users = LOAD 'users.dat' USING PigStorage(',') AS (name:chararray, age:int);
filtered_users = FILTER users BY age >= 18 AND age <= 25;
pages = LOAD 'pages.dat' USING PigStorage(',') AS (user:chararray, url:chararray);
joined_data = JOIN filtered_users BY name, pages BY user;
grouped_data = GROUP joined_data BY url;
click_counts = FOREACH grouped_data GENERATE group AS url, COUNT(joined_data) AS clicks;
sorted_counts = ORDER click_counts BY clicks DESC;
top5 = LIMIT sorted_counts 5;
STORE top5 INTO 'top_sites.txt';
程序解读如下:
- LOAD: 按照特定模式加载用户数据。
- FILTER: 根据年龄范围过滤用户数据。
- LOAD: 加载页面访问数据。
- JOIN: 在过滤后的用户数据和页面数据之间执行连接操作。
- GROUP: 按URL对连接后的数据进行分组。
- FOREACH...GENERATE: 对每个分组,计算点击次数。这一步看起来可能最不像传统的关系代数操作,我们后续会讨论。
- ORDER: 按点击次数降序排序。
- LIMIT: 取前5条结果。
- STORE: 将最终结果存储到新文件中。
Pig的核心价值 🚀
你可能会想,用Python写这些逻辑不是更简单吗?为何要用新语言?关键在于,Pig Latin中的这些操作(或操作组)对应着独立的MapReduce作业。
这意味着程序具备极佳的可扩展性。无论你的用户表或页面表有多大,这个程序都能有效运行,并利用Hadoop集群的分布式计算能力。

此程序将正常工作。

总结 📚


本节课中我们一起学习了Pig系统。Pig作为MapReduce之上的语言层,通过Pig Latin提供了更接近关系代数和人类思维模式的抽象,极大简化了大规模数据处理的编程。它将高级指令编译成可扩展的MapReduce作业,兼顾了开发效率与执行性能。理解Pig有助于我们运用关系代数思想来设计和推理可扩展的大数据算法。
杜克大学《大规模数据科学》第1课:Pig架构与性能 🐷

在本节课中,我们将学习Apache Pig的架构原理及其性能特点。Pig是一种用于分析大型数据集的高级平台,它构建在Hadoop MapReduce之上,旨在简化复杂的数据处理任务。
Pig系统工作原理
上一节我们提到了Pig Latin语言,本节中我们来看看Pig系统是如何执行这些程序的。
程序通过编写Pig Latin命令并将结果赋值给变量来运行。之后,可以在后续命令中引用这些变量。例如,我们先将数据加载到变量A和B中,然后可以基于A进行过滤、排序等操作。
需要强调的是,在尝试实际写出结果之前,系统不会执行任何实际工作。这种模式被称为惰性求值。
从程序到执行
那么,当你编写好程序后,系统内部发生了什么?
首先,Pig解析器会将程序的语法转换为一个称为执行计划的抽象表示。这个术语源自数据库领域,Pig项目的负责人Chris Olson正是一位杰出的数据库专家。
在这个抽象的执行计划中,操作(我们称之为算子)通常与Pig Latin命令一一对应,尽管并非总是如此。
编译为MapReduce任务
仅有执行计划还不够,我们无法直接执行它。下一步需要将执行计划编译成MapReduce作业。
编译的关键目标是最小化所需的MapReduce作业数量,因为启动每个作业都有不小的开销。例如,你不会希望仅仅为了过滤一个数据集就运行一个完整的MapReduce作业。理想情况下,应该将这项工作与其他正在进行的任务(如扫描和读取数据)合并处理,以最大化单次数据扫描的效用。
以下是一个示例,说明一个Pig程序如何被编译为单个MapReduce作业:
-- Pig Latin 示例
A = LOAD 'input1' USING PigStorage(',');
B = LOAD 'input2' USING PigStorage(',');
C = FILTER A BY $0 > 10;
D = JOIN C BY $0, B BY $1;
STORE D INTO 'output';
在这个案例中,整个程序可以编译为一个MapReduce作业:
- Map阶段:加载数据A和B,并对A进行过滤。
- Reduce阶段:执行连接操作。
最后,这些编译好的MapReduce作业会像往常一样被调度到Hadoop集群上运行。
Pig性能演进 📈
接下来,我们看看Pig的性能表现。这里的对比有些特别,因为Pig本身就构建在MapReduce之上。
下图展示了Pig性能与手写MapReduce代码的对比:

从图中可以看到一个清晰的模式:
- 2008年9月发布的Pig初始版本,其速度远慢于直接手写的MapReduce代码。
- 随后,经过一系列改进,Pig的性能随时间推移不断提升。
- 最终,Pig的性能达到了与手写MapReduce代码相当的水平。
这个案例揭示了一个常见规律:抽象化会带来一定的性能成本。然而,通过持续优化,通常可以恢复大部分手写代码的性能。与此同时,通过提供更高级的接口,我们显著提升了程序员的开发效率。值得赞赏的是,Pig团队坦诚地展示了他们从起步较慢到逐步优化的完整历程。
总结

本节课中,我们一起学习了Apache Pig的核心工作机制。我们了解到Pig采用惰性求值模型,其执行流程是:先将Pig Latin程序解析为抽象执行计划,然后将其编译优化为尽可能少的MapReduce作业,最后在Hadoop集群上运行。通过回顾Pig的性能演进史,我们也认识到高级抽象工具在追求开发效率的同时,通过持续优化也能达到出色的运行时性能。
大规模数据科学(大数据操作,第1课/共3课) - P97:数据模型 🗄️

在本节课中,我们将要学习Pig Latin语言所使用的核心数据模型。理解这个数据模型是掌握后续数据处理操作的基础。
数据模型概述
Pig Latin的数据模型包含四种基本类型。其中一种是原子类型,另外三种是集合类型。
原子类型
原子类型是最基本的数据单元。它指的是一个原始值,例如一个整数或一个字符串。
集合类型
以下是三种主要的集合类型。
元组
元组是一个字段序列。每个字段可以是任何类型,不限于原子类型。
包
包是一个元组的集合。这些元组不必是相同的类型。这与关系数据库中的表或关系不同。包允许重复元素,而集合不允许。
映射
映射类似于Python中的字典。它由字符串字面量键映射到任何其他类型的值组成。
数据模型示例
上一节我们介绍了数据模型的基本类型,本节中我们来看看一个具体的例子。
假设我们有以下数据结构:
<1, {(2, 3), (4, 5)}, [Apache#search]>
在这个例子中,最外层的结构是一个元组。它包含三个字段:
- 第一个字段是整数
1,这是一个原子。 - 第二个字段是一个包,用花括号
{}表示,其中包含两个元组(2, 3)和(4, 5)。 - 第三个字段是一个映射,包含一个键值对,将键
Apache映射到值search。
我们可以为这些字段命名,例如 F1、F2、F3。字段名称可以来自不同的地方,我们稍后会讨论。
数据访问与操作
了解了数据结构后,我们来看看如何访问和操作其中的数据。
以下是几种访问和操作数据的方式:
- 按位置访问:使用
$0可以获取元组的第一个字段。在我们的例子中,$0将返回原子1。 - 按名称访问:使用字段名称,例如
F2,可以获取对应的字段。这将返回我们之前定义的包{(2, 3), (4, 5)}。 - 投影操作:表达式
F2.$0表示对包F2中的每一个元组,提取其第一个元素(索引为0)。这将返回一个新的包{2, 4}。 - 映射查找:表达式
F3#Apache中的#符号表示在映射F3中查找键为Apache的值。如果F3不是映射类型,则会报错。在我们的例子中,它将返回原子search。 - 聚合函数:我们可以使用函数对数据进行聚合。例如,
SUM(F2.$0)会对包{2, 4}中的所有值求和,结果为11。
总结

本节课中我们一起学习了Pig Latin的核心数据模型。我们了解到该模型包含原子、元组、包和映射四种类型。这个模型是非关系型的,支持嵌套数据结构和多种数据类型,为灵活的大数据处理提供了基础。
课程1:大规模数据科学(大数据操作) - P98:加载、过滤与分组 🐘📊

在本节课中,我们将学习Pig Latin语言中的三个核心操作:LOAD(加载)、FILTER(过滤)和GROUP(分组)。这些是处理大规模数据的基础,能帮助我们从原始数据中提取有价值的信息。
概述
我们将首先了解如何将数据从HDFS加载到系统中,然后学习如何根据条件过滤数据,最后探索如何将数据按特定字段分组。这些操作共同构成了数据预处理和分析的基石。
加载数据 (LOAD)
上一节我们介绍了课程背景,本节中我们来看看如何将数据导入系统。LOAD命令用于将数据从HDFS(Hadoop分布式文件系统)加载到Pig中。在Hadoop生态系统中,输入数据在逻辑上通常被视为一个“包”(bag),即一个元组(tuple)的序列。
你可以简单地使用LOAD命令,但也可以使用USING关键字指定解析数据的函数。这看似微不足道,但实际上触及了传统数据库产品的一个核心问题,也体现了基于MapReduce系统(如Hadoop)的优势。有时,你面对的是原始格式的数据,需要自己进行解析。
传统数据库的价值主张是:设计一个模式(schema),然后将所有数据加载到该模式中,之后你就能从查询中获益。但问题在于:谁来执行这个加载任务?如果我有20TB的文本文件,我必须管理这个庞大的并行计算任务。这正是Hadoop及其扩展(如Pig和Hive)发挥作用的地方。
在许多数据科学应用架构中,Hadoop及其扩展被用于初始的处理和解析(即加载数据),然后将结果加载到更传统的数据库中,以便进行即席查询。我们在最早的课程片段中讨论的奥巴马竞选团队就使用了类似的架构。他们使用Hadoop进行ETL(提取、转换、加载)工作负载,然后使用Vertica数据库进行数据切片和切块分析。这正逐渐成为设计大规模数据分析架构的最佳实践。
LOAD命令的优点是,你可以指定自己的解析函数,从而处理原始格式的数据。此外,如果你的解析函数能生成多种不同的输出,你还可以指定一个模式(schema),为列命名。这可以看作是一种“读时模式”(schema on read):数据本身没有模式,但你在将其读入内存时可以强加一个模式。
以下是一个LOAD命令的示例,我们将以此作为后续的运行示例:
data = LOAD 'input/data.txt' USING PigStorage(',') AS (f1:int, f2:chararray, f3:float);
这个命令从input/data.txt文件加载数据,使用逗号作为分隔符,并指定了三个字段的名称和类型。
过滤数据 (FILTER)
在将数据加载到系统后,我们常常需要筛选出感兴趣的部分。FILTER命令用于根据布尔条件从数据集中移除某些元组。
由于Hadoop生态系统很大程度上基于文本,你可以在条件中使用正则表达式等功能,尽管这在某些情况下可能是一种限制。其语法是:FILTER某个大数据集BY某个条件。
以下是一个示例:
filtered_data = FILTER data BY f1 == 8;
这个命令会找出所有f1字段(我们在LOAD命令中命名的第一个列)等于8的元组。它非常简单直接。
分组数据 (GROUP)
上一节我们学习了如何过滤数据,本节中我们来看看如何将数据聚合。GROUP命令用于将数据按某些列聚集在一起。
它有几种不同的形式,但我们现在只关注基本的GROUP命令(稍后会讨论COGROUP)。GROUP命令表示:按某些列序列对这个大数据集进行分组。
这看起来很像SQL中的GROUP BY子句,有相似的味道,但它的行为相当不同。如果你有一个数据集A,并按f1分组,那么你得到的输出是元组,但现在第一个字段(因为我们按f1分组)是分组键。第二个字段现在是一个“包”(bag),它是与该特定键关联的所有元组的一个小集合表示。
例如,如果原始数据中只有一个f1等于1的元组,那么该元组会出现在分组中。如果有两个f1等于4的元组,那么这个“包”里就会有两个元组。这样,你就可以从一个扁平的结构开始,构建出一个嵌套的结构,出于各种原因(我们马上会看到)。
另一个需要注意的点是(这也是我对Pig不太满意的地方之一,因为它有些操作是隐式而非显式的),这些字段的名称会被默认设置。分组后的第一个字段默认命名为group,第二个字段默认命名为原始数据集的名称。当你编写Pig代码时,可能会觉得有点困惑,但你可以理解他们为什么这么做:原因是,这些“包”总体上包含了与原始数据集相同的信息,只是以某种方式嵌套了。所以,用原始数据集的名字来命名它是有道理的。
以下是一个GROUP命令的示例:
grouped_data = GROUP data BY f1;
这个命令会按f1字段对data进行分组。结果中的每个元组将包含分组键(f1的值)和属于该组的所有原始元组构成的“包”。
总结
本节课中我们一起学习了Pig Latin中三个基础且强大的操作:
LOAD: 用于从HDFS将原始数据加载到系统中,支持自定义解析和读时模式。FILTER: 用于根据条件筛选数据,是数据清洗和聚焦的关键步骤。GROUP: 用于按指定字段对数据进行聚合,将扁平数据转换为嵌套结构,为后续的聚合分析(如求和、计数)做准备。

掌握这些操作是构建大规模数据处理流程的第一步。它们允许你灵活地处理原始数据,并将其转换为更适合分析的形式,这正是Hadoop生态系统中数据科学工作负载的常见模式。
大规模数据科学(大数据操作,第1课/共3课) - P99:分组、去重、遍历、展平 🧮

在本节课中,我们将要学习Pig Latin语言中的几个核心操作:DISTINCT、GROUP BY、FOREACH和FLATTEN。这些操作是处理和分析大规模数据集的基础,理解它们的工作原理对于编写高效的数据流水线至关重要。
DISTINCT 操作符 🗑️
DISTINCT命令的功能正如其名:它用于移除数据集中的所有重复项。
对于一个简单的例子,如果你的数据包中有两个值相同的元素,DISTINCT的输出将只保留其中一个。
-- 假设关系 A 包含重复数据
A = LOAD 'data' AS (f0, f1, f2);
UNIQUE_A = DISTINCT A;
GROUP BY 与 DISTINCT 的关系 🔗
上一节我们介绍了DISTINCT,本节中我们来看看它与GROUP BY的关系。
我可能会提出一个观点:DISTINCT A 等同于 GROUP A BY (f0, f1, f2)。
首先,为什么这么说?请记住,GROUP操作符会为分组列的每一个唯一组合输出一个单独的元组,这听起来很合理。
如果你了解SQL,你可能会想到,在SQL中这在一定程度上也是成立的。你可以在SQL中使用DISTINCT关键字,也可以通过GROUP BY所有列来达到类似的效果,最终得到相同的结果。
那么,这两个表达式完全一样吗?它们会产生相同的输出吗?
并不完全一样。因为GROUP命令会产生一个分组结构。具体来说,它会生成一个group字段(即分组键)和一个包字段(包含所有属于该组的元组)。因此,你会得到一种信息的重复。相比之下,DISTINCT的输出则简洁得多。
所以你需要小心,并确保理解这些操作会产生什么结果。

GROUP BY 的工作原理 ⚙️
我们已经看到过GROUP BY是如何工作的。
回顾我们的MapReduce处理示意图或并行处理示意图:我们将数据分成若干部分,应用一个映射函数,该函数为每个元组分配一个键(分组依据),并将该元组作为值。例如,如果我们按f1分组,那么f1就成为键,值则是元组中的所有三个元素。
这些键值对通过网络被混洗到Reduce端,Reduce端会为每个键构造一个包含所有对应元组的包类型。
这是一个单一的MapReduce作业。
FOREACH 操作符 🔄
FOREACH命令可能是最复杂的操作符之一。它用于处理数据包中的每一个元组。
其基本写法如下:
-- 对关系A中的每个元组进行操作
B = FOREACH A GENERATE f0, (f1 + f2) AS sum;
在这里,我们生成了一个包含两个字段的新元组:一个是f0,另一个是f1和f2的和。你可以在GENERATE子句中调用用户自定义函数、编写算术表达式或进行多种操作。
以下是另一个例子:
-- 首先按f1分组
Y = GROUP A BY f1;
-- 然后对每个分组进行操作
Z = FOREACH Y GENERATE group, A.(f1, f2);
group是分组操作后自动赋予分组键的魔术名称。A是包含该组所有元组的包的魔术名称。A.(f1, f2)表示从包A的每个元组中投影出f1和f2字段。
这个操作的结果是:X(即分组键,也就是f1的值),以及该组中所有元组的f1和f2字段的列表。
这里的要点是,你可以通过编写这类表达式来操作这些嵌套对象。但需要思考的是,由于嵌套数据模型带来的额外灵活性,理解其内部过程会稍微复杂一些。
在作业中,你将有机会尝试这些操作,并确保理解它们的功能。
FLATTEN 操作符 📤
FLATTEN不是一个独立的操作符,它用在FOREACH的上下文中。
由于嵌套结构的复杂性,有时你可能希望恢复其扁平化的版本。例如,如果你想从存储在变量Z中的嵌套结构恢复成扁平关系,可以这样做:
-- Z是一个包含嵌套包的关系
FLAT_Z = FOREACH Z GENERATE group, FLATTEN(A);
FLATTEN的作用是“展开”包,为包中的每个元素生成一个独立的输出元组。
我个人不太喜欢这种设计,因为仅仅使用FLATTEN这个关键字就改变了FOREACH...GENERATE的语义,我认为这是一种非常令人困惑的实现方式。这里的原理很难解释清楚,最好是记住它的功能,通过练习来掌握它的用法。
总结 📝
本节课中我们一起学习了Pig Latin中四个关键的数据操作:
DISTINCT:用于移除关系中的重复元组。GROUP BY:根据指定字段对元组进行分组,形成嵌套结构。FOREACH:对数据包中的每个元组或每个分组进行迭代和转换,是进行复杂计算的核心。FLATTEN:在FOREACH中使用,用于将嵌套的包结构展平为扁平的元组流。


理解这些操作符的输入、输出和语义,是构建有效大数据处理流程的基础。GROUP BY会创建嵌套,而FOREACH和FLATTEN则用于在这些嵌套结构内部或之间进行转换和计算。
大规模数据科学(大数据操作,第1课/共3课) - P100:协同分组与连接 🧩

在本节课中,我们将要学习Pig Latin中的两个核心操作:协同分组与连接。我们将了解它们如何对多个数据集进行分组和关联,以及它们在大规模数据处理中的工作原理。
协同分组操作
上一节我们介绍了GROUP命令,它用于对单个数据集进行分组。本节中我们来看看COGROUP命令,它可以在多个数据集上执行类似的分组操作。
我们之前在讨论MapReduce时见过这种机制,但在Pig中,我们使这个操作变得明确,并赋予它一个特定的命令。接下来我们将探讨其原因。
COGROUP的语法如下所示:
COGROUP datasetA BY field_reference, datasetB BY field_reference;
这里,我们可以通过字段名或位置来引用分组键。如果datasetA和datasetB都是元组包,那么对它们进行协同分组的结果结构如下:
- 分组键:所有数据集共用的分组依据。
- 数据集A组:来自
datasetA的、与该键匹配的所有元组。 - 数据集B组:来自
datasetB的、与该键匹配的所有元组。
以下是关于COGROUP的两个要点:
- 如果某个数据集中没有与分组键匹配的元组,那么对应的组将为空。
- 这与MapReduce中的连接操作略有不同。在MapReduce中,所有匹配的元组会出现在同一个Reducer组中。而在Pig的
COGROUP中,来自不同数据集的元组被明确地分到不同的组中。
连接操作
在了解了协同分组之后,让我们来讨论另一个操作符:连接。它的功能正如其名。
JOIN的语法如下:
JOIN datasetA BY join_key, datasetB BY join_key;
给定datasetA和datasetB,连接操作的结果与我们过去讨论过的连接概念一致。它会查找datasetA中每个元组的连接键(例如$0),并找到datasetB中所有具有相同连接键的对应元组,然后进行组合。
连接操作并不局限于两个数据集。你可以连接任意数量的数据集,它们可以一起被处理。在底层,这通常对应一个MapReduce作业:
- Map阶段:来自所有数据集的每个元组都与由语法中字段引用所表示的连接键相关联。
- Shuffle阶段:这些键值对被混洗并跨网络传输。
- Reduce阶段:具有相同键的所有元组汇聚到同一个Reducer,最终生成连接后的元组。

因此,这个操作符本质上并非二元操作。它只是处理与各自连接键相关联的元组,并将它们全部通过网络进行混洗。
总结

本节课中我们一起学习了Pig Latin中的两个重要操作。我们首先探讨了COGROUP,它能够将多个数据集按相同的键进行分组,并保持各组独立。接着,我们学习了JOIN操作,它用于根据连接键合并多个数据集中的元组,并且可以处理两个以上的数据集。理解这些操作是掌握大规模数据关联与整合的关键。
大规模数据科学(大数据操作,第1课/共3课) - P101:连接算法 🧩

在本节课中,我们将要学习在大规模数据处理中,几种针对不同场景优化的连接算法。我们将探讨基础连接机制可能遇到的问题,并介绍三种特殊的连接算法:复制连接、倾斜连接和合并连接。
基础连接机制的问题
上一节我们介绍了基础的MapReduce连接机制。本节中我们来看看这种机制可能存在的问题。

基础连接机制通常为每个元组关联一个连接键,通过网络进行混洗,然后在Reduce端计算连接结果。然而,在某些情况下,这种机制效率不高。

以下是基础连接机制可能遇到的三种主要问题:
- 表大小差异巨大:如果一个表非常大,而另一个表非常小,将所有数据通过网络混洗会产生不必要的开销。
- 数据倾斜:如果连接键的分布极不均匀,例如一个键关联了海量数据,会导致单个Reducer承担绝大部分工作,从而削弱并行处理的优势。
- 已排序数据未被利用:如果两个关系已经按照相同的方式分组,确保单台机器上已包含来自两个关系的所有必要元组,则存在更高效的连接方式。
复制连接(广播连接)🚀
针对表大小差异巨大的情况,有一种更快的优化方法:复制连接。
其核心思想是,将小表复制到大表的所有分区中,从而所有连接工作都可以在Map阶段完成。

假设我们有一个被分成若干部分的大表,以及一个非常小的表。通常的做法是将所有元组通过网络混洗,在Reduce端进行连接。
但这里存在一个优化机会:如果小表足够小,可以放入单台机器的内存中,我们为什么不直接复制它呢?
我们可以将小表发送给每个Map函数。每个启动的Map函数会直接从HDFS中拉取小表的副本。这样,在Map端我们就拥有了所需的全部信息。
- 大表分区1中的每个元组都可以与小表中的对应元组进行连接。
- 大表分区2中的每个元组也可以与小表中的对应元组进行连接。
- 以此类推。
在Map阶段结束时,我们就得到了正确的答案,即所有连接后的元组。
这种方法之所以更高效,是因为我们无需将所有数据通过网络混洗。所有工作都在Map阶段完成,这带来了巨大的性能提升。这种算法也可能被称为广播连接。
核心条件:小关系必须能放入内存。
实现思路:每个Map函数中的Mapper直接从HDFS中拉取小关系的一个副本。
倾斜连接 ⚖️
现在,让我们看看如何处理数据倾斜的问题。这是基础连接机制的另一个特殊案例。
通常,我们有两个关系(例如一个蓝色关系和一个红色关系)。在Map阶段,每个元组与其连接键关联。但问题在于,大部分数据最终可能都汇聚到单个Reducer上。
原因可能是大部分数据都与单个连接键相关联。例如,如果你在订单ID和订单明细上进行连接,试图将所有明细项与其对应的订单关联起来,可能会出现一个订单包含数百万个零件,而其他订单只包含五个零件的情况。
公式描述问题:如果数据集中有显著比例的数据与单个连接键 K_skew 关联,即 count(tuples where key = K_skew) / total_tuples 的值很大,那么这个Reducer将承担绝大部分工作,并行化的优势就会大打折扣。


合并连接 🔄
第三种特殊连接算法是合并连接。这种算法利用了数据可能已预先排序或分组的特点。
它基于这样一个事实:你可能已经以相同的方式对两个关系进行了分组,从而确保在单台机器上,你已经拥有了来自一个关系的所有所需元组,以及来自另一个关系的所有所需元组。

这种方法避免了不必要的网络混洗,直接在本地的、已排序的数据块上进行合并操作,效率很高。它要求两个输入数据集都按照连接键排序,并且MapReduce作业的分区方式与排序方式一致。
总结
本节课中我们一起学习了大规模数据连接中的三种优化算法。
- 复制连接适用于小表可以完全放入内存的场景,通过广播小表到所有Map任务,在Map阶段完成连接,避免了大量的网络数据传输。
- 倾斜连接关注于解决因连接键分布极度不均而导致的任务负载不平衡问题,防止单个Reducer成为性能瓶颈。
- 合并连接则利用了数据已预先按连接键排序的特性,直接在本地进行高效合并,是处理已排序大数据集的理想选择。

理解这些算法的适用场景,对于设计和优化高效的大规模数据处理流程至关重要。
大规模数据科学(大数据操作,第1课/共3课) - P102:数据倾斜 🐌


在本节课中,我们将要学习MapReduce框架中一个常见的性能瓶颈——数据倾斜。我们将了解它如何产生、为何会严重影响作业执行时间,并探讨几种应对策略,特别是“倾斜连接”和“合并连接”。
数据倾斜问题
上一节我们介绍了MapReduce的基本工作原理。本节中我们来看看一个具体问题:数据倾斜。
在理想情况下,MapReduce作业中的各个任务(Task)应该在大致相同的时间内完成。然而,当数据分布不均匀时,某些任务可能会处理远超平均数量的数据,导致其执行时间异常长。这些运行缓慢的任务被称为 “掉队者” 或 “拖尾任务”。
下图展示了一个实际作业的任务执行时间线:

从图中可以看到,大多数Map任务在约20秒内完成,但有一两个Map任务却需要长达270秒。由于Reduce阶段必须等待所有Map任务完成后才能开始,图中Reduce任务开始前的大片空白区域实际上就是被浪费的并行计算资源。
这带来了几个问题:
- 并行性被破坏:一个缓慢的任务会拖慢整个作业。
- 性能损失严重:一个本可能只需50秒的作业,因为数据倾斜最终可能耗时350秒,丧失了并行计算的优势。
所以,当被问及MapReduce最大的性能瓶颈之一时,答案应该是 “掉队者”。
倾斜连接:一种解决方案
那么,我们能为此做些什么呢?本节我们来看一种针对连接操作中数据倾斜的解决方案。
考虑一个常见场景:在执行Reduce端连接时,某个连接键(例如一个热门商品ID)对应的记录数量异常庞大,导致处理该键的单个Reducer任务负载过重,成为“掉队者”。

以下是解决此问题的一种思路,称为 “倾斜连接”:

- 识别与拆分:首先识别出导致倾斜的大数据集(例如上图中的“蓝色”关系)。将其从原本的Reducer中移除。
- 广播小表:如果另一个关系(“红色”关系)足够小,可以将其广播到所有工作节点。
- 重新分配与并行计算:将那个庞大的数据集(蓝色)拆分成多个部分,分配给多个新增的Reducer。每个新增的Reducer都持有一份完整的广播数据(红色)副本。
- 执行连接:每个Reducer在本地将分配到的蓝色数据分片与完整的红色广播数据进行连接计算。
这个过程结合了广播连接和常规Reduce端哈希连接的思想。通过将倾斜数据分摊到多个Reducer上处理,我们实现了更均衡的负载,从而加速了整个连接操作。
在Pig等工具中,你可以通过指定skewed join来启用这种连接方式,但它通常不会自动执行。
合并连接:另一种优化策略
上一节我们介绍了处理数据倾斜的特殊连接。本节中我们来看看另一种优化策略——合并连接。如果说倾斜连接是解决“问题”的技巧,那么合并连接则是在条件满足时,可以显著提升性能的“机会”。
合并连接的应用有一个关键前提:参与连接的两个关系已经按照连接键进行了协同分区和排序。
这意味着,对于任意一个连接键值,来自两个关系的所有相关记录都已经被预先分配到了同一台物理机器上,并且各自内部已按连接键排序。
当这个条件成立时,会发生什么?

由于每个Mapper本地已经拥有了两个关系中对应分区的全部数据(且已排序),因此连接操作可以完全在Map阶段完成,无需经过Shuffle和Reduce阶段。Mapper可以像在传统数据库中一样,通过归并排序的方式,顺序读取两个已排序的数据流,直接输出连接结果。
核心优势:
- 避免网络传输:消除了Shuffle阶段巨大的网络开销。
- 本地化计算:所有计算在本地完成,效率极高。
那么,何时这个条件会成立呢?通常,这发生在前面的Pig操作(如COGROUP)已经按照连接键对数据进行了分区和排序之后。在Pig中,你可以通过显式指定merge join来利用这种情况,系统会检查条件是否满足并选择最优执行计划。
总结
本节课中我们一起学习了MapReduce中的数据倾斜问题及其解决方案。
- 我们首先了解了掉队者任务如何严重损害作业的并行性能。
- 接着,我们探讨了倾斜连接,这是一种通过识别倾斜键、广播小表、并拆分倾斜数据到多个Reducer来缓解负载不均的特殊连接策略。
- 最后,我们学习了合并连接,这是一种在数据已按连接键协同分区和排序的前提下,可以完全在Map阶段完成的高效连接算法,能极大减少网络开销。

理解这些概念和技巧,对于设计和优化大规模数据处理作业至关重要。
大规模数据科学(大数据操作,第1课/共3课) - P103:其他命令 🐘

在本节课中,我们将学习Pig Latin中除JOIN和COGROUP之外的其他重要命令。我们将了解这些命令的功能,并探讨为何在“大数据时代”下,能够处理多种格式的“原位数据”变得至关重要。
为何需要COGROUP和JOIN?🤔
上一节我们介绍了COGROUP和JOIN操作。本节中我们来看看为什么两者都需要存在。
COGROUP操作被明确设计出来的原因是,JOIN本质上是一个两步过程。第一步是基于连接键创建分组,第二步才是实际生成连接后的元组。而这个分组创建步骤,对于许多应用场景都非常有用,而不仅仅是用于生成连接结果。
例如,如果你想对每个关系中的贡献进行求和,你可以通过COGROUP一步完成。你不需要先进行连接,然后再进行另一次分组——而这在关系型数据库中通常是必须的。因此,COGROUP提供了一个机会,将原本需要两个MapReduce作业完成的任务合并为一个。
这里需要指出的是,JOIN本质上只是一种语法糖。你可以用COGROUP来表达JOIN:第一步是在相同的连接列上进行COGROUP,第二步是对每个分组运行一个命令来生成扁平的连接结果视图。
其他Pig Latin命令 📚
除了分组和连接,Pig Latin还提供了一系列其他命令来处理数据。
以下是几个我们不会深入讨论但非常重要的命令:
STORE:将数据写入HDFS,供未来的命令使用。UNION:合并两个数据集并移除重复项。CROSS:计算两个数据集之间所有可能的配对。这在需要计算某种相似度函数时非常有用。DUMP:将输出打印到屏幕。ORDER:对输出进行排序。
STORE命令与自定义函数 💾
让我们以STORE命令为例进行更深入的了解。就像你可以使用自定义函数来解析数据一样,你也可以使用自定义函数来写出数据。
这意味着数据可以更好地与你正在使用的其他系统兼容,例如MapReduce本身,或者其他期望特定数据格式的应用程序。
这一点与LOAD命令类似,但它实际上非常强大,并且与关系型数据库采取的“封闭花园”式方法有显著不同。在关系型数据库中,一切数据进入后都完全受数据库控制。而Pig的边界更具渗透性,你可以让数据以任何格式存放,仍然能用Pig处理它,同时也能用其他系统处理它。
处理“原位数据”的重要性 🌍
这种能力之所以至关重要,并不仅仅是为了优化性能或减少加载时间,更是因为如今的数据太大了,你无法移动它们。
你无法将所有数据都放入数据库,然后再全部提取出来转移到其他系统进行处理。数据量太大了,你无法移动一个PB级别的数据集。你必须将计算带到数据所在的地方,而不是将数据带到计算所在的地方。
因此,这种处理“原位数据”的能力,已经成为大数据时代的一个关键需求,而这在传统的关系型数据库时代并非如此关键。
总结 ✨

本节课中我们一起学习了Pig Latin中的其他命令,重点探讨了COGROUP与JOIN的关系,以及STORE等命令的用途。我们理解了Pig能够灵活处理多种格式“原位数据”的设计哲学,并认识到这在大数据场景下是比传统数据库“封闭花园”模式更适应现实需求的关键优势。核心在于,当数据规模巨大时,将计算移至数据所在处比移动数据本身更为可行和高效。
大规模数据科学(大数据操作,第1课/共3课)🚀:P104 评估与优化演练

在本节课中,我们将通过一个具体的Pig程序示例,来学习高级操作如何为系统提供自动优化的机会。我们将看到,系统如何在不需程序员明确指定的情况下,通过调整执行计划来提升效率。
上一节我们介绍了Pig Latin的高级操作概念,本节中我们来看看一个具体的程序示例及其优化过程。
我们考虑一个处理网络流量日志数据的Pig程序。数据包含三列:IP地址、时间戳和访问的URL。程序的目标是统计我们本地网络中两个特定网关的IP地址的访问次数。
以下是该程序的原始步骤:
-
加载数据:我们使用
LOAD命令读取数据。由于数据是Pig原生支持的简单分隔格式(如CSV),因此无需使用USING子句指定自定义解析函数。traffic_data = LOAD 'weblog.txt' AS (ip:chararray, time:chararray, url:chararray); -
按IP分组:使用
GROUP命令,根据IP地址对数据进行分组。grouped_by_ip = GROUP traffic_data BY ip; -
计算访问次数:对每个分组,我们生成IP地址及其对应的日志条目数量(即访问次数)。
access_counts = FOREACH grouped_by_ip GENERATE group AS ip, COUNT(traffic_data) AS count; -
过滤特定IP:我们只关心来自两个特定网关IP的流量,因此使用
FILTER命令进行筛选。filtered_ips = FILTER access_counts BY (ip == '192.168.1.1' OR ip == '192.168.1.2'); -
存储结果:最后,将过滤后的结果存储到HDFS上的文件中。
STORE filtered_ips INTO 'output_path';
🔍 发现优化机会
观察这个程序流程,我们可以发现一个潜在的效率问题。程序首先对整个庞大的数据集进行分组和计数操作,但最终只对其中两个IP地址的结果感兴趣。这意味着大量针对无关IP地址的计算是冗余的。
理想的执行顺序是先过滤,再分组和计数。这样,后续昂贵的分组操作就只在两个IP地址对应的少量数据上进行,从而大幅减少计算量。
⚙️ Pig系统的自动优化
在更复杂的程序中,手动发现并重写所有优化机会可能很困难。这就是Pig系统发挥价值的地方。
Pig采用惰性求值策略。当你输入一系列Pig Latin语句时,系统并不会立即执行它们,而是首先构建一个逻辑执行计划。只有在遇到 STORE 或 DUMP 这类输出命令时,系统才会开始优化并执行这个计划。
在优化阶段,Pig的优化器会分析整个逻辑计划。对于我们的示例,优化器能够安全地判断出将 FILTER 操作移动到 GROUP 操作之前不会改变最终结果。因此,它会自动重排执行顺序,生成一个更高效的物理执行计划。
优化后的逻辑执行顺序变为:
LOAD数据。FILTER出目标IP地址。GROUP过滤后的数据。FOREACH ... GENERATE计算计数。STORE结果。
这种优化之所以可能,是因为我们使用了Pig Latin提供的高级、声明式操作符(如 GROUP、FILTER)。系统理解这些操作符的语义,从而能够进行等价变换。如果使用像Java这样的低级语言编写MapReduce任务,系统将很难进行此类全局性优化。
📝 本节总结
本节课中我们一起学习了Pig程序优化的重要概念。通过一个网络日志分析的例子,我们看到了:
- 程序原始写法可能存在的效率问题。
- Pig的惰性求值机制如何为优化创造时机。
- 系统如何自动重排操作顺序(如将过滤提前)来提升性能,而无需程序员干预。
- 使用高级、声明式操作符的核心优势在于,将“做什么”的逻辑与“如何做”的优化分离开,让系统能够智能地寻找最佳执行路径。

理解这些原理,有助于我们更好地利用Pig这类工具,编写出既清晰又高效的大数据处理程序。
课程1:大规模数据科学 - 从抽象执行计划到MapReduce作业 🚀

在本节课中,我们将学习如何将一个抽象的查询执行计划,编译并优化成一系列高效的MapReduce作业。我们将重点了解Pig编译器如何通过操作符融合和作业合并来减少昂贵的MapReduce作业数量,从而提升大数据处理的性能。
上一节我们介绍了Pig如何将高级查询语言转换为抽象执行计划。本节中,我们来看看编译器如何将这个计划映射到具体的MapReduce作业序列。
编译器首先会识别执行计划中所有的GROUP和COGROUP操作符。这些操作符是执行数据分组的关键,通常需要网络间的数据混洗(shuffle)。
因此,编译器会为每一个GROUP或COGROUP操作符分配一个独立的MapReduce作业。
接下来,编译器会尝试向前和向后遍历执行计划,尽可能多地将其他操作合并到同一个MapReduce作业中。其核心目标是减少需要执行的MapReduce作业总数,因为启动和运行每个作业的开销都很大。
以下是编译器进行优化的几个关键策略:
- 合并过滤操作:
FILTER操作(例如,筛选特定IP地址)可以在从磁盘加载数据时,或在处理每条元组时立即应用。因此,它完全可以被合并到负责GROUP操作的同一个MapReduce作业的Map阶段中,无需独立的作业。 - 合并加载操作:
LOAD操作仅仅是读取磁盘数据,计算成本极低,同样可以合并到同一个MapReduce作业中。 - 优化Reduce阶段:对于
GROUP操作后紧跟的FOREACH ... COUNT(计数)操作,编译器足够智能,能识别出我们并不需要实际构造出完整的分组对象。它可以直接在Reduce阶段输出类似(IP, count)的键值对,而不是(IP, [list_of_all_records])再计数。这避免了构造和传递大型分组对象的开销,带来了显著的性能提升。
通过上述优化,一个包含加载、过滤、分组和计数的复杂查询,最终可能被编译成仅仅一个MapReduce作业。
这个作业的Map函数可能类似以下伪代码:
def map(record):
parsed_data = parse(record)
if parsed_data.ip in target_ips:
emit(parsed_data.ip, 1)
而在Reduce端,则直接对每个IP的计数进行求和,无需构造中间分组列表。
当然,并非所有操作都能合并。某些命令,例如SORT(排序),由于其特性,总是需要一个独立的MapReduce作业。
回顾:NoSQL系统与Pig的启示 🔄
在之前的几个小节中,我们讨论了NoSQL系统。我们认为数据科学家有必要理解它们,一方面你可能直接使用它们,另一方面你可能需要评估它们与其他系统(如关系型数据库)相比的优缺点。
我们谈到,“NoSQL”通常意味着无固定模式(No Schema)、无(或弱)事务(No Transactions) 以及无声明式查询语言(No Language)。它更像是对数据系统的一次“重启”,专注于实现高吞吐量的读写。
如今,大规模数据系统的设计空间被更充分地探索,出现了各种不同特性的排列组合。一个明显的趋势是,系统又开始重新引入模式、事务和查询语言等特性(例如Google的Spanner系统)。
因此,“NoSQL”是一个不断演进的概念。关键在于认识到,整个大规模数据系统的可能性空间正在被探索:NoSQL代表一个方向,关系型数据库代表另一个,同时还有新的方向在涌现,比如试图兼顾两者的“NewSQL”。
我们特别以Pig为例进行了讨论。Pig本身不是一个NoSQL存储系统,而是一个构建在MapReduce之上的分析层。
我们选择Pig,是因为它在Hadoop之上提供了一个类似关系代数的抽象层。这说明了关系代数的思想在很多不同场景下都会出现。
有趣的是,虽然Pig有清晰的关系代数风格,但它并非纯粹的关系数据模型(它支持嵌套数据结构)。这揭示了一个要点:我们可以从数据库领域“ cherry-pick ”(精选)一些概念和技术,而不必非要在传统数据库的语境下使用它们。当数据库似乎无法满足需求时,我们不必“把洗澡水和婴儿一起倒掉”。Pig这类系统正是这么做的。
作为数据科学家,在评估各种新兴系统时,你可以开始理解它们提供的特定功能集,并追溯这些功能的来源、历史以及其中的利弊权衡。
Pig的一个关键特点是 “读时模式”(Schema-on-Read) ,这与传统数据库的 “写时模式”(Schema-on-Write) 不同。“读时模式”允许你直接处理原始格式的数据(in situ data)。这是NoSQL乃至许多现代数据系统的一个新要求,因为数据量太大,无法到处进行格式转换。


本节课中我们一起学习了Pig编译器如何将抽象执行计划优化并编译成高效的MapReduce作业序列,其核心是通过操作符融合来最小化作业数量。同时,我们也回顾了NoSQL系统的特点和发展趋势,并通过Pig的例子,看到了如何借鉴数据库的经典思想(如关系代数)来构建适应大数据场景的新工具,特别是“读时模式”在处理海量原始数据时的重要性。
大规模数据科学(第1课/共3课)🚀:核心抽象与系统演进

在本节课中,我们将学习大规模数据系统的核心抽象概念,并了解这些概念如何在不同工具(如Pig和Spark)中体现。我们将重点探讨编程模型和计算模型,理解它们如何帮助开发者高效处理大数据,而无需深入底层细节。
背景与核心抽象 🧠
上一节我们介绍了多种大数据系统,包括数据库、MapReduce系统、NoSQL系统等。本节中,我们来看看这些系统背后共有的核心抽象。
本课程的重点在于抽象而非具体工具。我们已识别出一些关键抽象,它们与编程模型密切相关。
编程模型:集合级操作

我们反复看到关系代数的例子,即使在其最初设计的数据库系统之外也是如此。这是一个集合级编程的示例。
集合级编程允许你通过单次调用转换整个数据集,而无需编写操作单个记录和管理并行计算的底层代码。
公式示例:Result = π_{name}(σ_{age>30}(People)) (这是一个关系代数表达式,表示选择年龄大于30的人并投影其姓名)
计算模型:数据并行与分布式算法
在编程模型之下是计算模型。我们一直在讨论如何从数据并行分布式算法的角度思考。
这意味着要理解如何让大量机器同时处理单个数据集,并考虑由此产生的问题,例如容错性。
如果你有1000台机器,其中一台在计算过程中出错的概率变得非常高。你必须设计能够容忍这种情况的系统,有时甚至需要设计代码来容忍它。
此外,还有各种性能问题,例如使用磁盘与内存索引来加速搜索、事务处理或其缺失情况下的应对策略等。
案例分析:Pig系统 🐷
我们研究过一个具体系统:Pig。
Pig的编程模型非常类似于关系代数,但它支持嵌套类型和其他一些扩展功能。你可以在其中识别出关系代数的影子。
它有一个优化器,负责接收这些关系代数表达式,将其转换为其他表达式,然后映射到计算模型中。Pig的计算模型就是MapReduce(Hadoop)。
因此,开发者无需直接针对MapReduce层编程,而是在这个更高级别进行编程,让系统完成向下转换。这个主题我们将反复看到。
系统演进趋势:从Pig到Spark 📈
当我首次开设这门课程时,Pig比另一个更多还是研究平台的系统——Spark——更流行。
但从那时起,我们看到了这样的趋势(数据来自Google Trends):
以下是搜索词“Pig Hadoop”与“Spark Hadoop”的对比趋势图。

可以看到,Pig仍处于上升轨迹,它仍然是一个非常相关且值得学习的系统。但最近,它被一个名为Spark的系统显著超越。
我们再次强调,重点应放在抽象而非具体工具上。因为你会遇到这样的情况:你可能学会了某个系统的所有细节,却发现它已被另一个系统取代。但这没关系,因为你学习的是底层概念。
总结 ✨

本节课中我们一起学习了大规模数据科学的核心基础。我们探讨了集合级编程的抽象如何简化数据处理,以及数据并行计算模型如何解决分布式环境下的容错与性能挑战。通过分析Pig和Spark的演进,我们理解了专注于底层抽象概念的重要性,这能让我们在技术快速变迁中保持知识的持久性与适应性。在接下来的课程中,我们将深入这些抽象在具体系统中的实现与应用。
课程名称:大规模数据科学(大数据操作,第1课/共3课) - Spark示例 🚀

概述
在本节课中,我们将要学习一个使用Spark框架编写的程序示例。我们将详细解析这段代码的功能,理解其核心操作,并将其与之前学过的概念(如MapReduce)联系起来。通过这个具体的例子,我们将看到Spark如何以一种简洁、高效的方式处理分布式数据计算任务,特别是经典的“词频统计”问题。
Spark程序解析
这里是一个用Spark编写的程序。问题是,这段代码是做什么的?
第一行代码定义了一个变量,它通过Spark上下文从某个路径读取一个文件,并将其赋值给名为 textFile 的变量。
下一行代码调用了 textFile 对象的一个方法。这个方法就是 flatMap。
我们过去简要提到过 flatMap,但你可能对它不熟悉。不过我们马上就会看到它的作用。传递给这个 flatMap 的参数是一个有趣的表达式,它实际上是一个函数,一个匿名函数,或者如果你熟悉这个术语的话,也可以称为 lambda函数。这个函数接受一个我们称之为 line 的参数,然后对这个 line 执行一些操作。
在这个例子中,它对每一行的操作是:根据空白字符将其分割。因此,这个函数接收一行文本,并生成该行中的所有单词(或标记)。
flatMap 操作的结果随后被传递给 map 操作。map 操作听起来应该很熟悉。
map 操作同样接收一个函数作为参数。这个函数接收一个 word,并生成一个键值对:(word, 1)。
map 操作的结果接着被传递给 reduceByKey 操作。这个操作也应该开始变得熟悉起来。它接收一个函数,这里就是加法运算。加法运算接收一个左参数和一个右参数,并将它们相加。reduceByKey 会通过反复应用这个加法运算来进行归约。
最后,这个归约的结果被传递给变量 counts,然后 counts 可以保存为一个文本文件。
那么,整个计算过程是做什么的呢?我希望你已经认出来了,它就是 词频统计。
词频统计示例详解
以下是它的实际运作过程。我们已经见过一个详细分解的例子。
输入的文本行包含三个标记:“2”, “B”, “or”。flatMap 操作将这一行分割成三个独立的标记。
map 操作给每个标记附加上数字 1。
然后,reduce 操作根据键(即单词)将所有条目分组,并将数字 1 的实例相加,得到总计数。因此,在所有行中,“B”出现了两次,“2”出现了两次,“or”出现了一次,“not”出现了一次。
好的,我们已经反复看到这个过程了。这就是你在Spark中编写这个计算的方式。
Spark与Hadoop MapReduce的对比
既然我们理解了 map 和 reduce,我们就理解了在Spark中编程的一种方式。你可能会问,为什么这里的写法与Hadoop中的 map 不同?
在概念上其实只有一个 map,它允许接收一条记录,并一次性地输出任意数量的记录。本质上,这里他们称之为 flatMap。map 只产生单个输出项,而 flatMap 产生一个列表(或集合)的输出项。所以,flatMap 必须接收一行文本,并产生一个标记的列表(或集合)。而 map 接收单个记录(单个项)并产生单个项。这里并没有真正的概念差异,他们这样命名主要是出于类型安全的考虑。
Spark中的其他熟悉操作
太棒了,我们已经知道了在Spark中编程的一种方式。但这里还有其他熟悉的操作。你可以看到 join 这个词,因为我们已经详细研究过它,所以你知道什么是连接操作。
你可能也知道什么是 coGroup,因为我们在Pig中见过它。之所以需要 coGroup,并且在关系代数和数据库中看不到这个词,是因为只有当讨论嵌套类型时,它才有意义。让我们来看看这个。
这是一个Spark函数,叫做 parallelize。它接收宿主编程语言(在这个例子中是Python)中的一个集合类型(顺便说一下,我们刚刚切换了代码;上一张幻灯片的代码是Scala,而这张幻灯片的代码是Python)。
这只是一个常规的Python元组列表,parallelize 方法将其转换为一个分布式数据集。它在你的本地客户端上是一个列表,但这个方法使其在集群上可用(它不会变成一个巨大的数据集,大小和原来一样,只是使其在集群上可用)。
我们对两个不同的字面量列表执行了这个操作。然后,如果你将它们连接起来,join 会假设你根据键(在这个例子中是URL)进行连接。它生成的结果是关系型连接,索引为HTML,同时包含来自 pageNames 的记录。你可能注意到,这并不完全是关系型连接,因为这是一个元组,而不是扁平化的版本。
好的,这就是 join。而 coGroup 做的事情几乎相同,只是在这里,index.html 在 join 中出现了两次,而在 coGroup 中只出现一次,所有相关的记录都被归集在一起。这正是Pig中的 COGROUP 操作所做的。
总结


本节课中,我们一起学习了Spark框架的一个核心编程示例——词频统计。我们逐步解析了代码,理解了 flatMap、map 和 reduceByKey 等关键操作在Spark中的具体应用。我们还对比了Spark与Hadoop MapReduce在概念实现上的异同,并简要介绍了Spark中其他重要的数据操作,如 join 和 coGroup。通过这个具体案例,我们看到了Spark如何以简洁、强大的方式表达复杂的数据处理逻辑。
大规模数据科学(大数据操作,第1课/共3课) - P108:RDD的优势 🚀

在本节课中,我们将学习Spark的核心概念——弹性分布式数据集(RDD),了解其设计原理、操作方式以及它如何解决传统MapReduce框架的性能瓶颈。
Spark中使用的概念与你在其他系统中学习过的概念完全相同。你可以运用所有已学的知识来理解Spark的内部运作。然而,有一些与Spark特别相关的术语你需要熟悉,其中一个关键术语就是弹性分布式数据集。
什么是RDD?💡
RDD是一个分布式的键值对集合。这听起来应该很熟悉,因为它与MapReduce和Hadoop使用的数据模型完全相同,甚至与定义了主键的关系数据库记录模型也非常相似。键值对和记录之间的区别并不十分显著。
RDD的另一个关键区别在于,它可以跨任务持久化在内存中。这与MapReduce不同,在MapReduce中,出于容错原因,每个任务后所有数据都要写入磁盘。而RDD可以驻留在内存中,你可以在多次迭代甚至多个作业中对它们应用多个任务。这种内存缓存是Spark的关键特性,我们稍后会再次讨论。
RDD的操作:转换与行动 ⚙️
处理这些RDD时,主要有两种操作,这是Spark的术语:转换和行动。
- 转换是我们一直在讨论的算子,例如
map、reduce、filter、join和groupByKey等。它们接收一个或多个RDD,并生成另一个RDD。 - 行动则接收一个RDD,并生成一个值,将值从RDD提取到宿主语言(如Python、Scala或Java)中,然后可以使用该编程语言进行操作。
让我们看一个例子。
以下是Python代码示例:
# 从某个路径读取文本文件,创建一个RDD
textFile = sc.textFile("hdfs://...")
# 应用一个转换:filter
linesWithSpark = textFile.filter(lambda line: "Spark" in line)
# 应用一个行动:count
count = linesWithSpark.count()
# 或者应用另一个行动:first
firstLine = linesWithSpark.first()
在这个例子中:
sc.textFile从路径读取文件并创建RDD。filter是一个转换,它接收一个函数(这里是一个Python lambda匿名函数)。该函数对每一行进行评估,只保留包含单词“Spark”的行,结果保存到新的RDDlinesWithSpark中。count()和first()是行动,它们分别返回RDD中的记录总数(例如74)和第一条记录。
Spark使用的一个评估原则是惰性求值。这意味着转换操作并不会在代码行被遇到时立即执行。相反,它会一直等待,直到必须向宿主语言返回一个实际的值。在此之前,它会堆叠所有计算该值所需的转换,可能对它们进行优化,然后一并运行并提取值。
RDD操作与已有知识的联系 🔗
有一系列RDD操作。你可以像在MapReduce中一样直接编程,使用行为方式相同的 map 和 reduce 调用。但你也可以使用其他算子,这些算子看起来更像基于关系代数的系统(如Pig)甚至更一般的数据库系统中的操作。因此,你可以用多种不同的方式针对Spark进行编程,但所有这些概念你都已学过。
MapReduce的弱点与Spark的解决方案 🎯
现在,让我们回顾一下MapReduce/Hadoop的一些重要弱点,以及Spark如何解决它们。
上一节我们介绍了RDD的基本操作,本节我们来看看Spark旨在解决的核心问题。
以下是MapReduce的主要弱点:
-
手动编写Map和Reduce函数:这可能会变得繁琐且容易出错。这个问题很早就被认识到,并催生了像Hive和Pig这样的系统来部分解决它,允许用户在更高层次的模型(Hive用SQL,Pig用关系代数语言)上编程,由系统生成底层的MapReduce计算。
-
缺乏高效查找的索引:如果想在一个非常大的MapReduce数据集中查找特定记录,除了扫描整个数据集外别无选择。这个问题也较早被认识到,并通过在Hadoop生态系统中引入键值存储(如基于Google BigTable的HBase)来解决。
-
每一步都将完整输出写入磁盘:出于容错原因,MapReduce工作流的每一步都会将其完整输出写入磁盘。这对容错有好处,但对性能绝对是灾难性的。这第三个问题正是Spark设计要解决的。前两个问题已被其他系统解决,但Hadoop由于(尽管可能不完全是)将所有内容写入磁盘而导致的原始性能问题被认为是致命的。
因此,将所有数据放入内存并在内存中对大型数据集进行操作,是Spark自身的关键设计目标。
内存优化的重要性 ⚡️
那么,这种优化重要吗?答案是:绝对重要。
通过一个迭代计算任务的性能对比可以清楚地看到:
- 在第一次迭代时,Spark(约80秒)和Hadoop(约110秒)的时间大致相同。
- 但随着计算继续进行,Hadoop的每一次后续迭代都必须一遍又一遍地重新读取相同的数据集,因此时间线性增长。
- 而Spark只需要在第一次时读取数据,所有后续迭代都能够直接在内存中对其操作,不占用任何额外时间。
因此,对于这类重复计算,性能提升是巨大的。

总结:本节课我们一起学习了Spark的核心抽象——弹性分布式数据集(RDD)。我们了解了RDD是分布在内存中的键值对集合,支持转换(生成新RDD)和行动(返回值)两种操作,并采用惰性求值策略。更重要的是,我们探讨了Spark如何通过内存缓存机制,有效解决了传统MapReduce框架因每一步都需落盘而导致的严重性能瓶颈,从而特别适用于需要多次访问同一数据集的迭代式算法和交互式数据分析任务。
大规模数据科学(大数据操作,第1课/共3课)📊:图概述

在本节课中,我们将要学习图的基本概念及其在大规模数据科学中的重要性。图是一种强大的数据结构,用于表示实体(顶点)及其之间的关系(边)。我们将探讨图是什么、为什么它们无处不在,以及如何对图进行基本的分析。
什么是图?🔍
上一节我们介绍了课程概述,本节中我们来看看图的基本定义。
图由一对集合构成:一个顶点集合和一个边集合。顶点有时也被称为节点,但为了避免与计算机集群中的“节点”混淆,我们将统一使用“顶点”这一术语。
公式:一个图 G 可以定义为 G = (V, E),其中:
V是顶点的集合。E是边的集合,每条边是顶点的一个对(u, v)。
边可以是有向的(从源顶点指向目标顶点),也可以是无向的(顶点对之间的连接,顺序无关)。
为什么图无处不在?🌐
了解了图的基本定义后,我们来看看图在现实世界中如此常见的原因。
图能够非常直接地捕捉许多系统的本质:一组参与者(顶点)以及他们之间的任何互动(边)。这是一种非常通用的建模范式。
以下是图的一些常见应用场景:
- 万维网:每个网页是一个顶点,页面间的超链接是一条边。
- 互联网:每台计算机是一个顶点,计算机间的路由或数据包传输是边。
- 社交网络:每个人是一个顶点,人与人之间的好友或关注关系是边。社交网络作为政治和社会变革的驱动力,其影响力难以估量。
- 通信日志:例如,每次电话通话可以建模为通话双方之间的一条边。
从数据建模的角度看,图将数据分解为“对象”和“对象间的关系”,这几乎是最基础的数据模型。虽然这种简化可能会丢失一些信息,但它提供了极大的灵活性。
图分析任务分类 📈
我们已经知道图是什么以及为何重要,现在我们来探讨可以对图进行哪些分析。
Bör of Wihar 在2012年的一篇论文中将分析任务分类,其中一类是图分析。他将图分析任务进一步分为以下三种模式:
- 结构算法:分析图本身的拓扑性质。
- 遍历算法:探索图中的路径和连通性。
- 模式匹配算法:在图中寻找特定的子图结构。
这个分类非常清晰,我们将在接下来的内容中沿用。
结构算法:理解图的基本度量 📏
上一节我们介绍了图分析的任务分类,本节我们重点看看第一类:结构算法。
当面对一个需要处理或理解的图时,首先要做的就是收集一些基本度量指标。
核心问题:
- 图有多大?即,有多少个顶点和多少条边?
- 图中顶点的度数分布如何?
关键见解:在理解图的规模时,边的数量比顶点的数量更重要。原因在于,对于一个有 n 个顶点的图,其可能的最大边数是 n²(对于有向图)或 n(n-1)/2(对于无向图)。这个数字可能非常巨大。例如,一个拥有20亿用户的社交网络,其潜在的好友关系数量是天文数字。
代码:获取顶点 v 的度数(以有向图为例):
in_degree = len(graph.in_edges(v)) # 入度:指向顶点v的边数
out_degree = len(graph.out_edges(v)) # 出度:从顶点v出发的边数
因此,在评估图规模时,第一个问题应该是“有多少条边?”,而不是“有多少字节?”或“有多少个顶点?”。字节数可能包含大量与图结构无关的附加信息,而顶点数无法反映关系的稠密程度。
紧接着的第二个关键问题是“最高入度或出度是多少?”。这反映了图的度数分布是否倾斜。
在现实世界的图中,度数分布几乎总是高度倾斜的(即遵循幂律分布)。例如,社交网络中有极受欢迎的用户(拥有大量好友),万维网中有被大量链接的页面。这种不平衡性是处理超大规模图的主要挑战之一。
总结:通过边的总数可以了解图的总体规模,而通过最高度数可以了解数据分布的倾斜程度。能够计算这些基本度量(如统计顶点/边数、获取任意顶点的度数)是进行更复杂图分析的基础。

本节课中我们一起学习了图的基本概念,了解了其作为对象与关系模型的重要性,并掌握了评估图规模的两个核心度量:边的总数和顶点的度数分布。这些基础知识是后续进行更复杂的图遍历和模式匹配分析的起点。
大规模数据科学(第1课)📊:图的结构分析

在本节课中,我们将学习如何通过分析图的“出度”分布来理解其结构。我们将介绍出度直方图的概念,并探讨两种典型的分布模式:指数分布和幂律分布。
构建出度直方图
上一节我们介绍了图的基本概念,本节中我们来看看如何量化图中节点的连接情况。一个更详细的结构分析任务是构建图的出度直方图。
一个顶点的出度,即从该顶点出发的边的数量。我们用数学符号来定义:对于每个整数 D,令 n(D) 表示出度恰好为 D 的顶点数量。
例如,在下图中:
- 顶点 A 的出度为 0(没有出边)。
- 顶点 B 的出度为 2(有两条出边)。
- 顶点 C 的出度为 4(有四条出边)。
通过统计,我们可以得到:
- 出度为 0 的顶点有 1 个(A)。
- 出度为 1 的顶点有 3 个。
- 出度为 2 的顶点有 2 个,依此类推。
最后,我们将 D 与对应的 n(D) 绘制成图,就得到了出度直方图。
直方图揭示的模式
以下是直方图可能展现的几种模式及其含义:
-
模式一:多数低连接
在这种模式中,图中非常多的顶点具有非常小的出度,随着出度值增大,拥有该出度的顶点数量急剧减少。这表示图整体上连接稀疏,大多数节点只与少数其他节点相连。 -
模式二:多数高连接
与模式一相反,这种模式下,只有非常少的顶点具有低出度,而非常多的顶点具有高出度。这表示图非常稠密,大多数节点都与许多其他节点相连。 -
混合或双峰模式
直方图也可能呈现更复杂的形状,例如双峰分布,表明图中可能同时存在两类连接特性不同的节点群体。
指数分布与随机图
如果我们在直方图中看到第一种模式(多数低连接),并且在对数坐标下绘制时,数据点大致呈一条直线,那么这种分布可以建模为指数分布。
其公式可以表示为:n(D) ∝ e^(-c * D),其中 c 是一个常数。这意味着,找到一个出度为 D 的顶点的概率,随着 D 的增大而指数级下降。大多数顶点出度很小,高出度的顶点极为罕见。
这种分布是随机图的典型特征。随机图的构建方式是:给定一组顶点,然后完全随机地选择两个顶点并用边连接,不断重复此过程。用这种方法,几乎不可能产生连接数极高的“超级节点”。
然而,在现实世界中,纯粹的随机图并不多见。
幂律分布与现实数据
在实践中更常见的是幂律分布(又称齐夫分布)。
其公式可以表示为:n(D) ∝ D^(-γ),其中 γ 是一个常数(指数)。在这种分布中,虽然大多数顶点出度仍然较低,但存在少量连接数极高的“枢纽”节点,其数量远超随机图模型下的预期。
由人类活动产生的数据(如社交网络、引用网络、万维网链接)往往遵循这种分布。直觉上,这可以理解为“富者愈富”或“优先连接”机制:新加入的节点更倾向于连接到已经拥有较多连接的节点,从而导致连接分布极度不均。

本节课中我们一起学习了如何通过出度直方图分析图的结构。我们介绍了出度的定义与直方图的构建方法,并对比了指数分布(对应随机图)和幂律分布(对应许多真实网络)两种典型模式及其意义。理解这些分布有助于我们选择合适的数据处理模型和算法。
课程1.1:网络的结构与幂律分布 📊

在本节课中,我们将学习大规模网络(如万维网)的结构特性,特别是其节点连接度的分布规律。我们将探讨为何真实网络中的连接分布会呈现“长尾”特征,并介绍“优先连接”模型来解释这一现象。
上一节我们提到了随机图模型,本节中我们来看看真实网络(如社交网络或万维网)的连接方式有何不同。
在随机图模型中,我们随机选择两个顶点并用边连接它们。但在优先连接模型中,当你选择一个顶点进行连接时,会倾向于选择那些已经拥有较多连接的顶点。
那么,为何要关注这个模型,或者它为何会自然发生?考虑一下社交网络的情景:当一个人首次加入Facebook时,他更可能去连接那些已经非常受欢迎的人,还是去连接一群朋友不多的人?他更可能连接那些已经非常受欢迎的人。这个人可能就是最初介绍他使用Facebook的人。再以Twitter为例,你更可能关注那些已经拥有大量粉丝的人,还是关注粉丝不多的人?你加入Twitter的目的通常就是为了关注受欢迎的人。
我们也可以从互联网或万维网的角度思考:当你创建一个网页并放到网上时,你更可能链接到那些已经拥有大量链接的网站,还是链接到没有多少链接的网站?答案是,你更可能链接到那些已经拥有大量链接的网页。
核心概念:优先连接模型
在这种构建图的优先连接模型下,你会生成齐普夫分布或幂律分布。这些是你在自然界或万维网中倾向于发现的分布。它们的分布形状具有更“肥”的尾部,即长尾。因此,发现具有极高出度的顶点不再那么不可能。
在双对数坐标图中,这种效应可以更清晰地看到,因为两个坐标轴都采用对数尺度。虽然许多分布在双对数坐标下看起来都呈线性,但幂律分布被定义为在双对数坐标下呈线性。因此,你可以进行这种结构分析。
你可以为像万维网这样的大型图构建直方图。Bro在2000年就完成了这项工作,我们现在可能早该进行另一轮此类分析了,但对当前的互联网进行计算以生成这类图表已经变得相当困难。
以下是万维网的直方图。问题是:这是指数分布还是齐普夫幂律分布?从描述来看,它显然是幂律分布。这是一个对数坐标轴,这也是一个对数坐标轴,整体看起来非常接近线性。
接下来作者做了一件相当有趣的事情:他绘制了万维网整体结构的示意图。中间的圆圈是一个巨大的强连通分量,这意味着在该分量内的任意两个页面X和Y,如果你能从Y到达X,那么你也能从X到达Y。
此外,还存在“入”和“出”部分,它们是与强连通分量规模相当的顶点(页面)集合。“入”部分的页面可以链接到强连通分量,但无法从强连通分量链接回来;它们链接到互联网的主体部分。类似地,“出”部分可以从强连通分量链接出去,但无法链接回来。这三部分规模大致相当。
作者还指出,确实存在一些“管道”直接将“入”部分与“出”部分连接起来,而不经过强连通分量。此外,还有一些“卷须”似乎不指向任何地方,但这些相对于主要的三个部分而言可能是次要组件。同时,也存在一些较小的断开连接的组件。
理解互联网的这种基本生理结构是很有趣的,但同样地,现在要生成这样的分析图已经非常困难。

本节课中我们一起学习了真实网络(如万维网)的结构特性。我们了解到,与随机连接不同,真实网络中的连接遵循“优先连接”原则,这导致了节点连接度呈现幂律分布,即存在大量连接数极多的节点(长尾现象)。我们还通过万维网的结构示意图,认识了其核心的强连通分量以及“入”、“出”等组成部分。这种分析有助于我们理解大规模网络的内在组织规律。
大规模数据科学(大数据操作,第1课/共3课) - P112:连通性与中心性 📊

在本节课中,我们将学习图结构分析中的两个核心概念:连通性系数与中心性。我们将探讨如何量化图的连接强度以及如何识别图中重要的顶点。
图的结构分析:连通性系数
上一节我们介绍了图的基本概念,本节中我们来看看如何衡量一个图的连接强度。我们可以将连通性系数作为图结构分析的一项任务进行测量。
连通性系数的定义是:使图变得不连通所需移除的最少顶点数量。直观上,这可以理解为对图脆弱性的一种度量,反映了图中内置了多少冗余性。
例如,如果你在设计一个网络,而该图的连通性系数为1,这意味着如果有一台机器宕机,网络就可能被分割,导致部分节点之间无法通信。对于广告商而言,如果希望信息能触达尽可能多的人,可能会关注图的连通性系数。因为如果少数人不关注广告,在病毒式营销或社交媒体广告中,可能导致一大批其他人也无法接收到信息。
此外,回想我们在NoSQL课程中讨论过的CAP定理,其中的P代表分区容错性。如果系统或网络被分割,系统是否仍能运行?连通性系数可以帮助你估算网络被分割的可能性。
那么,这个图的连通性系数是多少呢?这首先取决于我们对“连通”的定义。
以下是两种常见的连通性定义:
- 强连通:如果从顶点 x 可以到达 y,并且从 y 也可以到达 x,则称 x 和 y 是强连通的。
- 连通:如果从顶点 x 可以到达 y,或者从 y 可以到达 x,则称 x 和 y 是连通的。这相当于忽略了边的方向性。
让我们暂时采用第二种定义,即假设图是无向的。在这个前提下,如果我们移除顶点 E,图会变得不连通吗?不会。因为你仍然可以通过上方的另一条边到达所有节点。
图中唯一看起来不具备冗余性的节点是 B。如果移除 B,网络将被分割成两个部分:A 和 F 一组,其他所有节点为另一组。
因此,这个图的连通性系数是 1,因为我们可以找到一个顶点,移除它会导致网络被分割。我们已经讨论了为何需要计算这个系数。
衡量顶点重要性:中心性
连通性系数衡量的是图本身的属性,但并未提供理解单个顶点相对重要性的方法。为此,人们定义了各种中心性的概念。
接近中心性
一种中心性是顶点的接近中心性,其定义是所有经过该顶点的最短路径的平均长度。
直观上,你可以将其与图的直径(图中所有最短路径的最大长度)进行比较。如果图的直径很长,而经过某个特定顶点的最短路径平均长度很短,那么这个顶点就不在定义图直径的那些路径上。从某种意义上说,它可能就不那么中心或重要。
介数中心性
另一种更常见的中心性是顶点的介数中心性。其定义是图中所有最短路径里,经过该顶点的路径所占的比例。
这好比“条条大路通罗马”。在美国,如果你需要从西雅图前往亚特兰大,可能所有路径都要经过一两条主要高速公路,或一两个城市(比如北线或南线的枢纽)。那么,这些中间枢纽的介数中心性就很高。
再比如,在一个公共交通系统中,如果所有列车都先汇集到一个中央枢纽,然后再分散出去,那么这个中央枢纽将具有很高的介数中心性,因为从点A到点B的最短路径总是要经过这个顶点。
那么,在这个图中,顶点 E 的介数中心性是多少呢?
以下是经过 E 的最短路径示例:
- 从 A 到 G 的最短路径:A -> B -> E -> G。
- 此外,还有一些以 E 本身为起点的最短路径,例如 E 到 C,E 到 G。
因此,经过 E 的最短路径数量是 3。E 的介数中心性就是 3 除以图中所有最短路径的总数(它是一个分数)。
总结

本节课中,我们一起学习了图分析中的两个关键指标。连通性系数帮助我们评估图的整体连接强度和冗余度。而中心性(特别是接近中心性和介数中心性)则提供了量化图中单个顶点重要性的方法,帮助我们识别网络中的关键枢纽。理解这些概念对于设计稳健的网络系统或进行有效的社交媒体分析至关重要。
大规模数据科学(大数据操作,第1课/共3课)|P113:PageRank算法详解 📊

在本节课中,我们将学习图数据中衡量节点重要性的核心算法——PageRank。我们将从简单的中心性概念开始,逐步深入到PageRank的原理、计算过程及其解决的问题。
概述:从度中心性到PageRank
上一节我们介绍了图的基本概念。本节中,我们来看看如何量化图中节点的重要性。一个直观的想法是使用度中心性,即一个节点的连接数。然而,度中心性存在局限:它无法区分连接质量。例如,连接了五个重要人物的节点,理应比连接了五个普通节点的节点更重要,但度中心性无法体现这一点。
为了克服这个局限,我们引入特征向量中心性,其核心实现就是PageRank算法。
PageRank的基本思想 🧠
PageRank的基本思想是迭代计算。在未达到收敛条件前,对图中的每个节点,通过累加其所有入边邻居节点的权重来计算其自身权重。
这可以表述为:如果一个重要节点(例如拥有高权重的节点)连接了你,那么你的重要性就应该增加。同时,你的重要性也会分配给你所连接的所有节点。
以下是该思想的核心迭代公式:
R(v) = Σ (R(u) / OutDegree(u))
其中,R(v) 是节点v的PageRank值,求和是针对所有指向v的节点u进行的,OutDegree(u) 是节点u的出度。
这个过程可以理解为在网络中反复传递“重要性权重”,直到整个网络的重要性分布稳定下来。
PageRank需要解决的问题 ⚙️
然而,上述简单方法存在两个主要问题。
问题一:出度稀释重要性
第一个问题是,一个拥有大量出边链接的节点,其每条出边传递的重要性会被稀释。例如,一个网络爬虫机器人关注了Twitter上的所有人,它关注你这件事并不显得你有多重要。同样,一个聚合了全网链接的网页,它链接到你也不能显著提升你的重要性。
解决方案:我们需要确保节点将其自身的权重平均分配给它所有的出边,而不是将全部权重赋予每一条边。上述公式中的除以出度 OutDegree(u) 正是为了解决这个问题。
问题二:距离衰减效应
第二个问题是,影响力的传递应随距离增加而衰减。如果我与一个重要人物(如奥巴马)相距27跳,这远不如与他直接相连来得重要。
解决方案:我们引入一个阻尼因子。它模拟了用户随机跳转的行为,并确保无论图的结构如何,重要性计算都能收敛,同时体现了“远处影响力减弱”的直觉。
引入阻尼因子d(通常取0.85)后的完整PageRank公式为:
R(v) = (1-d)/N + d * Σ (R(u) / OutDegree(u))
其中,N 是图中节点的总数,(1-d)/N 代表了随机跳转到图中任一节点的概率。
总结与回顾 ✅
本节课中,我们一起学习了PageRank算法。我们从简单的度中心性出发,指出了其不足,进而引出了基于邻居权重迭代计算的特征向量中心性思想,即PageRank的核心。
我们详细探讨了PageRank的基本计算过程,并针对其最初形式的两个缺陷——出度稀释和缺乏距离衰减——提出了解决方案:通过除以出度来分配权重,以及引入阻尼因子来模拟随机跳转并保证收敛。


最终,我们得到了完整、健壮的PageRank公式,该公式能够有效地衡量大规模图数据(如网页链接网络、社交网络)中节点的重要性。
大规模数据科学(大数据操作,第1课/共3课) - P114:PageRank详解 🧮

在本节课中,我们将深入探讨PageRank算法的核心定义与计算过程。PageRank是衡量网页图中顶点(即网页)重要性的经典算法,其思想直观且公式简洁。
上一节我们介绍了衡量顶点重要性的几种朴素想法及其局限性,本节中我们来看看PageRank如何通过一个巧妙的公式来解决这些问题。
PageRank的定义与公式
PageRank将“被高权重页面链接”和“传递的权重需按出链均分”这两个概念结合起来,形成了一个完整的定义。以下是其核心计算公式:
PR(A) = (1-d) + d * ( PR(B)/L(B) + PR(C)/L(C) + PR(D)/L(D) + ... )
在这个公式中:
- PR(A) 代表页面A的PageRank值。
- B, C, D... 代表所有链接到页面A的其他页面。
- L(x) 代表页面x拥有的出站链接总数。
- d 是一个阻尼因子,通常取值约为0.85。
让我们分解这个公式的含义。页面A的排名,是所有链接到A的页面的排名贡献之和。但每个贡献者(如页面B)的排名(PR(B))需要被其自身的出链总数(L(B))平分,这意味着一个页面将其影响力平均分配给它所链接的所有页面。
阻尼因子 d 确保了在随机游走模型中,权重会随着跳转距离的增加而衰减。公式中的 (1-d) 项则保证了所有页面的PageRank值总和为1,这使得PageRank值可以被直接解释为概率。
PageRank的随机游走解释
事实上,PageRank有另一种非常直观的理解方式:随机游走。
想象一个用户在网页图中随机浏览:
- 用户从随机一个页面开始。
- 在当前页面,用户随机点击该页面上的一个链接,跳转到下一个页面。
- 不断重复步骤2。
如果进行无数次这样的随机游走,统计用户停留在每个页面上的时间比例,那么这个比例恰好就等于该页面的PageRank值。这就好比在互联网上“随波逐流”,最终很可能会花大量时间停留在像维基百科或CNN这样被广泛链接的重要页面上。这种解释揭示了PageRank的本质:它衡量的是一个页面在网络中被随机访问到的可能性。
从直觉到公式
在继续之前,值得指出的是PageRank公式的相对简洁性。它源于一个直观的目标:如何量化图中顶点的重要性。
我们最初可能想到用“入链数量”来衡量重要性,但这存在明显问题(例如,一个被大量垃圾页面链接的页面并不重要)。接着我们可能考虑“经过该顶点的最短路径数量”,但这也有其缺陷。
PageRank的巧妙之处在于,它引入了“链接源的重要性也至关重要”这一概念。你的朋友如果很厉害,那么你的推荐也会更有分量。PageRank正是将这种“影响力的传递与均分”的直觉,用精确的数学公式表达了出来。许多伟大的算法都是这样从直观想法发展而来,公式只是用来精确表达这些直觉的工具。

本节课中我们一起学习了PageRank算法的标准定义、其核心计算公式,以及两种关键的理解视角:公式计算和随机游走模型。我们还回顾了PageRank如何从简单的直觉(如考虑链接源的重要性)一步步演化成最终的精炼公式。理解这个推导过程,比单纯记忆公式本身更为重要。
杜克大学《大规模数据科学》课程笔记 - 第1课:遍历任务:生成树与回路 🧭

在本节课中,我们将学习图分析中的遍历任务,重点介绍最小生成树和欧拉回路/路径这两个核心概念。我们将了解它们的定义、应用场景以及计算上的特点。
上一节我们介绍了结构分析任务的分类,本节中我们来看看具体的遍历任务示例。
最小生成树
最小生成树是指连接图中所有顶点的、总权重最小的边集。在无向图中,它构成了一个连通所有节点且无环的子图。
以下是一个寻找最小生成树的示例。考虑下图:

我们需要找到连接所有顶点的最小边集。忽略边的方向性,一种可能的生成树路径是:A -> B, B -> F, B -> E, B -> D, D -> C, E -> G。这使用了 6 条边。
另一种同样使用6条边的生成树是:A -> B, A -> F, B -> D, B -> E, D -> C, E -> G。
因此,该图的最小生成树包含 6 条边,并且可能存在多个不同的最小生成树方案。计算最小生成树有高效的算法(如Prim或Kruskal算法),但本课程作为任务概览,不深入讨论具体算法细节。
了解了寻找连接所有顶点的最经济方式后,我们接下来看看另一种经典的遍历问题:寻找访问所有边的路径。
路径与回路:欧拉问题
一个著名的遍历任务是欧拉的“柯尼斯堡七桥问题”,其目标是找到一条路径,恰好经过图中每条边一次。

这个问题的核心观察是:对于任何顶点,如果通过一条桥进入,则必须通过另一条桥离开(且不能是同一条)。这引出了以下结论:
以下是判断图中是否存在欧拉路径或回路的条件:
- 欧拉回路:如果希望路径起点和终点是同一个顶点,形成一个闭环,那么图中每个顶点的度数(相连的边数)必须均为偶数。公式表示为:对于所有顶点
v,degree(v) % 2 == 0。 - 欧拉路径:如果路径的起点和终点可以不同,那么条件可以放宽:最多只能有两个顶点的度数为奇数(分别作为起点和终点),其余顶点的度数必须为偶数。
这个结论非常有力,因为只需快速检查图中各顶点的度数,就能判断此类路径是否存在。
然而,图论问题的微妙之处在于,对问题描述稍作修改,其计算复杂度就会发生巨大变化。
问题变体:哈密顿路径与旅行商问题
如果我们把问题改为:寻找一条访问每个顶点恰好一次的路径(即哈密顿路径),那么问题就变得极其困难。
直观上理解,每个顶点都连接着多条边,涉及无数可能的路径。但你只能使用每个顶点一次,因此很难确定最优的访问顺序。这本质上是一个组合爆炸问题。
一个更复杂的扩展是旅行商问题:在访问每个顶点恰好一次的前提下,还要找到总行程成本最低的那条路径。对于这类问题,目前没有已知的高效精确算法,通常只能借助启发式或近似算法来求解。
总结
本节课中我们一起学习了图分析中的两类核心遍历任务:
- 最小生成树:寻找连接所有顶点的最小成本边集,有高效算法。
- 欧拉路径/回路:寻找遍历图中每条边恰好一次的路径,其存在性有简洁的判定条件(检查顶点度数)。


我们还看到,当问题变为寻找访问每个顶点恰好一次的路径(哈密顿路径)或寻求其最低成本解(旅行商问题)时,问题会变得非常困难,通常不属于大数据场景下的常见分析任务。在社交网络或网络分析等大数据语境中,更常关注的是连通性、社区发现等其它结构特性。
课程名称:大规模数据科学(大数据操作,第1课/共3课) - 最大流问题 🚀
在本节课中,我们将要学习图遍历任务中的另一个重要概念:最大流问题。我们将了解其基本定义、核心约束条件,并通过一个具体例子来理解如何寻找最大流子图。

概述 📋
最大流问题的输入是一个带有边容量标签的图,以及两个特殊顶点:源点和汇点。我们的目标是找到一个子图,使得从源点到汇点的总流量达到最大。
上一节我们介绍了图遍历的基本概念,本节中我们来看看一个更具体的优化问题——最大流。
核心概念与约束 ⚙️
该问题的一个核心观察是:对于图中除源点和汇点外的任何一个顶点,流入的流量必须等于流出的流量。
这个约束可以用以下方式理解:如果一个顶点不是源点或汇点,那么它不能“创造”或“消耗”流量,流经它的流量必须是守恒的。
示例解析 🔍
让我们通过一个具体例子来理解如何计算最大流。假设我们有一个图,其中:
- 源点是 A。
- 汇点是 F 和 G。
- 每条边上的数字代表其容量。
以下是分析最大流路径的步骤:
首先,考虑从源点A到汇点F的直接路径。这条路径只有一条边 A -> F,其容量为2。因此,沿这条路径的最大流量是 2。
然而,我们可以找到更优的路径。考虑路径 A -> B -> F。这条路径包含两条边:
A -> B的容量是 4。B -> F的容量是 3。
一条路径的最大流量受限于其容量最小的边(即瓶颈)。因此,路径 A -> B -> F 的最大流量是 min(4, 3) = 3。虽然边 A -> B 有未使用的容量,但3已经比直接路径的流量2更大,所以我们应该在子图中包含这条路径。
接下来,我们分析从源点A到另一个汇点G的流量。以下是可能的路径及其最大流量分析:
-
路径1:
A -> B -> D -> E -> G- 边容量分别为:5 (
B->D), 2 (D->E), 2 (E->G)。 - 该路径的瓶颈是容量为2的边,因此最大流量为 2。
- 边容量分别为:5 (
-
路径2:
A -> B -> D -> C -> G- 边容量分别为:5 (
B->D), 4 (D->C), 3 (C->G)。 - 该路径的瓶颈是容量为3的边 (
C->G),因此最大流量为 3。
- 边容量分别为:5 (
比较两条路径,路径2能提供更大的流量(3 > 2)。
最大流子图构建 🧩
综合以上分析,为了最大化从源点A到两个汇点(F和G)的总流量,我们应选择能提供最大流量的路径组合。
因此,最终的最大流子图应包含以下关键边:
- 从A到F:选择路径
A -> B -> F(流量3)。 - 从A到G:选择路径
A -> B -> D -> C -> G(流量3)。
这个子图可以描述为包含边 A->B, B->F, B->D, D->C, C->G。它确保了在满足每个顶点流量平衡约束的前提下,实现了从源点到汇点的最大可能流量。
总结 🎯
本节课中我们一起学习了最大流问题。我们了解到:
- 问题的目标是找到从源点到汇点流量最大的子图。
- 核心约束是:对于任何中间顶点,流入量等于流出量。
- 单条路径的最大流量由路径上容量最小的边决定。
- 解决方法是识别并组合那些能提供最大流量的路径,同时确保整个网络的流量平衡。

理解最大流算法是处理网络优化、运输和通信等问题的重要基础。
课程名称:大规模数据科学(大数据操作,第1课/共3课) - 第117讲:模式匹配 🧩

概述
在本节课中,我们将要学习图分析中的最后一类任务:模式匹配。模式匹配的目标是在图中寻找所有符合特定子图模式的实例。我们将通过简单的例子来理解其基本概念,并探讨其在现实数据分析中的应用,例如在药物相互作用网络中的查询。
模式匹配的基本概念
上一节我们介绍了图分析的不同任务类别,本节中我们来看看模式匹配。模式匹配的核心思想是,在给定的图中,找出所有与指定子图结构相匹配的实例。
一个非常简单的子图模式可以是:两个顶点(A和B)彼此相连。在匹配时,我们可以在顶点标签或边标签上附加条件,以更精确地定义我们寻找的模式。目标是找出图中所有符合该模式的实例。
例如,对于模式“两个顶点相互连接”,我们需要计算它在图中出现了多少次,或者找出所有能实例化该模式的顶点对。
模式匹配实例与重复计数
以下是匹配“两个顶点相互连接”模式时可能遇到的情况:
- 从A到B的连接是一个实例,我们可以将其标记为
$x = A和$y = B。 - 从G到C的连接是另一个实例,对应
$x = C和$y = G。
然而,如果不加注意,我们可能会重复计数。因为无向边A-B同时包含了A到B和B到A两个方向。因此,这个简单的模式在图中可能被计为四个实例,但其中只有两个在本质上是唯一的。
三角形计数:一个经典的匹配问题
一个非常流行的模式匹配问题是寻找图中的三角形。三角形是由三个顶点组成的序列,其中每对顶点都相互连接,即 A 连接 B,B 连接 C,C 连接 A。
三角形总数是衡量图连通性的一个指标。存在许多算法来计算这个数量,它也是计算机科学家用来比较不同系统和算法的经典基准问题。
不过,在与实际计算此指标的人交流时,它在社交网络分析中的实际信息价值似乎并不十分明确。它更多地被视为一个优秀的系统基准测试问题。因此,我们不会在此深入探讨具体算法。
但需要记住一个关键点:一个朴素的算法可能会将同一个三角形计数三次(例如,分别以A、B、C为起点)。这为设计更快的算法提供了优化空间。研究三角形计数的另一个原因是,它可能是最简单的一种模式,是进行更复杂模式分析的良好起点。

扩展到带标签的图与复杂模式
模式匹配问题本身是一个非常困难且计算代价高昂的问题,这催生了许多近似技术。即使是三角形计数,也有多种算法不计算精确数量,而是在一定误差范围内给出良好近似。
现在,让我们看另一个模式匹配的例子,这是一个来自我们合作者实际工作的简化版本。
在这个例子中,图上的边带有标签。我们不再只有源顶点和目标顶点,还假设边上有一个标签作为第三个属性。这个标签可以表示各种关系,例如“某人认识某人”,或者在本例中,“药物X干扰药物Y”,其标签就是“干扰”。
这比Twitter(所有关系都是“关注”)或Facebook(所有关系都是“好友”)的图更复杂,它在同一个图中编码了多种不同的关系。在进行真实数据分析时,这种情况更为现实。
假设我们有这样的关系:药物X干扰药物Y,药物Y调控基因Z的表达,而基因Z与疾病W相关。那么,我们可能想要寻找的模式是:找出所有干扰了其他某种药物的药物,并且该其他药物参与了某种疾病的治疗。
当然,我们还可以在这里添加其他限制条件,比如寻找特定疾病或特定药物。但目前,我们只是寻找所有此类模式。
用查询语言表达模式
从图形上看,我们的查询模式可能如下所示:我们想找到一条标记为“干扰”的边连接顶点X和Y;然后,顶点Y通过一条标记为“调控”的边连接到顶点Z;接着,顶点Z通过一条标记为“关联”的边连接到顶点W。我们希望找到这个模式的所有实例。
我暂时不讨论实现此功能的算法,而是想更多地谈谈表达这种模式的语言。有些具有数据库背景的学员可能已经在思考如何在SQL中实现这一点,这正是我想展示的例子之一。
总结

本节课中,我们一起学习了图分析中的模式匹配任务。我们从简单的“顶点互连”模式入手,理解了其基本概念和计数时的注意事项。接着,我们探讨了经典的三角形计数问题及其作为系统基准的意义。最后,我们将概念扩展到带有边标签的图,并介绍了一个来自药物网络的真实复杂模式匹配案例,为后续学习如何使用查询语言(如SQL)来表达这些模式打下了基础。模式匹配是一个计算上富有挑战性但应用广泛的领域,是图数据分析的重要组成部分。
大规模数据科学(大数据操作,第1课/共3课) - P118:查询边表 🧩

在本节课中,我们将学习两种用于查询图结构数据(特别是边表或三元组数据)的查询语言:SPARQL 和 Datalog。我们将了解它们的基本语法、核心概念,并通过实例理解它们如何表达复杂的图模式查询。
概述:图数据与三元组
上一节我们介绍了图数据的基本概念。本节中,我们来看看如何查询以三元组形式存储的图数据。
资源描述框架(RDF)定义了一个围绕三元组概念的形式化数据模型。三元组本质上就是图中的带标签的边。
从逻辑上看,你可以将其视为一个包含三列的表:主体(subject)、谓词(predicate) 和 客体(object)。
- 主体列指向图中的顶点。
- 客体列也指向图中的顶点。
- 谓词则是边的标签。
因此,理解RDF最有用的方式就是将其看作一个带标签的图。
SPARQL:一种类似SQL的RDF查询语言 🌐
首先,我们来考虑SPARQL。这是一种从SQL衍生而来的查询语言,与资源描述框架(RDF)相关联。如果你完成了Elastic Mare作业,就会接触到一些这类数据。
SPARQL查询语言的结构如下所示:
SELECT ?x
WHERE {
?x interferesWith ?y .
?y regulates ?z .
?z associatedWith ?w .
}
在这个查询中,你可以定义三元组模式。以上例子中,所有三元组模式都使用实际的字面量(如“interferesWith”)来实例化谓词,而所有顶点都是变量(如 ?x, ?y)。但这并非固定模式,你也可以将变量放在谓词位置,或将字面量放在顶点位置。
这个查询的含义是:找出所有满足条件的 x,使得 x 干扰 y,y 调控 z,并且 z 与 w 相关联。
如果这是我们的数据集,查询结果可能返回一个名为“Terazine”的虚构药物。因为我们可以追踪一条路径:Terazine干扰Btamin,Btamin调控某个基因,而这个基因与某种疾病相关联。这样的三元组数据可能有数十亿条。
Datalog:基于逻辑编程的查询语言 🔍
接下来,我们看另一个模式表达语言——Datalog。它在计算机科学的数据库课程中经常出现,虽然在工业界较少见,但正开始复兴,这也是我想向大家介绍它的原因之一。
Datalog基于逻辑编程范式,但比Prolog这样的通用逻辑编程语言更简单。
以下是使用Datalog表达相同查询的方式。我们假设存在一个关系 R,其结构与SPARQL中看到的三元组表相同。
Answer(x) :- R(x, "interferesWith", y),
R(y, "regulates", z),
R(z, "associatedWith", w).
这个语法的外观与SPARQL差异不大。对于每个谓词,我们都有一个 R 关系的实例。查询目标是:找出所有满足 x 干扰 y,y 调控 z,z 与 w 相关联的 x。我们使用了三个 R 关系的实例。
这本质上可以解释为一个连接(join) 操作,大多数Datalog系统也正是这样实现的:在 y 上连接 R,再在 z 上连接 R,然后返回变量 X 的实例化结果。
在SQL中实现相同的查询 💡
既然 R 是一个具有三个属性的关系,你完全可以用SQL来查询它。你可以想象将这张表加载到数据库中,并将查询重写如下:
SELECT i.subject
FROM R AS i, R AS r, R AS a
WHERE i.predicate = 'interferesWith'
AND r.predicate = 'regulates'
AND a.predicate = 'associatedWith'
AND i.object = r.subject
AND r.object = a.subject;
在这个SQL查询中:
- 我们对关系
R进行了三次自连接,并分别赋予别名i(干扰)、r(调控)、a(关联)。 - 对每个关系实例,我们过滤出谓词等于我们感兴趣值的元组(例如,
i.predicate = 'interferesWith')。 - 我们添加了连接条件:确保
i关系的客体等于r关系的主体,并且r关系的客体等于a关系的主体。 - 最后,返回
interferesWith关系的主体。
总结与关联
本节课中,我们一起学习了两种查询图边表(三元组数据)的语言:SPARQL 和 Datalog,并了解了如何在 SQL 中表达相同的查询逻辑。
- SPARQL 是专为RDF图数据设计的类SQL声明式查询语言。
- Datalog 基于逻辑编程规则,提供了一种简洁的方式来表达递归和图模式查询。
- SQL 通过自连接和过滤条件,也能实现相同的多跳路径查询。

我想强调的一点是,这些查询在逻辑上是等价的,它们都描述了相同的图模式匹配操作。理解不同查询语言如何表达相同语义,有助于我们根据系统和需求选择最合适的工具。在后续课程中,我们将看到这些模式如何应用于大规模数据处理。
课程名称:大规模数据科学(大数据操作,第1课/共3课) - 图的关系代数和Datalog 🧮➡️🗂️

概述
在本节课中,我们将学习如何用不同的查询语言表达同一个图查询,并理解它们背后共同的核心——关系代数。我们将看到,尽管SQL、Datalog和特定图查询语言的语法不同,但它们都可以转化为关系代数表达式。我们还将探讨当数据源是分散的关系表而非单一图时,Datalog等语言的优势。
我们使用三种不同的模式语言实现了同一个查询。这个事实表明它们之间存在共同点。这个共同点就是关系代数。这也是我本季度课程中一直强调它的原因之一:当你戴上关系代数的“眼镜”看世界,整个世界都开始像关系代数,你会开始发现这些语言本质上并没有那么不同。你可以忽略语法的细节,看到背后实际运行的是一个关系代数表达式。
在这个特定案例中,该表达式可能如下所示。我说“可能”,是因为幻灯片上的表述不一定严谨。这里有两个连接操作,正如我们所见,涉及关系R的三个实例。这个符号 σ 表示一个选择条件,我们用它来选择谓词P等于“调节”的元组。这里选择谓词P等于“与…相关”,这里选择谓词P等于“干扰”。
这样,我们就得到了只包含“干扰”谓词的元组、只包含“调节”谓词的元组,以及只包含“与…相关”谓词的元组。然后我们进行连接操作,条件是第一个结果的对象等于第二个结果的主体,并且第二个结果的对象等于第三个结果的主体。
如果你回顾之前的课程,我们讨论过关系代数。希望你能确信,这个表达式将产生等效的结果。
正如我们在关系代数中所说,这实际上规定了操作应用的顺序。而其他语言则更像是声明式地表达结果应该是什么,但未必规定如何得到结果。这通常是代数与这些语言之间的主要区别。这些语言的一个好处是,它们试图抛开具体实现方式的任何规定,只指定结果必须满足的条件,这就是我们所说的声明式语言。
那么,也许这三种语言都是等价的?但我认为并非如此。SQL实现和Datalog实现实际上有其优势。我将尝试通过以略微不同的方式重写查询来阐明这一点。
现在想象一下,你拥有的不是一个包含图中所有边的大表(称为triples或R),而是拥有三个不同的表。这些表可能来自不同的数据源或不同的数据库,你将它们合并到一个数据库中。每个表代表了原始图的不同部分。
你有一个药物相互作用数据库,其中包含一个名为“干扰”的关系,它有两列。每个元组的语义是:第一列的药物会干扰第二列的药物。
从一个完全不同的数据库,你拥有关于药物调节哪些基因的事实。这些信息可能从科学文献中挖掘而来,或者来自跟踪此类信息的公共数据库。
第三,你可能有一个不同的数据库,记录哪些基因与哪些疾病相关。
这是一个非常可能出现的场景。事实上,这源自我们与合作伙伴合作中的一个真实案例。这些不同类型的关系确实来自不同的来源。因此,你更可能以这种形式拥有它们,而不是作为一个大图。你实际上需要进行一些预处理才能将它们合并成一个图。
但在你完成合并之前,你无法使用那种特定的图查询语言。事实上,你无法使用任何图语言,也无法真正使用任何图数据库,因为它们只处理图结构。
虽然图是捕获任意关系的非常通用的数据模型,但现实世界的数据在其原始表示中并不总是以图的形式存在。它通常以一堆表的形式存在,而这些表中恰好编码了图的边。因此,能够在原始格式下处理它们是一种优势。
你可以在Datalog中做到这一点。这里的查询实际上变得更简单一些,你只需在这三个独立的关系之间进行连接,而不再需要查询并指定定义每个关系的谓词。
如果你混合了图和关系,你可以用Datalog同时处理它们。事实上,你可以像处理关系代数实现中的任何其他查询一样处理它们。
总结


本节课中,我们一起学习了如何透过不同查询语言的表面语法,看到其底层共同的关系代数核心。我们比较了声明式语言与规定操作顺序的关系代数之间的区别。更重要的是,我们探讨了在实际数据工程场景中,原始数据常以分散的关系表形式存在,而非现成的单一图。在这种情况下,像Datalog这样能直接在关系上操作并表达图查询的语言,展现了其灵活性和优势,避免了强制进行数据合并的预处理步骤。理解这一点,有助于我们根据数据源的实际情况,选择最合适的查询与处理工具。
大规模数据科学(大数据操作,第1课/共3课) - P120:查询混合图关系数据 📊➡️🔗

在本节课中,我们将探讨一个核心问题:在现实应用中,数据模型往往是关系型表格与图结构数据的混合体。我们将分析专门用于图查询的系统(如SPARQL)在处理此类混合数据时的优势与局限性,并思考理想的数据处理系统应具备哪些能力。
混合数据模型的现实需求
上一节我们讨论了图查询语言。但在实际应用中,数据很少以纯粹的图结构孤立存在。
现实世界的应用往往是混合型的:它们同时包含一些表格、一些关系和一些图结构。能够统一地处理这两种数据模型,是一个重要的优势。
因此,虽然专门图系统的出现,暗示了传统关系模型在应对某些建模需求时可能存在性能问题(我们将在后续章节讨论),但将整个世界建模为一个巨大的、扁平的图,并不一定是正确的做法。
似乎我们需要一个能够同时处理关系和图,并能在两者之间灵活转换的模型。
SPARQL的局限性分析
在进入更复杂的示例之前,让我们对SPARQL再做一个总结。虽然SPARQL是为图查询设计的,但我想指出几个问题。
以下是SPARQL面临的三个主要挑战:
-
缺乏代数封闭性
当我们讨论关系代数时,曾提出“代数封闭性”的概念。对于SPARQL,其输入是一个图,你可以查询一个图。但如果你回头看SPARQL的幻灯片并查看结果,输出实际上是一个记录列表,而不再是图,它不必是三元组了。因此,我无法将一个SPARQL查询的结果,再次作为图输入给SPARQL进行查询。更技术性的说法是:它不具备代数封闭性,它返回的类型与其输入的类型不同。这是一个很大的劣势,因为我们希望能够将查询链接在一起。这为诸如逻辑数据独立性等特性提供了基础,我们在关系型数据库课程中曾论证过,这是一个非常关键的特性。 -
表达能力有限
我们稍后会看到一些例子。之前讨论过的许多图遍历任务,在SPARQL中无法表达。从SPARQL 1.1版本开始,虽然支持了可以表达某些形式遍历的路径表达式,但尚不清楚是否支持通用的递归。而为了实现这些图遍历任务,你需要某种方式来整体遍历整个图,SPARQL不一定提供这种能力。 -
数据转换开销
正如上一张幻灯片所指出的,如果你的输入一开始是表格,那么在用SPARQL处理之前,你必须先将其“切碎”成三元组,并将整个世界建模为一堆图边。虽然图的好处在于你总可以找到某种方式将其切碎成图,但这个过程可能会使数据量膨胀三倍或六倍(实践中取决于你的建模方式和表结构)。为了恢复因切碎成图而损失的性能,这是一个需要克服的艰难障碍。
理想系统的愿景
综上所述,我认为这告诉我们,我们需要一个能够同时处理图和数据表的系统。
这个系统应该能够:
- 处理图结构,并能表达图遍历任务(SQL在这方面不擅长,SPARQL也不完全擅长)。
- 处理表格数据,无论它们以何种形式存在。

本节课中,我们一起学习了现实数据模型的混合性,分析了专门图查询语言SPARQL在应对这种混合性时的三大局限性,并由此引出了对能够统一、高效处理关系与图数据的理想系统的思考。理解这些优缺点,是选择合适工具和设计健壮数据架构的基础。
大规模数据科学(大数据操作,第1课/共3课) - 图查询示例:NSA 🕵️

在本节课中,我们将学习如何将现实世界中的复杂监控场景,转化为可在图数据库或逻辑编程语言中执行的查询模式。我们将以2013年新闻中提及的美国国家安全局(NSA)PRism系统为例,探讨如何使用类似Datalog的语言对多源数据进行关联分析,并生成预警信号。
背景:图模式搜索与新闻事件
上一节我们介绍了图模式搜索的基本概念,本节中我们来看看一个与之相关的现实案例。
2013年6月,图模式搜索问题因与NSA开发的PRism系统高度关联而成为新闻焦点。一篇《商业周刊》的文章描述了NSA感兴趣的任务类型,以及他们所使用的技术。
根据我们的理解,这些技术包括曾在NoSQL讲座中提到的Accumulo系统。该系统最初由NSA开发,随后开源并商业化,成为了其底层平台之一。NSA希望利用Accumulo及其他系统完成的应用类型,正是我们一直在讨论的模式搜索。
一个具体的监控场景示例
以下是《商业周刊》文章中描述的一个假设场景,它展示了如何从看似无关的独立事件中识别出潜在风险。
十月份,一位名叫某某的外国公民购买了一张从开罗到迈阿密的单程机票。他在迈阿密租了一间公寓。在此前的几周内,他从一个俄罗斯银行账户进行了多次大额取款,并多次致电叙利亚的几个人。最近,他租了一辆卡车,开车前往奥兰多,并独自游览了迪士尼世界。
文章想要说明的观点是:这些活动单独来看都不会引发任何警报,但 collectively(集体来看)可能就值得进一步调查。我们在此不评判这种做法是否正确,仅陈述该系统被用于处理此类事实。
将事实编码为数据
你可以使用Datalog这类语言来编码这些事实,当然也可以将其表示为RDF图(这更接近他们的实际做法)。
以下是他们可能访问到的数据库事实表示:
- 航班记录:某人于特定日期购买了从开罗到迈阿密的单程机票。
- 取款记录:多条记录显示某人于不同日期从俄罗斯某银行取款不同金额。
- 通话记录:可以表示为
call_from_to(Person, OtherPerson)且location(OtherPerson, Syria)。这是可能的最大警示信号。 - 租车记录:包含租车人、取车地、还车地和日期等属性。
这些是对他们所掌握事实的一种表示。关键在于,将这些事实聚合成某种警报或“值得进一步检查”的判断,可以表达为这些语言中的一个查询。接下来我们将重点讨论Datalog。
在Datalog中定义预警规则
以下是使用Datalog语言定义的一系列预警规则。每个规则都会向一个统一的flag关系中添加记录。
规则一:可疑航班
如果一个人从被监控的机场(flagged_airport)购买飞往美国机场的单程机票,则产生一条预警。
flag(Person, 1, Date) :-
flight(Person, Origin, Dest, Date, one_way),
flagged_airport(Origin),
us_airport(Dest).
其中,1是一个标识此特定规则的标记,Date是关联日期。
规则二:大额境外取款
计算每个人从境外银行的总取款额(仅统计单笔大于100的取款),如果总额超过阈值(如10,000),则产生预警。
foreign_withdrawals(Person, SUM(Amount)) :-
withdrawal(Person, Bank, Amount, Date),
foreign_bank(Bank),
Amount > 100.
flag(Person, 2, Date) :-
foreign_withdrawals(Person, Total),
Total > 10000.
注意,这里的flag关系与规则一中的相同。Datalog允许多条规则向同一关系添加结果,其语义是取所有结果的并集,类似于SQL中的UNION。
规则三:前往重要地点的租车行为
虽然这可能不是一个强预警信号,但可以作为查找模式。
flag(Person, 3, Date) :-
vehicle_rental(Person, _, Dest, Date),
important_location(Dest).
聚合预警并生成最终警报
在定义了多条预警规则后,我们可以编写另一个查询来聚合结果,并识别出需要人工分析师进一步关注的目标。
以下查询对每个人的预警进行汇总,计算其预警总数和发生预警的日期范围。
person_flags(Person, COUNT(FlagId), MIN(Date), MAX(Date)) :-
flag(Person, FlagId, Date).
这类似于SQL中的:SELECT person, COUNT(flag_id), MIN(date), MAX(date) FROM flag GROUP BY person。
最后,定义“警报”:对于那些在短时间内(例如10天内)预警总数超过某个阈值(例如3条)的人,生成最终警报。
alert(Person) :-
person_flags(Person, TotalFlags, MinDate, MaxDate),
MaxDate - MinDate <= 10,
TotalFlags > 3.
需要指出的是,这个简单示例在时间分桶(如按周或月)上处理得比较粗糙,实际应用中可能需要更精细的时间窗口分析。
总结与核心要点
本节课中我们一起学习了如何将一个涉及多数据源、多事件类型的监控场景,通过逻辑规则(以Datalog为例)进行形式化表达。
其核心思想在于:
- 将孤立事实编码为基本关系(如
flight,withdrawal)。 - 使用逻辑规则定义预警模式,每条规则对应一种可疑行为模式。
- 通过规则组合与聚合,从分散的低级预警中提炼出高级别警报。

这个例子表明,基于规则的逻辑语言能够处理大型图数据和多关系数据,表达相当复杂的条件。从某种意义上说,如果你拥有大规模高效实现这类语言的方法,这就是进行高级分析所需的全部工具。它用不多的代码量,就能将针对海量图数据的自然语言问题,转化为可执行的逻辑查询。
大规模数据科学(大数据操作,第1课/共3课) - 图查询示例:递归 📊

在本节课中,我们将学习如何使用Datalog语言进行递归查询,以探索数据中的关联关系。我们将通过一个具体的例子,了解如何从多个数据源整合信息,并利用递归规则来推断潜在的关联网络。
递归查询示例:谁可能提前知晓事件? 🔍
上一节我们介绍了如何从不同数据源(如邮件、通话、短信)构建一个统一的“联系”关系。本节中,我们来看看如何利用递归查询,从一个已知起点(例如,一个名叫Sam的人)出发,找出所有可能提前知晓某个事件的人。
假设我们想知道在6月3日之前,谁可能已经知道某个事件即将发生。我们从已知事实开始:一个名叫Sam的人知道这件事。那么,还有谁可能知道呢?我们可以使用与构建“联系”关系时相同的技巧,但这次我们将引入递归。
以下是构建“知晓”关系的递归规则:
- 基础情况:Sam是已知的知晓者。
knew("Sam", "事件X"). - 递归规则:如果一个人(person1)知道,并且此人在6月3日之前通过邮件联系了另一个人(person2),那么person2也可能知道。
knew(Person2, Event) :- knew(Person1, Event), emailed(Person1, Person2, Time), Time < "2023-06-03". - 扩展规则:同理,如果知晓者与另一个人在6月3日之前会面,那么另一个人也可能知道。
knew(Person2, Event) :- knew(Person1, Event), met(Person1, Person2, Time), Time < "2023-06-03".
核心机制:请注意,在这些规则中,我们正在定义的 knew 关系同时出现在了规则头部(我们要推导的结果)和规则体(推导的条件)中。这种自我引用就构成了递归。查询执行时,会从Sam开始,找到所有Sam在截止日期前联系过的人,然后将这些人作为新的起点,继续查找他们联系过的人,如此反复,直到找不到新的关联为止。
Datalog递归 vs. SQL递归 ⚖️
这种递归自引用的能力,是Datalog与标准SQL的一个关键区别。虽然在SQL中也可以通过公共表表达式(CTE) 和 WITH RECURSIVE 子句实现类似功能,但在实践中存在一些限制。
以下是SQL递归实现常见的局限性:
- 递归深度限制:许多数据库系统会设置递归步数的上限(例如100层),这对于许多图遍历应用来说可能太小。
- 性能差异大:不同数据库系统对递归查询的优化程度不一,性能可能不理想。
- 功能支持不均:这表明关系数据库的用户过去对此类功能需求不强。
然而,随着图数据变得越来越重要(例如Neo4j等图数据库的流行、基于MapReduce的图处理系统、RDF三元组存储等),这种递归遍历图的能力也日益关键。这或许预示着从SQL关系代数语言向Datalog这类语言的迁移。因此,可以预见Datalog在工业界的应用将会越来越广泛。
总结 📝

本节课中,我们一起学习了Datalog中递归查询的强大功能。我们通过一个“找出潜在事件知晓者”的示例,看到了如何从多源数据定义规则,并利用递归来探索数据中隐含的链式关联。同时,我们也对比了Datalog递归与SQL递归在表达能力和实践限制上的不同,并理解了递归图查询在现代数据科学中日益增长的重要性。
大规模数据科学(大数据操作,第1课/共3课) - P123:递归程序评估 🧠

在本节课中,我们将学习如何在大规模数据处理框架(如MapReduce)中实现递归查询,特别是针对图遍历任务。我们将以DataLog语言中的递归程序为例,探讨其直观评估方法以及处理循环图时面临的挑战。
上一节我们介绍了图任务、结构遍历、模式任务以及DataLog等模式语言。然而,我们尚未讨论如何大规模地实现这些操作,尤其是涉及递归的部分。本节中,我们来看看如何评估一个递归的DataLog程序。
假设我们有一个递归的DataLog程序,其目标是找出从特定顶点 W 出发,通过边关系 edge(X, Y) 可到达的所有顶点。程序定义如下:
- 初始规则:
A(W) :- edge(W, Y).- 此规则找出所有直接与顶点
W相连的顶点,并将它们放入关系A中。我们称这第一批顶点为A0。
- 此规则找出所有直接与顶点
- 递归规则:
A(Y) :- A(X), edge(X, Y).- 此规则指出:对于已在关系
A中的每个顶点X,找出所有从X出发通过一条边可达的顶点Y,并将这些Y也加入关系A。
- 此规则指出:对于已在关系
直观上,这个程序以广度优先的方式遍历图。首先应用规则1得到 A0,然后反复应用规则2,每一轮都找到距离 W 更远一层的邻居顶点。
以下是评估此递归程序的步骤概述:
- 计算
A0:找出所有满足edge(W, Y)的Y。 - 迭代计算:对于当前已发现的
A集合中的每个顶点,通过连接A(X)和edge(X, Y)来寻找新的顶点Y。 - 持续迭代:重复步骤2,直到没有新的顶点可以被加入关系
A为止。
然而,这种方法存在一个问题:如果图中存在循环,评估过程可能永远不会终止。例如,顶点可能被重复发现,导致无限循环。
为了确保程序能够终止并给出正确结果,我们需要增加一个关键步骤:在每一轮迭代中,移除所有之前已经发现过的顶点。这意味着我们只保留新发现的、尚未在关系 A 中的顶点。通过这种方式,即使图中存在循环,当没有新顶点可加入时,迭代过程也会自然停止。

本节课中我们一起学习了递归DataLog程序的基本评估逻辑。我们了解到,其核心是通过迭代的连接操作进行图遍历,但同时必须引入去重机制来处理图中的循环,从而保证评估过程能够正确终止。下一节,我们将探讨如何将这种递归评估逻辑具体实现在MapReduce等大规模计算框架中。
大规模数据科学(大数据操作,第1课/共3课) - P124:MapReduce中的递归查询 🧠

在本节课中,我们将要学习如何在MapReduce框架中实现递归查询。我们将探讨一个基础实现模式,分析其存在的性能问题,并介绍一种通过缓存优化来显著提升迭代计算效率的关键技巧。
基础实现模式
上一节我们介绍了递归查询的概念,本节中我们来看看其在MapReduce中的基础实现模式。
其核心思想是,在每次迭代中,MapReduce作业会处理整个图的所有边,并通过连接操作来发现新的可达节点。这个过程会反复进行,直到没有新的节点被发现为止。迭代的次数取决于图的直径。
以下是该模式的关键步骤:
- Map阶段:读取图中的所有边。
- Shuffle阶段:根据连接键(例如,目标节点ID)对数据进行网络传输和排序。
- Reduce阶段:执行连接操作,生成新的可达节点对,并消除重复项。
基础模式的问题
虽然上述模式在逻辑上是正确的,但它存在两个主要的性能瓶颈。
首先,在每次迭代中,Map阶段都需要从磁盘读取所有的边(例如,十亿条边),经过Shuffle阶段通过网络传输到Reduce端,仅仅是为了执行一个连接操作。如果只执行一次,这尚可接受。但对于需要多次迭代的递归查询,Map阶段没有进行实质性的计算工作,只是重复地进行数据读取和传输,这极其浪费。
其次,更微妙的一点是,答案集 A 在每次迭代中都在增长。虽然当不再发现新答案时迭代会停止,但已经发现过的“旧”值在后续每次迭代中,为了去重,仍然会被从磁盘读取并同样经过网络传输。这造成了不必要的重复处理。
优化方案:Reducer端缓存
针对上述问题,解决方案是在MapReduce程序的Reducer端引入一种缓存机制。
其核心优化思想是:与其在每次迭代中都通过网络Shuffle庞大的、不变的数据(如图的边集),不如只在第一次迭代时传输一次,并将其缓存在Reducer本地。在后续的迭代中,Reducer可以直接从本地磁盘读取这份缓存数据。同时,通过一个变体技巧,我们还可以将新产生的数据增量式地追加回同一个缓存中。
通过扩展MapReduce框架(同时保持其编程接口),我们可以实现这种优化。例如,在名为HaLoop的研究系统中,我们就为Hadoop添加了此类迭代处理能力,并用其实现了Datalog查询。
优化效果对比
这种缓存优化带来了显著的性能提升。下图展示了运行时间随迭代次数的变化:

- 未使用缓存:每次迭代耗时大致相同,因为每次都需要重新处理和Shuffle整个边关系。
- 使用缓存:第一次迭代由于需要准备缓存,耗时稍长。但后续迭代的耗时急剧下降,因为不再需要映射和Shuffle那个庞大的边关系数据。迭代次数越多,总体运行时间的优化效果就越明显。
通过结合此缓存技巧来优化去重步骤,并智能地利用MapReduce框架进行其他优化,我们能够将原始MapReduce处理迭代程序的糟糕性能,大幅降低到接近运行一个“空”Hadoop作业的原始开销水平(即仅在集群中启动任务而不做实际工作)。

事实证明,不进行优化的重复处理是性能的“杀手”。

总结


本节课中我们一起学习了MapReduce中递归查询的实现与优化。关键要点是:
- 需要理解并掌握所介绍的缓存优化技巧,这是处理迭代计算的核心。
- 认识到如果直接使用“开箱即用”的标准MapReduce,由于其会浪费性地在每次迭代中重复处理不变的数据,因此对迭代计算并不高效。在实际应用中,应考虑使用支持迭代优化的框架或自行实现类似机制。
大规模数据科学(大数据操作,第1课/共3课) - P125:终局问题 🎯

在本节课中,我们将探讨在大规模图遍历算法(如广度优先搜索)中遇到的一个关键挑战——“终局问题”。我们将分析其产生原因、对MapReduce等框架的影响,并讨论几种可能的应对策略。
上一节我们讨论了图遍历算法在迭代过程中的行为模式。本节中,我们来看看这种模式带来的具体问题及其解决方案。
迭代过程中新发现的结果数量,往往遵循一个相当标准的模式。这是由实践中常见图的度分布特性所导致的,我们在之前的直方图分析部分略有提及。
以下图表展示了这一模式:
- Y轴:每次迭代新发现的结果数量。
- X轴:迭代次数。
从图中可以看出,在最开始的几次迭代中,算法会迅速发现海量的新结果。请注意,这是一个对数坐标轴,因此实际的数量差异极其巨大。
然而,很快这个数字就会急剧下降并趋于平缓。算法仍在持续发现新的结果,但每次发现的新结果集越来越小,增量非常有限。
这种模式对MapReduce框架来说非常棘手。因为在这个长尾阶段——正如Friday和Oleman在一篇论文中所称的“终局”阶段——即使每次迭代只发现极少的新结果,你仍然需要支付启动大规模Hadoop集群、进行数据处理和结果检查的巨大开销。而此时,大部分结果其实早已被发现。
这种现象的产生是合理的。因为在最初的几次迭代中,算法会迅速触及图中连接广泛的“核心”部分,这些高度连接的顶点会带来巨大的扩散效应,使你发现大量结果。一旦越过这个阶段,基本上就探索了绝大部分可达区域,新结果的发现速度自然会骤降。
我们需要思考的是,如何应对这两种不同的工作状态:一种是进行大量有效工作的阶段,另一种是“终局”阶段,此时工作的效率很低。
以下是几种可以考虑的应对策略:
- 提前终止:当新产生的结果数量不再显著时,直接停止处理可能是合理的。虽然这会导致程序输出非精确答案,但在许多寻求近似解的案例中,前几次迭代后已能获得相当好的近似结果,额外的计算带来的收益很小。
- 更换计算框架:放弃MapReduce,寻找每次迭代开销更低的系统。例如,我们正在研究的一个系统就没有这个问题,其运行时间与发现的新元组数量成正比,而不是像Hadoop那样存在启动作业的固定高额成本。
- 切换处理策略:在“终局”阶段改变算法策略。例如,由于每次迭代只发现少量新结果,可以尝试不做重复检查以节省开销;或者合并多次迭代的逻辑,减少需要启动的MapReduce作业数量,从而提升效率。我们已探索过这些选项,它们在不同程度上都是有效的。

本节课中,我们一起学习了大规模图遍历中的“终局问题”。我们分析了其表现为新结果发现数量先急剧增长后快速衰减的模式,并理解了这对基于作业的框架(如Hadoop MapReduce)造成的效率瓶颈。最后,我们探讨了包括提前终止、更换框架和动态切换策略在内的多种解决方案,以应对这一挑战。
大规模数据科学(大数据操作,第1课/共3课) - P126:图的表示:边表与邻接表 📊

在本节课中,我们将学习图的两种核心表示方法:边表和邻接表。理解这些表示方式对于高效处理大规模图数据至关重要。
图的表示方法概述
我们之前一直隐含地假设使用一种特定的方式来表示图,即一个包含两列的表。一列是源节点,另一列是目标节点,可能还有其他列来表示边的标签等信息。每条边都会被明确地表示出来。
这种表示方式很有意义,因为它易于使用关系型语言进行操作,这也与我们之前讨论的模式匹配等任务相符。
边表表示法
这种表示法被称为图的边表表示。例如,一个包含边A->B和B->A的图可以表示为:
| 源节点 | 目标节点 |
|---|---|
| A | B |
| B | A |
| ... | ... |
你可以处理这个表。正如我们所见,我们可以进行模式匹配,也可以相当简单地完成结构性任务。
例如,要找出入度最高的五个顶点,我们可以使用一个分组查询。
以下是使用SQL标准语法的示例代码:
SELECT target, COUNT(*) AS in_degree
FROM edge_table
GROUP BY target
ORDER BY in_degree DESC
LIMIT 5;
这个表为我们处理数据提供了很大的灵活性,但我们也需要为此承担一些性能成本。
邻接表表示法
在实际用于处理图的编程语言库中,更常见的是邻接表表示法。
在这种表示法中,每个源节点都与一个包含所有相邻顶点的列表相关联。例如:
- 节点A关联列表
[B, F] - 节点B关联列表
[A, D, E, F]
这种表示法所需的总体空间更少,这在性能上可能带来显著差异。
在关系型数据库中,很难直接自然地表达这种列表结构,因为它本质上违反了第一范式。然而,在MapReduce编程模型中,这完全可行。在MapReduce中,一个键对应一个值,而这个值可以是任何东西,比如一个词袋,或者在这里,是一个相邻顶点的列表。
事实上,在处理图的MapReduce库中,这是最常见的表示方式。早期使用MapReduce表达PageRank算法的工作也采用了这种邻接表表示。
两种表示法的任务对比
现在,让我们看看如何使用邻接表表示法来完成同样的任务:找出入度最高的五个顶点。
你可以设想一个Map程序,它接收一个顶点及其邻接列表作为输入,然后输出以相邻顶点为键、原顶点为值的键值对。这实际上是将边的方向反转。
例如,对于节点A的邻接列表 [B, F],Map阶段会输出:
(B, A)(F, A)
在Reduce阶段,程序会接收一个顶点及其所有入边(即指向它的源节点)列表,然后可以统计数量并输出结果。最后,还需要第二个MapReduce作业来选出前五名。
总结

本节课中,我们一起学习了图的两种基本表示方法:边表和邻接表。边表易于在关系型数据库中使用SQL进行操作,而邻接表则更节省空间,是许多图处理库和MapReduce算法中的首选。理解它们的区别和适用场景,是进行大规模图数据处理的重要基础。
大规模数据科学(第1课)📊:图的表示——邻接矩阵

在本节课中,我们将学习图的第三种常见表示方法:邻接矩阵。我们将了解其结构、适用场景以及它与其他表示法的内在联系。
🧮 什么是邻接矩阵?
上一节我们介绍了边表和邻接表,本节中我们来看看邻接矩阵。邻接矩阵是一种仅当您拥有高效矩阵表示法,并希望将图作为矩阵进行操作时才真正有用的表示方法。
它的存在揭示了一个重要等价关系:图与方阵是等价的。一个方阵具有相同数量的行和列。
📐 如何构建邻接矩阵?
将图表示为方阵的规则如下:
- 矩阵的每一行对应一个顶点。
- 矩阵的每一列也对应一个顶点。
- 如果在矩阵的某个单元格中填入 1,则表示“行顶点”与“列顶点”是相邻的(即存在一条边)。
对于无向图,邻接矩阵将是对称的。对于有向图,则不一定对称。例如,A 与 B 相邻,所以 A行B列 的位置是 1;B 与 A 相邻,所以 B行A列 的位置也是 1。但 B 与 D 相邻,而 D 不与 B 相邻,因此 B行D列 是 1,而 D行B列 是 0。
💾 邻接矩阵的稀疏性与存储
您会发现,邻接矩阵几乎总是一个稀疏矩阵。矩阵中存在大量零值,因为现实中很少有顶点会与几乎所有其他顶点相连。
如果您还记得我们在关系数据库方面的一些工作,以及我们展示的如何在数据库中表示矩阵,那么您就会明白:一旦将数据表示为矩阵,您就可以使用线性代数进行处理,并可能利用已有的、擅长处理矩阵和线性代数运算的快速库。
然而,许多这类库在底层所做的,正是采用一种稀疏矩阵的表示方式,从而避免实际存储所有这些零值。这些稀疏表示看起来会非常像我们之前看到的两种表示法:邻接表或边关系表。
🔄 总结与关联
因此,从某种意义上说,将图转换为矩阵是为了获得使用快速矩阵运算库的便利,但这些库在底层处理时,采用的稀疏表示方式又使其本质上更接近于关系型表示。
请注意这三种表示法:边表、邻接表和邻接矩阵。它们各有优劣,适用于不同的场景和计算需求。

本节课中,我们一起学习了邻接矩阵的表示方法,理解了其构建规则、稀疏特性,以及它如何通过底层优化与邻接表、边表等表示法产生关联,从而在利用高效矩阵运算库的同时,保持存储和计算上的效率。
大规模数据科学(大数据操作,第1课/共3课) - P128:MapReduce中的PageRank 🧮

在本节课中,我们将学习如何在大规模图数据上实现PageRank算法。我们将首先回顾MapReduce框架,然后展示如何在MapReduce中实现PageRank,并讨论其局限性。最后,我们将介绍一种更高效的图处理模型——Pregel。
大规模图处理的背景
上一节我们介绍了图的不同表示方法以及如何在MapReduce中实现循环和递归程序。本节中,我们来看看如何在大规模图上实现PageRank算法。
图数据正变得越来越大。例如:
- 社交网络规模的图可能拥有约10亿个顶点(每人一个)和约1000亿条边。
- 网络规模的图更大,因为网页数量多于人口数量,可能拥有约500亿个顶点和1万亿条边。
- 人脑连接组(神经网络)可能拥有约1000亿个顶点和100万亿条边。
我们需要大规模系统来处理此类数据,MapReduce就是这样一个系统。尽管对于超大规模数据处理,MapReduce可能已接近其效用极限,但我们目前仍将以其为例进行讲解。

MapReduce中的PageRank实现

以下是PageRank在MapReduce中的一种实现方式。
Map函数
在Map函数中,我们接收一个节点ID和一个顶点对象。该顶点对象包含几个我们可以使用的方法:
vertex.page_rank获取其当前的PageRank值。vertex.adjacency_list获取其邻接列表。
vertex.page_rank 除以邻接列表的长度,得到我们将通过每条出边分配给每个邻居的PageRank比例。
以下是Map函数的核心逻辑:
# P 是分配给每个邻居的PageRank值
P = vertex.page_rank / len(vertex.adjacency_list)
# 发射一个特殊的键值对,将顶点对象本身传递到Reduce端
emit(node_id, vertex)
# 为每个出边邻居发送其应得的PageRank份额
for neighbor in vertex.adjacency_list:
emit(neighbor, P)
我们发射一个以节点ID为键、顶点对象为值的特殊键值对,稍后会解释其作用。然后,为每个出边邻居发送计算得到的PageRank份额 P。
Reduce函数
在Reduce端,我们接收一个节点ID和一系列值的列表。
我们初始化两个变量:m 对象(用于存储顶点)和 new_rank(新PageRank值,初始为0)。
以下是处理逻辑:
new_rank = 0
m = None
for value in values_list:
if isinstance(value, Vertex):
# 如果值是顶点对象,则保存它
m = value
else:
# 否则,它是一个PageRank贡献值,将其累加
new_rank += value
# 应用阻尼因子公式计算最终的新PageRank
# d 是阻尼因子,通常为0.85
d = 0.85
m.page_rank = (1 - d) + d * new_rank
# 发射更新后的顶点对象,以便进行下一轮迭代
emit(node_id, m)
我们需要判断列表中的每个值 P 是顶点对象还是一个PageRank数值。如果是顶点对象,则将其赋值给 m。这样做的原因是:我们需要一种方式将这个结构复杂的顶点对象(包含邻接列表等信息)传递到Reduce端,因为MapReduce的键只是节点ID。
如果值不是顶点对象,那么它就是来自其他节点的PageRank贡献值,我们将其累加到 new_rank 中。
最后,使用我们之前提到的公式(包含阻尼因子)计算该顶点 m 的新PageRank值,并发射一个键值对(节点ID和更新后的顶点对象)。这个输出的键值类型与Map函数所需的输入类型一致,因此我们可以多次运行此过程,进行多次迭代。
MapReduce实现的局限性及Pregel模型
上述实现存在一些问题,最主要的是整个图的状态在每次迭代中都需要被洗牌。那个包含邻接列表的复杂顶点对象在每次迭代中都需要通过网络发送到Reduce端。实际上,如果顶点状态能“驻留”在本地,我们只需要发送新的PageRank贡献值给邻居节点,这将节省大量网络通信开销。
此外,迭代控制(包括终止条件)必须在MapReduce框架外部进行管理。
基于这些原因,同时也是为了探索另一种并行计算模型,Google在2010年提出了 Pregel 模型。此后出现了开源实现,如Apache Giraph、斯坦福大学的GPS系统以及GraphLab等。Pregel专为大规模图上的批量算法设计。
Pregel的基本逻辑如下:
while (有顶点仍处于活跃状态) 且 (未达到最大迭代次数):
for 每个顶点:
处理从邻居收到的所有消息
更新自身的内部状态
向邻居发送消息
(可选)根据条件设置自身的活跃标志
如果顶点没有收到消息,或者其内部状态变化不大,则可以设置为非活跃状态以节省计算。关键在于,现在你编写的不是一个Map函数和一个Reduce函数,而是一个在每个时间步、在每个顶点上运行的单一函数。
总结

本节课中,我们一起学习了:
- 回顾了处理大规模图数据的挑战。
- 详细分析了在MapReduce框架中实现PageRank算法的具体步骤,包括Map阶段分配PageRank、Reduce阶段汇总计算并应用阻尼公式。
- 指出了该MapReduce实现的主要缺点:每次迭代都需要传输整个图结构,通信开销大。
- 引入了Pregel图处理模型作为更高效的替代方案,其以顶点为中心的计算方式更适合迭代式图算法。
大规模数据科学(第1课)📊:Pregel中的PageRank算法详解

在本节课中,我们将学习如何使用Pregel模型实现PageRank算法。PageRank是谷歌搜索引擎的核心算法之一,用于衡量网页的重要性。我们将通过一个具体的代码示例和分步计算过程,理解其在Pregel并行计算框架中的工作原理。
代码解析
以下是Pregel论文中给出的PageRank计算代码,使用C语言风格编写。代码的核心是一个compute方法,它在每个超步(super step)中在每个顶点上并行执行。
if (superstep >= 1) {
double sum = 0;
for each message in incoming_messages {
sum += message.value;
}
rank = 0.15 / numVertices + 0.85 * sum;
}
if (superstep < 30) {
for each neighbor in outgoing_edges {
send_message(neighbor, rank / numNeighbors);
}
} else {
voteToHalt();
}
代码逻辑说明:
- 从第一个超步开始,每个顶点汇总所有来自邻居顶点的消息值(即PageRank贡献值)。
- 使用公式
新Rank = 0.15/总顶点数 + 0.85 * 所有入链贡献值之和更新自身的Rank值。 - 如果超步数小于30,则将自身当前Rank值均分给所有出链邻居。
- 如果超步数达到30,则顶点投票暂停(
voteToHalt),进入非活跃状态。
补充说明:
实际实现中通常会增加一个收敛判断条件:如果顶点的Rank值前后两次迭代变化很小,该顶点也会投票暂停,从而提前结束计算,提高效率。
分步计算示例
上一节我们解析了代码,本节我们通过一个具体的图例来看看PageRank在Pregel中是如何一步步迭代计算的。
假设我们有一个包含5个顶点的简单有向图。
初始化:
每个顶点的初始Rank值为 1 / 顶点总数 = 1 / 5 = 0.2。

超步 1:
每个顶点同时执行compute方法。
- 每个顶点将自身的Rank值(0.2)均分,通过出链发送给邻居。
- 例如,顶点A有2个出链,每个邻居收到
0.2 / 2 = 0.1。 - 顶点B有3个出链,每个邻居收到
0.2 / 3 ≈ 0.066。
超步 2:
每个顶点汇总在上一步中收到的所有消息(即贡献值),并应用PageRank公式计算新值。
- 对于顶点C:它从A收到0.1,从B收到0.066。总和为0.166。
- 应用公式:
新Rank = 0.15/5 + 0.85 * 0.166 = 0.03 + 0.1411 = 0.1711 ≈ 0.172。 - 对于顶点D和E:它们没有入链,因此贡献值总和为0。其新Rank值为
0.15/5 + 0.85 * 0 = 0.03。
此时,顶点D和E的Rank值在本次迭代中没有发生变化(从0.2变为0.03是第一次变化,但本次迭代前后值未变)。在实际优化中,它们可以被标记为“非活跃”(图中标红),在后续超步中不再参与计算,除非收到新的消息。
后续超步:
过程持续进行。在超步3中,活跃顶点(A, B, C)重新计算并发送消息。
- 一个优化是:对于指向非活跃顶点的边,其贡献值可以复用上一轮的值,无需重新发送网络消息。例如,如果D是非活跃的,那么指向它的边在计算时直接使用旧的贡献值即可。
- 计算后,如果顶点A的Rank值也不再变化,则它也会变为非活跃状态。
- 最终,当所有顶点都变为非活跃状态,或达到最大超步数(如30)时,计算停止。
通常,PageRank在经过一定次数的迭代后会接近收敛值,无需运行过长时间。

总结

本节课中,我们一起学习了PageRank算法在Pregel模型中的实现。
- 我们分析了Pregel
compute函数的核心逻辑,包括Rank值更新公式新Rank = 0.15/N + 0.85 * sum(入链贡献)和消息传递机制。 - 我们通过一个分步示例,详细演示了Rank值如何初始化、如何在顶点间传播、如何迭代更新,以及如何通过“投票暂停”机制来优化计算过程。
- 关键点在于理解Pregel“以顶点为中心”的并行计算思想和“超步”同步迭代模式,这非常适用于像PageRank这样的图迭代算法。
📘 课程01:欢迎来到《数据科学数学技能》

在本节课中,我们将了解《数据科学数学技能》这门课程的设计背景、目标以及两位主讲教授的介绍。这门课程旨在帮助学习者填补数学基础知识的空白,为数据科学学习打下坚实的数学基础。
大家好,我是Daniel Eger。我是杜克大学工程学院的教授。我在杜克大学和在线平台上教授与数据科学相关的课程。
在2015年秋季于Coursera推出一门新的数据科学课程后,我注意到许多受过良好教育且学习动机强烈的学生,会因为一些基础的数学概念或未曾见过的数学符号而遇到困难。
因此,我仔细梳理了那门课程,列出了所有对非数学专业人士来说不明显的概念,并邀请Paul帮助我创建一门新的在线课程,以温和的方式介绍所有这些内容。
感谢你的邀请,Daniel。大家好,我是Paul Benitch。我是杜克大学的数学教授。在过去的四年里,我运营着一个名为DataPlus的创新暑期项目。在这个项目中,学生们以小组形式,面对跨学科的、面向客户的数据科学挑战。
在这些暑期项目中,我注意到,许多团队成员拥有非常有价值的领域专业知识和丰富的现实世界经验。但由于他们之前数学学习中的一些相对较小的知识缺口,他们无法充分发挥自己的潜力。因此,Daniel和我有一个共同的热情,那就是尽可能高效且轻松地填补这些知识缺口。
数学有助于形成数据科学的语言。完成这门课程后,你将理解这门语言的基本词汇,并能在工作场所和数据科学团队中更好地进行沟通。
如果你渴望自己成为一名数据科学家,学习这门课程是一个很好的第一步。
上一节我们了解了课程的设计初衷和目标,本节中我们来看看课程的具体安排和适用人群。

我们为以下人群设计了这门课程:从未学习过代数直至微积分预备水平的人,或者很久以前上过相关课程但坦白说,大部分内容都已遗忘的人。因此,我们假设学习者对这些内容没有任何先验知识。
在本课程中,你们会看到我们两位教授。我将引导大家学习前四个半星期的内容。我们将涵盖集合论的基本概念、代数符号、实数轴上的函数以及笛卡尔平面中的一些曲线。
然后,我将以一个非技术性的解释来结束我的部分,介绍一个基础的微积分概念:如何计算函数的瞬时变化率。在最后的一个半星期里,我将介绍指数和对数,并提供一个关于概率论的迷你课程,内容涵盖著名的贝叶斯定理及其使用方法。
我们祝愿大家在本课程中一切顺利,并感谢你们的加入。
本节课总结

在本节课中,我们一起学习了《数据科学数学技能》课程的背景、目标、适用人群以及两位主讲教授的介绍。我们了解到,这门课程旨在填补数学基础知识的空白,帮助学习者掌握数据科学所需的数学语言,无论是为了提升职场沟通能力,还是为成为数据科学家迈出第一步。课程内容从集合论、代数基础到微积分和概率论,设计循序渐进,适合初学者。
课程02:集合基础与术语 📚






在本节课中,我们将学习集合论的基础知识。集合是数据科学数学中的一个核心概念,它为我们组织和理解数据提供了基本框架。我们将从集合的定义开始,然后学习描述集合大小的“基数”概念,最后探讨如何通过“交集”和“并集”从现有集合构建新集合。



什么是集合? 🧩

集合是一个东西的集合。集合里的东西被称为元素。一个集合可以由任何东西构成,没有限制。





我们通常用大括号 {} 来包含一个集合的所有元素。例如:
- 集合
A = {1, 2, -3, 7}包含四个元素:1, 2, -3 和 7。 - 集合
E = {“苹果”, “猴子”, “Daniel Eger”}包含三个元素。


为了表示一个元素是否属于某个集合,我们使用特定的符号:
2 ∈ A表示“2是集合A的一个元素”。8 ∉ A表示“8不是集合A的一个元素”。





基数:集合的大小 🔢





基数是描述集合大小的一个花哨术语,它非常简单:一个集合的基数就是它包含的元素数量。




我们通常用竖线符号来表示基数。例如:
- 对于集合
A = {1, 2, -3, 7},其基数为|A| = 4。 - 对于集合
E = {“苹果”, “猴子”, “Daniel Eger”},其基数为|E| = 3。




从集合构建新集合:交集与并集 🔀

现在我们已经了解了单个集合,接下来看看如何组合它们。假设我们有三个集合:
A = {1, 2, -3, 7}B = {2, 8, -3, 10}D = {5, 10}


这些集合之间有一些共同的元素,也有一些不同的元素。交集和并集这两个概念可以帮助我们更严谨地讨论这些关系。





交集 (Intersection)




两个集合的交集是指同时属于这两个集合的所有元素构成的集合。你可以把它理解为“且”的关系。




我们用符号 ∩ 表示交集。例如:
A ∩ B = {2, -3},因为只有2和-3同时存在于A和B中。B ∩ D = {10},因为只有10同时存在于B和D中。A ∩ D = {},因为A和D没有共同的元素。这个特殊的集合被称为空集,记作∅。空集的基数定义为0,即|∅| = 0。




我们也可以用“条件定义法”来描述交集,这就像俱乐部门口的保安检查身份:
A ∩ B = { x | x ∈ A 且 x ∈ B }
这读作:“A与B的交集,是满足‘x属于A’并且‘x属于B’的所有x构成的集合。”任何想进入这个“俱乐部”的元素x,都必须同时通过A和B的检查。



并集 (Union)




两个集合的并集是指属于第一个集合或属于第二个集合(或同时属于两者)的所有元素构成的集合。你可以把它理解为“或”的关系。




我们用符号 ∪ 表示并集。例如:
A ∪ B = {1, 2, -3, 7, 8, 10}。它包含了A和B中所有的元素,重复的只算一次。A ∪ D = {1, 2, -3, 7, 5, 10}。


同样,我们可以用条件定义法来描述并集:
A ∪ B = { x | x ∈ A 或 x ∈ B }
这读作:“A与B的并集,是满足‘x属于A’或者‘x属于B’的所有x构成的集合。”想进入这个“俱乐部”的条件宽松多了,只要满足其中一个条件即可。



总结 📝





本节课我们一起学习了集合论的基础知识:
- 集合:一个东西的集合,其中的东西称为元素。
- 基数:集合中元素的数量,记作
|A|。 - 交集 (∩):找出两个集合共同拥有的元素(“且”的关系)。
- 并集 (∪):合并两个集合的所有元素(“或”的关系)。



我们还学习了两种表示集合的方法:直接列出元素,或者通过条件来定义集合成员。




这些看似抽象简单的概念,在下一节课中,我们将看到它们如何被应用于一个真实的医学检测案例,帮助我们理解现实世界中的数据科学问题。
课程03:集合论在医学测试中的应用 🧪
在本节课中,我们将学习如何运用集合论的基本概念来分析一个现实世界的问题:医学测试的准确性。我们将通过定义不同的集合,来理解真阳性、假阳性、真阴性、假阴性等核心概念及其计算方式。



上一节我们介绍了集合论的基本运算,本节中我们来看看如何将这些抽象概念应用于具体的医学测试场景。





定义基本集合



首先,我们设定一个临床试验的场景。令集合 X 代表所有参与某项疾病(我们称之为“非常糟糕综合征”,VBS)检测试验的人。
我们可以将 X 划分为两个互斥的集合:
- S: 真正患有 VBS 的人的集合。
- H: 未患有 VBS(健康)的人的集合。




根据定义,我们有:
- X = S ∪ H (所有人要么患病,要么健康)
- S ∩ H = ∅ (没有人既患病又健康)



定义测试结果集合




医学测试的目的是判断一个人属于 S 还是 H。测试会给出“阳性”或“阴性”的结果。我们定义:
- P: 检测结果为阳性(即测试显示患有 VBS)的人的集合。
- N: 检测结果为阴性(即测试显示未患有 VBS)的人的集合。



同样,对于测试结果,我们有:
- X = P ∪ N (每个人的测试结果非阳即阴)
- P ∩ N = ∅ (没有人同时得到阳性和阴性结果)




在理想情况下,测试是完美的:S = P 且 H = N。但现实中,测试总存在误差。




分析四种关键交集



为了量化测试的误差,我们需要分析以下四个关键的交集。以下是这四种情况的含义:





- S ∩ P: 真阳性。此人确实患病,且测试正确识别为阳性。
- H ∩ N: 真阴性。此人确实健康,且测试正确识别为阴性。
- S ∩ N: 假阴性。此人确实患病,但测试错误地给出了阴性结果。
- H ∩ P: 假阳性。此人确实健康,但测试错误地给出了阳性结果。




显然,我们希望真阳性和真阴性的集合尽可能大,而假阳性和假阴性的集合尽可能小。
计算关键比率


通过比较这些集合的大小(即集合的基数),我们可以计算出评估测试性能的关键指标。


以下是几个基础比例:
- |S| / |X|: 研究人群中真实患病者的比例。
- |H| / |X|: 研究人群中真实健康者的比例。这两个比例之和为1。




以下是评估测试准确性的核心比率:





- 真阳性率: 在所有真实患病者中,被测试正确识别出来的比例。
- 公式:
TPR = |S ∩ P| / |S| - 我们希望这个值接近1。
- 公式:




- 假阳性率: 在所有真实健康者中,被测试错误判为阳性的比例。
- 公式:
FPR = |H ∩ P| / |H| - 我们希望这个值接近0。
- 公式:





- 假阴性率: 在所有真实患病者中,被测试错误判为阴性的比例。
- 公式:
FNR = |S ∩ N| / |S| - 我们希望这个值接近0。
- 公式:




- 真阴性率: 在所有真实健康者中,被测试正确识别出来的比例。
- 公式:
TNR = |H ∩ N| / |H| - 我们希望这个值接近1。
- 公式:





总结与应用延伸



本节课中我们一起学习了如何用集合论框架分析医学测试。一个完美的测试应具备真阳性率和真阴性率接近1,同时假阳性率和假阴性率接近0的特性。虽然这在现实中难以实现,但这些概念为我们量化比较不同测试方法的优劣提供了精确的工具。




更重要的是,这套“真实情况 vs. 预测结果”的分析框架并不仅限于医学领域。在未来的机器学习(尤其是监督学习)、商业分析等场景中,只要存在一个真实的分类(如垃圾邮件/非垃圾邮件、欺诈交易/正常交易)和一个用于预测的模型或测试,我们都可以沿用真阳性、假阳性、真阳性率、假阳性率等完全相同的概念和词汇来评估系统的性能。
课程04:集合维恩图 📊



在本节课中,我们将学习一种直观展示集合的工具——维恩图。我们将介绍维恩图的基本概念,用它来证明一个重要的公式——容斥原理,并重新审视之前的医学检测例子,以理解如何用图形化方式分析集合关系。





什么是维恩图? 🎨



维恩图是一种用图形表示集合及其关系的可视化方法。它没有严格的数学证明,但能帮助我们直观地理解集合运算。



让我们从一个集合开始。用传统记法表示一个集合 A:
A = {1, 5, 10, 2}
集合 A 的基数(元素个数)是 4。


另一种表示方法是用一个大圆圈,将元素画在圆圈内部:
A
┌─────┐
│ 1 │
│ 5 │
│ 10 │
│ 2 │
└─────┘
这明确地展示了 A 是一个包含四个元素的“袋子”。元素的顺序无关紧要,这只是一种视觉表示。







用维恩图表示交集与并集 🔄



维恩图的优势在于能直观展示集合间的关系,例如交集。



让我们定义三个集合:
- A =
- B =
- C =



我们可以用维恩图表示它们。首先画出集合 A,包含元素 1, 2, 10, 5。
然后画出集合 B,它包含元素 5, -7, 10, 3。注意 A ∩ B = {10, 5},即两个集合共享的元素。我们可以让两个圆圈重叠来表示这一点。
重叠区域就是 A ∩ B。B 独有的元素是 -7 和 3。
最后,画出集合 C,包含元素 8 和 11。C 与 A 和 B 都没有共同元素,因此它的圆圈是分开的。
从图中可以直观看出:
- A ∩ C = ∅ (空集)
- B ∩ C = ∅ (空集)






容斥原理 📐



利用维恩图,我们可以理解并证明一个重要的公式——容斥原理。这个公式描述了如何通过单个集合及其交集的基数来计算两个集合并集的基数。




容斥原理的公式是:
|A ∪ B| = |A| + |B| - |A ∩ B|
其中 |X| 表示集合 X 的基数。


让我们用之前的例子验证这个公式。
- A ∪ B = {1, 2, 10, 5, -7, 3},所以
|A ∪ B| = 6 |A| = 4|B| = 4|A ∩ B| = 2




根据公式计算:4 + 4 - 2 = 6,结果正确。


这个公式为什么成立?从维恩图可以直观理解:如果我们简单地将 |A| 和 |B| 相加,那么 A ∩ B 区域(即元素 10 和 5)就被计算了两次。因此,我们需要减去一次重复计算的部分,即减去 |A ∩ B|。




用维恩图分析医学检测案例 🏥



上一节我们介绍了容斥原理,本节中我们来看看如何用维恩图重新审视之前的医学检测案例,这能帮助我们更清晰地理解其中的集合划分。


让我们回顾一下案例中的集合定义:
- X:所有接受检测的人。
- H:健康的人(未患 VBS 综合征)。
- S:患病的人(患有 VBS 综合征)。显然,H ∩ S = ∅,且 X = H ∪ S。
- N:检测结果为阴性的人。
- P:检测结果为阳性的人。同样,N ∩ P = ∅,且 X = N ∪ P。



我们可以用维恩图来表示这些关系。首先,用一个矩形表示全集 X。用一条竖线将其分为两部分:左边是健康人群 H,右边是患病人群 S。
然后,用一条横线(或另一种颜色/形状的划分)将矩形再次划分:上半部分是检测阴性人群 N,下半部分是检测阳性人群 P。
这样,整个矩形就被分成了四个区域。

现在,我们来分析这四个区域分别代表什么:
- 左上区域:既是 H(健康)又是 N(阴性)。这是真阴性人群,检测结果正确。
- 右上区域:既是 S(患病)又是 N(阴性)。这是假阴性人群(S ∩ N),他们患病但检测结果显示未患病,非常危险。
- 左下区域:既是 H(健康)又是 P(阳性)。这是假阳性人群(H ∩ P),他们健康但检测结果显示患病,会带来不必要的焦虑。
- 右下区域:既是 S(患病)又是 P(阳性)。这是真阳性人群,检测结果正确。




维恩图清晰地展示了理想检测与现实检测的差距。一个完美的检测,其维恩图中假阴性(S ∩ N)和假阳性(H ∩ P)这两个区域应该不存在或非常小。图形帮助我们直观地理解了检测准确性的含义。





总结 📝




本节课中我们一起学习了维恩图这一强大的可视化工具。我们了解了如何用圆圈表示集合,如何通过重叠展示交集,并利用图形直观理解了容斥原理 |A ∪ B| = |A| + |B| - |A ∩ B| 的由来。最后,我们应用维恩图重新分析了医学检测案例,将抽象的集合划分(健康/患病、阳性/阴性)转化为清晰的图形区域,从而更深刻地理解了真阳性、假阳性、真阴性和假阴性等概念。维恩图虽然不是计算工具,但它能极大地帮助我们建立对集合关系的直观认识。
课程05:数轴与实数线 📏





在本节课中,我们将学习一个贯穿数据科学始终的核心概念:实数线。我们将介绍什么是实数线,并学习正数、负数、非负数、非正数以及绝对值等基本概念。



什么是实数线?🔢

上一节我们介绍了有限集的概念。本节中,我们将看到第一个无限集的例子,即实数线。


实数线是一条直线,它向两端无限延伸。最右端是正无穷大,最左端是负无穷大。这条直线上的每一个点都代表一个实数。这个无限集合通常用符号 R 表示。




我们通常在直线的中间位置标记 0,然后在两侧等距地标记整数:1, 2, 3, 4, 5…… 以及 -1, -2, -3…… 这些整数构成了实数集的一个子集,称为整数集,用符号 Z 表示。



Z = {…, -3, -2, -1, 0, 1, 2, 3, …}




实数线上的其他数字



并非实数线上的所有数字都是整数。例如,在1和2之间,就存在着无穷多个实数。





为了更清楚地理解,我们可以放大1和2之间的线段。在这段区间内,任何数字都是实数。例如:
- 1.1 大约在十分之一的位置。
- 1.4 大约在十分之四的位置。
- 1.1538 大约在某个位置。



实际上,任何由整数部分和小数部分(可以是有限位,也可以是无限位)组成的数字都是实数。这个规律适用于数轴上的任何子区间。例如,在-3和-2之间,存在像 -2.5 这样的数字。



因此,一个实数可以是任何整数加上任意你想要的数字串。有些数字串会终止(如1.5),有些则不会终止且不重复(如圆周率π),后者被称为无理数。本节课的核心要点是:实数就是这条线上的任何一个数字,它们的数量是无穷的。





正数、负数与非正非负数


数学家们常常将大集合划分成更小的类别。对实数进行的第一个重要划分就是正数和负数。



再次画出我们的实数线,中间是0。
- 在0右侧的所有数字称为正实数。
- 在0左侧的所有数字称为负实数。



以下是具体例子:
- 正实数的例子:5.3, 0.001。
- 负实数的例子:-11.7。




如果我们把0也包括进去,那么:
- 正实数加上0,就构成了非负实数。
- 负实数加上0,就构成了非正实数。






绝对值 📏




观察数轴可以发现,数字常常成对出现:一个正数和一个对应的负数。例如,7.1 和 -7.1。虽然它们不相等,但它们有一个重要的共同点:它们到0的距离相同。



从7.1到0的距离是7.1,从-7.1到0的距离也是7.1。这个“距离”的概念就是绝对值。




绝对值的定义



对于任意一个实数 x,其绝对值(记作 |x|)定义为 x 到 0 的距离。




根据上面的例子:
- |7.1| = 7.1
- |-7.1| = 7.1



注意,|-7.1| 也等于 -(-7.1)。这引出了一个通用的定义方法,称为分段定义。


绝对值的分段定义

对于任意实数 x ∈ R,其绝对值定义如下:




|x| = x, 如果 x ≥ 0 (即x是非负数)
|x| = -x, 如果 x < 0 (即x是负数)


让我们通过例子来理解这个定义:
- 计算 |8.7|
- x = 8.7,它是非负数。
- 因此,我们使用第一个公式:|8.7| = 8.7。
- 在数轴上,8.7到0的距离确实是8.7。




- 计算 |-10|
- x = -10,它是负数。
- 因此,我们使用第二个公式:|-10| = -(-10) = 10。
- 在数轴上,-10到0的距离确实是10。






总结 🎯


本节课我们一起学习了:
- 实数线(R):一条代表所有实数的无限延伸的直线。
- 数的分类:正数、负数、非负数、非正数。
- 绝对值(|x|):一个数在数轴上到原点的距离,并通过分段定义给出了其计算方法。



在下一节课中,我们将深入探讨不等式的概念,学习如何比较数的大小(大于、小于、大于等于、小于等于),并将这些概念与绝对值联系起来。
📊 课程06:数的比较:小于与大于



在本节课中,我们将学习不等式的基本概念,包括“小于”、“大于”、“小于等于”和“大于等于”这些符号的含义。我们还会了解一个在数据科学中常用的非正式概念:“远小于”。


概述


我们将从绘制实数轴开始,直观地理解“小于”和“大于”的含义。接着,我们会探讨“小于等于”和“大于等于”这两个包含等号的关系。最后,我们会介绍“远小于”这个在数据科学中常见的表述。

小于与大于


首先,让我们在实数轴上理解“小于”的含义。



假设这是实数轴,这里是零点,这里是数字2,这里是数字3.1。


考虑这个陈述:2 < 3.1。


这个指向左边的“<”符号,读作“小于”。


这个陈述“2小于3.1”意味着:在实数轴R上,数字2位于数字3.1的左侧。这确实是正确的,2在3.1的左边。


再比如,-11.78也小于3.1。事实上,-11.78同样位于3.1的左侧。


因此,一般来说,当我写 a < b 时,我的意思是:在实数轴上,无论a在哪里,b都必须在其右侧,即a在b的左边。



一个常见的误解是认为“a小于b”意味着a在数值上“更小”。当a和b都是正数时,这似乎说得通(比如2比3.1“小”)。但当a是负数时,这种说法就不准确了。例如,我不会说-11.78在通常意义上比3.1“小”,它只是在其左边。



当我们写 3.1 > 2 时,意思是:在实数轴上,3.1位于2的右侧。这是正确的。



事实上,a < b 为真,当且仅当 b > a 也为真。这个“当且仅当”是一个有趣的数学符号,表示两者等价。
“远小于”的概念

接下来是这个有趣的符号:x << y。
你几乎不会在正规的数学教科书中看到它,因为它不是一个严格的数学概念。但在数据科学中,你会经常看到它。它的真正含义是:x 远小于 y。




例如,“1远小于1百万”可能是一个合理的说法。我说“可能”,是因为没有真正的方法来判断这是真还是假。而像“2 < 3.1”这样的陈述,你可以判断真假。“远小于”更多是一种主观判断,但我们通常对其含义有共识。例如,1百万相对于1来说,通常被认为是“远大于”。

小于等于与大于等于


上一节我们介绍了“小于”和“大于”,本节我们来看看包含等号的情况。
这个陈述:a ≤ b。读作:a 小于或等于 b。


从书写上看,你先写小于号“<”,然后在下面加一条横线。从某种意义上说,你应该画两条线来表示“小于或等于”,但通常你看到的都是在小于号下加一条线。它的意思是:a 小于 b,或者 a 等于 b。这实际上就是一个简写。


在实数轴上,如果这里是b,那么 a ≤ b 意味着:要么a在b的左边(a < b),要么a正好在b的位置上(a = b)。


那么,我们如何判断一个像“a ≤ b”这样的具体陈述是否为真呢?让我们看几个例子。


假设有人说:2 ≤ 3.1。
我们相信他吗?这个人实际上是在说以下两个断言中至少有一个为真:
- 2 < 3.1
- 2 = 3.1
第一个断言为真,第二个为假。但合在一起,这个“或”关系的陈述为真。这是“或”的好处:只需满足一个条件即可。


另一方面,如果有人声称:2 ≤ 2。
这意味着:
- 2 < 2
- 2 = 2
第一个为假,第二个为真。合在一起,陈述为真。


再看一个我们会失败的例子:有人说 2 ≤ 0.8。
这意味着:
- 2 < 0.8
- 2 = 0.8
第一个为假,第二个也为假。因此,整个陈述为假。


总结



本节课中,我们一起学习了不等式的基本概念。

我们学习了“小于”(<)、“大于”(>)、“小于等于”(≤)和“大于等于”(≥)这些严格数学概念的含义,它们都与数字在实数轴上的相对位置有关。
我们还了解了“远小于”(<<)的含义,这虽然不是一个严格的数学概念,但却是人们(尤其是在数据科学中)经常使用的直观表述。




通过具体的例子,我们掌握了如何判断这些不等式陈述的真假。
📘 课程07:不等式代数运算






在本节课中,我们将学习如何对不等式进行代数运算。我们将从回顾等式的代数运算开始,然后介绍不等式运算的规则,并重点强调一个关键区别:当不等式两边乘以负数时,必须翻转不等号的方向。





上一节我们介绍了不等式的基本概念及其在数轴上的表示。本节中,我们来看看如何对不等式进行代数运算。




首先,让我们回顾一下等式的代数运算。我们这样做是为了求解未知数 X。




以下是等式运算的两个基本规则:


- 等式两边加上或减去同一个数:如果
A = B,那么A + C = B + C。 - 等式两边乘以或除以同一个非零数:如果
A = B且C ≠ 0,那么C × A = C × B。







这些规则使我们能够解方程。例如,对于方程 x + 3 = 10,我们可以在两边减去 3 来求解 x:
x + 3 - 3 = 10 - 3
x = 7










现在,让我们转向不等式。对于不等式,加减法规则与等式类似。




不等式两边加上或减去同一个数:如果 A < B,那么对于任意数 C,都有 A + C < B + C。



这个规则允许我们像处理等式一样处理不等式。例如,对于不等式 x + 3 < 10,我们可以在两边减去 3:
x + 3 - 3 < 10 - 3
x < 7
这意味着 x 可以是任何小于 7 的数。






然而,当涉及到乘法时,不等式有一个非常重要的注意事项。
以下是乘法规则:





- 如果
A < B,且C > 0(正数),那么A × C < B × C。 - 如果
A < B,且C < 0(负数),那么A × C > B × C。此时必须翻转不等号的方向。







让我们看一个例子。假设我们有不等式 -2x < 10。为了求解 x,我们需要在两边乘以 -1/2。因为乘的是负数,我们必须翻转不等号:
(-1/2) × (-2x) > (-1/2) × 10
x > -5
这意味着 x 可以是任何大于 -5 的数。









本节课中我们一起学习了不等式的代数运算。关键要点是:对不等式进行加减运算时,规则与等式相同;但当不等式两边乘以或除以一个负数时,必须记得翻转不等号的方向。这是处理不等式时唯一但非常重要的区别。
📊 课程08:数区间与区间表示法
在本节课中,我们将学习实数线上的区间概念,以及如何用不同的符号表示这些区间。区间是数据科学中描述数值范围的基础工具。
实数线与无限集合

我们之前讨论过有限集合,但实数线本身是一个无限集合,它包含无穷多个元素。实数线有许多子集,这些子集同样也是无限大的。


让我们直接开始。请看这个符号:[2, 3.1]。这是一个无限集合,但它有有限的边界。接下来我们将描述这个集合的含义。


这个区间等于满足以下两个条件的所有实数 x 的集合:
- x ≥ 2
- x ≤ 3.1



换句话说,在实数线上画出这个区间:这里是2,这里是3.1。x 被限制在这两个边界之间,并且可以等于边界值。


x 可以是任何满足 2 ≤ x ≤ 3.1 的数。


以下是该区间内的一些例子:
- 2.3 在区间内,因为 2 ≤ 2.3 ≤ 3.1。
- 3 在区间内。
- 3.1 也在区间内。


但是,1 不在区间 [2, 3.1] 内,因为 1 不小于或等于 2(尽管它确实小于或等于 3.1)。


这就是一个闭区间。



开区间

上一节我们介绍了包含端点的闭区间。现在,让我们看看另一种形式。


假设我给你 (5, 8),这里我用圆括号代替了方括号。这代表一个无限集合,即所有满足 x > 5 且 x < 8 的实数 x 的集合。


我们这样表示:在数轴上,在5和8的位置画一个空心的小圆圈,然后取它们之间的所有点。

这意味着你必须在5和8之间,但不能等于这两个端点。

例如:
- 5.5 在开区间 (5, 8) 内。
- 5.0001 也在开区间 (5, 8) 内。
- 但是,5 不在开区间 (5, 8) 内,因为5不小于5(尽管它小于8)。


你可能会想,闭区间 [5, 8] 和开区间 (5, 8) 有什么区别?它们恰好相差两个数:5 和 8 在闭区间内,但不在开区间内。


半开区间


我们已经了解了闭区间和开区间。现在,我们来看介于两者之间的两种形式,称为半开区间。

例如:(-7.1, 15]。你可能已经猜到了,左边是开区间符号,意味着使用严格不等式;右边是闭区间符号,意味着使用“小于或等于”。这是一个无限集合,包含所有满足 -7.1 < x ≤ 15 的实数 x。



我们这样画:在 -7.1 处画一个空心点,在15处画一个实心点,然后取中间的所有部分。

再看另一个例子:[20, 20.3)。这表示所有满足 20 ≤ x < 20.3 的实数 x 的集合。


在数轴上画出来:在20处画一个实心点,在20.3处画一个空心点。


这里有一个有趣的观点:从感觉上看,20到20.3这个区间在实数线上相比 -7.1 到 15 来说非常小。然而,它内部仍然包含无穷多个数。这就是实数线奇妙的地方,背后有深奥的数学原理。



无穷区间

我们已经见过闭区间(如 [2, 3.1])、开区间(如 (5, 8))和两种半开区间(如 (-7.1, 15] 和 [20, 20.3))。有时,第一种半开区间被称为“左开”,第二种被称为“右开”,但这并不太重要。


现在,让我们看一种更特殊的形式。假设我们写 [2, ∞)。这里的圆括号表示无穷大不是一个可以“达到”的端点。这代表所有满足 x ≥ 2 的实数 x 的集合。你不需要写“小于无穷大”,因为每个数都小于无穷大。

我们常在数轴上这样表示:标出0和2,然后从2开始向右画一条射线,表示一直延伸下去。这被称为射线。


你也可以有 (-∞, 7.1) 这样的形式,它表示所有满足 x < 7.1 的实数 x 的集合。你应该明白这个意思了。



与不等式代数的联系


最后,让我们将区间与之前的不等式代数视频联系起来。


我们已经熟悉这样的想法:如果有人要求你解方程 x + 5 = 10,你通过代数运算解得 x = 5。所以,x = 5 就是答案,一个数字就是答案。

另一方面,假设有人给你以下问题:如果 1 ≤ x + 5 < 10 成立,告诉我关于 x 的所有信息。

注意,这里答案不是一个单一的数字。例如,如果 x = 4,那么 4 + 5 = 9,9 小于10且大于等于1。但 x = 3.9 也成立。事实上,答案是一个区间。

让我们对这个不等式做一点代数运算:从所有部分减去5。
- 从左边减去5:1 - 5 = -4
- 从中间减去5:x + 5 - 5 = x
- 从右边减去5:10 - 5 = 5

于是我们得到:-4 ≤ x < 5
换句话说,第一个谜题的答案是,x 可以是这个范围内的任何值。只要 x 在左闭右开的半开区间 [-4, 5) 内,就满足原不等式。
总结
本节课中,我们一起学习了:
- 闭区间(如 [a, b]):包含两个端点,表示 a ≤ x ≤ b。
- 开区间(如 (a, b)):不包含两个端点,表示 a < x < b。
- 半开区间(如 (a, b] 或 [a, b)):只包含一个端点。
- 无穷区间(如 [a, ∞) 或 (-∞, b)):表示一端没有界限的区间。
- 如何将解不等式得到的结果用区间表示,这是描述数值范围的有力工具。



理解这些不同的区间表示法,对于描述数据范围、定义函数域以及解决各种数学问题都至关重要。
课程09:求和符号Σ简介 📊




在本节课中,我们将学习求和符号Σ的含义与用法。求和符号是一种简洁的数学表达方式,用于表示一系列数字的相加。我们将通过三个具体例子,逐步拆解其结构,理解如何将符号转换为实际的加法运算。







理解求和符号的结构


上一节我们介绍了本节课的目标。本节中,我们来看看求和符号的基本组成部分。


求和符号通常用大写的希腊字母 Σ 表示。它的核心作用是提供一种紧凑的方式来表达一连串的加法运算,避免写出冗长的算式。

一个完整的求和表达式包含以下几个部分:
- 求和符号 (Σ): 表示“求和”或“相加”的操作。
- 索引变量: 通常使用
i,j,k等字母,它是一个“哑变量”或计数器。 - 起始值: 写在Σ下方,表示索引变量开始的值。
- 终止值: 写在Σ上方,表示索引变量结束的值。
- 通项公式: 写在Σ后面,定义了对于每个索引值需要进行何种运算。
其通用形式可以表示为:
n
Σ f(i)
i=m
这表示将函数 f(i) 在 i 从 m 到 n (通常每次增加1) 的所有结果相加。







示例一:计算平方和



首先,我们通过一个简单例子来实践。计算以下求和式:
4
Σ i²
i=1



根据求和符号的定义,我们按以下步骤展开:

- 确定索引范围: 索引
i从1开始,到4结束,按1递增。所以i会依次取值为 1, 2, 3, 4。 - 应用通项公式: 对每个
i的值,计算i²。- 当
i = 1时,i² = 1² - 当
i = 2时,i² = 2² - 当
i = 3时,i² = 3² - 当
i = 4时,i² = 4²
- 当
- 将所有结果相加: 将第二步得到的所有值用加号连接起来。




因此,原求和式等价于:
4
Σ i² = 1² + 2² + 3² + 4²
i=1
最后进行算术计算:
1² + 2² + 3² + 4² = 1 + 4 + 9 + 16 = 30
所以,Σ (i=1 to 4) i² = 30。





示例二:计算线性表达式之和


理解了第一个例子后,我们来看一个稍复杂的情况。计算:
5
Σ (2i + 3)
i=1


我们遵循同样的步骤:




- 确定索引范围:
i从1到5,取值为 1, 2, 3, 4, 5。 - 应用通项公式: 对每个
i,计算(2i + 3)。i = 1:2*1 + 3 = 5i = 2:2*2 + 3 = 7i = 3:2*3 + 3 = 9i = 4:2*4 + 3 = 11i = 5:2*5 + 3 = 13
- 将所有结果相加。
因此,原式展开为:
5
Σ (2i + 3) = (2*1+3) + (2*2+3) + (2*3+3) + (2*4+3) + (2*5+3)
i=1
计算总和:
5 + 7 + 9 + 11 + 13 = 45
所以,Σ (i=1 to 5) (2i + 3) = 45。





示例三:索引变量与起始值的变化



前面的例子索引都从1开始并使用 i。本节中我们来看看这些规则可以如何变化。计算:
7
Σ (j / 2)
j=3


请注意两个变化:索引变量是 j,且起始值是3。但解题逻辑不变:

- 确定索引范围:
j从3开始,到7结束,取值为 3, 4, 5, 6, 7。 - 应用通项公式: 对每个
j,计算j / 2。 - 将所有结果相加。
展开算式:
7
Σ (j / 2) = (3/2) + (4/2) + (5/2) + (6/2) + (7/2)
j=3
计算总和:
(3/2) + 2 + (5/2) + 3 + (7/2) = (3+5+7)/2 + 5 = (15)/2 + 5 = 7.5 + 5 = 12.5
或者用分数表示:
(3+4+5+6+7) / 2 = 25 / 2
所以,Σ (j=3 to 7) (j / 2) = 25/2。





核心概念:哑变量



通过以上例子,我们可以总结一个关键概念:哑变量。



在求和符号 Σ 中使用的索引变量(如 i, j, r)被称为“哑变量”。它们就像临时工或计数器,本身没有独立的数值意义。它们的作用仅限于:
- 在指定的范围内(如下标和上标所示)依次取值。
- 将其值代入通项公式中进行计算。

这意味着,只要范围和通项公式相同,使用哪个字母作为索引变量不影响最终结果。例如:
7 7 7
Σ (j / 2) = Σ (r / 2) = Σ (😊 / 2) = 25/2
j=3 r=3 😊=3
虽然通常约定俗成使用 i, j, k 等字母,但从数学上讲,任何符号都可以。








总结



本节课中我们一起学习了求和符号 Σ 的用法。
- 求和符号
Σ提供了一种简洁的方式来表示一系列数值的加法。 - 其结构包括:索引变量(哑变量)、起始值、终止值和通项公式。
- 计算时,只需让索引变量从起始值逐步增加到终止值(通常每次加1),将每个值代入通项公式计算,最后将所有结果相加。
- 理解“哑变量”的概念至关重要,它说明索引变量本身只是一个占位符,不影响求和的本质。



掌握求和符号是理解更复杂数学和统计概念的基础,它能让表达和计算变得更加清晰高效。
课程10:求和符号简化规则 📝




在本节课中,我们将学习如何简化求和符号(Σ)的表达式。我们将介绍三个核心规则,这些规则能帮助我们更高效地处理和计算求和问题。


上一节我们介绍了求和符号的基本概念和简单示例。本节中,我们来看看如何通过一些规则来简化更复杂的求和表达式。




规则一:提取常数因子 🧮



当求和符号内部的表达式包含一个常数因子时,可以将该常数因子提取到求和符号外部。



以下是具体说明:





- 例如,计算 ∑_{i=1}^{4} 3i²。
- 直接计算:3×1² + 3×2² + 3×3² + 3×4²。
- 根据乘法分配律,可以提取公因子3:3 × (1² + 2² + 3² + 4²)。
- 这等价于 3 × ∑_{i=1}^{4} i²。



因此,通用规则可以表示为:
∑_{i=a}^{b} c × f(i) = c × ∑_{i=a}^{b} f(i)
其中 c 是一个常数。



规则二:分解求和项 ➕




当求和符号内部的表达式是多个项的和时,可以将该求和分解为多个独立的求和。




以下是具体说明:




- 例如,计算 ∑_{i=1}^{4} (i² + 2i)。
- 直接计算:(1²+2×1) + (2²+2×2) + (3²+2×3) + (4²+2×4)。
- 我们可以重新排列加法顺序,将所有的平方项和所有的“2i”项分别相加:(1²+2²+3²+4²) + (2×1+2×2+2×3+2×4)。
- 这等价于 ∑_{i=1}^{4} i² + ∑_{i=1}^{4} 2i。




因此,通用规则可以表示为:
∑_{i=a}^{b} [f(i) + g(i)] = ∑_{i=a}^{b} f(i) + ∑_{i=a}^{b} g(i)




规则三:常数求和 🔢




当求和符号内部的表达式是一个与索引变量无关的常数时,求和结果等于该常数乘以求和的项数。



以下是具体说明:




- 例如,计算 ∑_{k=1}^{10} 5。
- 这意味着将数字5连续相加10次:5 + 5 + ... + 5 (共10个5)。
- 结果等于 10 × 5 = 50。

因此,通用规则可以表示为:
∑_{i=a}^{b} c = c × (b - a + 1)
其中 c 是一个常数,(b - a + 1) 是求和的项数。








本节课中我们一起学习了简化求和符号的三个重要规则:提取常数因子、分解求和项以及对常数求和。掌握这些规则能让我们更灵活、更快速地处理涉及求和符号的数学问题。
课程11:求和符号与均值方差 📊






在本节课中,我们将完成关于求和符号的系列讲解,并将其应用于统计学中的核心概念:均值与方差。我们将学习如何使用求和符号简洁地表达这些概念,并理解它们如何量化数据集的“中心”和“离散”程度。






均值与求和符号


上一节我们介绍了求和符号的基本用法。本节中,我们来看看如何用它来定义数据的均值。




均值,即平均值,是数据集中所有数值之和除以数值的个数。假设我们有一个包含 n 个实数的集合 X,其元素为 x1, x2, ..., xn。






那么,集合 X 的均值 μ_x 可以用求和符号表示为:





公式:
μ_x = (1/n) * Σ_{i=1}^{n} x_i





以下是计算均值的步骤:
- 使用求和符号
Σ_{i=1}^{n} x_i将所有元素x1到xn相加。 - 将总和除以元素个数
n。




例如,对于集合 Z = {1, 5, 12},其均值为 (1+5+12)/3 = 6。





在求和符号 Σ_{i=1}^{n} x_i 中,索引 i 是一个“哑变量”,它仅用于从 1 计数到 n。而 n 本身是一个变量,它决定了求和的范围。




数据中心化



在深入方差之前,我们先了解一个在数据科学中常用的技巧:数据中心化。






数据中心化是指将数据集中的每个数值减去该数据集的均值,从而生成一个均值为零的新数据集。这样做有很多好处,例如可以简化后续的统计分析(在线性回归等模型中会经常用到)。





其操作非常简单:
- 计算原数据集的均值
μ。 - 将原数据集中的每个数值
x_i减去均值μ,得到新的数值x'_i = x_i - μ。




新数据集的均值必然为零。从几何上看,这相当于将数轴上的数据整体平移,使得均值点与零点重合。




方差与标准差




均值描述了数据的“中心”位置,但仅凭均值无法区分数据分布的“离散”程度。例如,集合 Z = {1, 5, 12} 和 W = {5, 6, 7} 的均值都是 6,但 Z 中的数据显然比 W 中的数据更分散。




为了量化这种“离散”程度,我们引入方差的概念。






对于包含 n 个元素的集合 X,其方差 σ_x² 定义为每个元素与均值之差的平方的平均值。





公式:
σ_x² = (1/n) * Σ_{i=1}^{n} (x_i - μ_x)²






以下是理解方差的要点:
(x_i - μ_x)衡量了每个数据点偏离均值的距离。- 我们对其平方,是为了消除正负偏差相互抵消的影响,只关心偏差的大小。
- 最后对所有平方偏差求平均(除以
n),就得到了方差。





方差的平方根称为标准差,记作 σ_x。


公式:
σ_x = √(σ_x²)








标准差与原始数据具有相同的量纲,更便于解释。



让我们计算之前两个例子中的方差:
- 对于
W = {5, 6, 7},均值μ_w = 6。- 方差
σ_w² = [(5-6)² + (6-6)² + (7-6)²] / 3 = (1+0+1)/3 = 2/3 - 标准差
σ_w = √(2/3)
- 方差
- 对于
Z = {1, 5, 12},均值μ_z = 6。- 方差
σ_z² = [(1-6)² + (5-6)² + (12-6)²] / 3 = (25+1+36)/3 = 62/3 - 标准差
σ_z = √(62/3)
- 方差





正如我们所料,Z 的方差(和标准差)远大于 W 的方差,这准确地反映了 Z 的数据比 W 更分散的直观感受。






总结






本节课中我们一起学习了如何将求和符号应用于统计学。
- 我们使用求和符号
Σ简洁地定义了数据集的均值μ。 - 我们介绍了数据中心化的方法,即通过减去均值得到一个均值为零的新数据集。
- 为了衡量数据围绕均值的离散程度,我们定义了方差
σ²和标准差σ,它们同样可以用求和符号优雅地表示。



理解这些概念及其数学表达是后续学习更复杂数据科学模型的基础。
课程12:笛卡尔平面与点坐标绘制 📍




在本节课中,我们将学习笛卡尔平面的基本概念,并掌握如何在平面上绘制点。这是将一维数轴扩展到二维空间的关键一步,对于理解数据可视化、函数图像等至关重要。






上一节我们介绍了实数轴,它是一种在一条线上表示数字的方法。本节中,我们来看看笛卡尔平面。



笛卡尔平面,通常表示为 R²,是一种表示两个信息的方式。它由两条垂直的数轴构成:水平的x轴和垂直的y轴。平面上的每一个点都对应一个有序数对 (x, y)。



一个非常特殊的点是原点,即两条轴的交点,其坐标为 (0, 0)。







现在,我们来看看如何在平面上绘制具体的点。以下是绘制点的基本步骤:
- 从原点出发。
- 根据第一个数字(x坐标)移动:正数向右,负数向左。
- 根据第二个数字(y坐标)移动:从x坐标确定的位置出发,正数向上,负数向下。






让我们通过几个例子来实践一下:




- 点 A (2, 3):从原点向右移动2个单位,再向上移动3个单位。
- 点 B (-1, 5):从原点向左移动1个单位,再向上移动5个单位。
- 点 C (4, -0.5):从原点向右移动4个单位,再向下移动0.5个单位。
- 点 D (-5, -5):从原点向左移动5个单位,再向下移动5个单位。




这就是在平面上绘制点的全部思想。






在绘制点时,需要注意一个符号细节:在平面坐标的语境下,(-5, -5) 这样的写法表示一个点的x坐标为-5,y坐标为-5。这与表示开区间的符号 (-5, -5) 看起来相同,但含义完全不同。为了避免混淆,有时也会写作 (-5, -5) 或 a = (2, 3)。







笛卡尔平面被坐标轴自然地分成了四个区域,我们称之为象限。以下是各象限的定义:



- 第一象限:包含所有
x > 0且y > 0的点。 - 第二象限:包含所有
x < 0且y > 0的点。 - 第三象限:包含所有
x < 0且y < 0的点。 - 第四象限:包含所有
x > 0且y < 0的点。



坐标轴本身(x轴和y轴)不属于任何象限。







那么,在平面上绘制点有什么用呢?一个非常实际的应用是可视化数据关系。


假设我们测量了三个人的身高(厘米)和体重(千克),并得到以下数据:




- A:
(177, 88.3)(美国男性平均身高体重) - B:
(164, 74.7)(美国女性平均身高体重)



当我们把这些点绘制在平面上(以身高为x轴,体重为y轴),可以直观地看到:
- 点B位于点A的左下方,这清晰地表明平均而言,美国女性比男性更矮、更轻。
- 如果我们从点B水平向右移动,意味着体重相同但身高增加。
- 如果我们从点B垂直向上移动,意味着身高相同但体重增加。



这种可视化方法能帮助我们快速理解两个变量之间的关系。需要注意的是,在这个具体例子中,只有第一象限(正身高、正体重)的数据是有实际意义的。






本节课中我们一起学习了笛卡尔平面的基本构成、如何在平面上根据坐标 (x, y) 绘制点、四个象限的定义,以及通过一个身高体重的例子了解了绘制点在数据可视化中的实际应用。掌握这些是后续学习函数图像、线性关系等更复杂概念的基础。
📐 课程13:平面中的距离公式与数据科学应用


在本节课中,我们将学习笛卡尔平面中两点间距离的计算公式,并探讨该公式在数据科学中的两个重要应用:最近邻算法和聚类分析。


📐 回顾:勾股定理
在进入笛卡尔平面之前,我们先回顾一个基础概念:勾股定理。






假设我们有一个直角三角形,其中一个角是直角。设两条直角边的长度分别为 x 和 y,斜边的长度为 z。


勾股定理告诉我们:


z² = x² + y²

或者等价地:


z = √(x² + y²)


这个定理是理解距离公式的关键。



📏 笛卡尔平面中的距离公式


上一节我们回顾了勾股定理,本节中我们来看看如何将其应用于笛卡尔平面。

假设平面上有两个点:
- 点 A,坐标为 (a, b)
- 点 C,坐标为 (c, d)



我们想知道点 A 和点 C 之间的距离。


距离公式 给出了答案:



距离 AC = √[(c - a)² + (d - b)²]


这个公式为什么成立?我们可以通过构造一个直角三角形来理解。

连接点 A 和点 C。从点 A 出发,画一条水平虚线到点 (c, b);再从点 (c, b) 出发,画一条垂直虚线到点 C。这样就形成了一个直角三角形。
- 水平虚线的长度是 |c - a|(x 坐标的差值)。
- 垂直虚线的长度是 |d - b|(y 坐标的差值)。
- 点 A 到点 C 的连线就是这个直角三角形的斜边。
根据勾股定理,斜边的长度就是 √[(c - a)² + (d - b)²]。这就是距离公式的由来。



🔢 距离公式计算示例

理解了公式的原理后,我们通过几个例子来练习计算。



假设我们有以下点:
- 点 A = (1, 1)
- 点 B = (5, 4)
- 原点 O = (0, 0)
- 点 D = (1.5, 1)



以下是计算过程:

1. 计算点 A 和点 B 之间的距离:
距离 AB = √[(5 - 1)² + (4 - 1)²] = √[4² + 3²] = √[16 + 9] = √25 = 5
2. 计算点 A 和原点 O 之间的距离:
距离 AO = √[(1 - 0)² + (1 - 0)²] = √[1² + 1²] = √2 ≈ 1.4





3. 计算点 A 和点 D 之间的距离:
由于它们只有 x 坐标不同,距离 AD = |1.5 - 1| = 0.5




👥 数据科学应用一:最近邻


现在,我们来看距离公式在数据科学中的一个直接应用:最近邻。



假设我们有一个点集 S = {O, B, D},我们想知道对于点 A 来说,S 中哪个点离它最近。


根据上面的计算结果:
- 距离 AD = 0.5 (最近)
- 距离 AO ≈ 1.4 (第二近)
- 距离 AB = 5 (最远)



因此,我们可以说:
- 点 A 在集合 S 中的 最近邻 是点 D。
- 第二近邻 是原点 O。
- 第三近邻(即最远点)是点 B。



在数据科学和机器学习中,最近邻算法通过计算距离来判断一个新数据点与已知数据点的相似性,常用于分类和预测。






🧩 数据科学应用二:聚类分析


距离概念的另一个重要应用是 聚类分析。


观察下图中的点分布,我们可以直观地将它们分为三组或三个“簇”。
(假设这里有一张图,显示点分成了三个明显的群组)

假设点 A 和 B 属于簇1,点 C 属于簇2,点 D 属于簇3。

那么,通常会有以下关系:
距离 AB << 距离 AC 并且 距离 AB << 距离 AD


这意味着,属于同一个簇内的点(如 A 和 B)彼此之间的距离,远小于它们与其他簇中点(如 C 或 D)的距离。通过计算点与点之间的距离,我们可以将数据自动分组到不同的簇中,从而发现数据内在的结构和模式。聚类是无监督学习中的核心方法。





📝 总结


本节课我们一起学习了以下内容:
- 距离公式:掌握了计算笛卡尔平面中任意两点 (a, b) 和 (c, d) 之间距离的核心公式:√[(c - a)² + (d - b)²]。
- 公式原理:理解了该公式源于勾股定理,通过在两点间构造直角三角形得以证明。
- 数据科学应用:
- 最近邻:利用距离找出数据集中与目标点最相似的点,是监督学习中的重要方法。
- 聚类分析:通过比较点间距离,将数据划分为不同的组或“簇”,是无监督学习中的重要方法。


距离是数据科学中最基本且强大的概念之一,它为许多高级算法奠定了基础。
📐 课程14:平面上的直线方程(第一部分)



在本节课中,我们将学习如何描述笛卡尔平面上的直线。我们将从一个核心概念——斜率开始,逐步推导出描述直线的点斜式方程。这个公式是理解直线方程的基础,虽然看起来有些符号,但我们会逐一解释清楚。






斜率的概念





上一节我们介绍了笛卡尔平面。本节中,我们来看看如何描述一条直线的倾斜程度,这被称为斜率。



首先,我们在XY平面上取两个点:点A (a, b) 和点B (c, d)。连接这两点的线段AB的斜率,通常用字母 M 表示,其定义如下:


斜率公式:
M = (d - b) / (c - a)




这个公式可以理解为“垂直变化量(上升量)”除以“水平变化量(移动量)”。让我们通过一个具体的例子来理解。




以下是计算斜率的步骤:
- 确定两个点的坐标。
- 用第二个点的y坐标减去第一个点的y坐标,得到垂直变化量。
- 用第二个点的x坐标减去第一个点的x坐标,得到水平变化量。
- 将垂直变化量除以水平变化量,结果即为斜率。



示例:正斜率



假设点A坐标为 (1, 2),点B坐标为 (3, 3)。连接A和B的线段斜率计算如下:
M = (3 - 2) / (3 - 1) = 1 / 2
斜率为 1/2。这意味着,从点A出发,在直线上每向右移动1个单位(x增加1),就需要向上移动0.5个单位(y增加0.5)。





示例:负斜率


现在,考虑点C (-1, 1) 和原点O (0, 0)。连接C和O的线段斜率计算如下:
M = (0 - 1) / (0 - (-1)) = (-1) / 1 = -1
斜率为 -1。这意味着,从点C出发,在直线上每向右移动1个单位,就需要向下移动1个单位(y减少1)。






从线段到直线





我们已经讨论了线段的斜率。现在,让我们将这个概念扩展到无限延伸的整条直线。


考虑一条经过点 (2, 1) 和点 (3, 2) 的直线L。我们知道连接这两点的线段斜率为1。




关键在于,对于直线L上的任意一点 (x, y),它与已知点 (2, 1) 所连线段的斜率也必须等于这条直线的斜率,即1。


因此,对于直线L上任意一点 (x, y),以下关系必须成立:
(y - 1) / (x - 2) = 1
将这个等式改写,我们得到:
y - 1 = 1 * (x - 2)
这是一个非常深刻的结论。因为 (x, y) 是直线L上任意一点,所以这个等式定义了整条直线。换句话说,直线L是所有满足 y - 1 = 1 * (x - 2) 这个关系的点 (x, y) 的集合。



我们可以验证点 (3, 2) 是否满足这个方程:
2 - 1 = 1 * (3 - 2) => 1 = 1
等式成立,说明 (3, 2) 在直线上。而点 (5, 1) 不满足该方程,因此不在直线上。






点斜式方程



基于以上的推导,我们可以得到直线方程的通用形式,称为点斜式。



点斜式方程:
如果一条直线L的斜率为 M,且已知该直线上一点为 (x₀, y₀),那么这条直线的方程可以表示为:
y - y₀ = M * (x - x₀)



这个公式非常直观:它直接表达了直线上任意一点 (x, y) 与已知点 (x₀, y₀) 之间的垂直和水平变化关系,其比值恒为斜率M。








总结





本节课中我们一起学习了:
- 斜率的定义:它衡量了一条线段的倾斜程度,计算公式为
M = (y₂ - y₁) / (x₂ - x₁)。 - 整条直线的斜率是恒定的,其上任意两点连线的斜率都相同。
- 利用斜率和直线上一个已知点,我们可以推导出直线的点斜式方程:
y - y₀ = M * (x - x₀)。这个方程描述了直线上所有点必须满足的条件。



在下一节课(第二部分)中,我们将从这个点斜式方程出发,推导出更常见的斜截式方程 y = Mx + B,并通过更多例子加深理解。
📐 课程15:笛卡尔平面斜截式直线方程

在本节课中,我们将学习一种描述直线的更简洁、更常用的公式——斜截式方程。我们将从上一节介绍的点斜式方程出发,推导出斜截式方程,并通过具体例子理解其含义和应用。



🔍 从点斜式到斜截式


上一节我们介绍了点斜式直线方程。现在,我们来看一个具体的例子,并以此为基础引出斜截式方程。
我们有一条直线 L。已知它经过点 (2, 1),并且斜率为 m = 1。
因此,根据点斜式公式,该直线的方程为:
y - 1 = 1 * (x - 2)


这意味着,任何位于直线 L 上的点 (x, y) 都必须满足这个方程。




🎯 认识Y轴截距
本节课的重点是介绍一个更简单的公式,称为斜截式公式。它应用更广泛。之所以先介绍点斜式,是因为可以很容易地从点斜式推导出斜截式。
首先,这是一个关键概念:直线 L 上有无穷多个点,但有一个点特别重要,我们称之为 y轴截距。




y轴截距 是直线与 Y轴 相交的唯一点。

我们来思考这个点的坐标。它的 x坐标 显然是 0,因为Y轴上的所有点都满足 x = 0。我们不知道它的 y坐标 是多少。在数学中,当我们不知道某个值但想用它进行计算时,通常会用一个符号来表示它。
我们把这个点记为 (0, b)。字母 b 常用来表示y轴截距。





🕵️ 求解截距 b


现在,让我们来当一回侦探,找出 b 的值。既然我们知道点 (0, b) 在直线 L 上,并且知道直线的方程,我们就可以求出 b。


因为 (0, b) 在直线上,所以我们可以将 x = 0 和 y = b 代入点斜式方程:
b - 1 = 1 * (0 - 2)



现在进行简单的代数运算:
b - 1 = -2
b = -1

结果符合预期,从图中看,截距确实在y轴的负半轴。因此,y轴截距的坐标是 (0, -1)。



✨ 推导斜截式方程


现在,让我们用新找到的截距点 (0, -1) 来重写点斜式方程。已知斜率为 m = 1,点斜式公式告诉我们:


y - (-1) = 1 * (x - 0)


这等价于:
y + 1 = x
或者
y = x - 1


这就是直线的斜截式方程。


📝 斜截式方程的一般形式


我们可以将其推广到一般情况:

如果一条直线 L 的斜率为 m,并且与y轴相交于点 (0, b),那么该直线的方程可以写为:


y = m * x + b

在这个公式中:
- m 是直线的斜率。
- b 是y轴截距,即截点的y坐标。


斜截式是一种非常直观的描述直线的方式,因为它几乎能让你“看到”方程就画出直线。



🎨 斜截式的直观理解


例如,如果给你方程 y = 2x + 1,我可以立即画出这条直线的草图:
- y轴截距是 1,所以直线穿过点 (0, 1)。
- 斜率是 2,这意味着直线比45度角更陡峭。

斜截式公式 y = m x + b 的美妙之处在于:
- 斜率 m 告诉你直线的倾斜角度。
- 截距 b 告诉你直线在y轴上的锚点位置。

这使得它比点斜式在某些方面更直观、更易用。




💡 综合应用示例

让我们用一个完整的例子来结束本节课。假设有一条直线 L,它经过点 (1, 1) 和 (3, 0)。请找出它的方程。


第一步:草图与求斜率
首先,画出点 (1, 1) 和 (3, 0),并连接它们得到直线。

接下来计算斜率 m:
m = (0 - 1) / (3 - 1) = -1 / 2


所以直线的斜率是 -1/2。


第二步:使用点斜式
我们可以使用点斜式公式。选择点 (1, 1),得到方程:
y - 1 = (-1/2) * (x - 1)

这已经是直线的一个有效方程。
有趣的一点: 我们也可以选择另一个点 (3, 0) 来写点斜式方程:
y - 0 = (-1/2) * (x - 3)


这两个方程看起来不同,但它们描述的是同一条直线。你可以通过代数变换将其中一个转化为另一个,我鼓励你在课后尝试一下,以加深理解。


📚 本节课总结


在本节课中,我们一起学习了:
- y轴截距的概念:直线与y轴的交点,坐标为 (0, b)。
- 如何从已知直线方程和截距的x坐标(恒为0)求解截距 b。
- 斜截式直线方程的推导和一般形式:y = m * x + b。
- 斜截式方程的直观意义:斜率m控制倾斜度,截距b控制上下位置。
- 通过具体例子综合运用了点斜式和斜截式来求解直线方程。


斜截式是描述直线最常用、最便捷的形式之一,熟练掌握它对于后续的数学学习至关重要。
课程16:函数——集合间的映射关系 📚







在本节课中,我们将学习函数的核心概念。我们将从一个更抽象的视角出发,将函数理解为两个集合之间的映射关系,而不是仅仅将其看作一个图形。这种理解方式将为后续学习函数图像和实际应用打下坚实的基础。




从集合到集合的映射 🔄



上一节我们介绍了集合的基本概念。本节中,我们来看看如何用“函数”这个工具来连接两个集合。






一个函数 F 从集合 A 映射到集合 B,可以看作是一个规则、公式或“机器”。这个机器将集合 A 中的每一个元素 a,都转换(或映射)为集合 B 中的一个特定元素 F(a)。




用公式可以表示为:
F: A → B
其中,对于每一个 a ∈ A,都存在一个唯一的 F(a) ∈ B。




这个过程可以想象成一个卡通场景:一个元素 a 从左边进入机器,经过内部处理(我们可能不知道具体过程),最终从右边输出一个结果 F(a)。a 是输入,F(a) 是输出,而函数 F 就是这个转换规则本身。




函数实例解析 📝




理解了抽象定义后,让我们通过几个具体的例子来巩固这个概念。





以下是几个不同场景下的函数示例:




- 抽象示例:假设集合 A = {1, 2, 10},集合 B = {苹果, Daniel Egar, 猴子}。我们可以定义一个函数 F,规定 F(1) = 苹果,F(2) = 苹果,F(10) = 猴子。这就是一个完全有效的函数,它展示了定义函数的自由性。
- 医学测试示例:设集合 X 为某项疾病研究中的所有参与者,集合 Y = {+, -} 代表检测结果的“阳性”和“阴性”。那么,“检测”函数 test: X → Y 就表示对每个人进行医学测试,test(某人) = + 表示该人检测结果为阳性。
- 商业利润示例:设集合 Y 代表所有年份(如2010, 2011, 2012...),集合为实数集 R(代表金额,可正可负)。那么,“利润”函数 profit: Y → R 就表示计算每年的利润,例如 profit(2011) = 1007 美元,profit(2012) = -10000 美元。





函数与监督学习 🤖





在现实生活中,我们通常无法预先知道一个函数对所有输入的完整输出结果。这正是机器学习,特别是监督学习领域的核心问题。



在监督学习中,我们通常会获得一些输入-输出对的示例。例如,我们可能知道某些年份的利润数据,或者某些人的检测结果。




我们的任务就是根据这些有限的示例,去“找出”或“拟合”出那个潜在的、完整的函数规则,从而进行趋势分析或模式识别。整个监督学习领域,从本质上讲,就是研究如何从给定的输入-输出示例中推断出函数关系。









本节课中我们一起学习了函数的本质:它是两个集合之间的一种映射规则。我们从抽象的集合视角定义了函数,并通过实例了解了其广泛的应用。最后,我们探讨了函数概念在机器学习等现代数据科学领域中的重要意义,即从部分数据中推断整体规律。
📈 课程 P17:函数在笛卡尔平面中的图像







在本节课中,我们将学习如何将抽象的“函数”概念,通过其图像在笛卡尔平面中直观地展现出来。我们将理解函数图像的定义,学习如何绘制简单函数的图像,并掌握一个判断平面曲线是否为函数图像的重要准则——垂直线检验法。




上一节我们介绍了函数作为一个从集合A到集合B的映射规则这一抽象概念。本节中,我们来看看当函数定义在实数集上时,如何用图像来直观地表示它。


从抽象到具体:实数集上的函数


假设我们有一个函数 F,其定义域和值域都是实数集 R。我们可以沿用之前的抽象图示:一条传送带将输入值 x 送入函数机器 F,经过处理后,输出值 F(x) 从另一端送出。

然而,由于实数集是无限的,我们无法像处理有限集合那样逐一列出所有输入输出对。因此,我们通常使用一个公式来定义这类函数。公式是一个规则,它明确告诉我们如何根据输入 x 计算出输出 F(x)。







以下是定义函数的公式示例:




- 示例1:线性函数
- 公式:
F(x) = 2x - 1 - 这个规则告诉我们,对于任何输入 x,输出是 2x - 1。
- 计算示例:
F(1) = 2*1 - 1 = 1F(0) = 2*0 - 1 = -1F(5.1) = 2*5.1 - 1 = 9.2
- 公式:





- 示例2:绝对值函数
- 公式:
G(x) = |x| - 这个函数通常用分段方式定义:
- 如果
x ≥ 0,则G(x) = x - 如果
x < 0,则G(x) = -x
- 如果
- 公式:




什么是函数的图像?📊



那么,函数的图像是什么呢?它就是我们熟悉的在坐标平面上绘制的曲线。例如,方程 y = 2x - 1 的图像是一条直线。





这条直线并不是函数本身,而是函数 F(其公式为 F(x) = 2x - 1)的图像。图像的价值在于,它能让我们一次性看到所有的输入输出配对。



例如,在图像上:
- 当
x = 0时,对应的点是(0, F(0)),即(0, -1)。 - 当
x = 5.1时,对应的点是(5.1, F(5.1)),即(5.1, 9.2)。






函数图像的正式定义





对于一个从 R 到 R 的函数 G,其图像是一个平面点集。正式定义如下:




graph(G) = { (x, y) ∈ R² | y = G(x) }



这个集合包含了所有满足“y坐标等于G在x处的函数值”的点 (x, y)。图像就是将这些点绘制在坐标系中所形成的图形。





如何绘制函数图像?✏️



如果你不知道一个函数的图像形状,绘制它的基本方法是:计算一系列输入输出值,在坐标系中标出这些点,然后观察规律并用平滑曲线连接它们。这个过程与监督式学习中通过采样点来推测函数模式的思想类似。
让我们以函数 H(x) = x² 为例,通过制作数值表来绘制其图像。





以下是部分输入输出值:




| x | H(x) = x² |
|---|---|
| -2 | 4 |
| -1 | 1 |
| 0 | 0 |
| 1 | 1 |
| 2 | 4 |
| 3 | 9 |


在坐标系中标出这些点:(-2,4), (-1,1), (0,0), (1,1), (2,4), (3,9)。观察这些点的分布,可以发现它们关于y轴对称,并形成一条U型曲线(抛物线)。用平滑曲线连接这些点,就得到了 y = x² 的图像。





在后续课程中,你将系统学习线性函数、二次函数、三次函数、指数函数等各类函数的图像特征。





垂直线检验法 ✅




并非平面上的每一条曲线都是某个函数的图像。判断一条曲线是否为函数图像,有一个简单而重要的准则——垂直线检验法。




请看下图中的三条曲线:红色、蓝色和黄色。


垂直线检验法的内容是:一条曲线是某个函数的图像,当且仅当任何一条垂直直线与该曲线至多相交于一个点。
让我们应用这个准则:
- 红色曲线:任何垂直线(如x=某值)都只与它相交于一点。因此,红色曲线可以是一个函数的图像(例如
y = x - 1)。 - 蓝色曲线:同样,任何垂直线也只与它相交于一点。因此,蓝色曲线也可以是一个函数的图像。
- 黄色曲线:存在一些垂直线(如图中所示),与它相交于两个点。这意味着对于同一个x值(输入),曲线给出了两个不同的y值(输出),这违反了函数的定义(一个输入只能对应一个输出)。因此,黄色曲线不可能是一个函数的图像。









垂直线检验法总结如下:
- 若任何垂直线与图像相交一次,则该图像代表一个函数。
- 若存在某条垂直线与图像相交多于一次,则该图像不代表一个函数。





本节课中,我们一起学习了如何将实数集上的函数可视化为其在笛卡尔平面中的图像。我们理解了图像是满足 y = F(x) 的所有点 (x, y) 的集合,掌握了通过列表描点法绘制未知函数图像的基本思路,并学会了使用垂直线检验法来判断一条平面曲线是否代表一个函数。图像是理解和分析函数性质极为强大的工具。
📈 课程18:函数的单调性:增函数与减函数



在本节课中,我们将学习函数的单调性,即增函数与减函数的概念。我们将通过直观的图像和精确的数学定义来理解这两种特殊的函数类别,并学习如何判断一个函数是严格递增、严格递减,还是两者都不是。




上一节我们介绍了函数的基本概念,本节中我们来看看函数的单调性,即函数值随输入值变化的趋势。


我们主要关注从实数集到实数集的函数。今天的核心是理解增函数和减函数。需要明确的是,大多数函数并不属于这两类,但识别哪些函数属于哪一类非常重要。



观察左侧的三个函数图像:
- F(x) 用红色表示。
- G(x) 用蓝色表示。
- H(x) 用黄色表示。
你会注意到,随着沿X轴向右移动,F(x)的图像始终在上升。G(x)的图像则始终在下降。而H(x)的图像先下降,后上升。




因此,我们说:
- F 是一个严格递增函数。
- G 是一个严格递减函数。
- H 两者都不是。




数学家们不满足于直观的定义,他们喜欢用符号来精确描述。让我们看看这些符号定义如何对应我们已有的直观理解。


设 F 是一个从实数集到实数集的函数。



我们说 F 是严格递增的,如果满足以下条件:
对于任意两个输入值 A 和 B,只要 A < B,就必然有 F(A) < F(B)。
换句话说,输入值的顺序关系在输出值上得到保持。
以红色函数F为例,取点A和B(A < B)。观察它们在图像上的对应点F(A)和F(B),可以看到F(A) < F(B)。无论A和B选在何处,这个关系都成立。因此,F是严格递增的。
我们说 F 是严格递减的,如果满足以下条件:
对于任意两个输入值 A 和 B,只要 A < B,就必然有 F(A) > F(B)。
输入值的顺序关系在输出值上发生了翻转。

以蓝色函数G为例,取点A和B(A < B)。观察G(A)和G(B),可以看到G(A) > G(B)。因此,G是严格递减的。


另一方面,黄色函数H两者都不是。你可以自己验证:如果在下降部分取A和B(A < B),会发现H(A) > H(B);如果在上升部分取A和B(A < B),会发现H(A) < H(B)。没有一致的关系。





我们已经看了一些图像例子,现在让我们通过公式来定义一些函数,并判断它们的单调性。



以下是三个函数:
- F(x) = 2^x
- G(x) = 3^{-x}
- H(x) = x^2



让我们判断哪些是严格递增、严格递减,或者两者都不是。



首先,绘制 F(x) = 2^x 的图像。这是一个指数函数,x在指数位置。



以下是部分函数值点:
- x = 0 时,F(0) = 2^0 = 1 → 点 (0, 1)
- x = 1 时,F(1) = 2^1 = 2 → 点 (1, 2)
- x = 2 时,F(2) = 2^2 = 4 → 点 (2, 4)
- x = -1 时,F(-1) = 2^{-1} = 1/2 → 点 (-1, 0.5)



连接这些点,图像从左下向右上快速增长。显然,F(x) = 2^x 是严格递增的。



其次,绘制 G(x) = 3^{-x} 的图像。



以下是部分函数值点:
- x = 0 时,G(0) = 3^0 = 1 → 点 (0, 1)
- x = 1 时,G(1) = 3^{-1} = 1/3 → 点 (1, 1/3)
- x = -1 时,G(-1) = 3^{1} = 3 → 点 (-1, 3)



图像从左上下滑向右下。因此,G(x) = 3^{-x} 是严格递减的。

最后,绘制 H(x) = x^2 的图像。这是一个抛物线,经过点 (1,1), (-1,1), (0,0) 等。
图像在x<0时下降,在x>0时上升。因此,H(x) = x^2 既不是严格递增,也不是严格递减。



这里有一个重要的观察:虽然H在整个定义域上不是单调的,但它在部分区间上是。
- H 在区间 [0, +∞) 上是严格递增的。
- H 在区间 (-∞, 0] 上是严格递减的。



理解了数学定义和例子后,我们来看看现实世界中的应用。



以下是两个现实场景的例子:



例子1:儿童身高随年龄的变化
- X轴:出生后的年数。
- Y轴:身高。
图像大致形状是:从出生开始快速增长,在青少年时期(约17岁)增速放缓并趋于稳定,成年后可能保持稳定或随年龄增长略有下降。所以,这个函数在很长一段时间内是递增的,然后变得平缓,最后可能略微下降。


例子2:汽车价值随购买年限的变化
- X轴:购买后的年数。
- Y轴:汽车价值。
图像大致形状是:从购买时的高价值开始,一旦驶离经销商便迅速贬值,随后贬值速度放缓,最终稳定在一个较低的价值附近。这主要是一个递减函数。




最后,我们介绍一个判断函数是否严格单调的简单视觉方法:水平线检验法。


观察红色(严格递增)和蓝色(非严格单调)的函数图像。

对于严格递增(或严格递减) 的函数,任何一条水平线(绿色)与函数图像有且仅有一个交点。这是因为函数值一旦达到某个水平,就永远不会再回到那个值(对于递增函数,它只会变得更大;对于递减函数,只会变得更小)。





对于非严格单调的函数(如蓝色抛物线),你可以找到一些水平线,它们与函数图像有两个交点。这意味着函数值在下降后又上升,回到了同一个输出值。


因此,水平线检验法可总结为:如果一个函数的图像与每一条水平线都恰好相交一次,那么这个函数是严格递增或严格递减的。




本节课中我们一起学习了:
- 严格递增函数的定义:若 A < B,则 F(A) < F(B)。
- 严格递减函数的定义:若 A < B,则 F(A) > F(B)。
- 通过函数图像和公式(如 2^x, 3^{-x}, x^2)来判断函数的单调性。
- 理解了函数可以在整个定义域上单调,也可以在特定区间上单调。
- 探讨了单调性在现实世界(如身高增长、资产折旧)中的应用。
- 学习了利用水平线检验法快速判断函数是否严格单调。


掌握函数的单调性有助于我们理解数据的变化趋势,是数据分析中的重要基础概念。
课程19:函数的复合与反函数 🔄



在本节课中,我们将学习函数的两个重要操作:函数的复合与反函数。我们将通过具体例子理解复合函数的含义,并了解其顺序的重要性。接着,我们将探讨一种特殊的复合关系——反函数,理解其几何意义,并明确并非所有函数都存在反函数。



函数的复合



上一节我们介绍了函数的基本概念。本节中,我们来看看如何将两个函数“组合”起来,这被称为函数的复合。

假设我们有两个函数:
- f(x) = x²
- g(x) = x + 5
函数可以看作是将输入映射到输出的机器。复合函数 g ∘ f(读作“g 复合 f”)的定义是:先将输入 x 送入函数 f,得到输出 f(x),再将这个输出作为输入送入函数 g。
用公式表示就是:
(g ∘ f)(x) = g(f(x))


让我们计算一个具体例子。对于任意输入 x:
- 首先计算
f(x) = x²。 - 然后将
x²作为g的输入,计算g(x²) = x² + 5。



因此,(g ∘ f)(x) = x² + 5。
例如,计算 (g ∘ f)(2):
(g ∘ f)(2) = g(f(2)) = g(2²) = g(4) = 4 + 5 = 9


以下是关于复合顺序的重要警告:



复合函数的顺序至关重要,改变顺序通常不会得到相同的结果。让我们尝试计算 (f ∘ g)(x),即先执行 g,再执行 f:
(f ∘ g)(x) = f(g(x)) = f(x + 5) = (x + 5)²
显然,(x + 5)² 并不等于 x² + 5。因此,我们不能随意交换复合函数的顺序。



反函数




现在,我们来看一种特殊的复合关系。当两个函数复合后能够“抵消”彼此的作用,恢复到原始输入时,我们称它们互为反函数。


让我们通过一个例子来理解。假设有两个函数:
- f(x) = 2x
- g(x) = (1/2)x


计算复合函数 (g ∘ f)(x):
(g ∘ f)(x) = g(f(x)) = g(2x) = (1/2) * (2x) = x




我们发现,对于任意输入 x,g(f(x)) 的结果都变回了 x。函数 g 完全“撤销”了函数 f 的作用(乘以2)。在这种情况下,我们称 g 是 f 的反函数,记作:
g = f⁻¹



注意:这里的 f⁻¹ 表示反函数,不是 1/f(x),这是一种容易混淆但通用的记法。



反函数的几何意义



反函数在图像上有一个优美的几何关系:一个函数与其反函数的图像关于直线 y = x 对称。


以 f(x) = 2x(绿色直线)和其反函数 f⁻¹(x) = (1/2)x(红色直线)为例。在坐标系中,绿色直线经过反射变换(即交换 x 和 y 坐标的角色)后,恰好得到红色直线,而这条反射的对称轴就是直线 y = x(蓝色虚线)。




从图像上理解反函数:要找到使得 f(x) = 4 的 x 值,我们可以在 y=4 处画一条水平线,它与 f(x) 的图像相交,交点的横坐标 x=2 就是答案。这个“由输出找输入”的过程,正是反函数所做的事情。


一个重要警告:并非所有函数都有反函数


上述关于反函数的几何图像引出了一个关键限制:并非每一个函数都有反函数。




让我们以函数 f(x) = x² 为例。它的图像是一条开口向上的抛物线。




假设我们想知道,哪个 x 值经过函数 f 后得到了输出 4,即解方程 f(x) = 4 或 x² = 4。在图像上,我们在 y=4 处画一条水平线,它会与抛物线相交于两个点,对应的 x 值分别是 2 和 -2。




这就产生了问题:反函数作为一个“机器”,必须为每一个输入(这里是 4)给出唯一的输出。但这里我们得到了两个可能的答案(2 和 -2),无法唯一确定。因此,函数 f(x) = x² 没有定义在全体实数上的反函数。


这个例子关联到我们之前学过的水平线检验:



如果一个函数的图像无法通过水平线检验(即存在一条水平线与图像相交于多于一个点),那么这个函数就没有反函数。


直观上说,只有那些在整个定义域上严格单调递增或严格单调递减的函数(其图像与任何水平线至多有一个交点),才拥有反函数。







本节课总结



在本节课中,我们一起学习了:
- 函数的复合:将两个函数按顺序连接,
(g ∘ f)(x) = g(f(x))。复合的顺序不可随意交换。 - 反函数:如果
g(f(x)) = x,则g是f的反函数,记作f⁻¹。在图像上,函数与其反函数关于直线y = x对称。 - 反函数的存在性:并非所有函数都有反函数。一个函数拥有反函数的必要条件是它的图像能通过水平线检验,即函数是严格单调的。
📐 课程 P20:切线——曲线在某点处的斜率




在本节课中,我们将学习微积分中的一个核心概念:切线。我们将探讨如何描述曲线在特定点处的瞬时变化率,并引入导数的概念。通过几何直观和代数公式,我们将理解切线斜率如何反映函数在该点的变化速度。






概述:瞬时变化率与切线


假设我们有一个函数 y = f(x) 的图像,用绿色曲线表示。



现在,我们选取一个特定的点 x = a。我们想问的问题是:函数 f(x) 在 x = a 这一点变化得有多快?这是一个我们习以为常但实际上非常微妙的问题。就像说“此刻我在路上的速度是每小时55英里”,这并不意味着下一小时我会移动55英里,而是描述此刻的瞬时速度。


这个瞬时变化率在微积分中是一个关键概念。其几何图像是:在 x = a 处,我们可以画一条与函数图像相切的红色直线,这条线被称为切线。





从直线斜率到切线斜率



在之前的课程中,我们学习了如何计算直线的斜率。切线也是一条直线,它的斜率恰好等于函数在该点的瞬时变化率。


即使函数图像本身是一条曲线,但在每一个点上,我们都可以画出一条与之相切的直线。这条切线的斜率,我们称之为函数在该点的导数,记作 f'(a)。


那么,如何计算 f'(a) 呢?计算直线斜率需要两个点,但我们目前只有一个点 (a, f(a))。这就是微积分变得“棘手”的地方,我们需要引入极限的概念。核心公式如下:


f'(a) = lim (h -> 0) [ (f(a+h) - f(a)) / h ]
在接下来的内容中,我们将逐步拆解这个公式的含义。


一个简单的例子:直线函数



为了理解这个概念,让我们从一个非常简单的例子开始。


假设我们有函数 y = 3x。我们可以将其想象为一个(不现实的)商业模型:x 代表商品售价,y 代表总收入。这个模型说,价格越高,收入无限增长,这显然不现实,但便于我们理解。


假设当前定价为 $a。我们想知道:如果我将价格提高一点,收入会增加多少?对于直线函数,这个问题很简单。



我们在图像上找到点 (a, 3a)。如果将价格提高 $1,新点坐标为 (a+1, 3(a+1))。计算这两点间线段的斜率:
斜率 = (y值变化量) / (x值变化量) = [3(a+1) - 3a] / [(a+1) - a] = 3 / 1 = 3



关键在于,提高 $1 会使收入增加 $3。如果提高 $2,收入会增加 $6。这正是直线斜率的含义:斜率代表了 x 每变化一个单位时,y 的恒定变化量。






回到现实:曲线函数


现在,让我们看一个更现实的模型。下图中的绿色曲线代表一个更符合实际的价格-收入关系函数 y = f(x)。



通常,提高价格会使收入增加(因为每件商品赚得更多)。但价格过高时,顾客会减少购买,导致总收入下降。这就是图中曲线先上升后下降的原因。

现在再次提出关键问题:在定价为 $a 时,如果我将价格提高一点,收入会增加还是减少?变化率是多少?


与直线不同,这个问题的答案取决于我们所在的价格点。例如:
- 在点
a处,提高价格可能会使收入显著增加。 - 在点
b处,提高价格带来的收入增幅较小。 - 在点
c处,提高价格反而会使收入下降。




因此,曲线上不同点处的切线斜率是不同的。在 x = a 处,切线的斜率(即导数 f'(a))就精确地回答了“收入随价格变化的瞬时速率”这个问题。







如何计算切线的斜率?

回顾一下,计算直线斜率需要两个点。对于切线,我们只有一个点 (a, f(a))。


为了得到第二个点,我们引入一个很小的变化量 h。在曲线上取另一个点,其坐标为 (a+h, f(a+h))。



现在,我们可以计算通过点 (a, f(a)) 和 (a+h, f(a+h)) 的割线的斜率:



割线斜率 = [f(a+h) - f(a)] / [(a+h) - a] = [f(a+h) - f(a)] / h



这个公式给出了两点间的平均变化率。



微积分的核心思想来了:如果我们让第二个点无限接近第一个点,即让 h 无限趋近于0,那么这条割线就会旋转并无限逼近我们想要的切线。


因此,切线的斜率(即导数)就是这个割线斜率在 h 趋近于0时的极限:



f'(a) = lim (h -> 0) [ (f(a+h) - f(a)) / h ]



总结




本节课我们一起学习了:
- 切线的几何意义:曲线在某一点处的切线,是唯一在该点与曲线“刚好接触”的直线。
- 导数的概念:函数在某一点的导数
f'(a),就是该点处切线的斜率,它代表了函数在该点的瞬时变化率。 - 从割线到切线:通过取曲线上两个点计算割线斜率,然后让两点无限靠近(
h -> 0),其极限即为切线斜率。 - 核心计算公式:导数定义为
f'(a) = lim (h -> 0) [ (f(a+h) - f(a)) / h ]。



在下一课中,我们将使用一个具体的函数例子,实际演示如何运用这个公式来计算导数。
📐 课程21:导函数的概念



在本节课中,我们将深入学习导数的定义公式,并通过一个具体的函数例子,一步步计算其导数,从而理解导函数的概念。我们将使用函数 f(x) = x² 作为贯穿始终的例子。





回顾与引入

上一节我们介绍了计算切线斜率的公式:

f'(a) = lim (h→0) [f(a+h) - f(a)] / h



这个公式可能看起来有些抽象。在本节中,我们将通过一个具体的函数 f(x) = x²,来详细拆解并计算这个公式,从而理解导数的实际含义。
下图是函数 y = x² 的图像。我们将关注图像上的三个点(A, B, C),并思考在这些点处切线的斜率。











斜率符号的直观判断


在开始计算之前,我们可以先直观判断一下这些点处切线斜率的正负。
- 在点 A 处,切线是向上倾斜的,因此斜率 f'(a) 应该是一个正数。
- 在点 B 处,切线比点 A 处更陡峭,因此斜率 f'(b) 不仅为正,而且应该大于 f'(a)。
- 在点 C 处,切线是向下倾斜的,因此斜率 f'(c) 应该是一个负数。




进行这样的直观检查,有助于我们在后续计算中验证答案的合理性。




应用公式计算 f'(a)



现在,我们严格使用导数定义公式来计算函数 f(x) = x² 在任意点 x = a 处的导数。



根据公式:
f'(a) = lim (h→0) [f(a+h) - f(a)] / h




由于 f(x) = x²,所以:
- f(a) = a²
- f(a+h) = (a+h)²


将以上代入公式:


f'(a) = lim (h→0) [(a+h)² - a²] / h

接下来是代数运算步骤:




- 展开 (a+h)²:
f'(a) = lim (h→0) [a² + 2ah + h² - a²] / h - 消去 a² 和 -a²:
f'(a) = lim (h→0) [2ah + h²] / h - 分子提取公因子 h:
f'(a) = lim (h→0) [h(2a + h)] / h - 约去分子和分母的 h(注意 h ≠ 0):
f'(a) = lim (h→0) (2a + h) - 最后,求极限。当 h 无限趋近于 0 时,(2a + h) 就无限趋近于 2a。



因此,我们得出结论:
f'(a) = 2a






验证结果与理解导函数



这个结果 f'(a) = 2a 非常有趣。让我们验证它是否符合之前的直观判断:

- 如果 a > 0,则 2a > 0,这与点 A 和点 B 处斜率为正相符。
- 如果 a < b,则 2a < 2b,这与点 B 处斜率大于点 A 处斜率相符。
- 如果 c < 0,则 2c < 0,这与点 C 处斜率为负相符。
- 当 a = 0 时,f'(0) = 0,这意味着在原点 (0,0) 处的切线是水平的,这与图像完全吻合。

更重要的是,由于 a 可以是任意值,我们实际上得到了一个新的函数。这个函数以 x 为输入,输出的是原函数 f(x) = x² 在 x 点处的切线斜率。

我们称这个新函数为 导函数,记作 f'(x)。对于 f(x) = x²,其导函数为:
f'(x) = 2x



下图展示了 f(x) = x²(绿色曲线)和其导函数 f'(x) = 2x(蓝色直线)的关系。









如何理解导函数的图像


以下是理解导函数图像 y = 2x 的关键点:


- 对于任意 x 值,蓝色直线上对应的 y 值(即 2x),就是绿色曲线在 x 点处切线的斜率。
- 当 x 为正且增大时,2x 也增大,意味着曲线右侧的切线越来越陡。
- 当 x 为负且减小时,2x 也减小(变得更负),意味着曲线左侧的切线向下倾斜得越来越厉害。
- 在 x=0 处,2x=0,对应曲线在原点的水平切线。






总结


本节课我们一起学习了导函数的核心概念。



- 我们从一个具体的函数 f(x) = x² 出发,运用导数定义公式,一步步计算出了它在任意点 a 的导数:f'(a) = 2a。
- 通过将 a 推广为变量 x,我们得到了导函数 f'(x) = 2x。导函数本身也是一个函数,其功能是:输入 x,输出原函数在 x 点处的切线斜率。
- 我们通过图像直观地验证了导函数值的含义,并理解了导函数图像(一条直线)如何反映了原函数图像(一条抛物线)上各点斜率的变化规律:从左侧的负斜率,平滑过渡到原点斜率为0,再到右侧的正斜率。


理解导函数是理解函数如何变化的关键一步。
课程22:整数指数的运用 📊

在本节课中,我们将要学习整数指数的基本概念和运用。指数是数学中一种强大的工具,用于简化重复乘法的表达方式。我们将从正整数指数开始,逐步探讨零指数和负整数指数,并了解它们在科学记数法中的应用。

正整数指数 🔢
上一节我们介绍了指数的基本概念,本节中我们来看看正整数指数。正整数指数是用于计数的数字,例如1、2、3等。
当我们有一个数字,比如9,它等于3乘以3。3是9的一个因数,并且它重复出现了两次。在数字27中,因数3出现了三次。在数字81中,因数3出现了四次。
我们可以使用正整数指数,即写在因数右上角的数字,来计算同一个因数在一个数字中重复出现的次数。

以下是几个例子:


- 9 = 3 × 3 = 3²,其中2是指数。
- 27 = 3 × 3 × 3 = 3³,其中3是指数。
- 81 = 3 × 3 × 3 × 3 = 3⁴,其中4是指数。

我们将其读作“三的四次方”或简称为“三的四次方”。

让我们看另一个例子。数字248832等于12 × 12 × 12 × 12 × 12。能够将其写作12⁵(其中5是指数)带来了极大的便利,这个数字就是12的五次方。
需要注意的是,由于历史原因,数字的二次方和三次方有特殊的名称。

- 4² 可以读作“四的二次方”,也可以读作“四的平方”。
- 4³ 可以读作“四的三次方”,也可以读作“四的立方”。

零指数 0️⃣


上一节我们讨论了正整数指数,本节中我们来看看零指数。这些问题很简单。

为什么这么说?因为根据指数的定义,任何非零的数字,其零次方都等于1。
以下是几个例子:
- 3⁰ = 1
- 2⁰ = 1
- (2π)⁰ = 1
- (1/x³)⁰ = 1 (前提是 x ≠ 0)



即使对于数学家来说,0⁰ 也有些过于奇怪,因此它通常被定义为未定义。


负整数指数 🔄


上一节我们介绍了零指数,本节中我们来看看负整数指数。


2的负一次方写作 2⁻¹,它等于 1 / 2¹,也就是 1/2。

2的负二次方写作 2⁻²,它等于 1 / 2²,也就是 1/4。

2的负三次方写作 2⁻³,它等于 1 / 2³,也就是 1/8。


你应该能看出这里的规律:将一个数字进行负指数运算,等同于用1除以该数字的正指数形式。

现在,让我们考虑除以一个负指数的情况。


根据完全相同的逻辑:

- 1 / 2⁻¹ 简单地等于 2¹ 或 2。
- 1 / 2⁻² 简单地等于 2² 或 4。
- 1 / 2⁻³ 简单地等于 2³ 或 8。


我们可以用字母来代表几乎任何数字,从而表达负指数的一般规则。

以下是负指数的一般规则:

- x⁻ⁿ = 1 / xⁿ
- 1 / x⁻ⁿ = xⁿ




科学记数法 🧪
指数最常见的用途之一是我们所说的科学记数法。科学记数法是一种书写数字的方式,可以避免写出大量零。
我们的做法是:取数字中具有有效数字的部分,将小数点放在第一个有效数字之后,然后乘以10的适当次方。

例如,太阳的质量是 5,972,000,000,000,000,000,000,000 千克。在科学记数法中,我们取有效数字部分 5, 9, 7, 2,并将小数点放在第一个数字之后,得到 5.972。然后我们乘以 10²⁴。10²⁴ 意味着你需要将小数点向右移动24位。
让我们再试一个。如果我们有一个小于1的数字,我们需要将小数点向左移动,并且指数为负。
例如,电子的质量是 0.0000000000000000000000000000009109 千克。在科学记数法中,我们写作 9.109 × 10⁻³¹,因为我们需要将小数点向左移动31位。
需要记住的关键点是:我们只需要保留有效的非零数字,并且小数点左边始终有一位数字。
总结 📝
本节课中我们一起学习了整数指数的运用。我们从正整数指数开始,了解了它如何简化重复乘法的表达。接着,我们探讨了零指数(任何非零数的零次方等于1)和负整数指数(负指数表示倒数)。最后,我们学习了如何利用指数,通过科学记数法来简洁地表示非常大或非常小的数字。掌握这些基本概念是理解更复杂数学运算的重要基础。
课程23:指数代数简化规则 📚

在本节课中,我们将要学习指数运算的五个核心简化规则。掌握这些规则,你将能够理解和解决几乎所有基于整数指数的代数问题。我们会先简要介绍所有五个规则,然后通过一系列例子来演示如何应用这些规则简化指数表达式。
概述


为了保持记号的清晰和进行代数运算,指数运算有五条简化规则。如果你能记住并练习这五条规则,你将能够理解和解决几乎所有基于整数指数的代数问题。


请注意,在这些规则中,我们使用“幂”这个词特指一个指数的值。




规则一:乘法规则 ✖️



当对相同底数、不同指数的因子进行乘积运算时,我们将指数相加。


其公式如下:
x^n * x^m = x^(n+m)





规则二:幂的幂规则 🔼


当一个已经包含指数的数再被提升到一个指数时,我们取这两个指数的乘积作为新的幂。


其公式如下:
(x^n)^m = x^(n*m)



规则三:积的幂规则 📦



当我们有两个不同的因子,并且它们被提升到同一个指数时,我们将指数分配给乘积中的每一个元素。


其公式如下:
(x * y)^n = x^n * y^n


为了更清晰地说明,让我们看一个例子:(2 * 3)^3。这等于 (2*3) * (2*3) * (2*3)。然后我们分别收集2和3,得到 2^3 * 3^3。
规则四:分数的幂规则 ➗


这种情况是,我们有一个分子和一个分母,并将整个分数提升到一个指数。同样,我们将指数分配给分子和分母。


其公式如下:
(x / y)^n = x^n / y^n



当将两个整数的比值提升到一个幂时,将指数分配给每个数。


规则五:除法与负指数规则 ➖



这条规则运作如下:如果我们有 x^n / x^m,这等同于 x^(n-m)。



你可能会注意到,这实际上结合了我们已有的规则。因为我们真正在做的是:x^n * x^(-m),这等同于 x^(n-m)。



应用示例


上一节我们介绍了五条核心规则,本节中我们来看看如何应用这些规则解决具体问题。以下是几个简化指数表达式的例子,每个例子都说明了应使用哪条规则。


示例 1: 7^3 * 7^7
- 应用规则: 乘法规则。
- 解答: 因为我们有共同的底数7,所以结果是
7^(3+7),即7^10。



示例 2: (4^3)^5
- 应用规则: 幂的幂规则。
- 解答: 我们得到
4^(3*5),即4^15。



示例 3: (8 * 9)^7
- 应用规则: 积的幂规则。
- 解答: 我们分配指数,得到
8^7 * 9^7。这是科学计数法可能有用的情况,因为结果是一个很大的数。


示例 4: (2/7)^3
- 应用规则: 分数的幂规则。
- 解答: 我们得到
2^3 / 7^3,即8/343。


示例 5: 10^5 / 10^3
- 应用规则: 除法与负指数规则。
- 解答: 这等于
10^5 * 10^(-3),即10^(5-3),也就是10^2,等于100。





进阶示例



现在让我们尝试一些稍复杂的例子。



示例 6: (x^3 * y^4 * z^5) / (x^3 * y^5 * z^2)
处理这类问题的方法是分离每个因子。
- 我们有
x^3 / x^3。 - 我们有
y^4 / y^5。 - 我们有
z^5 / z^2。 - 应用规则: 使用除法与负指数规则。
- 解答: 这等于
x^(3-3) * y^(4-5) * z^(5-2),即x^0 * y^(-1) * z^3。由于x^0 = 1,所以结果是y^(-1) * z^3,或者写成z^3 / y。

示例 7: [(x^2 * y^2) / (x^(-3) * y^2)]^(-1)
我们首先分离每个因子,最后处理 -1 次方。
- 应用积的幂规则,分子是
x^2 * y^2,分母是x^(-3) * y^2。 - 解答: 这等于
[x^(2 - (-3)) * y^(2-2)]^(-1),即[x^5 * y^0]^(-1)。由于y^0 = 1,所以是(x^5)^(-1),最终等于x^(-5)或1 / x^5。


现在你应该尝试在练习题中自己做一些。




分数指数 📊



我们想简要地再谈一个主题:如何处理指数本身是分数的情况。


答案是,你将其视为两个独立的操作:上方的数字是标准指数,下方的数字是根号。


在给出的例子中,我们有 8^(2/3)。这意味着:
8的平方,再开立方根;或者8的立方根,再平方。
顺序无关紧要。


让我们看看结果是什么:
8的立方根是2,因为2 * 2 * 2 = 2^3 = 8。- 然后将其平方:
2^2 = 4。
同样,也可以先计算8^2 = 64,再取立方根,64的立方根也是4。



另一个例子: 125^(4/3)
- 取
125的立方根,等于5。 - 将其提升到四次方:
5 * 5 * 5 * 5 = 625。


因此,只要将有理数(分数)指数的每一部分视为独立操作,你就能轻松解决这类问题。





总结

本节课中我们一起学习了指数运算的五条核心简化规则:
- 乘法规则:同底数相乘,指数相加。
- 幂的幂规则:幂次再乘方,指数相乘。
- 积的幂规则:乘积的幂等于各因子幂的乘积。
- 分数的幂规则:分数的幂等于分子分母分别取幂。
- 除法与负指数规则:同底数相除,指数相减。
我们还了解了如何处理分数指数,即将其分解为“取根”和“乘方”两个步骤。我建议你稍加练习,这些规则就会变得自然而然,通过解决问题和练习,它们将不再显得困难。
课程24:对数与指数的关联性 📊

在本节课中,我们将要学习对数与指数之间的紧密联系。理解指数是理解对数的基础,因为这两个概念本质上是同一个关系的两种不同表达方式。我们将学习对数的两种形式、基本规则,并通过实例演示如何运用这些规则来简化和解决包含对数的问题。
概述



一旦你对指数有了基本的理解,理解对数就不会有太大的跨越。原因在于这两个概念紧密相关。对数本质上是在回答一个问题:“底数要自乘多少次方才能得到这个数?”



对数的两种形式



上一节我们介绍了对数的基本概念,本节中我们来看看如何用两种不同的数学形式来表达它。




对数的公式可以有两种不同的形式。

指数形式:一个底数 B 自乘 X 次方,等于结果 N。
B^X = N



对数形式:以 B 为底,N 的对数等于 X。
log_B(N) = X


这两种形式捕捉的是同一个核心思想,即三个数字之间的关系:一个底数、一个指数和一个结果。




例如:
- 指数形式:
2^3 = 8 - 对数形式:
log_2(8) = 3

同理,2^4 = 16 等价于 log_2(16) = 4。

对数的基本规则



正如指数有基本规则一样,对数也有几个基本规则,用于简化和解决包含对数的问题。



以下是三个主要的对数运算法则:

1. 乘积法则
对数的乘积法则表述如下:
log_B(X * Y) = log_B(X) + log_B(Y)
2. 商法则
对数的商法则表述如下:
log_B(X / Y) = log_B(X) - log_B(Y)


3. 幂与根法则
当一个数 X 被自乘 N 次方时,其对数规则如下:
log_B(X^N) = N * log_B(X)




这个规则无论 N 是正数、负数、整数还是分数都适用。


规则应用示例




现在,我们通过一些具体的例子来看看如何应用这些规则。



示例 1:乘积法则
log_B(35) = log_B(7) + log_B(5),因为 7 * 5 = 35。



示例 2:商法则
log_2(16 / 4) = log_2(16) - log_2(4)
我们知道 2^4 = 16,所以 log_2(16) = 4。
我们知道 2^2 = 4,所以 log_2(4) = 2。
因此,log_2(16 / 4) = 4 - 2 = 2。


示例 3:幂与根法则
log_10(7^5) = 5 * log_10(7)
log_B(1000^(1/3)) = (1/3) * log_B(1000) (这是1000的立方根)
log_B(X^(-1)) = -1 * log_B(X)


组合运用规则




在实际问题中,我们经常需要组合运用多个对数规则。让我们尝试一个例子。




假设我们需要简化 log_B(X^2 * Y^(-3))。
- 首先应用乘积法则:
log_B(X^2) + log_B(Y^(-3)) - 然后对每一项应用幂法则:
2 * log_B(X) + (-3) * log_B(Y) - 最终结果为:
2 * log_B(X) - 3 * log_B(Y)

再比如简化 log_B(X^2 / Y^(-1/2))。
- 首先应用商法则:
log_B(X^2) - log_B(Y^(-1/2)) - 然后对每一项应用幂法则:
2 * log_B(X) - ((-1/2) * log_B(Y)) - 简化符号:
2 * log_B(X) + (1/2) * log_B(Y)

一个实用技巧与方程求解



在处理对数时,有一个实用的变换技巧:
log(A) + log(B) = log(A * B)
同时,它也等于 log(A) - log(1/B)。在某些问题中,这种变换非常有用。




对数一个更高级的用法是求解对数方程。我们可以将方程两边视为同一个底数的指数。



示例: 求解方程 log_2( (39X) / (X-5) ) = 4。
- 将方程两边都作为2的指数:
2^{ log_2( (39X)/(X-5) ) } = 2^4 - 因为
2^{log_2(A)} = A,所以左边简化为:(39X) / (X-5) = 16 - 解这个方程:
- 两边乘以
(X-5):39X = 16(X - 5) - 展开:
39X = 16X - 80 - 移项:
39X - 16X = -80 - 合并:
23X = -80 - 解得:
X = -80/23
- 两边乘以




总结




本节课中我们一起学习了对数与指数的核心关联。我们认识到对数是“求幂运算的逆运算”,并掌握了它的指数形式 B^X = N 和对数形式 log_B(N) = X。我们重点学习了三个基本运算法则:乘积法则、商法则和幂与根法则,并通过多个例子练习了如何应用及组合这些法则来简化表达式和求解方程。理解这些规则是掌握对数运算的关键。
课程25:对数换底公式 📐

在本节课中,我们将要学习对数运算中一个非常实用的工具——换底公式。这个公式允许我们在不同的对数底数之间进行转换,例如将底数为2的对数转换为底数为10的对数,这在数据科学中处理不同来源的数据时尤其有用。
上一节我们介绍了对数的基本概念,本节中我们来看看如何在不同底数的对数之间进行转换。

在数据科学中,我们通常使用以10为底的对数,有时也使用以2为底的对数。此外,我们还会用到一种称为自然对数(记作 Ln)的对数,它以常数 e 为底,我们将在后续课程中讨论。但无论如何,无论底数是什么,我们都可以使用一个简单的公式将对数从一个底数转换到另一个底数。
一个重要的核心概念是:使用不同的底数计算同一个数的对数,得到的结果是不同的。

例如:
- 计算以2为底12的对数,我们得到:
log₂(12) ≈ 3.585 - 计算以10为底12的对数,我们得到:
log₁₀(12) ≈ 1.079

你可以验证:2^3.585 ≈ 12 且 10^1.079 ≈ 12。虽然结果不同,但它们都正确地描述了12与各自底数的指数关系。同样地:
log₂(7) ≈ 2.807(因为2^2.807 ≈ 7)log₁₀(7) ≈ 0.8451(因为10^0.8451 ≈ 7)



换底公式 🧮
现在,我们来学习通用的换底公式。

假设我们有一个以 x 为底的对数 logₓ(b),我们想将它转换为以 a 为底的新对数。

换底公式如下:

logₐ(b) = logₓ(b) / logₓ(a)


这个公式的含义是:要得到以新底数 a 为底、真数为 b 的对数值,可以用以旧底数 x 为底、真数为 b 的对数值,除以以旧底数 x 为底、真数为新底数 a 的对数值。


公式应用示例 💡

让我们通过具体例子来理解这个公式是如何工作的。

示例1:将 log₁₀(12) 转换为以2为底的对数。

根据公式,我们想求 log₂(12)。这里旧底数 x = 10,新底数 a = 2,真数 b = 12。

所以:
log₂(12) = log₁₀(12) / log₁₀(2)

我们已知:
log₁₀(12) ≈ 1.079log₁₀(2) ≈ 0.30103


计算:
log₂(12) ≈ 1.079 / 0.30103 ≈ 3.585
结果与我们直接计算 log₂(12) 得到的一致。


示例2:将 log₂(7) 转换为以10为底的对数。


这里,我们想求 log₁₀(7)。旧底数 x = 2,新底数 a = 10,真数 b = 7。

所以:
log₁₀(7) = log₂(7) / log₂(10)


我们已知:
log₂(7) ≈ 2.807log₂(10) ≈ 3.3219

计算:
log₁₀(7) ≈ 2.807 / 3.3219 ≈ 0.845

结果也与我们已知的 log₁₀(7) 值相符。


核心要点总结 ✨
本节课中我们一起学习了对数运算的换底公式。
以下是本课的核心要点:
- 公式:要将对数
logₓ(b)转换为以a为底,使用公式logₐ(b) = logₓ(b) / logₓ(a)。 - 操作:用原对数值除以以原底数为底、新底数为真数的对数值,即可得到新底数下的对数值。
- 练习:掌握这个公式的最佳方式就是进行练习。通过计算一些例子,你会对这个转换过程变得更加熟悉和直观。
记住这个强大的工具,它可以帮助你在不同场景下灵活地运用对数进行计算和分析。
课程26:连续过程的增长率 📈


在本节课中,我们将要学习指数增长率的两种不同形式:离散增长率和连续增长率。我们将重点探讨一个特殊的常数——欧拉数(e),并学习如何使用自然对数(ln)来计算连续增长率。通过简单的例子,你将理解这些概念在现实世界中的应用。





离散指数增长率



上一节我们介绍了指数增长率的概念,本节中我们来看看它的第一种形式:离散指数增长率。

离散增长率非常直观。假设你有1美元,它以特定的年利率 R 增长,并且你考虑一段特定的时间间隔 T。这里的利率 R 表示你的钱在离散的时间间隔 T 内会增长多少。

其计算公式为:
最终金额 = 初始金额 × (1 + R)^T


例如,如果年利率 R 为100%(即1),时间 T 为1年,那么一年后你将拥有2美元。这就是100%的离散指数增长率。两年后是4美元,三年后是8美元,依此类推。




连续指数增长率与欧拉数 e



然而,我们在这里特别感兴趣的是另一种形式:连续指数增长率,以及一个被称为欧拉常数或 e 的特殊常数。


现在,我将向你展示如何直观地理解这个特殊数字 e。


假设我们仍然有100%的年利率。一个聪明人提出:“既然你愿意支付我100%的年利息,那你是否也愿意支付我50%的半年利息呢?”银行可能会觉得这很公平。
这意味着,半年后,我的本金会乘以因子 1.5。然后,在接下来的半年里,我不仅对本金,还对上半年的利息收取利息。所以,一年后的总增长因子是 1.5 × 1.5 = 2.25。
这个聪明人可能会继续问:“如果你同意一年付两次利息,那一年付四次(即每季度一次)怎么样?”这样,每季度的利率是25%,增长因子是 1.25,重复四次:1.25^4 ≈ 2.441。


一个显而易见的问题是:随着时间间隔变得越来越小(比如每月、每天、每小时),这个最终的数字会无限增大吗?答案可能令人惊讶:不会。它会增加,但最终会趋于一个稳定的极限值。



以下是不同复利频率下的计算结果:



- 每月复利:因子为
1 + 1/12,重复12次。(1 + 1/12)^12 ≈ 2.613 - 每周复利:因子为
1 + 1/52,重复52次。(1 + 1/52)^52 ≈ 2.693 - 每日复利:因子为
1 + 1/365,重复365次。(1 + 1/365)^365 ≈ 2.7146 - 每小时复利:因子为
1 + 1/8760,重复8760次。结果约为2.71813 - 每分钟复利:因子为
1 + 1/525600,重复525600次。结果约为2.71828 - 每秒复利:因子为
1 + 1/31536000,重复31536000次。结果约为2.71828




可以看到,当复利频率达到每分钟或每秒时,这个数字在小数点后五位已经稳定下来。这个极限值 2.71828... 就是著名的欧拉常数,记作 e。


连续增长的应用示例

理解了 e 之后,我们来看一个连续增长的应用问题。


假设有一头小象,初始重量为200公斤,它以每年5%的连续复利速率增长。我们想知道三年后它的重量是多少。


计算公式为:
最终重量 = 初始重量 × e^(速率 × 时间)



代入数据:
最终重量 = 200 × e^(0.05 × 3) = 200 × e^0.15


计算 e^0.15 的值(可以使用计算器),然后乘以200,得到结果约为 232.37 公斤。





自然对数 ln



除了 e 这个非常有价值的概念,我们还有以 e 为底的对数,写作 ln(x),称为自然对数。


为什么叫“自然”对数?因为它正是我们用来计算自然界中连续增长率的对数。


让我们通过一个例子来理解它的用途。假设有一群兔子,在食物无限的情况下,其总质量以每年200%的连续复利速率增长。初始兔群总质量为10公斤。我们想知道,需要多少年,兔子的总质量才能达到地球的质量(约 5.972 × 10^24 公斤)。


以下是解题步骤:


- 建立方程:
5.972 × 10^24 = 10 × e^(2 × T),其中2代表200%的增长率,T是所需年数。 - 两边同时除以10:
5.972 × 10^23 = e^(2T) - 为了解出
T,我们在等式两边取自然对数(ln):ln(5.972 × 10^23) = 2T - 因此,
T = ln(5.972 × 10^23) / 2

使用计算器或电脑计算 ln(5.972 × 10^23),然后除以2,你会发现结果约为 27.37 年。



这意味着,以每年200%的连续速率增长,这群兔子只需要大约27.37年,其总质量就会和地球一样重。





总结



本节课中我们一起学习了:
- 离散指数增长率:通过公式
(1 + R)^T计算在固定时间间隔内的增长。 - 连续指数增长率与欧拉数 e:当增长或复利在每一瞬间都发生时,其极限由常数 e (≈2.71828) 描述,计算公式为
初始值 × e^(速率 × 时间)。 - 自然对数 ln:以 e 为底的对数,是求解连续增长率相关方程(如计算达到某个规模所需时间)的关键工具。

掌握连续增长、e 和 ln 的概念,对于理解金融、生物种群增长、放射性衰变等众多领域的模型至关重要。
课程27:概率的定义与表示法 📊



在本节课中,我们将要学习概率的基本定义、核心表示法以及概率分布的概念。我们会从最基础的概念出发,解释概率如何量化我们对一个陈述的相信程度,并介绍如何用数学符号来表示和计算概率。

概率的定义

概率是对一个陈述(或事件)真实性或虚假性的相信程度。

当存在不确定性时,我们会给一个陈述赋予一个介于0和1之间的值。这个范围代表了不确定性的程度。

- 当我们确定一个陈述为真时,其概率为 1。
- 当我们确定一个陈述为假时,其概率为 0。




例如,假设你坐在一间没有窗户的办公室里,不知道外面是否在下雨。此时,你对“正在下雨”这个陈述的相信程度(概率)就会是0到1之间的某个值。当你了解到外面的真实天气状况后,这个概率就会更新为1(确定下雨)或0(确定没下雨)。

概率的表示法



我们使用特定的数学符号来表示概率。


- 我们写作
P(X),表示陈述X的概率,或由陈述X所代表的结果的概率。 - 我们使用波浪号
~来表示一个陈述的否定(非)。


例如,P(下雨) 表示“正在下雨”的概率,而 P(~下雨) 则表示“没有下雨”的概率。



概率分布

每当有一个陈述及其否定时,我们就构成了一个简单的二元概率分布。


换句话说,像“正在下雨”和“没有下雨”这样一对陈述,共同形成了一个概率分布。当我们掌握了关于情况的完整信息时,这两个陈述中必须有一个为真。


需要注意的是,即使在我们掌握完整信息之前,这两个陈述的概率之和也必须等于1。例如,如果你认为现在有75%(即3/4)的概率在下雨,那么你就必须认为有25%(即1/4)的概率没有下雨。

这个原则被称为排中律,它阐明了概率分布的基本规则:构成一个分布的所有结果的概率之和必须为1。



公式表示:
对于一个二元分布,有:P(X) + P(~X) = 1


概率分布的精确定义


那么,什么精确定义了一个概率分布呢?它是一个由两个或更多陈述组成的集合,这些陈述必须满足互斥且完备的条件。


以下是这两个条件的详细解释:


- 互斥:在给定完整信息的情况下,这些陈述中最多只有一个可以为真。例如,“正在下雨”和“没有下雨”显然不可能同时为真。
- 完备:在给定完整信息的情况下,这些陈述中至少有一个必须为真。例如,关于天气,要么下雨,要么不下雨,没有第三种“既下雨又不下雨”的状态。


因此,当你坐在室内不确定天气时,你可以为“下雨”和“不下雨”这两个陈述分配反映不确定性的概率值。但一旦信息完备,这两个陈述中恰好有一个必须为真。


更复杂的分布与无差别原则
通常,我们会遇到由多于两个陈述构成的概率分布,这些陈述同样是互斥且完备的。在许多情况下,我们可能面对大量结果,并且没有特别的理由认为某个结果比其他结果更可能出现。
例如,考虑一副标准的52张扑克牌。此时,可能的结果数量 n = 52。假设我们想知道从一副洗匀的牌中抽到黑桃A的概率。就我所知,黑桃A并没有什么特别之处,并且牌是洗匀的。根据无差别原则,我会分配给这个结果的概率是 1/52。
根据无差别原则,我们可以按以下方式计算许多概率:

一个特定事件(事件是单个结果的集合)的概率,定义为属于该事件的结果数量除以所有可能结果的总数。


公式表示:
P(事件) = (属于该事件的结果数) / (所有可能结果的总数)

让我们用两个例子来说明:

例子1:抽到一张Q
- 事件:抽到一张Q。
- 属于事件的结果:红桃Q、方块Q、黑桃Q、梅花Q。共4个结果。
- 所有可能结果总数:52张牌。
- 概率:
P(抽到Q) = 4 / 52 = 1/13




例子2:掷一个六面骰子得到偶数
- 事件:点数为偶数。
- 属于事件的结果:2, 4, 6。共3个结果。
- 所有可能结果总数:6个面。
- 概率:
P(点数为偶数) = 3 / 6 = 1/2

这个简单的概念使我们能够解决大量的概率问题。
总结
本节课中,我们一起学习了概率的核心概念。我们首先将概率定义为对陈述真实性的相信程度,其值介于0(假)和1(真)之间。接着,我们学习了概率的表示法 P(X) 和否定符号 ~。然后,我们探讨了概率分布,它由一组互斥且完备的陈述构成,且所有陈述的概率之和必须为1。最后,我们介绍了在无差别原则下,通过计算“有利结果数”与“所有可能结果数”的比值来求解概率的基本方法。这些是理解更复杂概率论主题的基石。
课程28:联合概率 🎲

在本节课中,我们将要学习联合概率的概念。联合概率描述的是两个独立事件同时发生的可能性。我们将探讨其定义、表示方法、与独立性的关系,并学习如何使用维恩图来直观理解。


联合概率的定义与表示


上一节我们介绍了概率的基本概念,本节中我们来看看如何描述两个事件同时发生的情况。

联合概率是指来自两个独立概率分布的两个事件同时为真的概率。我们关注的是事件A为真且事件B为真的概率。

其数学表示如下:
P(A, B)


这个符号读作“A和B的联合概率”,或者“A为真且B为真的概率”。



一个重要的点是,在联合概率中,事件的顺序无关紧要。P(A, B) 与 P(B, A) 是相等的。


以下是关于概率符号的说明:
- 我们使用大写字母(如A, B)来指代整个概率分布,即一组互斥且完备的陈述集合。
- 我们使用小写字母(如a1, b2)来指代分布中的单个陈述。
- 单个事件的概率可以表示为 P(a1) 或 P(b2)。
因此,联合概率 P(x1, y1) 与 P(y1, x1) 是相等的。这个看似显而易见的原理,在实际应用中非常有用。
概率分布的独立性
现在,我们来定义两个概率分布的独立性。
如果两个概率分布是独立的,那么知道其中一个事件的结果,并不会影响我们对另一个事件为真的信念。也就是说,知道一个事件的结果不会改变另一个事件的概率。
你可以将独立的概率视为互不关联、没有联系的事件。例如,抛一枚硬币和掷一个骰子就是独立的。

当两个概率独立时,计算它们同时发生的联合概率有一个简单的公式。

以下是计算独立事件联合概率的公式:


P(A, B) = P(A) × P(B)

我们为两个概率的乘积起了一个特殊的名字,称为乘积分布。
因此,当联合分布等于乘积分布时,根据定义,这两个概率分布就是独立的。

使用维恩图理解概率


我们经常使用维恩图来帮助理解。维恩图可以展示集合的交集和并集,从而直观地表示两个事件同时发生(交集)或至少一个发生(并集)的概率。
例如,考虑事件“抛硬币得到正面”(概率为1/2)和“掷骰子得到3点”(概率为1/6)。我们可以用两个圆圈来表示这两个事件的概率空间。



- 整个图形外围区域代表了所有可能结果的全集。
- 两个圆圈的重叠区域(交集) 就代表了“硬币正面且骰子为3点”的联合概率。根据独立事件的公式,这个面积等于 (1/2) × (1/6) = 1/12。

维恩图对于理解“或”概率的概念可能更加有用。


我们想知道事件“硬币正面或骰子为3点”发生的概率,记作 P(A or B)。在概率论中,“或”意味着事件A发生,或事件B发生,或两者同时发生。


如果我们简单地将两个圆圈的面积(概率)相加,那么中间的重叠区域就会被计算两次。

为了避免重复计算,我们需要从两个概率之和中减去一次重叠区域的概率(即联合概率)。

以下是计算“或”概率的公式:


P(A or B) = P(A) + P(B) - P(A, B)

在我们的例子中:
- P(正面) = 1/2 = 6/12
- P(骰子为3) = 1/6 = 2/12
- P(正面, 骰子为3) = 1/12

因此,“硬币正面或骰子为3点”的概率为:
P(正面 or 3点) = 6/12 + 2/12 - 1/12 = 7/12



总结

本节课中我们一起学习了联合概率的核心内容。


我们首先明确了联合概率是描述两个事件同时发生的概率,其符号表示为 P(A, B),且顺序不影响结果。


接着,我们探讨了概率分布的独立性:当两个事件独立时,知道一个事件的结果不影响另一个。此时,联合概率等于各自概率的乘积,即 P(A, B) = P(A) × P(B)。


最后,我们借助维恩图直观地展示了事件“与”(交集)和“或”(并集)的概率关系,并推导出了计算“或”概率的公式:P(A or B) = P(A) + P(B) - P(A, B)。

理解这些概念是处理更复杂概率问题的基础。
📊 课程29:排列与组合

在本节课中,我们将要学习概率论中的两个核心概念:排列与组合。我们将探讨如何计算事件按特定顺序发生的概率,以及不考虑顺序时,一组事件发生的概率。理解这两个概念对于解决许多实际问题至关重要。
🧩 排列:顺序很重要


上一节我们介绍了概率的基本概念,本节中我们来看看当事件的顺序很重要时,如何计算可能性。这种情况被称为排列。


例如,假设我有5名员工和5项不同的任务。我需要将这5个人分配到5个不同的岗位上。我可以将第一个人分配到5个岗位中的任何一个。




现在,一个岗位已被占据,因此我可以将第二个人分配到剩下的4个岗位中的任何一个。



以此类推,第三个人有3个选择,第四个人有2个选择,最后一个人只剩下1个选择。


因此,将5个人分配到5个不同岗位的总方法数为:
5 × 4 × 3 × 2 × 1 = 120



在这个例子中,分配的顺序是重要的,所以这被称为排列。



🤝 组合:顺序不重要

现在,让我们考虑另一种情况。假设我有5个人,我想组建一个由5人组成的团队,团队中所有成员地位平等,没有特定的角色分配,也没有顺序要求。


在这种情况下,实际上只有一种可能的组合:我只需要将这5个人全部选入团队即可。


当顺序不重要时,可能的组合数量总是少于排列的数量。这是因为同一个团队的多种排列顺序,在组合中只被算作一种。
例如,这里有3个不同物体的6种排列顺序。

这对应着6种排列。但团队的成员是固定的,所以只有一种组合。
🎱 彩票例子:排列与组合的对比
为了更好地理解,让我们想象一个彩票游戏。一个碗里有6个不同的球,分别标有数字1到6。我们将一次抽出一个球,共抽4次,以确定彩票的中奖号码。
这个彩票的规则可以有两种理解方式:数字的顺序重要或不重要。
情况一:顺序重要(排列)
如果顺序重要,我们讨论的就是排列的数量。我们需要计算从6个独特物体中抽取4个来填充4个位置的方法数。
- 第一个球可以是1到6中的任何一个(6种选择)。
- 抽走一个后,剩下5个球可选第二个位置(5种选择)。
- 剩下4个球可选第三个位置(4种选择)。
- 剩下3个球可选最后一个位置(3种选择)。
因此,排列的总数为:
6 × 5 × 4 × 3 = 360
如果你需要预测中奖号码的顺序才能赢,那么一场公平的赌注赔率应该是360比1,即下注1美元可能赢得360美元。
排列数有一个通用公式:
P(n, m) = n! / (n - m)!
其中 n! 表示n的阶乘,n 是独特物体的总数(本例中为6),m 是选取的数量(本例中为4)。所以:
P(6, 4) = 6! / (6-4)! = 6! / 2! = 6×5×4×3 = 360
情况二:顺序不重要(组合)
另一方面,如果赢彩票只需要猜中正确的4个数字,而不考虑顺序,那么我们现在讨论的就是组合。
假设我抽出了数字2、3、4、5。我只需要猜中这四个数字即可。
在这种情况下,我需要将排列数360除以24,得到15种可能的四球组合。

因此,在一个基于组合的公平彩票中,下注1美元如果中奖只能赢得15美元,因为现在猜中四个数字的几率是15分之一。

为什么除以24?因为4个球可以有24种不同的排列顺序。第一个位置有4种选择,第二个位置有3种选择,第三个位置有2种选择,最后一个位置有1种选择,即 4 × 3 × 2 × 1 = 24。

组合数的通用公式是:
C(n, m) = n! / [(n - m)! × m!]


这个公式有一个特殊的名称,叫做 “n选m”。在本例中,6选4的计算是:
C(6, 4) = 6! / [(6-4)! × 4!] = 360 / 24 = 15

🚜 另一个例子:司机与机器



让我们考虑另一个例子。我需要派遣一辆自卸卡车、一辆推土机和一辆压路机去一个建筑工地。我有8名合格的司机,每人都能驾驶所有三种机器。

情况一:关心司机与机器的匹配(排列)


如果我关心具体哪位司机操作哪台机器,那么:
- 第一台机器可以由8名司机中的任何一位驾驶(8种选择)。
- 第二台机器由剩下的7位司机驾驶(7种选择)。
- 第三台机器由剩下的6位司机驾驶(6种选择)。


因此,派遣团队的不同方式总数为:
8 × 7 × 6 = 336

这使用的是排列公式,其中 n=8, m=3:
P(8, 3) = 8! / (8-3)! = 8! / 5! = 8×7×6 = 336


情况二:只关心派遣哪三位司机(组合)
如果不关心哪位司机开哪台车,我只想知道可以派出多少支不同的3人司机团队随车辆前往工地。


那么,我使用组合公式:
C(8, 3) = 8! / [(8-3)! × 3!] = 8! / (5! × 3!)

这相当于将之前的排列答案336除以6(因为3个司机有3! = 6种排列方式):
336 / 6 = 56


因此,我可以派出 56 支不同的司机团队。这就是组合与排列的区别。


🔄 有放回 vs 无放回

接下来,在定义某种情况的概率时,我们需要考虑有放回和无放回的概念。

无放回


当我们从一副牌中抽牌时(例如扑克手牌),抽出的牌会从牌堆中移除,在本次抽牌过程中不再有可能再次抽到那张牌。
例如,如果我第一张牌抽到A的概率是1/13。那么我第二张牌再抽到A的概率就不同了:现在牌堆里剩下3张A和51张牌,所以第二张抽到A的概率是 3/51。


任何将事物从可能性领域中移除的情况都属于无放回。
有放回
另一方面,如果我试图预测或猜一个000到999之间的数字,当然允许使用0到9的每个数字超过一次,你可以重复使用每个数字两次或三次。这里我们是在生成一个数字,并且数字是被有放回地使用。


📝 总结
本节课中我们一起学习了概率论中的关键工具:排列与组合。
- 排列用于计算当顺序重要时,从一组物体中选取特定数量物体的方法数,公式为 P(n, m) = n! / (n - m)!。
- 组合用于计算当顺序不重要时,从一组物体中选取特定数量物体的方法数,公式为 C(n, m) = n! / [(n - m)! × m!],也记作 “n选m”。
- 我们还区分了有放回和无放回抽样对概率计算的影响。
结合排列与组合的概念,以及有放回与无放回的概念,我们几乎可以处理基础概率课程中可能出现的所有概率情况。
课程30:阶乘与组合数C(m,n)的应用 🧮

在本节课中,我们将学习概率论中两个核心的计数工具:阶乘 和 组合数。我们将通过经典的“抽球”问题来理解这些概念,并学习如何计算从一组物品中选出特定数量物品的不同方式。

抽球问题:有放回与无放回

概率论教学常常会讨论“抽球”问题。一个“瓮”是一个容器,你看不到里面。通常里面有两种不同颜色的弹珠,有时颜色更多。核心思想是从容器中抽取弹珠。

从容器中抽取弹珠有两种基本方式:有放回 和 无放回。


有放回抽取

在有放回抽取中,每次抽取后,我们会将弹珠放回瓮中。这意味着每次抽取的概率是独立的,不会改变。


例如,假设一个瓮里有2个白球和1个蓝球。那么:
- 每次抽到白球的概率是 2/3。
- 每次抽到蓝球的概率是 1/3。


因此,连续两次抽到白球的概率是:
P(两次白球) = (2/3) * (2/3) = 4/9


无放回抽取

在无放回抽取中,我们抽出的弹珠不再放回。这意味着每次抽取后,瓮中的弹珠总数和颜色构成都会改变,因此概率会发生变化。

继续使用上面的例子:
- 第一次抽到白球的概率是 2/3。
- 如果第一次抽到了白球(概率2/3),那么瓮里剩下1个白球和1个蓝球。此时第二次抽到白球的概率变为 1/2。
- 如果第一次抽到了蓝球(概率1/3),那么就不可能连续抽到两个白球。

因此,无放回情况下连续两次抽到白球的概率是:
P(两次白球) = (2/3) * (1/2) = 2/6 = 1/3



阶乘的概念


上一节我们通过抽球问题看到了概率如何随抽取方式变化。现在,我们来看一种在排列计数中非常重要的运算。

当我们计算将不同的人安排到不同的职位时,会用到一种乘法模式:开始时可选位置多,结束时可选位置少。

这种运算有一个通用名称,叫做 阶乘。它是指将一个数乘以比它小1的整数,再乘以比那个数小1的整数,依此类推,直到乘以1。



阶乘 的表示法是在数字后面加一个感叹号 !。
例如:
5! 读作“5的阶乘”。
5! = 5 × 4 × 3 × 2 × 1 = 120

当我们计算一个阶除以另一个阶时,许多数字会相互抵消。
例如:
7! / 5! = (7 × 6 × 5 × 4 × 3 × 2 × 1) / (5 × 4 × 3 × 2 × 1) = 7 × 6 = 42


关于阶乘还有一个重要约定:0的阶乘定义为1,即 0! = 1。




组合数 C(m, n)
理解了阶乘后,我们可以用它来解决一个更常见的问题:从一组物品中选出特定数量的物品,有多少种不同的选法?
当我们从M个物品中无放回地抽取N个物品时,可以形成的独特组合的数量有一个专门的名称和记号。
这种问题在概率论中非常常见,因此它有自己的名称和符号。


它被称为 “M选N”,记作 C(m, n) 或 (M N)。

例如,如果我们想知道从10个人中选出5个人,可以组成多少个不同的委员会(或团队,假设所有角色相同)。

这里,M = 10, N = 5。
这可以描述为 10选5。


它的计算公式是:
C(10, 5) = 10! / [ (10-5)! × 5! ] = 10! / (5! × 5!)



组合数的计算技巧


现在,我们来看一个在没有计算器时估算答案的小技巧。

以 C(10, 5) 为例:
C(10, 5) = 10! / (5! × 5!)
我们可以展开并约分:
= (10 × 9 × 8 × 7 × 6 × 5 × 4 × 3 × 2 × 1) / [ (5 × 4 × 3 × 2 × 1) × (5 × 4 × 3 × 2 × 1) ]
分子中的一个 5!(即 5×4×3×2×1)会与分母中的一个 5! 抵消。

剩下:
= (10 × 9 × 8 × 7 × 6) / (5 × 4 × 3 × 2 × 1)

然后我们可以进行约分:
- 10 ÷ 5 = 2
- 9 ÷ 3 = 3
- 8 ÷ 4 = 2
- 6 ÷ 2 = 3
- 7 保持不变
所以结果是:
= 2 × 3 × 2 × 7 × 3 = 252



因此,从10个人中选出5个人组成一个团队,共有 252 种不同的组合方式。


总结


本节课我们一起学习了概率论中的两个基础计数工具。
- 我们首先通过抽球模型区分了有放回与无放回抽取对概率的影响。
- 接着,我们学习了阶乘的概念及其符号
!,它用于计算连续递减整数的乘积。 - 最后,我们引入了组合数
C(m, n),它用于计算从M个不同元素中无顺序地选取N个元素的所有可能组合的数量,其核心公式为M! / [N! × (M-N)!]。
掌握阶乘和组合数是理解更复杂概率问题的重要基石。
课程31:加法法则、条件概率与乘法法则 📊

在本节课中,我们将要学习概率论中的三个核心概念:边际概率与加法法则、条件概率,以及乘法法则。我们将了解如何从联合概率推导出单个事件的概率,如何计算在已知某些信息条件下的概率,以及如何将这些概念联系起来。
边际概率与加法法则
上一节我们介绍了联合概率。在概率问题中,我们常常知道两个事件同时发生的联合概率,但需要求出其中单个事件发生的概率,而不考虑另一个事件。
边际概率就是指单个事件发生的概率。例如,如果我们想知道事件 X1 发生的概率,并且已知 X1 与 Y1、Y2、Y3 同时发生的联合概率,那么 X1 的概率就是它的边际概率。
加法法则告诉我们,边际概率等于所有相关联合概率的总和。假设 Y 是一个完备的概率分布(即其事件互斥且穷尽),那么 X1 的边际概率计算公式如下:
P(X1) = P(X1, Y1) + P(X1, Y2) + P(X1, Y3) + ...
例如,如果 P(X1, Y1)=1%, P(X1, Y2)=10%, P(X1, Y3)=4%,那么 P(X1) = 1% + 10% + 4% = 15%。

同样地,Y2 的边际概率 P(Y2) 等于 P(X1, Y2) + P(X2, Y2) + P(X3, Y2) 的总和。
以下是加法法则的两种表述形式:
- 二元分布:对于事件 A 和 B,有 P(A) = P(A, B) + P(A, ¬B)。
- 多元分布:对于事件 A 和一系列互斥且穷尽的事件 B1, B2, ..., Bn,有 P(A) = Σ P(A, Bi),其中 i 从 1 到 n。
条件概率
在理解了如何从整体中提取单个事件的概率后,本节我们来看看条件概率。条件概率衡量的是在已知某个其他事件确定发生的情况下,目标事件发生的概率。
条件概率的定义是:在事件 B 确定成立的前提下,事件 A 发生的概率。其记号为 P(A | B),竖线右侧的事件被视为已知为真。
计算条件概率的通用方法是:将满足 A 定义的相关结果数量,除以在我们当前“缩小的宇宙”(即 B 必须为真的所有可能结果)中的总结果数量。
例如,投掷一个六面骰子。如果已知掷出的点数是奇数(事件 B),那么掷出点数为3(事件 A)的条件概率是多少?在奇数结果 {1, 3, 5} 这个缩小的样本空间中,满足 A 的结果只有 {3}。因此,P(点数为3 | 点数为奇数) = 1/3。
反之,如果已知掷出的点数是3(事件 B),那么点数为奇数(事件 A)的条件概率是 1,因为3本身就是奇数。
乘法法则
现在,我们希望将联合概率、边际概率和条件概率这三个概念联系起来。这通过一个非常重要的规则——乘法法则来实现。


乘法法则的公式如下:


P(A | B) = P(A, B) / P(B),其中 P(B) ≠ 0。


这个公式表明,事件 A 在事件 B 发生条件下的概率,等于 A 和 B 同时发生的联合概率,除以 B 自身发生的边际概率。
请注意,在条件概率 P(A | B) 中,我们假设 B 为真。但在公式右边的 P(B) 中,B 不一定为真,它的概率可以是0以外的任何值。



独立性的新视角


乘法法则允许我们从另一个角度来定义事件的独立性。

你可能还记得,两个事件 A 和 B 独立的定义是它们的联合概率等于各自边际概率的乘积:P(A, B) = P(A) * P(B)。


我们可以利用乘法法则从这个旧定义推导出一个等价的新定义。将等式两边同时除以 P(B)(假设 P(B) ≠ 0):



P(A, B) / P(B) = P(A) * P(B) / P(B)

根据乘法法则,左边等于 P(A | B)。右边约分后等于 P(A)。因此我们得到:


P(A | B) = P(A)


这个新定义的直观含义是:知道事件 B 发生,并不会改变事件 A 发生的概率。事件 B 的结果对事件 A 的概率没有影响,因此它们是独立的。

反之亦然,如果 P(A | B) ≠ P(A),那么事件 A 和 B 就是相依的。任何两个概率分布的关系,非独立即相依,没有中间状态。
本节课中我们一起学习了概率论的三个基础法则。我们首先通过加法法则从联合概率求得边际概率。然后引入了条件概率的概念,它描述了在已知信息下事件发生的可能性。最后,我们通过乘法法则将联合概率、边际概率和条件概率联系起来,并由此得到了事件独立性的一个等价且直观的新定义:P(A | B) = P(A)。掌握这些法则对于理解和构建更复杂的概率模型至关重要。
课程32:贝叶斯定理第一部分 🧮

在本节课中,我们将学习概率论中最著名且最强大的定理之一——贝叶斯定理。我们将从熟悉的乘积法则出发,推导出贝叶斯定理的公式,并探讨其在解决“逆概率问题”中的应用。通过一个具体的例子,我们将理解如何利用贝叶斯定理,在已知观测数据的情况下,推断出最可能产生这些数据的未知过程或参数。
现在,我们来到概率论中最著名且最强大的定理之一:贝叶斯定理。
我们从熟悉的乘积法则开始。你会注意到,如果我们在这个等式的两边同时乘以事件B的概率,我们仍然会得到一个基于乘积法则的等价形式,它看起来像这样:
P(A|B) * P(B) = P(A, B)

现在,我们可以进行替换。我们可以用 P(B, A) 替换 P(A, B),因为我们知道它们是完全等价的。然后,我们可以再次应用乘积法则到 P(B, A) 上,其等价形式如下:


P(B|A) * P(A) = P(B, A)


于是,我们得到了这个等价关系,这就是贝叶斯定理:


P(A|B) = [P(B|A) * P(A)] / P(B)
贝叶斯定理最强大的用途之一是解决我们所谓的“逆概率问题”。


什么是逆概率问题? 🔄
逆概率问题是指那些答案形式为“某个具有特定概率参数的过程,正在被用来生成观测数据”的概率是多少的问题。
我将给出一些例子,这会让概念更清晰。我们有时会用希腊字母 θ 来表示可能导致或生成我们所观测数据的不同参数。有时,我们也直接用下标 i 来表示可能生成观测数据的众多过程中的某一个。就我们的目的而言,它们本质上是等价的。
我们来看一个例子。假设我们有两个瓮。一个瓮里有20%的白球,另一个瓮里有10%的白球。我们观察到从一个瓮中(有放回地)连续抽出了三个白球,但我们不知道我们观察的是哪个瓮。那么,我们正在观察瓮1的概率是多少?我们正在观察瓮2的概率又是多少?
这些概率就是“过程”的概率。瓮1代表过程1,或者说参数“白球概率=0.2”。瓮2代表过程2,或者说参数“每次抽取得到白球的概率=0.1”。
在一个更常规的“正向概率问题”中,我们感兴趣的是在已知过程的情况下,观察到某个特定结果的概率。例如,如果我们知道是瓮1(已知过程),那么连续观察到三个白球的概率就是 0.2 * 0.2 * 0.2 = 0.008。这是一个常规的概率问题。
而在这个问题中,我们是从已知的结果出发,感兴趣的是我们观察到瓮1或瓮2的可能性有多大。这是一个关于未知过程的问题。
用条件概率的术语来表述,我们感兴趣的是:在给定“连续观察到三个白球”这一观测数据(B)的条件下,过程参数(A_i,即瓮1或瓮2,或者说白球概率等于0.2或0.1)的概率是多少。
贝叶斯定理告诉我们,这等于:
P(A_i | B) = [P(B | A_i) * P(A_i)] / P(B)
在这里,我们利用求和法则将 P(B) 分解为一系列联合概率之和:
P(B) = P(B, A_1) + P(B, A_2) = [P(B|A_1) * P(A_1)] + [P(B|A_2) * P(A_2)]

似然与先验概率 📊


贝叶斯定理公式中的 P(B | A_i) 部分被称为“似然”。它解决的是一个简单的正向概率问题:在给定参数(过程)的条件下,观测到数据的概率是多少。
所以,如果我们知道观察的是瓮1,那么连续观察到三个白球的似然是 0.2^3 = 0.008。类似地,如果每次抽到白球的概率是10%,那么连续三个白球的似然是 0.1^3 = 0.001。
那么我们的解是什么呢?我们将从“无差别原则”开始。无差别原则告诉我们,在没有其他信息的情况下,我们没有理由在两个瓮之间做出选择,因此:
P(A_1) = 0.5
P(A_2) = 0.5

换句话说,在我们观察到任何数据之前,我们对正在观察哪个瓮持中立态度,它们具有相等的概率。因为这发生在观测任何数据之前,我们称之为参数的“先验概率”。

应用贝叶斯定理进行计算 🧮


现在,我们来看具体的计算。对于瓮1(A_1),根据贝叶斯定理:


P(A_1 | B) = [P(B | A_1) * P(A_1)] / P(B)


其中,分母 P(B) 为:


P(B) = [P(B|A_1) * P(A_1)] + [P(B|A_2) * P(A_2)]

代入数值:
- P(B|A_1) = 0.008
- P(A_1) = 0.5
- P(B|A_2) = 0.001
- P(A_2) = 0.5



计算如下:

P(A_1 | B) = (0.008 * 0.5) / [(0.008 * 0.5) + (0.001 * 0.5)]


分子和分母中的 0.5 可以约去:


P(A_1 | B) = 0.008 / (0.008 + 0.001) = 0.008 / 0.009 = 8/9
因此,我们观察到的是瓮1的概率是 8/9。显然,我们观察到瓮2的概率就是 1 - 8/9 = 1/9。
总结 📝
本节课中,我们一起学习了贝叶斯定理的推导及其在逆概率问题中的应用。我们从一个具体的“抽球”例子出发,理解了如何利用贝叶斯公式,在已知观测结果(连续三个白球)后,更新我们对潜在未知过程(是哪个瓮)的信念。我们引入了几个关键概念:
- 贝叶斯定理公式:P(A|B) = [P(B|A) * P(A)] / P(B)
- 似然:在给定假设下,观测到数据的概率 P(B|A)
- 先验概率:在观测数据之前,对假设的初始信念 P(A)
- 后验概率:在观测数据之后,对假设更新后的信念 P(A|B)
通过计算,我们看到,尽管最初两个瓮被选中的先验概率相等(各为0.5),但在观察到“连续三个白球”这个强证据后,后验概率强烈倾向于白球比例更高的瓮1(8/9)。这就是贝叶斯推断的核心思想:用数据更新认知。
课程33:贝叶斯定理第二部分 🔄


在本节课中,我们将学习贝叶斯定理的一个强大应用:如何根据新获得的数据来更新我们的概率估计。我们将通过一个具体的例子,演示如何将先验概率更新为后验概率。




贝叶斯定理真正强大的地方在于,它允许我们根据新的数据来更新概率。

假设我们之前从一个瓮中抽取了三颗弹珠,并计算了该瓮是“瓮1”(白球概率0.2)还是“瓮2”(白球概率0.1)的概率。现在,我们抽取了第四颗弹珠。


这颗弹珠也是白色的。
那么,我们该如何更新我们的概率呢?
我们会建立与之前相同类型的贝叶斯定理公式。但是,公式中的先验概率 P(A1) 和 P(A2) 将不再是初始的1/2,而是我们根据前三颗弹珠计算出的后验概率(八分之九和九分之一)。这些新的概率成为了我们计算中的“新先验概率”。
让我具体说明一下。在观察到三颗白球后,再观察到第四颗白球的条件下,瓮1的概率计算如下:

公式:
P(瓮1 | 第四颗白球) = [P(第四颗白球 | 瓮1) * P(瓮1 | 前三颗白球)] / P(第四颗白球)

其中,分母 P(第四颗白球) 是观察到第四颗白球的总概率,需要通过全概率公式计算:


公式:
P(第四颗白球) = [P(第四颗白球 | 瓮1) * P(瓮1 | 前三颗白球)] + [P(第四颗白球 | 瓮2) * P(瓮2 | 前三颗白球)]


代入具体数值进行计算:
- 分子:
0.2 * (8/9) - 分母:
[0.2 * (8/9)] + [0.1 * (1/9)]
计算结果如下:
P(瓮1 | 新数据) ≈ 94.12%P(瓮2 | 新数据) ≈ 5.88%
与更新前的概率(瓮1为88.9%,瓮2为11.1%)相比,在得到第四颗白球这一新证据后,瓮1的概率进一步上升,而瓮2的概率则下降了。
贝叶斯公式中的各个部分有专门的名称。为了更具一般性,我们用参数 θ 来表示不同的可能性(例如,θ1 代表瓮1的0.2白球概率,θ2 代表瓮2的0.1白球概率)。
以下是贝叶斯定理各组成部分的名称:
公式:
P(θ_i | 数据) = [P(数据 | θ_i) * P(θ_i)] / P(数据)
- 后验概率:公式左边的
P(θ_i | 数据)。表示在观察到新数据之后,我们对参数θ_i为真的概率的最佳估计。 - 先验概率:公式中的
P(θ_i)。表示在观察到新数据之前,我们对参数θ_i为真的初始信念。它可以是基于无差别原则的初始猜测(如1/2),也可以是上一轮计算得到的后验概率。 - 似然:公式中的
P(数据 | θ_i)。这是在给定某个参数θ_i为真的条件下,观察到当前数据的“标准”正向概率。 - 边际概率/证据:公式分母的
P(数据)。这是观察到当前数据的总概率,通过对所有可能参数的似然与先验的乘积求和得到(即全概率公式)。
本节课中,我们一起学习了贝叶斯定理的核心应用——概率更新。我们通过一个连续抽取弹珠的例子,演示了如何将新证据(第四颗白球)融入计算,从而把“先验概率”更新为“后验概率”。我们还明确了贝叶斯公式中“后验概率”、“先验概率”、“似然”和“边际概率”这四个关键术语的定义。理解这个过程是掌握贝叶斯推理的基础。
📊 课程34:二项式定理与贝叶斯定理

在本节课中,我们将学习概率论中两个非常重要的定理:二项式定理和贝叶斯定理。我们将了解二项式定理如何计算在多次独立试验中获得特定次数“成功”的概率,以及贝叶斯定理如何利用观测到的数据来更新我们对某个事件或假设的信念。这两个定理是数据科学中进行统计推断的基础工具。
🎯 二项式定理
上一节我们介绍了概率的基本概念,本节中我们来看看二项式定理。二项式定理之所以称为“二项”,是因为它适用于只有两种可能结果的场景,例如“成功”或“失败”。一个典型的例子是抛硬币,我们可以将“正面”视为成功。
二项式定理的公式如下:


P(S successes in N trials) = C(N, S) * p^S * (1-p)^(N-S)



其中:
- N 是试验的总次数。
- S 是成功的次数。
- p 是单次试验成功的概率。
- C(N, S) 是组合数,表示从N次试验中选出S次成功的方式有多少种,计算公式为
N! / (S! * (N-S)!)。 - (1-p) 是单次试验失败的概率。


这个公式可以理解为:(成功序列的可能数量) 乘以 (某个特定成功序列发生的概率)。

让我们通过一个例子来应用这个公式。
示例:抛硬币


假设我们抛一枚公平的硬币(p = 0.5)100次。我们想知道恰好得到72次正面的概率。


以下是计算步骤:
- N = 100(试验次数)
- S = 72(成功次数,即正面)
- p = 0.5(单次抛掷得到正面的概率)


将这些值代入公式:
P(72 heads in 100 tosses) = C(100, 72) * (0.5)^72 * (0.5)^(100-72)




计算结果是一个极小的概率,约为 3.94 × 10^(-6)。这意味着在100次公平的抛掷中,恰好得到72次正面的可能性非常低。


🔄 从二项式到贝叶斯定理


我们已经看到如何计算在已知概率(如公平硬币的0.5)下,观察到特定数据的概率。现在,让我们提出一个更深入的问题:如果我们观察到了数据(如72次正面),我们如何判断这数据更可能来自哪个“源头”或假设?


这就引出了贝叶斯定理。贝叶斯定理允许我们根据新的证据(观测数据)来更新对某个假设可能性的初始判断(先验概率)。


问题设定


假设我们面前有两枚硬币,但不知道正在抛的是哪一枚:
- 公平硬币 (Fair Coin):抛出正面的概率
p = 0.5。 - 弯曲硬币 (Bent Coin):抛出正面的概率
p = 0.55。
我们进行了100次抛掷,观察到了72次正面。问题是:根据这个结果,这枚硬币是公平硬币的概率有多大?是弯曲硬币的概率又有多大?



应用贝叶斯定理



我们使用贝叶斯定理来计算在观察到数据(72/100次正面)后,硬币是公平的后验概率。

贝叶斯定理公式为:
P(Hypothesis | Data) = [ P(Data | Hypothesis) * P(Hypothesis) ] / P(Data)


应用到我们的问题:
- 假设 (Hypothesis H):硬币是公平的 (
Fair)。 - 数据 (Data D):100次抛掷中得到72次正面。

公式具体展开为:
P(Fair | 72 Heads) = [ P(72 Heads | Fair) * P(Fair) ] / [ P(72 Heads | Fair)P(Fair) + P(72 Heads | Bent)P(Bent) ]



以下是公式中各项的解释:
- P(Fair | 72 Heads):在观察到72次正面的条件下,硬币是公平的概率(这是我们想求的)。
- P(72 Heads | Fair):如果硬币是公平的,得到72次正面的概率。这正是我们之前用二项式定理算出的值:3.94 × 10^(-6)。
- P(Fair):我们认为硬币是公平的先验概率。在没有任何额外信息的情况下,我们通常假设两枚硬币被选中的可能性相同,所以设为 0.5。
- P(72 Heads | Bent):如果硬币是弯曲的(
p=0.55),得到72次正面的概率。这同样可以用二项式定理计算,结果约为 0.0001972。 - P(Bent):硬币是弯曲的先验概率,同样设为 0.5。


现在将所有数值代入公式进行计算:
P(Fair | 72 Heads) ≈ (3.94e-6 * 0.5) / ( (3.94e-6 * 0.5) + (0.0001972 * 0.5) )


计算结果,硬币是公平的概率小于2%,而硬币是弯曲的概率大于98%。




📝 总结
本节课中我们一起学习了:
- 二项式定理:用于计算在固定次数的独立二元试验(成功/失败)中,获得特定次数成功的精确概率。其核心公式为 P = C(N, S) * p^S * (1-p)^(N-S)。
- 贝叶斯定理:一个强大的推理框架,允许我们结合先验知识(对假设的初始信念)和新的证据(观测数据),来更新对假设可能性的评估。其核心思想体现在公式 P(H|D) ∝ P(D|H) * P(H) 中。
通过将两者结合——使用二项式定理计算似然概率 P(Data | Hypothesis),再将其代入贝叶斯定理——我们能够从观测到的数据中做出有力的推断,例如判断一个观测结果更可能来自哪个潜在的概率过程。这是现代数据科学和机器学习中进行统计建模和决策的基础。

浙公网安备 33010602011771号