UCLA-Stats102A-计算统计学笔记-全-
UCLA Stats102A 计算统计学笔记(全)
01:课程大纲与描述 📚

在本节课中,我们将学习Stats 102A《使用R的计算统计学》课程的基本信息,包括课程结构、评分标准、核心目标以及学术诚信要求。

课程概述与教师介绍 👨🏫

大家好,欢迎来到Stats 102A《使用R的计算统计学》课程。本学期我将尝试录制较短的视频,并每周发布一系列内容。让我们从课程大纲的几个要点开始。

我的名字是Miles Chen,我将担任本学期的教授。你们可以称呼我为陈教授、陈博士,或者直接叫我Miles。无论哪种方式,我都可以接受。
评分标准与课程要求 📊

以下是本课程的评分构成。我将为发布的视频设置小测验,这会计入你的成绩。此外,还有作业、期中考试和期末考试。同时,你们需要参与Campus Wire论坛的讨论,你们应该会收到加入该论坛的邀请。
作业是成绩中占比最重的部分,这反映了我认为作业可能是本课程中最重要的环节。
我采用直线评分标准,不进行分数曲线调整。请查阅大纲了解具体细节。

视频测验与作业提交 📝


如前所述,视频将伴随简短的测验。请务必完成测验以获得学分,并像对待测验一样对待它们,不要分享答案。如果你观看了视频,应该能够回答这些问题。
关于作业提交,你们需要将作业“编织”成PDF格式。作业将以R Markdown文件的形式发布,你们需要完成它们,将其编织为PDF,然后上传到GradeScope。上传时正确标记页码至关重要,如果标记不正确,我们将无法看到输出结果。
办公时间与沟通方式 💬
我每周会安排几次办公时间,这是我首选的沟通联系方式。你们可以给我发邮件,但我回复邮件较慢,这不是我最快的沟通方式。因此,如果需要联系,请优先考虑参加办公时间。如果需要私下会面,我可以在Zoom上设置分组讨论室。我很乐意纠正任何错误。请确保在加入办公时间时,在Zoom上填写好你的名字。
关于作业的问题,可以在办公时间提问,我很乐意提供帮助。但如果你在Campus Wire论坛上发帖,可能会从同学那里得到更快的回复。Campus Wire是一个讨论论坛,我要求你们参与其中。如果遇到私人问题,可以参加办公时间,也可以在Campus Wire上直接给我发消息。
无论是邮件还是其他信息,请尽量保持简短。例如,如果你需要作业延期,只需简单说明“我遇到了一些个人问题,可以申请作业延期吗?”。如果需要更多细节,我会询问。大多数情况下,作业延期我通常会批准三天(72小时)。如果需要更长时间或安排补考,建议为此类请求参加办公时间。如果是关于作业的具体问题,例如“我在作业上卡住了,不太知道如何入手”,那么Campus Wire是更适合的地方。

课程目标与数据科学基础 🎯
现在,让我们谈谈这门课的内容。这是Stats 102A,标题是《使用R的计算统计学导论》。我喜欢将其视为数据科学的基础课程。
数据科学通常被认为由三个不同部分组成。Drew Conway创建了这个数据科学的维恩图,将其分解为编码技能、数学与统计知识以及领域专业知识。
在这个时代,我们的数据集如此庞大,以至于除了使用计算机,我们无法以任何其他方式与之交互。很多时候,数据集已经超出了Excel等传统工具能高效处理的范围。因此,你必须使用像R或Python这样的编程语言进行编码。你需要对编码有一定的熟悉度。

我们将编码能力与数学和统计知识相结合。你们在统计100系列课程(如Stats 100A和100B)中获得了大量的数学和统计基础。这些课程提供了必要的理论基础,确保你理解这些思想的来源。将编码能力与这些基础数学和统计知识相结合,我们可能称之为机器学习,即应用算法从数据中学习的能力。
另一个要素是实质性专业知识,即主题领域的专业知识。这基本上是你将要进行数据分析或数据科学的任何领域。如果你想分析棒球数据,你需要成为棒球专家;如果你想进行医疗诊断,例如查看肾脏患者的数据,你应该了解这些数字的含义。如果你对这些数字一无所知,你的数据分析就不可能真正有用。你必须成为该主题领域的专家。
作为统计与数据科学或数据理论专业的学生,我们无法提供这种实质性专业知识,因为学科领域太多。这真的取决于你自己,在你想要进行数据分析的任何主题上成为专家。然后,将我们教授的编码技能、数学和统计知识应用到你感兴趣的任何主题上,你就能进行数据科学工作。

传统研究是将你的主题专业知识应用于传统或经典的数学和统计知识。而编码技能与实质性专业知识的结合,Drew Conway称之为“危险区”,因为如果你没有数学和统计基础,可能会以不恰当的方式应用分析工具和方法,拼凑出一些结果,但可能做出了本不该做的假设。
课程结构与主题 📖
就本课程而言,我认为这是对计算统计学和数据科学领域的一个入门介绍。课程旨在通过培养你在R中的编码技能,为你从事数据科学做好准备。然后,我们将把这些技能应用到一些统计应用中。
从主题上,我们可以将课程分为两部分。前半部分将更多涉及R的中高级编程。你们所有人都上过先修课程Stats 20,在那里学习了R编程入门。我们将在此基础上进行扩展。其中一些内容会是复习,一些内容则会深入探讨一些细节方面。我们可能还会通过额外的包(如正则表达式和网络爬虫)来扩展,因为这些在数据收集或清理等方面很重要。
之后,我们还将介绍一些计算统计学的方法。在课程的后半部分,当我们研究这些计算方法时,与另一门名为Math 151A(数值方法)的课程有相当多的重叠。你可以将我们在本课程中学习数值方法的那些周,视为Math 151A的简化轻量版。如果你已经或将要学习Math 151A,你当然会获得更深入的内容,但这对于统计和数据科学专业的学生不是必需的。不过,学习一些数值方法很重要。
编程与计算主题 🔧
以下是我们将涵盖的一些关于编程和编码的主题:
- 我们将学习一些数据结构以及子集选取。
- 流程控制,例如条件语句和循环等。
- 函数、作用域和环境。
- 我们还将学习tidyverse中的一些内容,如
tidyR和dplyr,我认为这些是非常重要且有用的包。 - 正则表达式和面向对象编程。

我认为这些都是相当有用的主题,也是我选择重点关注的。就计算统计学方法而言,我们将讨论浮点运算。理解计算机存储和处理数字的方式并非实数,这一点很重要。浮点数试图模仿实数,但它们肯定不相同。因此,这会产生某些影响,我们将讨论这些。一旦理解了这一点,我们就可以思考数值方法,如求根方法或优化方法。我们还将学习随机化检验和自助法等重采样技术,这些在计算统计学中很有用。
培养关键技能 🧠
然而,我在课堂上能教的内容是有限的。其余真正重要的东西需要你们自己培养,这些是我无法教授的。我将通过视频向你们展示内容,但像阅读和编写代码的能力,必须通过自己练习来培养。我将通过作业来鼓励这一点,你们必须在作业中读写代码,但这是你们自己需要负责培养的。


另一个重要方面是调试代码。编写代码时,你肯定会犯错误。要成为一名高效的程序员或编码员,你需要能够调试代码,能够通读自己的代码,理解错误可能发生的位置,找出并修复它。问题解决是指如何将一个复杂的程序分解成可以解决的小部分。如何分解问题,并使用可用的工具或解决方案来解决它?此外,阅读文档也很重要。我在课堂上能教的内容有限。最终,你必然会遇到我从未提及或你从未见过的函数。学习使用它们的方法是阅读这些函数、包或模块的相关文档。你未来的成功在很大程度上取决于你阅读和理解文档的能力。这些都是你们需要在课外自己努力的事情,是我无法仅通过视频传授的。
观看视频是一种非常被动的行为,你只是坐在那里听我讲,看屏幕上的东西。我认为很多技能的培养需要你积极尝试参与作业,主动运用大脑中需要思考这些问题的部分。我会布置相当多的作业,它们篇幅长且具有挑战性。这些作业旨在成为结构化的练习,帮助你培养这些技能。当你处理作业时,我希望你以培养这些技能为目标,有意识地对待它们,而不是仅仅试图完成作业。如果你把一切都当作“我只需要完成它”,我认为你会错过发展和练习的机会。
编码能力的重要性 ✍️

我喜欢把这门课看作一门“编码II”课程。为了从UCLA毕业,你必须满足所谓的“写作II”要求。你必须上一门课程,比如历史课,满足写作II课程的要求,你需要写论文和文章等。UCLA要求这样做,是因为写作和表达思想的能力很重要。因此,每个从UCLA毕业的学生都必须满足这个要求。虽然没有编码要求,但如果你要以统计学和数据科学或数据理论专业毕业,能够将想法转化为代码将非常重要。
因此,我喜欢把这门课看作一门“编码II”课程,我借鉴了UCLA对写作要求的理由,即编码对于与数据交互和从数据中学习至关重要,编写清晰、可读代码的能力将是任何数据相关职业的关键优势。编码能力强的学生可以创建强大的工具来实现想法和解决方案。所以,把这门课看作一门“编码II”课程,将会有很多需要编写大量代码的作业,这是有意为之的。
学术诚信与剽窃 ⚖️
这让我想谈谈学术诚信和剽窃问题。事实是,有很多工具可以让你非常容易地获得我布置的任何作业的答案。互联网上有很多高质量的代码,完全满足你的需求,如果你稍加搜索,就能找到这些解决方案。我们还有AI工具,如ChatGPT和GitHub Copilot等。这些工具非常出色,其中很多都经过代码训练。如果你问它们“如何解决这个问题”,你甚至可以直接复制粘贴一些作业提示,放入聊天机器人,它会为你写出解决方案。
如果你的工作只是完成任务,这些工具是很大的帮助,它们会帮助你完成任务。但这不是我布置作业的目的。例如,我可能会布置一个作业,要求你在R中编写“蛇梯棋”游戏。我实际上并不需要一个能运行的“蛇梯棋”游戏,那不是重点。我不是一家说“嘿,我们需要这个产品准备就绪”的公司。我布置这个作业给你的原因,是因为你思考的过程、你找到解决方案并创建和理解它的过程,将帮助你培养作为编码员的技能。

你可以把它想象成去健身房。当你去健身房举重时,举重的目标不是把重物从地上举起来。举重的整个过程是为了帮助你达到增强力量的真正目标。有一个概念叫“没有痛苦,就没有收获”。如果你进行重量训练,举重时没有挑战自己,只举一磅的重量,说“嘿,这很容易”,不让自己感到疲劳,你就不能指望有任何收获。如果你不举任何对你来说重的东西,你就不会变得更强壮。所以,如果你想在健身房变得更强壮,你必须举起对你来说困难的东西,体验肌肉酸痛,然后在休息和睡眠时,你的身体会重建得更强壮。同样,你的大脑也需要受到挑战。如果你的大脑在编写代码时完全没有挣扎,那么你的大脑就没有理由建立那些能提高你编码能力的额外神经连接。另一方面,如果你面临挑战,你的大脑努力思考解决方案,当你第一次看到这个问题,思考它很困难,你的大脑努力工作并因此感到疲劳,那么当你睡觉时,你的大脑会说“嘿,这挺难的,我们能建立新的连接吗?”。当它建立了新的连接,它就能更好地找出解决方案。这就是那种“豁然开朗”的感觉,你能够更好地理解主题,因为你的大脑形成了一些新的连接,当再次面对这个任务时,它就能做到。这就是你成为更好编码员的方式。
因此,如果你求助于ChatGPT或类似工具,或者剽窃他人的代码,就像去健身房让一个更强壮的人或机器人帮你把重物从地上举起来。如果目标是举起重物,那很好,但这不是举重的目标。举重的目标是增强力量,让你自己去做。同样,复制粘贴别人的代码或AI的代码,如果目标是完成一项编码任务,那是可行的,但这不会帮助你提高编码技能。
我真诚地呼吁你们内心学习和成为更好编码员、更好地进入市场的愿望。我呼吁这种愿望,所以你们不要只是求助于AI或抄袭他人的代码。同样,如果你去健身房说“哦,这些重物太重了,你能帮我举起来吗?”,然后一个强壮的人帮你举起来,你拍手说“哦,太好了,我们进行了一次很棒的锻炼”,这完全违背了初衷。让别人(无论是真人还是AI工具)为你完成作业,以这种方式完成作业不会对你有帮助。我希望你经历挣扎,挑战自己,以这种方式进步。

关于努力与诚信的呼吁 🤝
我认为学生之所以剽窃,有时是因为他们混淆了课程的目标。我认为剽窃的学生相信,为了完成作业而完成作业、避免得低分比学习的目标更重要。但这不是正确的处理方式。课程的目标是你们的学习。现在存在一个冲突问题,我无法创建能精确评估每个学生在课程期间学习多少的个性化评分方案,每个人都必须按照相同的标准评分。

尽管如此,我希望你们能根据所学来评判自己在课程中的表现,而不是字母成绩。如果你觉得自己学到了很多,我希望你能说那是成功的。
当你面对具有挑战性的作业时,我希望你付出真诚的努力。我希望你不要在网上寻找解决方案,或使用AI工具或他人的代码。如果截止日期到了,你无法完成作业要求的所有内容,那就提交你能完成的部分。这可能意味着你不会在作业上得到100分。作业是根据正确性评分的,不仅仅是完成,实际完成并正确完成很重要。因此,如果你编写的作业不能正常工作,你会被扣分。但我希望你对此能够接受,并将其视为你仍需努力和实践的领域,而不是你编码能力的失败。如果情况严重,我们会认真对待剽窃问题,必要时会升级到学生院长办公室处理。但我对你们的呼吁是,在作业中保持诚实。

合作政策 📄

有一项合作政策。作业零是阅读大纲并签署合作政策。基本上,当涉及到Campus Wire讨论论坛,甚至只是与同学和朋友交谈时,你们可以互相讨论,但不能分享实际的代码。你们可以与其他学生进行口头协作,但不能向他人展示你的代码或查看他人的代码。例如,你可以回答“你是如何解决第二题的?”,有很多内容你可以在不写出代码的情况下回答。你可以写“我创建了一个for循环,在每次迭代中,我做了这个,我子集了向量X”等等,而不是“这是我写的用来解决问题的代码”。你不能要求别人为你调试代码或检查你的工作。同样,你不能说“我能看看你是怎么做的第二题吗?来检查一下我的工作”。如果你展示了代码,那将是违规行为。

有时你会遇到错误信息。你可以复制粘贴错误信息,错误信息中包含大量信息,可以帮助你找出问题所在。所以,阅读文档以及阅读和理解错误信息很重要。当你遇到错误信息时,可以复制粘贴你的错误信息,但不要复制粘贴你的代码。

至于课堂上出现的其他代码,你们可以讨论。在Campus Wire上,如果某张幻灯片上有代码,你不太理解代码中发生了什么,可以复制它并放到你的Campus Wire帖子中说“嘿,这一行是怎么回事?”,这没问题,我同意。但至于作业和作业问题,请不要复制粘贴你的代码。

总结 📋

本节课中,我们一起学习了Stats 102A课程的基本框架。我们介绍了课程概述、评分标准、作业提交方式以及沟通渠道。我们探讨了课程的核心目标,即作为数据科学的基础,融合编码技能与统计知识。课程将分为R编程提升和计算统计学方法应用两大部分。我们强调了通过实践作业来自主培养阅读、编写、调试代码及解决问题能力的重要性。最后,我们深入讨论了学术诚信问题,呼吁大家重视学习过程本身,通过真诚努力和挑战自我来获得真正的进步,并明确了合作政策的具体界限。
02:成绩与生活平衡 🧭


在本节课中,我们将探讨一个超越技术本身的重要话题:如何看待学业成绩,以及如何在学业、人际关系和个人健康之间找到平衡。我们将分享一些个人见解,帮助你建立更健康的学习和生活观念。


成绩不代表你的全部价值 💯

上一节我们介绍了课程的基本信息,本节中我们来看看如何正确看待成绩。
成绩很重要,但它们并不能定义你。进入UCLA这样的大学,很大程度上是因为你在高中或社区大学取得了好成绩。在大学里,你将大量精力投入到学习和作业中,因此成绩显得格外重要。
它们确实在某些方面发挥作用,例如研究生院的申请。但必须记住,成绩不是生活中最重要的事情。没有人会在回顾人生时说:“我真后悔当时没拿到A-,而是拿了B+。”这不会成为人们后悔的事情。
一个关于成绩与身份认同的故事 📖
我想分享一个我个人的故事。高中时我努力学习,取得了好成绩,并作为高年级学生被梦想的学校加州理工学院录取。我梦想成为火箭科学家,为NASA工作。收到录取通知书时,我欣喜若狂,觉得所有努力都得到了回报。
然而,进入加州理工学院后,学习异常艰难。秋季学期,我得到了人生中第一个C。冬季学期,我得到了第一个D。到了春季学期,情况变得更糟,我挂掉了多门课程,被处以留校察看。学校告知,如果秋季返校,必须达到一定的GPA要求。我感到绝望,认为自己无法做到,并陷入了抑郁。
问题的根源在于,在那之前,我将太多的个人价值和身份认同与学业成就绑定在一起。我觉得自己不是体育明星、不常参加社交活动都没关系,因为我有好成绩,这就是我作为人的价值所在。这在高中成绩好时没问题,但当大一失去好成绩、只剩下坏成绩时,我开始怀疑:如果我的价值来自成绩,而我没有成绩,那我的价值是什么?这种想法让整个情况雪上加霜。
我感激我的父母,尤其是我的父亲,在那段时间给予了我极大的支持。他基本上是说,这个梦想(去加州理工)不值得以牺牲我的个人和心理健康为代价。最终我选择了退学,转学到了密歇根州的一所小型工程学院——凯特林大学,并在那里完成了本科学位。
我感激这段痛苦的经历,因为它迫使我将自我认同与成绩分离开来。我作为一个人,其价值在于我是谁,而不在于我的成绩。如今,我不再从个人成就或职业成功中寻找价值,而是从我的信仰、与他人的关系中寻找内在价值。对我而言,这是一次被迫放弃我所珍视之物的痛苦经历,但它让我迈出了这一步。


我分享这个故事,是希望你们不必经历同样的痛苦,但能够退一步认识到:你的生活、你这个人,不由你的成绩定义。未来,你作为人的价值也不由你的职业、头衔或收入定义。你作为一个人,其价值远超过这些东西所能衡量的。我想让你们知道,我理解取得好成绩的感觉很好,因为我曾是那个喜欢取得好成绩的人。但同样,你们的成绩不代表你们。
工作与生活的平衡 ⚖️

接下来,我们谈谈工作与生活的平衡。我们可以把精力投入到几个主要领域,我认为大致可以分为三类:工作、人际关系和自我。
以下是这三个领域的简要说明:
- 工作:包括实际的工作、实习等。对你们大多数人来说,目前“工作”主要指学业和学术活动,或其他任何职业义务。
- 人际关系:包括与家人、朋友的关系,或浪漫伴侣关系,以及其他社交义务。有些俱乐部活动可能同时属于人际关系和工作范畴。
- 自我:你必须照顾好自己。这包括照顾你的身体健康(如饮食、睡眠、锻炼)、心理健康(包括睡眠、娱乐、做能带来快乐的事),如果你是宗教或灵性人士,还包括照顾你的灵性健康。



一天只有24小时,不可能在每个类别都投入100%的精力。因此,你需要决定和选择如何分配你的时间。
我相信,良好的工作生活平衡是通过有意识地决定什么对你重要,并相应地投入时间和精力来实现的。一般来说,你在某件事上投入越多,收获就越多;投入越少,收获就越少。我认为,通过接受你投入时间较少、优先级较低的事情自然无法蓬勃发展的后果,你可以在生活中找到满足感。

平衡选择的例子与心态 🤔
这是一个有点傻的例子。假设你有一个朋友圈子。有一天,你开始了一段浪漫关系,于是你选择将大量时间和精力投入到伴侣身上。因为你花了很多时间与伴侣相处,这段关系会蓬勃发展。但同时,你与原来那群朋友相处的时间变少了,自然就会与他们变得有些疏远。
有时,当你意识到并看到这种疏远开始时,可能会感觉有些敌意。但这不一定是因为你的朋友对你谈恋爱感到生气,这只是你花在他们身上的时间减少的自然结果。我认为他们不一定是在“报复”你谈恋爱,这只是你与某人相处时间减少时自然会发生的事情。


你个人有权选择什么对你重要。我认为正确的态度是:嘿,我最近花了很多时间和我的伴侣在一起,现在我们的关系很好,这部分生活正在绽放。至于友谊,我很感激,但我也能接受我们不如以前亲密的事实。当你花更多时间与一个人相处,而减少与其他人相处的时间时,这自然会发生。

所以,当你能接受在某个方面投入较少时间所带来的自然结果时,我认为你可以减少自己的苦涩和嫉妒感,找到满足。
另一个例子是,当你开始工作时,会遇到各种各样的人。有些人会将大量时间和精力投入到公司目标中。当这种情况发生时,公司不一定是在惩罚那些有工作之外生活(如家庭、朋友)的人,但很自然地,从公司或管理者的角度来看,他们应该提拔谁?是提拔那个完成要求工作后还继续留下来做更多事的人,还是提拔那个完成要求工作后立即离开去陪伴家人、朋友或伴侣的人?从这个角度看,奖励留下来做更多事的人是合理的。
但这同样不是对有工作之外生活的人的惩罚。这只是你选择什么对你重要。如果你处于认为在公司内晋升非常重要的阶段,那么你应该相应地投入时间,努力晋升。但另一方面,在此期间,你的家庭、朋友或浪漫关系可能不会那么蓬勃发展。反之,如果花时间经营这些关系对你更重要,那么绝对应该相应地投入时间,但同时要认识到,你可能不会像那些把所有额外时间都花在公司的人那样得到那么多奖励。所以,要意识到这一点,认可这一点,并对自己的选择感到满足。
自我照顾的重要性与学业权衡 🛌

我还想说,照顾好自己也非常重要。如果你不照顾好自己,最终往往会以低于100%的效率运作,生产力也会下降。例如,如果你睡眠不足,可能会脾气暴躁,让人难以相处;或者可能生病,影响身体健康或心理健康。所以,请确保照顾好自己。
我认为统计和数据科学系的许多人都说过,自我照顾很重要,照顾好身心健康很重要。这些不只是我们喜欢说的客套话或空洞的陈词滥调,我们是真心实意的。

有时这意味着什么,你需要自己判断。但有时,照顾好自己意味着无法100%完成作业。如果生活中有太多事情发生,你只需要休息几天,无法做作业,那就意味着你无法完成作业。其自然结果是你的作业成绩会受到影响。如果你不交作业,你会因为迟交而失分;如果你只做了一半作业,你只能得到一半的分数。这就是会发生的事情。
但我希望你们能达到这样一种境界:能够说,你知道吗?我此刻的心理或身体健康更重要,我想照顾好我的健康,我可以接受我的作业成绩因此受到影响。当你能坦然接受优先考虑自身身心健康而让作业成绩受损这一自然结果时,我认为你可以更愉快地度过这个学期,拥有更多的快乐和更好的健康。
如果情况变得困难,你可以向教授、向我申请延期。有时我会同意,有时我会拒绝,这取决于具体情况。我倾向于更宽容一些,但这不保证我总是会批准你的延期请求。但即使有人不批准,也不意味着你突然就必须被成绩束缚。你总是可以说,嘿,即使这次没有延期,我也想休息一下,我可以接受失去这些分数。我认为这是你有权为自己做出的决定。
警惕无益的娱乐 🎮

最后我想提一下,要注意我所说的“无益的娱乐”。我喜欢娱乐,喜欢做有趣的活动,我认为这对心理健康非常重要。我喜欢与人相处,看电视,看电影,玩游戏等等。运动、读书、听音乐或播客等,这些都是你可以做的很棒的事情。
当你参与某种娱乐活动时,它应该是工作的休息,应该给你带来快乐和精神能量,以便在需要回归工作时,你心情更好,更享受生活。但是,有些活动,甚至可能与某些人相处,最终可能产生相反的效果,让你感到精疲力竭。也有一些电子游戏、应用程序和社交媒体网站,其设计初衷就是让人上瘾,旨在让你无限滚动。它们给你即时的多巴胺刺激,让你一轮又一轮地玩下去或不停地刷,因为这就是它们赚钱的方式。
你最终可能会在这些应用或网站上花费数小时,结束后却不知道时间去哪了,也没有感觉更好,没有觉得生活更有乐趣。在这些情况下,我认为我们应该保持警惕。我不是说你必须删除社交媒体账户,你可以选择这样做,但不必如此。不过,请务必对你参与的活动类型以及你的时间流向保持警惕和选择性。因为,你不想把生命中的数小时浪费在那些实际上并没有让你对生活更兴奋的事情上。
这就是我想说的全部内容。感谢观看本视频,我们将在下一个视频中开始讨论R语言。
总结 📝

本节课中,我们一起学习了如何正确看待学业成绩,认识到成绩不能定义个人的全部价值。我们还探讨了如何在学业(工作)、人际关系和自我照顾之间找到平衡,强调了有意识选择和接受相应后果的重要性。最后,我们提醒大家要警惕那些消耗精力而非补充能量的“无益的娱乐”。希望这些思考能帮助你在大学及未来的生活中建立更健康、更平衡的视角。
03:配置RStudio与故障排除 🛠️



在本节课中,我们将学习如何正确配置RStudio工作环境,并掌握一些常见问题的排查方法,特别是与R Markdown文档“编织”相关的问题。这些技巧将帮助你更顺畅地完成作业。






文件组织与管理 📁


上一节我们介绍了RStudio的基本界面,本节中我们来看看如何组织你的工作文件。良好的文件管理习惯是避免许多问题的关键。


强烈建议为每一次作业创建一个独立的文件夹。例如,你可以创建一个名为 homework_1 的文件夹。将作业所需的所有文件(如PDF说明、TXT数据文件和RMD文件)都下载并放入这个文件夹中。这样做可以保持工作区的整洁,并确保文件路径正确。


以下是创建和管理作业文件夹的步骤:
- 在你的电脑上(如“文档”或“桌面”)创建一个新文件夹,命名为
homework_1。 - 从课程网站下载所有作业相关文件。
- 将这些文件从“下载”文件夹移动到
homework_1文件夹中。 - 在RStudio中,通过
文件 -> 打开文件...导航到homework_1文件夹,打开你的.Rmd文件。





设置工作目录 🗂️



为了确保R能正确找到你的数据文件,必须设置正确的工作目录。






打开你的RMD文件后,请导航到菜单栏的 会话 -> 设置工作目录 -> 到源文件位置。执行此操作后,你会在控制台看到类似 setwd(“.../homework_1”) 的命令。这意味着R现在将 homework_1 文件夹视为其工作根目录。
如果工作目录设置不正确,当你尝试用类似 read.delim(“month_names.txt”) 的命令读取文件时,会遇到错误:
错误于 file(file, “rt”) : 无法打开链结
此外: 警告信息:
In file(file, “rt”) : 无法打开文件‘month_names.txt’: No such file or directory
这表示R在当前工作目录下找不到指定的文件。解决方法是确保文件在正确的文件夹中,并且已正确设置工作目录。

环境面板与代码调试 🧹




RStudio中的“环境”面板会显示当前会话中创建的所有对象(如变量、数据框)。随着你不断运行代码,这个面板会变得杂乱,有时还会导致代码在R中能运行,但无法“编织”成PDF的问题。





一个常见的故障场景是:你在R控制台中创建了一个对象(例如 x <- 1:3),然后在RMD文件中编写了使用该对象的代码(例如 x + 10)。在RMD中点击“运行”按钮时,代码可以正常执行,因为环境里已经有 x 了。但当你点击“编织”时,却会报错“object ‘x’ not found”。这是因为R Markdown为了确保结果可重现,在每次编织时都会从一个全新的、空的环境开始。







以下是排查此类问题的核心方法:
- 清空环境:点击环境面板上的扫帚图标,或运行命令
rm(list = ls())。 - 从头运行:在清空环境后,从头开始依次运行你的RMD文件中的每个代码块。这样能模拟编织时的真实环境。
- 定位错误:当某个代码块因对象不存在而报错时,你就找到了问题所在——你需要确保在RMD文件内部,所有用到的对象都已被正确创建。




如果问题依然顽固,可以尝试“新建文档”法:
- 创建一个全新的R Markdown文档并尝试编织,确认它能正常工作。
- 将原有问题文档中的内容(标题、文本、代码块)逐段复制到新文档中。
- 每复制一段,就尝试编织一次。
- 当编织失败时,问题就出在你刚刚复制的那段内容里。你可以集中精力检查并修复那段代码或文本。

重要设置与快捷操作 ⚙️


正确的全局设置可以避免许多潜在问题。
请进入 工具 -> 全局选项... 进行以下设置:
- 常规选项卡:取消勾选“退出时保存工作空间到 .RData 文件”,并选择“从不”。
- 同样,取消勾选“启动时恢复 .RData 文件到工作空间”。
这样做可以确保每次启动RStudio都是一个干净的会话,避免旧变量干扰新代码,并促使你养成保存脚本的好习惯。


此外,将R的语言设置为英语非常重要,这可以避免在编织PDF时因错误信息包含非英文字符而与LaTeX发生冲突。你可以在系统环境变量或R的启动参数中进行设置。




掌握一些快捷操作能提升效率:
- 注释/取消注释代码:选中多行,按
Ctrl/Cmd + Shift + C。 - 运行当前行或选中代码:
Ctrl/Cmd + Enter。 - 运行整个脚本:
Ctrl/Cmd + Shift + S。 - 清空控制台:
Ctrl/Cmd + L。 - 编织文档:
Ctrl/Cmd + Shift + K。 - 新建R脚本:
Ctrl/Cmd + Shift + N。




总结 📝




本节课中我们一起学习了配置RStudio高效工作环境的核心步骤。我们强调了为每次作业创建独立文件夹、正确设置工作目录的重要性。重点掌握了通过清空环境并从头运行来调试R Markdown编织故障的方法,并介绍了“新建文档”排查法等高级技巧。最后,我们配置了关键的全局选项,并记住了一些实用的快捷键。遵循这些实践,将能有效减少你在完成计算作业时遇到的技术障碍。
04:R语言中的向量 📊


在本节课中,我们将要学习R语言中最核心的数据结构之一:向量。我们将了解向量的两种主要类型、它们的数据类型、自动类型转换(强制转换)的规则,以及如何为向量添加属性和名称。

原子向量与列表
R语言中最重要的对象家族是向量。我们有两种类型的向量:原子向量和通用向量(通常称为列表)。
在原子向量中,所有元素必须是相同类型的,不能混合不同类型的值。而在列表中,可以包含不同类型的对象。

此外,NULL是一个特殊值,用于表示不存在的对象或零长度的通用向量。
原子向量的类型
原子向量主要有六种类型,其中四种最为常用:
- 逻辑型:用于存储
TRUE或FALSE值(也称为布尔值)。 - 数值型:包含两种子类型:
- 双精度型:用于存储浮点数(带小数点的数字)。
- 整型:用于存储整数。
- 使用
mode()函数检查时,两者都返回"numeric"。
- 字符型:用于存储字符串。

以下是创建和检查类型的示例代码:
# 创建双精度向量
double_vec <- c(1, 2, 3)
typeof(double_vec) # 返回 "double"

# 创建整型向量
int_vec <- c(1L, 2L, 3L) # 使用 L 后缀
typeof(int_vec) # 返回 "integer"
# 使用冒号运算符创建整数序列
seq_vec <- 1:3
typeof(seq_vec) # 返回 "integer"
# 检查类型
is.double(double_vec) # TRUE
is.integer(int_vec) # TRUE
强制转换


当我们将不同类型的数据组合成一个原子向量时,R会自动进行强制转换,将所有元素转换为最不严格(最通用)的类型。

强制转换的优先级顺序为:逻辑型 < 整型 < 双精度型 < 字符型。
上一节我们介绍了向量的基本类型,本节中我们来看看当不同类型混合时会发生什么。

以下是强制转换的示例:
# 逻辑型 + 整型 + 双精度型 -> 双精度型
logical_vec <- c(TRUE, FALSE)
integer_val <- 1L
double_vals <- c(5, 6, 7)
combined_num <- c(logical_vec, integer_val, double_vals)
typeof(combined_num) # "double"
# 结果: 1, 0, 1, 5, 6, 7 (TRUE->1, FALSE->0)
# 逻辑型 + 双精度型 + 字符型 -> 字符型
char_vals <- c("A", "B")
combined_char <- c(logical_vec, double_vals, char_vals)
typeof(combined_char) # "character"
# 结果: "TRUE", "FALSE", "5", "6", "7", "A", "B"

在使用数学或逻辑运算符时,也会发生自动强制转换。


以下是相关操作的示例:

logical_vec <- c(FALSE, FALSE, TRUE)
# 数学函数强制转换为数值
sum(logical_vec) # 1 (0+0+1)
mean(logical_vec) # 0.333 (1/3)
# 显式类型转换
as.numeric(logical_vec) # 0, 0, 1
as.character(logical_vec) # "FALSE", "FALSE", "TRUE"
as.logical(c(0, 1, -5)) # FALSE, TRUE, TRUE
# 无法转换时产生 NA
as.numeric("dog") # NA (并带有警告)
关于逻辑值转换,需要注意:
0强制转换为FALSE,任何非零数值都转换为TRUE。- 只有特定的拼写(如
TRUE,True,FALSE,False)能被识别。缩写T和F可能被覆盖,不建议使用。


列表(通用向量)


列表是R中最灵活的对象,它是一个有序的对象集合。列表中的组件可以是R中的任何对象,包括其他列表。
我们可以将数据结构这样理解:
- 一维,元素类型相同:原子向量。
- 一维,元素类型不同:列表。
- 二维,元素类型相同:矩阵。
- 二维,列类型不同:数据框。
- 高维,元素类型相同:数组。

以下是创建和操作列表的示例:
# 创建一个包含不同类型元素的列表
list1 <- list(1:3, "A", c(TRUE, FALSE, TRUE), c(2.3, 5.9))
str(list1) # 显示结构

# 使用 c() 组合列表(会强制转换向量为列表)
list_a <- list(1, 2)
vec_b <- c(3, 4)
combined_list <- c(list_a, vec_b) # 结果为长度为4的列表: 1, 2, 3, 4
# 使用 list() 嵌套列表
nested_list <- list(list(1, 2), c(3, 4))
str(nested_list) # 顶层列表长度为2,第一个元素是子列表
属性

原子向量和列表都可以拥有属性。属性是一个命名的元数据列表,可以附加任何额外信息。
两个特别重要的属性是:
- 维度:将向量转换为矩阵或数组。
- 类:用于R的面向对象编程系统(如S3)。


以下是查看和设置属性的示例:
# 查看内置数据框的属性
data(trees)
attributes(trees)
# 输出通常包含: $names (列名), $class (类,如"data.frame"), $row.names (行名)

# 自定义属性
x <- 1:10
attr(x, "description") <- "这是一个数字序列"
attributes(x)
# 也可以一次性设置多个属性
attributes(x) <- list(description="我的向量", creator="我", date=Sys.Date())

向量的命名

我们可以为向量的每个元素赋予名称,这些名称会存储在names属性中。
以下是命名向量的方法:

# 创建时直接命名
named_vec <- c(a=1, b=2, c=3)
named_vec

# 创建后赋值名称
vec <- 1:3
names(vec) <- c("A", "B", "C")
vec
# 使用 setNames 函数
vec2 <- setNames(1:3, c("X", "Y", "Z"))
vec2
# 移除名称
unname(vec) # 返回一个不带名称的新向量
names(vec) <- NULL # 直接移除原向量的名称属性



本节课中我们一起学习了R语言中向量的核心概念。我们了解了原子向量(元素类型必须一致)和列表(可包含不同类型)的区别,掌握了逻辑型、整型、双精度型和字符型这四种主要数据类型。我们探讨了强制转换的规则,即当混合类型时,数据会向更通用的类型(字符型 > 双精度型 > 整型 > 逻辑型)转换。此外,我们还学习了如何为数据结构添加属性和名称来存储元数据。理解这些基础数据结构是有效使用R进行数据操作和分析的关键。
05: 因子、矩阵与数据框 📊



在本节课中,我们将学习R语言中三种更复杂的数据结构:因子、矩阵和数据框。这些结构是处理和分析数据的基础,理解它们对于后续的统计计算至关重要。
因子(Factors)


上一节我们介绍了向量和列表。本节中,我们来看看因子。因子用于表示分类变量。从内部看,因子本质上是一个整数向量,但它带有属性。最重要的属性是水平,它定义了因子可以取哪些类别值,并且其类被标记为“因子”。




例如,我们有一个字符向量 gender:
gender <- c("M", "F", "F", "X", "M", "F")
使用 factor() 函数可以将其转换为因子:
gender_factor <- factor(gender)
打印 gender_factor 会显示值 M F F X M F 以及水平 F M X。注意,打印因子时默认没有引号。
以下是关于因子的几个关键点:


- 内部存储机制:因子内部存储的是整数索引,而非原始字符。例如,水平
F M X分别对应整数1 2 3。原始向量c("M", "F", "F", "X", "M", "F")在内部被存储为c(2, 1, 1, 3, 2, 1)。可以使用as.integer(gender_factor)查看。 - 水平的顺序:默认情况下,R会提取字符向量中的所有唯一值,并按字母顺序将它们设置为水平。因此,
F排在M之前,M排在X之前。 - 处理数值型因子时的陷阱:如果将数值向量转换为因子,直接进行数值运算可能会得到意外结果。因为R将其视为分类变量,而非数值。
如果对因子直接使用x <- c(0, 1, 10, 5) x_factor <- factor(x) mean(x_factor) # 返回 NAas.numeric(),返回的是其内部整数索引(1, 2, 3...),而非原始数值。要计算原始数值的均值,需要先转换为字符,再转换为数值:mean(as.numeric(as.character(x_factor))) # 返回正确结果 4 - 赋值限制:尝试为因子分配一个不在其水平列表中的值,会导致产生
NA。gender_factor[1] <- "Mail" # 会生成 NA




矩阵(Matrices)

现在,让我们从一维结构转向二维结构。R中的矩阵本质上是一个带有维度属性的原子向量。维度属性是一个包含行数和列数的向量。




创建矩阵的一种方法是先创建向量,然后为其设置维度属性:
m <- 1:10 # 创建一个整数向量
attr(m, "dim") <- c(2, 5) # 设置维度属性为2行5列
现在,m 变成了一个2x5的矩阵,数据按列优先的顺序填充。此时,class(m) 会返回 “matrix”。

要移除矩阵结构,只需将维度属性设为 NULL:
attr(m, “dim”) <- NULL
这样,m 就又变回了一个一维的原子向量。




数组(Arrays)与数据框(DataFrames)



矩阵是二维数组。数组是更一般的概念,其维度属性的长度可以大于2,从而形成三维或更高维的数据结构。
a <- 1:12
attr(a, “dim”) <- c(2, 3, 2) # 创建一个2行、3列、2层的三维数组
数据填充顺序是:先填满第一层的行,再填列,然后移动到下一层。

最后,我们介绍数据框。数据框是R中最常用的数据结构之一,用于存储表格数据。与矩阵不同,数据框的每一列可以包含不同类型的数据(如数值、字符、逻辑值)。

从内部看,数据框被存储为一个列表,列表中的每个元素都是一个等长的向量,对应数据框的一列。

R自带了一个示例数据集 trees。我们可以检查它的结构:
class(trees) # 返回 “data.frame”
typeof(trees) # 返回 “list”,表明其内部是列表
str(trees) # 显示数据结构:一个包含3列(Girth, Height, Volume)、每列31个数值向量的数据框
数据框的属性包括列名、类(data.frame)和行名(默认为1到n的整数序列)。



本节课中我们一起学习了R语言中三种重要的数据结构:用于处理分类数据的因子,用于存储同类型二维数据的矩阵,以及用于存储表格型异质数据的数据框。理解这些结构的内部原理和特性,是高效进行数据操作和统计分析的关键。
06:R语言中的NA、Null、NaN与向量循环使用规则 🧮


在本节课中,我们将学习R语言中的几个特殊值:NA、NULL和NaN。理解它们之间的区别对于正确处理数据至关重要。我们还将探讨R语言中向量化运算的一个强大特性——循环使用规则。
特殊值:NA、NULL与NaN
R语言使用几种特殊值来表示缺失、不存在或未定义的数值。

NA:缺失值

NA用于表示缺失或未知的值。每种数据类型都有其对应的NA表示:
NA逻辑型NA_integer_整型NA_real_双精度浮点型NA_character_字符型
当NA被放入一个向量时,它会被强制转换为该向量的类型。例如,将NA放入字符向量,它会变成字符型的NA。
检测NA:不能使用 == 运算符来检查一个值是否为NA。因为 NA == NA 的结果是 NA(未知)。必须使用 is.na() 函数。
is.na(NA) # 返回 TRUE
NA == NA # 返回 NA

NULL:空对象
NULL表示一个不存在的对象。它是一个长度为零的向量,并且是它自己的数据类型 (NULL 型)。
检测NULL:使用 is.null() 函数。
is.null(NULL) # 返回 TRUE
NULL的特性:任何与NULL进行的运算通常都会返回一个长度为零的向量。将NULL合并到向量中,相当于没有添加任何东西。
length(c(4, 5, NULL, 3)) # 返回 3,向量为 c(4, 5, 3)
NaN:非数字
NaN 表示 “非数字”,它总是双精度浮点型。它通常由未定义的数学运算产生,例如 0 / 0 或 Inf - Inf。
注意:在Python中,
NaN也常用来表示缺失值,但在R中,表示缺失值的标准方式是NA。
概念辨析:NA vs NULL
理解NA和NULL的区别是关键:
NA用于表示存在但未知的值。NULL用于表示根本不存在的值。
举例说明:假设我们记录人物信息。
- Joe的年龄未知 → 在“年龄”字段填入
NA(年龄存在,但我们不知道)。 - Joe没有伴侣 → 在“伴侣姓名”和“伴侣年龄”字段填入
NULL(这些属性对Joe来说不存在)。 - 如果Joe有伴侣,但伴侣年龄未知 → 在“伴侣年龄”字段填入
NA。
零长度向量
R中每种数据类型都可以有长度为零的向量。它们与NULL不同,因为它们具有明确的类型。
# 零长度逻辑向量
l <- logical(0)
length(l) # 0
typeof(l) # "logical"
# 零长度双精度向量
d <- double(0)
length(d) # 0
typeof(d) # "double"

对零长度向量使用 is.na() 或 is.null() 会返回长度为零的逻辑向量(logical(0)),因为提问的对象本身是空的。
在列表中的应用:
- 空列表
list()的长度为0。 - 包含一个
NULL元素的列表list(NULL)长度为1。 - 包含一个零长度向量的列表
list(numeric(0))长度也为1。
这三者是不同的概念。
向量化运算与循环使用规则
上一节我们介绍了R中的特殊值,本节我们来看看R语言一个强大的特性:向量化运算。

R能够非常快速地对整个向量执行元素级运算。
x <- c(1, 2, 3)
y <- c(100, 200, 300)
x + y # 返回: 101 202 303
x * y # 返回: 100 400 900

循环使用规则
当对两个长度不同的向量进行运算时,R会自动循环使用较短的向量,直到其长度与较长的向量匹配。
规则匹配时:
c(1, 2, 3) + c(100, 200, 300, 400, 500, 600)
# 计算过程:1+100, 2+200, 3+300, 1+400, 2+500, 3+600
# 返回: 101 202 303 401 502 603

规则不匹配时(发出警告):
如果较长向量的长度不是较短向量长度的整数倍,R仍会执行操作,但会发出警告。
c(1, 2, 3) + c(100, 200, 300, 400, 500) # 会收到警告
矩阵中的循环使用
在矩阵运算中,循环使用是按列进行的。这有时可能不符合我们的直觉。
假设我们有一个矩阵 m:
m <- rbind(c(1, 2, 3),
c(4, 5, 6),
c(7, 8, 9),
c(10, 11, 12))
x <- c(100, 200, 300)
直接执行 m + x,向量x会沿着矩阵m的列向下循环添加,这可能不是我们想要的行方向添加。
实现按行添加的技巧:
如果想实现按行添加(即每一行都加上向量x),可以通过转置矩阵来实现。
# 步骤:转置 -> 按列循环加x -> 再转置回来
result <- t(t(m) + x)
这样,x中的元素(100, 200, 300)就会被依次添加到m的每一行中。
总结
本节课中我们一起学习了R语言中三个关键的特殊值:
NA:代表存在但缺失的值,使用is.na()检测。NULL:代表不存在的对象,使用is.null()检测。NaN:代表非数字的浮点值,由无效数学运算产生。

我们还探讨了零长度向量的概念,以及R语言强大的向量化运算和循环使用规则。理解循环使用规则,特别是其在矩阵中按列进行的特性,对于正确执行向量和矩阵运算至关重要。当需要按行操作时,可以巧妙地运用转置函数 t() 来实现目标。
07:原子向量的子集选取



在本节课中,我们将学习如何在R语言中对原子向量进行子集选取。子集选取是数据操作的基础,它允许我们从向量中提取、排除或重新排列元素。我们将探讨四种主要方法:使用正整数、负整数、逻辑向量和字符向量。
子集选取方法概述



原子向量是R中最基本的数据结构之一。子集选取意味着从向量中选择一部分元素。以下是一个简单的向量 x:
x <- c(2.1, 4.2, 3.3, 5.4)
向量中每个元素的位置(索引)从1开始。我们将学习如何通过不同的索引方式来操作这个向量。
使用正整数选取
使用正整数进行子集选取会返回指定位置上的元素。其基本语法是 向量[索引向量]。
以下是使用正整数选取子集的示例:
x[3]返回第三个元素:3.3。x[c(3, 1)]返回第三和第一个元素:c(3.3, 2.1)。order(x)函数返回一个索引向量,如果按此顺序选取x,则x[order(x)]会将向量从小到大排序。- 可以重复选取同一位置,例如
x[c(1, 1)]会返回c(2.1, 2.1)。 - 如果索引不是整数,R会将其截断(而非四舍五入)。例如,
x[c(2.1, 2.9)]会被当作x[c(2, 2)]处理。 - 使用非精确的浮点数(如
1.999999999)作为索引可能导致意外结果,因为它会被截断为1。

使用负整数排除
使用负整数进行子集选取会排除指定位置上的元素,返回其余所有元素。

以下是使用负整数排除元素的示例:
x[-3]返回除第三个元素外的所有元素:c(2.1, 4.2, 5.4)。x[-c(3, 1)]排除第三和第一个元素,返回:c(4.2, 5.4)。- 重要规则:不能在同一个索引向量中混合使用正整数和负整数,例如
x[c(-1, 2)]会导致错误。必须统一使用负号来排除元素。
使用逻辑向量筛选

使用逻辑向量(仅包含 TRUE 和 FALSE 的向量)进行子集选取,会返回所有对应位置为 TRUE 的元素。这是根据条件筛选数据的强大工具。
以下是使用逻辑向量筛选的示例:
- 直接使用逻辑向量:
x[c(TRUE, TRUE, FALSE, FALSE)]返回前两个元素:c(2.1, 4.2)。 - 使用逻辑条件:
x[x > 3]返回所有大于3的元素:c(4.2, 3.3, 5.4)。 - 如果逻辑向量比被选取的向量短,R会循环使用该逻辑向量。例如,
x[c(TRUE, FALSE)]等价于x[c(TRUE, FALSE, TRUE, FALSE)],返回c(2.1, 3.3)。 - 如果逻辑向量中包含
NA(缺失值),则对应位置的输出也是NA。例如,x[c(TRUE, TRUE, NA, FALSE)]返回c(2.1, 4.2, NA)。

特殊索引情况
在子集选取中,有两个特殊但有用的索引值。
以下是两种特殊索引的说明:
- 空索引:在方括号中不放入任何内容(
x[])会返回整个原始向量。这在矩阵或数据框操作中非常有用,例如选择所有行或所有列。 - 零索引:使用
x[0]会返回一个零长度的向量。这通常不直接用于数据选取,但在编写函数时,用于测试函数是否能正确处理空输入非常有用。


使用字符向量(命名向量)选取

如果向量具有名称属性(即命名向量),则可以使用字符向量(名称)来选取子集。首先,我们需要为向量设置名称。
以下是使用命名向量进行子集选取的步骤和示例:
- 创建命名向量:
y <- c(a = 2.1, b = 4.2, c = 3.3, d = 5.4) - 使用名称选取:
y[c("d", "c", "a")]会按指定名称顺序返回元素:c(5.4, 3.3, 2.1)。名称也可以重复。 - 精确匹配:名称的拼写必须完全一致,包括大小写。如果名称不匹配,R会返回
NA。例如,y[c("a", "e")]会返回c(2.1, NA)。

应用:创建查找表
命名向量的一个巧妙应用是创建“查找表”或“映射表”,用于批量替换值。
假设我们有一个包含性别代码的向量 x:
x <- c("M", "F", "U", "F", "F", "M")
我们希望将 "M" 替换为 "male","F" 替换为 "female","U" 替换为 NA。
以下是创建和使用查找表的步骤:
- 创建查找向量:这个向量的元素是目标值,名称是原始代码。
lookup <- c(M = "male", F = "female", U = NA) - 使用原始向量作为索引:通过
lookup[x],R会使用x中的每个值作为名称去lookup中查找对应的元素。 - 移除名称(可选):结果会保留查找向量的名称,使用
unname()函数可以移除它们。result <- unname(lookup[x]) # 结果: c("male", "female", NA, "female", "female", "male")
这种方法简洁高效,特别适用于将分类代码(如月份缩写、国家代码)转换为可读标签。
课程总结



本节课中,我们一起学习了在R中对原子向量进行子集选取的四种核心方法。我们了解了如何使用正整数提取特定位置的元素,使用负整数排除不需要的元素,使用逻辑向量根据条件筛选数据,以及如何为向量命名后使用字符向量通过名称进行选取。最后,我们还探讨了如何利用命名向量创建“查找表”来实现值的批量替换,这是一个非常实用的数据清理技巧。掌握这些子集选取技术是进行有效数据分析和操作的基础。
08:R语言中的列表子集选取

在本节课中,我们将要学习R语言中列表的子集选取操作。列表与原子向量类似,但因其可以包含不同类型的元素,所以选取方式有所不同。我们将重点理解单方括号 [] 和双方括号 [[]] 的区别,并通过比喻和示例来掌握其用法。

列表与原子向量的相似与不同

列表和原子向量都是一维数据结构,这是它们的相似之处。但列表可以包含其他对象,例如向量、矩阵,甚至其他列表。因此,我们需要区分“列表本身的内容”和“列表中的元素”。为此,R语言提供了两种主要的子集选取操作符:单方括号 [] 和双方括号 [[]]。
上一节我们介绍了向量的子集选取,本节中我们来看看列表的选取有何不同。


核心概念:单方括号 vs. 双方括号
理解这两种操作符是掌握列表子集选取的关键。


- 单方括号
[]:总是返回一个列表。你可以把它想象成选取一个或多个“火车车厢”,返回的仍然是一个(可能更小的)火车。 - 双方括号
[[]]:返回列表中的实际内容。它只能处理一个索引值,并“拉出”该位置存储的对象。 - 美元符号
$:功能与双方括号类似,但它是通过名称来选取元素。例如list$name等同于list[["name"]]。


列表的火车比喻
一个理解列表的好方法,是将其想象成一列火车。
- 列表本身就是一列火车。
- 列表中的每个元素就是一个火车车厢。
- 每个车厢里可以装载不同的“货物”(如向量、字符、数据框等)。



根据这个比喻:
x[[5]]获取的是5号车厢里的货物。x[4:6]获取的是由4、5、6号车厢组成的一列新火车。

基础示例解析

让我们通过代码示例来具体理解。



示例1:创建基础列表
# 创建一个包含三个元素的列表
x <- list(c(1, 2, 3), "A", c(4, 5, 6))
以下是不同操作的结果:
x[1]- 返回:一个列表,其第一个元素是向量
c(1, 2, 3)。 - 解释:选取了第一号车厢(一个列表)。
- 返回:一个列表,其第一个元素是向量
x[[1]]- 返回:向量
c(1, 2, 3)。 - 解释:提取了第一号车厢里的货物(一个向量)。
- 返回:向量

示例2:更多单方括号操作
假设 x 是我们的三节车厢火车。
x[1:2] # 返回由第1、2节车厢组成的新火车(一个列表)
x[-2] # 返回没有第2节车厢的火车(一个列表)
x[c(1,1)]# 返回第1节车厢重复两次的火车(一个列表)
x[0] # 返回一辆空火车(一个长度为0的列表)


使用名称进行选取


当列表元素有名称时,选取会更加直观。

示例3:带名称的列表
x <- list(a = c(1,2,3), b = "A", c = c(4,5,6))

以下是选取操作:



x[["a"]]或x$a- 返回:向量
c(1, 2, 3)。 - 解释:提取名为“a”的车厢里的货物。
- 返回:向量
x[c("a", "b", "b")]- 返回:一个列表,包含a车厢、b车厢和另一个b车厢(重复)。
- 解释:单方括号允许使用向量进行选取,返回一个列表。
重要区别:双方括号 [[]] 一次只能提取一个元素。
x[[c("a", "b")]] # 这会报错:“下标出界”
这种写法在R中被称为“递归子集选取”,它期望 x[["a"]] 本身也是一个列表,然后继续从中选取 "b"。由于其易混淆性,不建议初学者使用。

深入理解:结构查看与嵌套列表


使用 str() 函数查看列表结构非常有用。


示例4:查看结构
d <- list(a = c(1,2,3), b = c(TRUE, TRUE, FALSE), c = "A")
str(d)
str(d) 的输出清晰地展示了列表 d 的结构:三个有名称的元素,分别是双精度向量、逻辑向量和字符向量。

示例5:嵌套列表
列表中可以包含列表,形成嵌套结构。
l1 <- list(1:8, letters[1:4], c(5,4,3,2,1)) # 一个无名列表
l2 <- list(l1, c(10,20,30), letters[4:7]) # l2的第一个元素是列表l1
理解嵌套列表的选取需要层层递进:
l2[1]- 返回:一个列表,其唯一元素是列表
l1。 - 解释:选取l2的第一节车厢(里面装着一列小火车l1)。
- 返回:一个列表,其唯一元素是列表
l2[[1]]- 返回:列表
l1。 - 解释:提取l2第一节车厢里的货物(即小火车l1本身)。
- 返回:列表
l2[[1]][[1]]- 返回:向量
1:8。 - 解释:先提取l2中的l1,再提取l1中的第一个向量。
- 返回:向量
l2[[1]][[1]][2]- 返回:数值
2。 - 解释:在得到向量
1:8后,选取它的第二个元素。
- 返回:数值

边界情况与错误处理

处理边界索引(如越界或空值)时,单双括号的行为不同,这也是重要的知识点。
以下是处理原子向量和列表时的主要规则:
- 原子向量越界索引
vec[5](vec长度仅为3):返回NA。vec[[5]]:报错“下标出界”。
- 列表越界索引
list[5](list长度仅为3):返回一个包含NULL的列表。list[[5]]:报错“下标出界”。
- 使用
NULL或0索引vec[0]或vec[NULL]:返回一个零长度的原子向量。list[0]或list[NULL]:返回一个零长度的列表。vec[[NULL]]或list[[NULL]]:报错。

本节课总结

本节课中我们一起学习了R语言列表的子集选取。
- 核心操作符:单方括号
[]用于选取“车厢”(返回列表),双方括号[[]]或美元符号$用于提取“货物”(返回内容)。 - 核心比喻:将列表想象成一列火车,有助于理解不同操作符的结果。
- 嵌套选取:对于嵌套列表,需要从外向内逐层选取,例如
list[[1]][[2]]。 - 边界情况:单括号对越界索引更宽容(返回
NA或含NULL的列表),而双括号会直接报错,这有助于调试代码。




熟练掌握列表的子集选取,是有效操作R中复杂数据结构的基础。请务必通过实际练习来巩固这些概念。
09:R中二维数据结构的子集选取


在本节课中,我们将学习如何在R中对矩阵、数组和数据框等二维数据结构进行子集选取。我们将探讨多种选取方法,并理解“简化”与“保留”结构之间的区别。

矩阵与数组的子集选取

上一节我们介绍了一维结构(原子向量和列表)的子集选取。本节中,我们来看看二维结构,首先从矩阵或数组开始。

最常见的矩阵子集选取方法是使用多个向量,每个维度对应一个向量,用逗号分隔。如果某个维度留空,则会返回该维度的所有元素。
例如,我们有一个3x3的矩阵 A:
A <- matrix(1:9, nrow=3, ncol=3, dimnames = list(NULL, c("A", "B", "C")))
# A B C
# [1,] 1 4 7
# [2,] 2 5 8
# [3,] 3 6 9

以下是几种选取方式:
A[1:2, ]:选取第1、2行,所有列。A[c(TRUE, FALSE, TRUE), c("B", "A")]:使用逻辑和字符向量组合选取第1、3行的B列和A列。A[0, -2]:选取0行(即无行),并排除第2列,结果是一个仅有列名A和C的空结构。


默认情况下,对矩阵进行子集选取会进行“简化”。例如,A[1, ] 返回的是一个命名向量,而不是一个1行的矩阵。


由于矩阵本质上是带有维度属性的原子向量,因此也可以使用单个向量进行选取。此时,R会将矩阵视为一个长向量,按列优先的顺序进行索引。
vowels <- matrix(letters[1:25], nrow=5)
vowels[8:9] # 返回第8和第9个元素,即 "h" 和 "i"

你还可以用一个坐标矩阵来选取子集。矩阵的每一行代表你想要选取的元素的坐标。
# 创建一个坐标矩阵
coord <- matrix(c(1,5, 3,1, 2,3, 1,1), ncol=2, byrow=TRUE)
vowels[coord] # 返回 (1,5), (3,1), (2,3), (1,1) 位置上的元素


对于更高维度的数组,原理相同。你需要提供一个列数与数组维度数相同的坐标矩阵。
arr <- array(1:12, dim=c(2,3,2))
coord_arr <- matrix(c(1,2,1, 2,3,2), ncol=3, byrow=TRUE)
arr[coord_arr] # 返回指定坐标的元素

数据框的子集选取
数据框在内部以列表形式存储,但同时也具有二维结构。因此,你可以像子集列表一样选取数据框,也可以像子集矩阵一样选取。
让我们创建一个简单的数据框 df:
df <- data.frame(x=1:4, y=4:1, z=letters[1:4])
像矩阵一样选取(使用逗号):
df[df$y %% 2 == 0, ] # 选取y列为偶数的所有行,所有列

像列表一样选取(不使用逗号):
df[c("x", "z")] # 选取名为"x"和"z"的列(返回数据框)
这与 df[, c("x", "z")] 的结果类似。

关键区别在于逗号的使用:
df[1:3, ]:选取第1到3行,所有列。df[1:3]:选取第1到3列。

当只选取一列时,不同方法的结果形式不同:
df["x"]或df[1]:返回一个单列的数据框(保留结构)。df[, "x"]或df[, 1]:返回一个原子向量(简化结构)。df$x或df[["x"]]:同样返回一个原子向量(简化结构)。


简化与保留

在子集选取时,理解结果是“简化”了原始结构还是“保留”了原始结构非常重要。

以下是控制简化行为的总结:
- 原子向量:
[保留名称;[[丢弃名称。 - 列表:
[总是返回列表;[[提取列表中的元素。 - 因子:
[默认保留未使用的水平;可以使用drop=TRUE参数来丢弃未使用的水平。 - 矩阵:默认会简化(例如,单行/单列变为向量)。使用
drop=FALSE参数可以保留矩阵结构。a <- matrix(1:4, nrow=2) a[1, ] # 简化成向量 a[1, , drop=FALSE] # 保留为 1x2 的矩阵 - 数据框:
[选取单列时返回数据框(保留);[[或$选取单列时返回向量(简化)。



常见错误与示例



让我们通过 mtcars 数据集来看一些常见的子集选取错误。

错误1:遗漏逗号
mtcars[mtcars$cyl <= 5] # 错误:未定义列被选中
原因:没有逗号,逻辑向量 mtcars$cyl <= 5 被解释为要选取的列索引,而不是行索引。由于数据框没有那么多列,所以报错。
正确做法:
mtcars[mtcars$cyl <= 5, ] # 正确:选取气缸数<=5的所有行


错误2:逻辑运算错误
mtcars[mtcars$cyl == 4 | 6, ] # 结果不正确,可能选中了所有行
原因:| 6 中的数字6被强制转换为逻辑值 TRUE,因此整个条件对所有行都成立。
正确做法:
mtcars[mtcars$cyl == 4 | mtcars$cyl == 6, ]


错误3:选取行时遗漏逗号
mtcars[1:13] # 错误:尝试选取第13列,但可能不存在
正确做法:
mtcars[1:13, ] # 正确:选取前13行

总结 🎯



本节课中,我们一起学习了R中二维数据结构的子集选取。
- 对于矩阵和数组,可以使用多个索引向量、单个向量或坐标矩阵进行选取,并需要注意默认的简化行为。
- 对于数据框,可以混合使用列表式(单括号)和矩阵式(逗号分隔)的选取方法,理解逗号的有无会导致完全不同的结果。
- 我们深入探讨了简化与保留的概念,知道了如何使用
drop参数来控制输出结构。 - 最后,我们分析了几种常见的子集选取错误,帮助你避免在编程中踩坑。




熟练掌握这些子集选取技巧,是高效进行数据操作和分析的基础。
10:条件语句



在本节课中,我们将学习R语言中的条件语句。条件语句是编程中控制代码执行流程的核心工具,它允许程序根据特定条件决定执行哪些代码块。我们将从逻辑运算符开始,逐步深入到if、else if、else以及ifelse语句的使用。

逻辑运算符

上一节我们介绍了条件语句的基本概念,本节中我们来看看构成条件的基础——逻辑运算符。这些运算符用于产生逻辑值(TRUE或FALSE),它们是构建条件判断的基石。
R语言中的向量化逻辑运算符包括:
|:逻辑“或”。只要x或y中有一个为TRUE,结果即为TRUE。公式:x | y&:逻辑“与”。只有x和y都为TRUE时,结果才为TRUE。公式:x & y!:逻辑“非”。将TRUE变为FALSE,将FALSE变为TRUE。公式:!xxor():逻辑“异或”。当x和y中只有一个为TRUE时,结果为TRUE。公式:xor(x, y)
这些运算符是向量化的,意味着它们可以对向量中的每个元素进行逐对计算,并自动进行循环补齐。
此外,R还有非向量化的逻辑运算符,它们只接受长度为1的逻辑值:
||:非向量化的“或”。&&:非向量化的“与”。
如果提供给||或&&的参数长度大于1,R通常会报错。但在某些情况下,如果运算符能根据第一个值确定结果(例如FALSE && ... 或 TRUE || ...),则不会评估第二个参数,因此也不会报错。
处理缺失值NA时,逻辑运算的结果取决于运算符:
TRUE | NA结果为TRUE。TRUE & NA结果为NA。FALSE | NA结果为NA。FALSE & NA结果为FALSE。

以下是用于检查多个值的逻辑函数:
any():如果参数中任何一个元素为TRUE,则返回TRUE。可以看作是|运算符对多个值的推广。all():如果参数中所有元素都为TRUE,则返回TRUE。可以看作是&运算符对多个值的推广。

关于any()和all()函数,有一个重要的边界情况:对长度为零的逻辑向量应用这些函数时,any(logical(0))返回FALSE,all(logical(0))返回TRUE。这是为了确保向一个逻辑向量添加空向量不会改变any()或all()的结果。
比较运算符
掌握了如何组合逻辑值后,我们来看看如何生成它们。比较运算符用于比较两个值,并返回逻辑结果。

R中的比较运算符也是向量化的,它们包括:
==:等于!=:不等于<:小于>:大于<=:小于等于>=:大于等于%in%:属于。检查左侧向量的每个元素是否出现在右侧的集合中。例如:x %in% c(1, 3, 5)
比较字符向量时,顺序基于字母表,规则如下:符号 -> 数字(0-9)-> 小写字母(a-z)-> 大写字母(A-Z)。需要注意的是,字符串"10"会排在"2"之前,因为字符串比较是逐字符进行的。
R还提供了一系列is.*()函数,用于检查对象的类型、类别或特性,它们返回逻辑值。例如:
is.na():检查是否为缺失值。is.null():检查是否为NULL。is.numeric():检查是否为数值型。


If 语句

现在我们已经能够创建逻辑值,本节我们将学习如何使用if语句来根据这些值有条件地执行代码。if语句是实现条件代码执行的主要方式。
if语句的基本结构如下:
if (condition) {
# 当 condition 为 TRUE 时执行的代码
}
其中的condition必须是一个长度为1的逻辑值(TRUE或FALSE),不能是NA,也不能是长度大于1或长度为零的向量,否则会导致错误。


如果条件为真时需要执行的代码只有一行,花括号{}可以省略,但为了代码清晰,建议始终使用花括号。
让我们看几个例子:
- 有效条件:
if (1 %in% c(1, 3)) { print("hello") }会打印“hello”。 - 长度大于1的条件(错误):
if (c(1, 3) <= 2) { ... }会产生错误,因为条件c(TRUE, FALSE)的长度为2。 - 使用
any()/all()处理向量条件:if (any(c(1, 3) >= 2)) { print("can you hear me now") }会成功执行并打印。 - 条件为
NA(错误):if (NA) { ... }或if (c(1,2,3)[5] >= 2) { ... }会产生“missing value where TRUE/FALSE needed”错误。 - 长度为零的条件(错误):
if (which(c(1,2,3) == 4)) { ... }会产生“argument is of length zero”错误。

为了提高代码可读性,建议将复杂的条件判断赋值给一个具有描述性的变量,然后在if语句中使用这个变量。


Else 与 Else If
单一的if语句只能处理一种情况。为了处理多个分支条件,我们需要引入else if和else子句。它们允许我们在初始条件不满足时,检查其他条件。
if...else if...else结构允许进行多分支判断:
if (condition1) {
# 代码块1
} else if (condition2) {
# 代码块2
} else {
# 代码块3
}
else if和else必须紧跟在前面if语句的闭合花括号之后。else子句只有在所有前面的if和else if条件都为FALSE时才会执行。
需要注意条件的顺序。例如,在下面的代码中,“bigger than one”永远不会被打印,因为当x > 0为TRUE时,程序不会进入else分支去检查x > 1。
x <- 3
if (x > 0) {
print("positive")
} else if (x > 1) { # 这个条件永远不会被评估
print("bigger than one")
}

向量化的条件选择
标准的if语句不是向量化的,它一次只能处理一个逻辑值。如果我们希望对整个向量的每个元素应用条件判断,除了使用循环,还有一个更高效的工具——ifelse()函数。

ifelse()函数是向量化的条件选择函数,其语法为:
ifelse(test, yes, no)
test:一个可以产生逻辑值向量的表达式。yes:当test中对应元素为TRUE时返回的值。no:当test中对应元素为FALSE时返回的值。
ifelse()会对test向量中的每个元素进行判断,然后从yes或no中选取对应的结果,最终返回一个与test等长的向量。例如:
x <- 1:10
result <- ifelse(x %% 2 == 0, "even", "odd")
# result 将是 c("odd", "even", "odd", "even", ...)

总结

本节课中我们一起学习了R语言中条件语句的完整体系。我们从基础的逻辑运算符(&, |, !)和比较运算符(==, >, %in%等)开始,它们是生成逻辑条件的基础。接着,我们深入探讨了如何使用if语句进行条件判断,并扩展到了多分支的else if和else结构。最后,我们介绍了向量化的条件选择函数ifelse(),它可以高效地对整个向量应用条件逻辑。理解并熟练运用这些工具,对于编写能够根据不同情况做出决策的R程序至关重要。
11:循环

在本节课中,我们将要学习R语言中的循环结构。循环允许我们重复执行一段代码,每次执行时可能带有略微不同的条件或值。我们将介绍for循环、while循环和repeat循环,并探讨如何高效地存储循环结果。此外,我们还将学习break和next语句来控制循环流程,并讨论向量化操作作为替代循环、提升代码效率的重要方法。

🔄 for循环
for循环是R中最简单、最常见的循环类型。它遍历一个向量(可以是原子向量或列表)中的每个元素,并对每个元素执行代码块。

以下是for循环的基本语法示例:
for (x in 1:10) {
cat(x^2, " ")
}
这段代码会输出从1到10每个数字的平方,结果以空格分隔:1 4 9 16 25 36 49 64 81 100。

for循环也可以遍历列表。例如:
L <- list(c(1,2,3), c("a","b","c","d","e","f","g"), c(TRUE, FALSE))
for (y in L) {
print(length(y))
}
这段代码会依次输出列表中每个元素的长度:3、7、2。
📝 存储循环结果

在循环中存储结果时,最佳实践是预先分配好存储空间,而不是在循环过程中动态增长对象。这可以显著提升性能。
以下是三种存储策略的比较:
-
预先分配空间(最佳):如果你知道结果的数量,可以预先创建一个足够长的向量来存储。
n <- 1e7 results <- rep(NA, n) # 预先分配空间 for (x in seq_along(results)) { results[x] <- x^2 }这种方法速度最快。
-
动态调整大小(尚可):如果你不确定结果数量,可以从一个空对象开始,在每次迭代中调整其大小。
results <- 0 # 从一个元素开始 for (x in 1:1e7) { results[x] <- x^2 # R会自动调整向量大小 }这种方法比第一种慢,但可以接受。


- 逐步追加(避免使用):在循环中不断使用
c()函数合并结果。
这种方法效率极低,应尽量避免。results <- c() # 空向量 for (x in 1:1e5) { # 仅10万次,而非1000万次 results <- c(results, x^2) # 非常低效! }
🔁 while循环与repeat循环
上一节我们介绍了按固定次数迭代的for循环,本节中我们来看看基于条件迭代的循环。
while循环会重复执行代码块,直到其条件语句的值为FALSE。
i <- 1
results <- numeric(10)
while (i <= 10) {
results[i] <- i^2
i <- i + 1
}
# 循环结束后,i的值为11
repeat循环会无限重复执行,直到遇到break语句。
i <- 1
results <- numeric(10)
repeat {
results[i] <- i^2
i <- i + 1
if (i > 10) break # 满足条件时跳出循环
}



注意:务必确保while循环的条件最终会变为FALSE,或在repeat循环中设置合理的break条件,否则会导致无限循环。
⏸️ break与next语句

break和next是控制循环流程的两个重要语句。
break:立即终止当前正在运行的最内层循环。for (i in 1:10) { if (i %% 2 == 0) break # 当i为偶数时,终止整个循环 cat(i, " ") } # 输出: 1

next:跳过当前迭代中剩余的代码,直接进入循环的下一次迭代。for (i in 1:10) { if (i %% 2 == 0) next # 当i为偶数时,跳过本次迭代 cat(i, " ") } # 输出: 1 3 5 7 9

🔢 seq_along与seq_len函数

在创建循环索引时,seq_along()和seq_len()函数非常有用。它们能安全地生成序列,特别是在处理长度可能为0的向量时。

以下是它们的用法:
L <- list(c(1,2,3), c("a","b","c"), c(TRUE, FALSE))
# 使用 seq_along 安全地生成索引
for (i in seq_along(L)) {
print(length(L[[i]]))
}
seq_along(x)会生成一个从1到length(x)的序列。seq_len(n)会生成一个从1到n的序列。
关键区别在于当向量长度为0时:
L <- list() # 空列表,长度为0
# 以下方法会出错
for (i in 1:length(L)) { # 相当于 1:0
print(L[[i]]) # 尝试访问不存在的元素
}
# 以下方法是安全的,循环不会执行,也不会报错
for (i in seq_along(L)) { # 生成一个空序列
print(L[[i]])
}
因此,推荐使用seq_along()或seq_len()来生成循环索引。


⚡ 向量化:循环的替代方案

在R中,循环通常较慢。尽可能地将操作向量化是提升代码性能的关键。向量化意味着直接对整个向量或矩阵进行操作,而不是使用循环逐个元素处理。

考虑一个分段函数:
# 非向量化版本,只能处理单个值
f <- function(x) {
if (x <= 0) {
return(-x^3)
} else if (x <= 1) {
return(x^2)
} else {
return(sqrt(x))
}
}
# 对向量使用此函数会报错
x <- seq(-2, 2, by=0.01)
# y <- f(x) # 错误!

为了对向量使用这个函数,你可能会写一个循环:
plot_values <- numeric(length(x))
for (i in seq_along(x)) {
plot_values[i] <- f(x[i])
}



然而,更高效的方法是使用向量化的ifelse()函数重写函数:
f_vectorized <- function(x) {
ifelse(x <= 0, -x^3,
ifelse(x <= 1, x^2,
sqrt(x)))
}
plot_values <- f_vectorized(x) # 直接对整个向量操作,无需循环
ifelse(test, yes, no)会向量化地评估test条件,并从yes或no中选取对应的值组成结果向量。


🚀 利用内置的向量化函数

R提供了许多高度优化的向量化函数,应优先使用它们。


例如,计算一个大矩阵每行的平均值:
set.seed(123)
X <- matrix(rnorm(1e6 * 100), nrow=1e6, ncol=100)



# 方法1: 循环(慢)
system.time({
row_means_loop <- numeric(nrow(X))
for (i in 1:nrow(X)) {
row_means_loop[i] <- mean(X[i, ])
}
})



# 方法2: 使用apply函数(较慢)
system.time({
row_means_apply <- apply(X, 1, mean)
})

# 方法3: 使用向量化的rowSums函数(最快!)
system.time({
row_means_fast <- rowSums(X) / ncol(X)
})

# 验证结果相同
all.equal(row_means_loop, row_means_apply, row_means_fast)
在这个例子中,rowSums()配合除法比循环或apply()快几个数量级。对于列操作,也有对应的colSums()、rowMeans()和colMeans()函数。

📚 总结


本节课中我们一起学习了R语言中的循环结构。我们掌握了如何使用for、while和repeat循环来重复执行代码。我们了解了高效存储循环结果的策略,特别是预先分配空间的重要性。我们还学习了使用break和next来控制循环流程,以及使用seq_along()和seq_len()来安全地创建循环索引。




最重要的是,我们认识到在R中,向量化操作通常是比显式循环更高效的选择。我们学习了如何利用ifelse()函数和内置的向量化函数(如rowSums)来重写代码,从而大幅提升计算性能。记住Donald Knuth的忠告:避免不成熟的优化,但在那关键的3%情况下,不要放弃利用向量化提升效率的机会。
12:函数编写策略(可选)🎯
在本节课中,我们将学习如何有效地编写R函数。我们将回顾函数的基础知识,并深入探讨一种实用的策略:将复杂任务分解为更小的、可管理的部分,然后逐步构建解决方案。这对于完成更复杂的作业(如作业二)尤其重要。
函数基础回顾 📚
上一节我们介绍了课程概述,本节中我们来看看函数的基础知识。你们已经在作业一中编写过函数,对函数的基本形式应该有所了解。
在R中,函数的基本结构如下:
function_name <- function(arg1, arg2, ...) {
# 函数体:执行各种操作
result <- some_calculation
return(result) # 返回结果
}
我们使用关键字 function 来创建函数。参数名放在括号内,函数体包含在大括号 {} 中,其中可以执行各种计算、比较或赋值操作。函数最后会返回一个输出对象。
关于R函数,有几个需要注意的特点:
- 如果函数体只有一行代码,可以省略大括号,但这通常不是好的编码风格。
- 如果没有显式使用
return()命令,R将自动返回函数体中最后一个被求值的表达式的结果。 - 函数本身也是对象,可以像其他对象一样被操作,例如放入列表中。
函数只能返回一个对象。如果需要返回多个值,必须将它们组合成一个单一对象,例如向量或列表。
以下是返回多个值的示例:
# 返回向量
function1 <- function(x) {
c(x, x^2, x^3)
}
# 返回带名称的列表
function2 <- function(x) {
list(value = x, square = x^2, cube = x^3)
}
函数参数与调用 🔧
在定义函数时,我们指定了参数的名称。调用函数时,可以通过位置或名称来提供参数值。
- 按位置匹配:如果不指定参数名,R会按顺序将提供的值分配给参数。
- 按名称匹配:如果指定了参数名,可以以任意顺序提供参数。
- 未使用的参数:如果函数定义中包含了某个参数,但在函数体内未使用它,调用时即使不提供该参数的值,R通常也不会报错。
- 缺失的参数:反之,如果在函数体内使用了某个参数,但调用时没有为其提供值,R会报错,除非该参数有默认值。
可以通过在定义时赋值来设置默认值:
f <- function(x=1, y=1, z=1) {
paste("x=", x, "y=", y, "z=", z)
}
函数的作用域与返回值 🌐
理解函数作用域对于调试至关重要。在函数内部创建或修改的变量,其作用域仅限于该函数内部。
一般来说,不应尝试在函数内部直接修改全局环境中的变量。如果希望将函数内部的值传递到外部,应将其作为返回值的一部分。值通过参数传入函数,并通过返回对象传出函数。
请看以下示例:
x <- 10 # 全局环境中的x
f <- function(x) {
x <- x + 55 # 修改函数内部的x
return(x)
}
result <- f(x) # result 是 65
print(x) # 全局环境中的 x 仍然是 10
要改变全局变量 x 的值,需要显式赋值:x <- f(x)。
何时需要编写函数?🤔
函数的目的是让代码更易于重用和维护,使我们的生活更轻松。

一个实用的经验法则是:当你发现自己在第三次复制粘贴同一段代码(即该代码出现在四个地方)时,就应该考虑将其封装成函数了。原因在于,如果需要修改这段代码的逻辑(例如更改图表线条粗细),你只需要在函数中修改一次,而不是在多个地方重复修改,这能有效避免错误和不一致。

为函数起一个清晰易懂的名字非常重要。函数名应明确表明其功能。对于返回逻辑值(TRUE/FALSE)的函数,其名称最好是一个疑问句形式,例如 is_prime()。
函数编写策略:分解任务 🧩
上一节我们讨论了编写函数的时机,本节中我们来看看核心策略:如何将一个大任务分解为小任务。这是程序员需要培养的关键技能。我们将通过编写一个冒泡排序算法来演示这一过程。
冒泡排序的原理是重复遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
我们的目标是编写一个函数 bubble_sort(x),将向量 x 按升序排序。
我们可以将这个大任务分解为几个小任务:
- 比较并交换相邻元素:这是最基础的操作。
- 单次遍历(冒泡):对整个向量执行一次完整的比较和交换。
- 多次遍历:重复执行单次遍历,直到整个向量有序。
第一步:在全局环境中验证思路
不要一开始就写函数。建议先在全局环境中逐步编写和测试代码逻辑,这样更容易查看中间结果和调试。
首先,我们尝试实现最小的任务:交换一个向量中的前两个元素。
# 初始尝试(错误示范)
x <- c(5, 4, 3, 2, 1)
x[1] <- x[2]
x[2] <- x[1] # 此时x[1]已经是4,所以x[2]也被赋值为4,丢失了原来的5
print(x) # 输出: 4 4 3 2 1 (错误)
我们发现直接覆盖会导致数据丢失。正确的做法是使用一个临时变量:
# 正确方法
x <- c(5, 4, 3, 2, 1)
temp <- x[1]
x[1] <- x[2]
x[2] <- temp
print(x) # 输出: 4 5 3 2 1 (正确)
然后,我们加入比较条件,只在需要时交换:
x <- c(5, 4, 3, 2, 1)
if (x[1] > x[2]) {
temp <- x[1]
x[1] <- x[2]
x[2] <- temp
}
print(x) # 输出: 4 5 3 2 1
第二步:实现单次遍历
接下来,我们需要将相邻元素的比较和交换应用到向量的每一对元素上。这需要一个循环。
x <- c(5, 4, 3, 2, 1)
n <- length(x)
for (i in 1:(n-1)) { # 注意是 n-1,因为比较的是 i 和 i+1
if (x[i] > x[i+1]) {
temp <- x[i]
x[i] <- x[i+1]
x[i+1] <- temp
}
}
print(x) # 经过一次遍历,最大的数被“冒泡”到最后: 4 3 2 1 5
每执行一次这个循环,当前向量中最大的元素就会被移动到正确的位置(末尾)。
第三步:实现多次遍历
一次遍历只能确保一个元素归位。对于一个长度为 n 的向量,我们最多需要 n-1 次遍历。我们可以将单次遍历的代码放入另一个循环中。
x <- c(5, 4, 3, 2, 1)
n <- length(x)
for (sweep in 1:(n-1)) {
for (i in 1:(n - sweep)) { # 优化:已排序的部分无需再比较
if (x[i] > x[i+1]) {
temp <- x[i]
x[i] <- x[i+1]
x[i+1] <- temp
}
}
}
print(x) # 输出: 1 2 3 4 5
第四步:封装成函数
在全局环境中验证逻辑正确后,我们就可以将其封装成一个整洁的函数了。
bubble_sort <- function(x) {
n <- length(x)
for (sweep in 1:(n-1)) {
for (i in 1:(n - sweep)) {
if (x[i] > x[i+1]) {
temp <- x[i]
x[i] <- x[i+1]
x[i+1] <- temp
}
}
}
return(x) # 记住要返回值
}
# 测试函数
test_vec <- sample(1:100, 20)
sorted_vec <- bubble_sort(test_vec)
print(sorted_vec)
应用于作业二:绘制游戏棋盘 🎲
作业二要求模拟“蛇梯棋”游戏,其中一个任务是绘制棋盘。这听起来很复杂,但同样可以分解。
绘制棋盘的基本要素是绘制网格线。我们可以使用R的 segments() 函数来画线,它需要起点 (x0, y0) 和终点 (x1, y1) 的坐标。
首先,思考如何画一条水平线:
plot.new()
plot.window(xlim=c(0,10), ylim=c(0,10), asp=1) # 设置坐标范围和纵横比
segments(x0=0, y0=0, x1=10, y1=0) # 从(0,0)到(10,0)画线
要画多条水平线,可以使用循环:
plot.new()
plot.window(xlim=c(0,10), ylim=c(0,10), asp=1)
for (row in 0:10) {
segments(x0=0, y0=row, x1=10, y1=row)
}
同理,绘制垂直线:
for (col in 0:10) {
segments(x0=col, y0=0, x1=col, y1=10)
}
这样就得到了一个基本的网格。在此基础上,你可以继续添加任务:在特定格子内添加数字标签、绘制代表“梯子”和“蛇”的箭头等。关键是从纸笔草图开始,确定每个元素的坐标,然后将每个小任务转化为代码。
总结 📝

本节课中我们一起学习了高效的函数编写策略。核心要点是:不要急于编写完整的函数,而应先将复杂问题分解为简单的子任务。在全局环境中逐步实现并测试每个子任务,确保每行代码都符合预期。最后,将验证过的代码块组装并封装成函数。这种方法能显著降低调试难度,并帮助你更有条理地构建复杂程序,例如作业二中的棋盘绘制和游戏模拟。记住,清晰的函数名和结构化的思考是写出好代码的关键。
13:R语言中的作用域规则 🧭



在本节课中,我们将要学习R语言中一个核心概念——作用域。理解作用域规则对于编写和调试R函数至关重要,它能帮助我们明白R如何查找和使用变量。

概述:绑定与作用域
作用域和绑定是R查找对象和名称的方式。

所有在R会话中创建并存储的对象都保存在一个称为工作空间或全局环境的地方。例如,当你创建一个向量并将其赋值给名称x时,这个对象就存储在全局环境中。
然而,当你运行一个函数时,函数会创建一个局部环境。你可以把它想象成一个独立的小沙盒。在这个函数内部,你可能会创建函数值或其他对象,这些对象只存在于这个局部沙盒中,不会出现在全局环境里。

当R查找值时,局部环境中存在的值会优先于全局环境中同名的值或对象。这被称为对象屏蔽。这意味着,如果全局环境中有一个名为x的向量,但函数内部也有另一个名为x的向量,那么当在函数内部调用x时,R将引用局部对象,而不是全局对象。
名称绑定与对象创建

让我们看一个简单的代码行:x <- c(1, 2, 3)。我们通常认为这是在创建一个包含值1、2、3的对象x。
从技术上讲,其工作原理是:首先在内存中的某个位置创建一个包含值1、2、3的向量对象。然后,赋值操作符将名称x绑定到该对象上。因此,当你要求R查找对象x时,它会查找这个名称,该名称指向内存中的位置,并从中获取值1、2、3。

考虑以下代码:
x <- c(1, 2, 3)
y <- x
z <- c(1, 2, 3)
y和z的输出都是c(1, 2, 3),但技术上是两个独立的对象。x和y指向我们首先创建的同一个对象(向量1,2,3),而z指向另一个新创建的、包含相同值的独立对象。

修改时的复制行为
R有一个重要的特性叫做修改时复制。让我们看看这里会发生什么:
x <- c(1, 2, 3)
y <- x
y[3] <- 4
我们创建了一个向量c(1,2,3),并将名称x绑定到它。然后将名称y绑定到x,所以x和y都指向同一个对象。但在第三行,我们修改y的第三个元素为4。当R看到我们要改变y的值时,它会创建一个副本。x仍然指向最初包含1,2,3的对象,而y现在指向一个修改后的新对象c(1,2,4)。所以x显示c(1,2,3),而y显示c(1,2,4)。

基本作用域规则

赋值是将名称绑定到值,而作用域是查找与名称关联的值的过程。变量的作用域是定义该变量的代码区域。
R的基本作用域规则很简单:它会在当前环境中查找对象,如果找不到,就会在更高层的作用域中搜索。记住,函数内部的对象创建在局部环境中,这些对象优先于全局环境中的对象。
让我们看一个例子。假设在全局环境中y <- 1。我们有一个函数:
g <- function(x) {
y <- 2
return(x + y)
}
当我们运行g(3)时,3作为参数x的值传入。在函数内部,y被赋值为2。当执行return(x + y)时,R查找y的值,它在函数的局部环境中找到了2。因此,计算3 + 2,返回5。函数内部定义的名称y屏蔽了外部的名称y。全局环境中y的值为1,但这与此函数的操作无关。

词法作用域
R的主要作用域规则是词法作用域。基本规则是:如果R在函数体作用域内找不到变量,它会在下一个更高层的作用域中查找,依此类推。
通常,在编写函数时,你不应过度依赖R的词法作用域规则。引用未在函数内部定义的对象或值通常被认为是欠佳的技术。但R最初是为统计学设计的,编程是其次,因此它不强制要求良好的编程习惯。R会尽力处理各种情况,但最好还是避免此类做法,养成良好的习惯。


让我们确保理解词法作用域规则。假设我们清空环境,然后在全局环境中执行y <- 1。我们有一个函数:
f <- function(x) {
return(x + y)
}
当我们运行f(3)时,3作为x的值传入。当执行return(x + y)时,R在函数f的局部环境中找不到y,于是它搜索更高层的作用域(即全局环境),找到了y的值为1。因此,它将返回3 + 1 = 4。
作用域的隔离性

在某个作用域内定义的变量仅存在于该作用域内,并且仅在该作用域持续期间存在。如果外部作用域存在同名变量,这些变量和对象是独立的,不会与内部作用域的变量或对象交互,它们不会被覆盖或修改。
函数是一个独立的环境。函数与“外部世界”(全局环境)沟通的主要方式是通过调用时传入的参数,以及函数返回的对象。这是函数与外界交互的唯一途径。函数内部发生的事情基本上都留在函数内部。

例如,如果在函数内部运行rm(list = ls()),这只删除函数内部定义的对象。你不应该这样做,因为这会删除传入函数的所有参数。但这说明了它只影响局部的沙盒环境。
超级赋值运算符
有一个赋值运算符称为超级赋值运算符,即<<-。它不会在局部环境中创建对象,而是尝试修改在父环境中找到的现有变量。如果它在父环境中找不到该变量,它会沿着作用域阶梯向上查找,直到找到为止。如果到达全局环境仍未找到,它将在全局环境中创建该变量。





作为一般准则,你应该避免使用超级赋值。虽然有时它似乎能让问题变得更简单,但它也可能使调试变得非常困难。

让我们看一个例子:
x <- y <- z <- 1
f <- function() {
y <<- 3
return(y)
}
运行前几行代码会在全局环境中创建x、y、z(值均为1)和函数f。调用f()时,它会执行函数体内的代码。y <<- 3不会在局部环境中创建y,而是立即修改更高层环境(即全局环境)中的y。然后return(y)查找y,在局部环境中找不到(因为y未在局部创建),于是在全局环境中找到y,其值已被改为3。因此f()返回3。之后检查全局环境中的x、y、z,会发现y的值现在是3。
嵌套函数中的作用域


考虑更复杂的嵌套函数情况:
x <- y <- z <- 1
f <- function() {
y <<- 2
g <- function() {
z <<- 3
return(x + y + z)
}
return(g())
}
首先,在全局环境中x、y、z均为1。执行f()时:
y <<- 2修改全局环境中的y为2。- 定义函数
g(但尚未执行)。 - 执行
return(g()),调用g()。 - 在
g()内部,z <<- 3尝试修改更高层作用域中的z。它在f的局部环境中找不到z,于是继续向上,在全局环境中找到z并将其修改为3。 g()返回x + y + z。查找x:不在g中,不在f中,在全局环境中找到值为1。查找y:不在g中,不在f中,在全局环境中找到值为2(已被修改)。查找z:不在g中,不在f中,在全局环境中找到值为3(已被修改)。因此返回1 + 2 + 3 = 6。f()返回6。最终,全局环境中的x、y、z值分别为1、2、3。
避免使用超级赋值的原因

超级赋值可能带来混淆。例如,考虑一个函数,它本意是修改传入的参数:
append_square <- function(foo, x) {
foo <<- c(foo, x^2)
}
f <- 2
append_square(f, 5)
运行后,全局环境中的f变成了c(2, 25)。这似乎有效。

但如果这样做:
bar <- 10
append_square(bar, 6)
你期望bar变成c(10, 36),但bar仍然是10。相反,全局环境中的f被改成了c(10, 36)。这是因为函数中的foo <<- ...总是修改全局环境中名为foo的对象,而不是局部参数foo。它查找名称"foo",并在全局环境中修改或创建它,与传入的实际参数名称bar无关。


更好的处理方式是让函数返回值,然后显式赋值:
append_square_better <- function(vec, x) {
return(c(vec, x^2))
}
f <- 2
f <- append_square_better(f, 5) # f 变为 c(2, 25)
bar <- 10
bar <- append_square_better(bar, 6) # bar 变为 c(10, 36)
关键区别在于,你需要使用常规赋值来捕获函数的输出。虽然超级赋值看似方便,但它会导致代码行为不直观且难以调试。



总结


本节课中我们一起学习了R语言中的作用域规则。我们了解了:
- 绑定是将名称关联到对象。
- 作用域是R查找这些名称所关联值的规则。
- 函数会创建局部环境,其中的变量会屏蔽外部同名变量。
- R使用词法作用域,即从当前环境开始,逐级向上层环境查找变量。
- 修改时复制是R管理内存的一个重要行为。
- 超级赋值运算符
<<-可以跨作用域修改变量,但由于其容易导致混淆和错误,通常应避免使用。



理解这些概念有助于你编写出更清晰、更可预测、更易于维护的R代码。在阅读他人代码时,如果遇到超级赋值,你也能明白其工作原理。记住,良好的实践是:让函数通过参数接收输入,通过返回值输出结果,尽量减少对外部环境的隐式依赖。
14:R语言中的环境


在本节课中,我们将深入学习R语言中的环境概念。环境是R中实现作用域规则的核心数据结构,理解它对于掌握函数如何查找变量以及包如何工作至关重要。
环境是一种数据结构
上一节我们介绍了局部环境和全局环境的基本概念。本节中,我们将深入探讨环境本身的特性。

环境本质上是一种数据结构,它负责实现作用域规则。环境的主要工作是将名称绑定到值。你可以将环境想象成一个“名称袋”,其中的每个名称都指向内存中存储的一个对象。计算机将值存储在内存中,而你通过指向该内存对象的名称来访问这些值,这些名称就存储在环境里。
你可以使用 new.env() 函数创建自己的环境。

# 创建一个新环境
e <- new.env()
# 在环境中绑定名称和值
e$a <- FALSE
e$b <- c("hello", "world")
e$c <- c(1.0, 2.0)
e$d <- 1L:10L
在这个例子中,我们创建了一个名为 e 的环境,并在其中绑定了名称 a, b, c, d 到不同的R对象。
环境的操作
与R中所有事物一样,环境本身也是对象。操作环境的语法与列表类似,你可以使用双括号 [[]] 或美元符号 $ 来获取值。

# 检查环境类型
typeof(e) # 返回 "environment"

# 获取环境中的值
e$c
e[["d"]]
然而,环境本质上是无序的。这意味着你不能像列表那样通过数字索引(例如 e[[1]])来获取第一个元素。环境就像一个背包,里面的物品是随意放置的,没有“第一件”或“第三件”的概念。
以下是查看环境内容的常用函数:

# 列出环境中的所有名称
ls(e)
# 列出环境名称及其结构的详细信息
ls.str(e)
环境和列表虽有相似之处,但也有重要区别。例如,你不能对环境使用单括号 []。此外,在环境中将对象设置为 NULL 并不会移除该名称,只是将其值设为 NULL 对象。若要从环境中完全移除一个对象,必须使用 rm() 函数。
# 设置值为NULL,名称d依然存在
e$d <- NULL
ls(e) # d 仍然在列表中

# 使用rm()函数彻底移除名称d
rm(d, envir = e)
ls(e) # d 已被移除

父环境与作用域链

每个环境都有一个父环境,这是另一个环境。图中蓝色箭头指向父环境。父环境用于实现词法作用域。即,如果R在当前环境中找不到某个名称,它会去父环境中查找,并依此类推,形成一个环境链,直到到达顶层环境——空环境,这是唯一没有父环境的环境。


# 获取环境的父环境
parent.env(e)
特殊环境

R中有几个特殊的环境需要了解:
- 当前环境:
environment(),即你当前正在操作的环境。 - 全局环境:
globalenv(),通常是你启动R后工作的“工作空间”。你创建的向量和函数默认都存储在这里。 - 基础环境:
baseenv(),是基础包(base package)的环境,包含像c(),sum()这样的核心函数。它的父环境是空环境。 - 空环境:
emptyenv(),是所有环境的最终祖先,它没有父环境。
全局环境的父环境有点特殊,它是最后被附加的包。这引出了搜索路径的概念。

搜索路径

你可以通过 search() 函数查看搜索路径,它列出了全局环境的所有父环境。
search()
当R在全局环境中找不到某个名称(例如一个函数)时,它会沿着搜索路径向上查找。例如,你从未定义过 sd()(标准差)函数,但你可以调用它,因为R在 stats 包中找到了它。lm()(线性模型)函数也位于 stats 包中。
搜索路径的结构可以这样可视化:全局环境 -> 最后加载的包 -> ... -> 基础环境 -> 空环境。当你使用 library() 加载一个新包(如 ggplot2)时,该包的环境会被插入到全局环境和之前加载的包之间,成为全局环境新的父环境。

函数与环境
我们之前讨论过,许多环境是因使用函数而产生的。每个函数都与四种类型的环境相关联:
- 封闭环境:函数被创建时所在的环境。每个函数有且只有一个封闭环境,用于词法作用域。
- 绑定环境:函数名称被绑定(即赋值)时所在的环境。
- 执行环境:函数被调用时创建的临时环境,用于存储执行过程中的变量。
- 调用环境:函数被调用时所在的环境。


在大多数简单情况下,封闭环境和绑定环境是同一个(例如在全局环境中定义函数)。让我们通过一个例子来理解执行环境。

# 在全局环境中定义函数h
h <- function(x) {
a <- 2
return(x + a)
}
# 调用函数
y <- h(1)
执行过程如下:
- 调用
h(1)时,创建一个新的执行环境。 - 参数
x被赋值为1。 - 在函数体内,名称
a被赋值为2。 - 计算
x + a得到3并返回。 - 返回值
3被赋给全局环境中的y。 - 函数调用结束,执行环境被销毁。全局环境中的
h仍然指向原函数,但执行时的临时变量a和参数x已不存在。
命名空间与包管理
搜索路径的顺序依赖于包加载的顺序,这可能导致问题:如果后加载的包中有同名函数,它会“屏蔽”先加载包中的函数。R通过命名空间机制来解决这个问题,确保包内部函数能正确找到其依赖的其他函数,不受用户加载包顺序的影响。
对于用户而言,有两个重要的环境与包相关:
- 包环境:包的外部接口。R通过搜索路径在这里查找函数。其父环境由搜索路径决定。
- 命名空间环境:包的内部接口。它控制着包内的函数如何找到其所需的变量(如其他内部函数),并将内部实现细节对用户隐藏。


当你想明确使用某个特定包中的函数,避免被屏蔽时,可以使用双冒号运算符 ::。
# 明确使用base包中的mean函数
base::mean(some_vector)

# 明确使用mosaic包中的mean函数(如果已加载)
mosaic::mean(some_vector)
词法作用域与动态作用域
R默认使用词法作用域,即函数在查找变量时,依据的是其封闭环境(定义时的环境)。
然而,R也支持动态作用域,即函数可以依据其调用环境来查找变量。这可以通过 get() 函数配合 parent.frame() 来实现。


# 动态作用域示例:从调用环境中获取变量y的值
get("y", envir = parent.frame())
我们将在下一个视频中展示动态作用域的具体例子。

总结

本节课中,我们一起深入学习了R语言中的环境。我们了解到环境是一种将名称绑定到值的数据结构,它是R作用域规则的基础。我们探讨了如何创建和操作环境,理解了父环境、作用域链以及搜索路径的概念。我们还分析了函数与四种环境(封闭、绑定、执行、调用)的关系,并通过例子看到了执行环境的生命周期。最后,我们介绍了包如何通过命名空间和包环境来管理函数,以及如何使用 :: 运算符来精确调用函数,并简要提及了词法作用域与动态作用域的区别。掌握这些概念对于编写健壮、可预测的R代码至关重要。
15:作用域自测题解析 🧠

在本节课中,我们将通过一系列代码示例,深入理解R语言中的作用域规则。作用域决定了变量在何处被查找和赋值,是理解函数行为的关键。我们将分析多个自测题,逐步拆解代码的执行过程,以掌握词法作用域和动态作用域的核心概念。

概述
本节教程将解析一系列关于作用域的代码片段。我们将学习如何追踪变量的查找路径,理解局部环境、全局环境以及函数定义环境之间的关系。核心在于掌握R语言的词法作用域规则:函数在定义时确定其变量的查找环境,而非在调用时。
示例一:嵌套函数的作用域

上一节我们介绍了作用域的基本概念,本节中我们来看看一个具体的嵌套函数示例。
x <- 0
y <- 10
f <- function() {
x <- 2
y <- 100
h <- function() {
x <- 50
return(x + y)
}
return(h())
}
以下是代码执行步骤分析:


- 在全局环境中,
x被赋值为0,y被赋值为10。 - 函数
f在全局环境中被定义。 - 调用
f()时,创建f的局部执行环境。在该环境中,x被赋值为2,y被赋值为100。 - 在
f的环境中,函数h被定义。 f的最后一行执行h()。- 执行
h()时,创建h的局部环境。在该环境中,x被赋值为50。 h返回x + y。查找x时,在h的局部环境中找到值50。- 查找
y时,在h的局部环境中未找到,于是向其封闭环境(即f的执行环境)查找,找到值100。 - 因此,
h()返回50 + 100 = 150。 f()返回h()的结果,即150。

结论:函数 f 的返回值是 150。
示例二:函数定义环境的重要性
理解了嵌套函数后,我们来看一个强调函数定义环境的例子。
y <- 10
h <- function() {
x <- 3
return(x + y)
}
g <- function() {
x <- 2
y <- 100
return(h())
}
以下是代码执行步骤分析:
- 在全局环境中,
y被赋值为10。函数h在全局环境中被定义。 - 执行
h():在其局部环境中,x被赋值为3。它返回x + y。- 查找
x:在h的局部环境中找到3。 - 查找
y:在h的局部环境中未找到,于是向其封闭环境(即全局环境)查找,找到10。 - 因此,
h()返回3 + 10 = 13。
- 查找
- 执行
g():在其局部环境中,x被赋值为2,y被赋值为100。然后它调用h()。 h在全局环境中被定义,其封闭环境是全局环境,而非g的执行环境。因此,h内部的y仍然指向全局环境的10,而非g中的100。- 所以,
g()返回h()的结果,即13。
结论:h() 返回 13,g() 也返回 13。函数 g 内部的 x 和 y 赋值与 h 的执行无关。


示例三:动态作用域简介

前面我们探讨的是R默认的词法作用域。现在,我们通过 parent.frame() 函数简要了解动态作用域的概念。
x <- 0
y <- 10
p <- function() {
y <- get(“y”, envir = parent.frame())
x <- 3
return(x + y)
}
r <- function() {
x <- 2
y <- 100
return(p())
}
以下是代码执行步骤分析:

p函数使用get(“y”, envir = parent.frame())。这表示从调用环境(而非定义环境)中查找y。- 在全局环境中调用
p():parent.frame()指向全局环境,y获得值10。p()返回3 + 10 = 13。 - 在
r()中调用p():此时p的调用环境是r的执行环境。因此,y从r的环境中获取值100。 p()返回3 + 100 = 103,故r()返回103。
核心概念:parent.frame() 实现了动态作用域,变量的值取决于函数的调用栈,而非定义位置。

示例四至六:环境查找练习
以下是一组渐进式的练习,帮助我们巩固环境查找规则。


示例四:基础查找


a <- 1
b <- 2

l <- function() {
print(a)
print(b)
}
l()
执行过程:l 在全局环境中定义。执行 l() 时,在局部环境中找不到 a 和 b,于是向封闭环境(全局环境)查找,找到 a=1, b=2。输出 1 和 2。

示例五:局部变量不影响封闭环境

m <- function() {
a <- 4
l()
}

m()
执行过程:执行 m(),局部变量 a 被赋值为 4。然后调用 l()。l 的定义环境是全局环境,因此它仍然从全局环境中查找 a 和 b(值分别为 1 和 2)。输出 1 和 2。m 中的局部赋值不影响 l 的查找。
示例六:函数在何处定义至关重要
n <- function() {
a <- 4
l <- function() {
print(a)
print(b)
}
l()
}

n()
执行过程:执行 n(),局部变量 a 被赋值为 4。在 n 的内部定义了函数 l。此时 l 的封闭环境是 n 的执行环境。调用 l() 时:
- 查找
a:在l的局部环境中未找到,向其封闭环境(n的环境)查找,找到4。 - 查找
b:在l和n的环境中均未找到,继续向全局环境查找,找到2。
输出4和2。
示例七:综合应用
最后,我们来看一个综合性的例子,融合了之前的各种概念。


a <- 1
b <- 2
l <- function() {
print(a)
print(b)
}
o <- function() {
a <- 4
l() # 第一次调用 l
l <- function() { # 在 o 内部重新定义 l
print(a)
print(b)
}
l() # 第二次调用 l (调用新定义的 l)
b <- 5
l() # 第三次调用 l (调用新定义的 l)
}
o()
以下是代码执行步骤分析:
- 全局环境:
a=1,b=2, 定义了函数l(输出全局的a和b)。 - 执行
o():- 局部变量
a被赋值为4。 - 第一次调用
l():此时o内部尚未定义l,故调用的是全局环境中的l。它输出全局的a和b,即1和2。 - 在
o内部重新定义了函数l。这个新l的封闭环境是o的执行环境。 - 第二次调用
l():调用的是o内部新定义的l。它查找a,在o的环境中找到4;查找b,在o的环境中未找到,于是向全局环境查找,找到2。输出4和2。 - 在
o的环境中,将b赋值为5。 - 第三次调用
l():仍然调用o内部定义的l。此时在o的环境中,a=4,b=5。因此输出4和5。
- 局部变量
最终输出顺序为:1 2, 4 2, 4 5。
总结

本节课中我们一起学习了R语言的作用域规则。我们通过多个自测题,深入理解了词法作用域是R的默认规则,即函数在定义时确定其变量的查找环境(封闭环境)。关键点包括:
- 变量查找遵循“局部环境 -> 封闭环境 -> … -> 全局环境”的链式规则。
- 函数的定义位置(即其封闭环境)决定了它如何查找非局部变量,而非其被调用的位置。
- 使用
parent.frame()可以实现动态作用域,让查找依赖于调用环境。 - 在更复杂的环境中,函数可以被重新定义,从而改变其行为和作用域链。

掌握这些概念对于编写正确、可预测的R代码至关重要。
16:数据导入、导出与Lubridate包 🗂️

在本节课中,我们将要学习如何在R中导入和导出数据,并介绍一个处理日期数据的强大工具——Lubridate包。掌握这些技能是进行数据分析的基础。

数据导入 📥
上一节我们介绍了R的基础操作,本节中我们来看看如何将外部数据读入R环境。R提供了多种读取文件的函数。
以下是读取文件的基本命令:
read.csv:这是许多用户熟悉的函数,用于读取逗号分隔值文件。read.table:读取表格数据的通用函数。readLines:逐行读取文本文件。

有时read.csv会将字符串读为因子。虽然默认设置是stringsAsFactors = FALSE,但为了保险起见,我仍建议在调用时显式指定此参数。
除了基础R的函数,还有许多其他包可以更高效地处理数据导入。

以下是其他常用的数据导入包:
- readr:
tidyverse套件的一部分。我倾向于使用readr中的read_csv函数,它通常比基础的read.csv更快、更高效。 - readxl:用于导入Excel文件(
.xls或.xlsx格式)。 - haven:允许你导入来自其他统计软件(如Stata、SPSS或SAS)的文件。
- data.table:专为快速处理而设计,能很好地处理大型文件。如果你要处理数十万或数百万行的大型数据集,
data.table是理想选择。
如果你加载了readr包,最常用的函数将是read_csv,它用于加载最常见的文本数据格式——逗号分隔值文件。


readr包还提供了读取其他格式文件的函数,例如制表符分隔文件、定界文件和固定宽度文件。你可以查阅这些不同函数的帮助文件以了解更多细节。
关于data.table,它专为追求最快性能而设计。如果你有一个庞大的数据文件,可以加载data.table包并使用fread(快速读取器)函数。它的操作方式与read_csv类似,但速度更快。

这里有一张幻灯片对比了readr包的read_csv和data.table包的fread的性能。
如果你想使用Excel文件,需要加载readxl库。你可以通过指定文件名来读取第一个工作表。
如果你的Excel文件包含多个工作表(因为Excel支持此功能),你可以通过工作表名称或工作表编号来指定要读取哪个工作表。
有时,你可能希望直接从网络加载数据。这在你想将脚本分享给同事或同学时特别有用,这样你就不需要同时提供脚本和数据文件本身。
以下是直接从网络下载数据的步骤:
- 使用
downloader包中的download函数。虽然R有原生的download.file函数,但它是一个早期的函数,在处理现在普遍使用的HTTPS安全协议时可能不太顺畅。 - 在
download函数中指定文件的URL地址和你希望保存到的本地文件名。 - 文件下载到工作目录后,你就可以使用
read_csv等函数将其加载到R中。
这种方法可以让你将下载数据的指令直接包含在R脚本中,当他人运行你的脚本时,数据会自动下载并加载。
数据导出 📤

上一节我们介绍了如何导入数据,本节我们来看看如何将R中的对象导出,以便在其他程序中使用或后续使用。
如果你想将对象保存为.RData文件格式(以便之后重新加载到R中),可以使用save函数。你需要指定要保存的对象列表和保存的文件路径。
如果你想将原子向量保存为纯文本,可以使用write函数。你需要指定对象名、文件名和列数,它会为你创建一列数值。
如果你有一个数据框并想将其保存为CSV文件,可以使用write.csv函数。你需要指定数据框的名称和要保存的文件名。默认行为是包含行名,我个人不喜欢这样,因此建议包含选项row.names = FALSE。

以上就是导出数据的方法。
使用Lubridate处理日期 📅
我想特别提请注意一个名为lubridate的包,它允许你轻松处理日期信息。很多时候,当你从外部下载CSV文件并导入R时,文件中可能包含日期信息。在R中处理日期信息可能非常繁琐和恼人,但lubridate包极大地简化了这个过程。
据我所知,Python中没有类似lubridate的包,因此这是R中一个非常出色的工具。在Python中,你仍然需要经历繁琐的格式化过程。

那个繁琐的格式化系统看起来像这样:你必须使用像%B %d, %Y这样的格式代码,并且要求非常严格。如果你搞错了一个细节,比如日期字符串是“January 15, 1999”,但你不小心漏掉了逗号,程序就无法理解。或者日期是“12-15-2001”带连字符,如果你漏掉了连字符,它也会出错。所以这很烦人。此外,如果你的数据中混合了多种日期格式,基础方法通常无法处理。

使用lubridate包,你可以处理混合格式的日期。你只需要告诉它日期字段的顺序(例如,月-日-年),它就能理解所有格式。无论是“January 15, 1999”、“12-15-2001”还是“03/18/2002”,所有这些格式都能正常工作。
lubridate非常棒,但它确实要求你指定字段的顺序。例如,“03-04-05”这样的日期是模糊的,它可能是“年-月-日”(2003年4月5日),也可能是“月-日-年”(2005年3月4日),还可能是“日-月-年”(2005年4月3日)。如果你尝试用“月-日-年”的格式去解析一个像“25-13-05”的日期(第25个月?),那显然不会成功,它会返回NA。
总之,这就是lubridate包。如果你预计需要处理日期信息,我强烈推荐使用它。


本节课中我们一起学习了在R中导入和导出数据的多种方法,包括使用基础函数、readr、readxl、haven和data.table等包。我们还重点介绍了强大的lubridate包,它极大地简化了日期数据的处理过程,是数据分析工作中不可或缺的工具。
17:使用 rvest 在 R 中进行基础网络爬虫 🕸️



在本节课中,我们将学习如何使用 R 语言中的 rvest 包从网页上抓取数据。我们将介绍网络爬虫的基本概念、如何使用 Selector Gadget 工具识别网页元素,以及如何编写代码来提取和整理信息。
概述
网络爬虫是从互联网上自动收集数据的过程。rvest 包是 R 语言中一个强大的工具,它模仿“收获”一词,专门用于从网页上抓取数据。理解一些基础的 HTML 和 CSS 知识将有助于你更好地使用 rvest。

网络爬虫基础与限制

上一节我们介绍了网络爬虫的概念,本节中我们来看看 rvest 包的工作原理及其适用场景。


rvest 适用于结构相对静态、具有明确定义 HTML 标签的网页。它对于通过 JavaScript 动态生成内容的网站(例如无限滚动页面)或需要登录才能查看内容的网站(如 Facebook 或 X)效果不佳。
网页主要由 HTML 编写,并通过不同的标签和 CSS 类来定义格式。CSS 类用于保持页面样式的一致性。例如,一个简化的 HTML 片段可能如下所示:


<ul class="flat-list buttons">
<li class="comments">...</li>
<li class="share-button">...</li>
<li class="save-button">...</li>
</ul>
<a href="https://example.com">链接文本</a>




在上面的代码中,href 属性定义了超链接的目标地址。rvest 就是通过识别这些标签和属性来定位和提取信息的。





使用 Selector Gadget 工具



为了高效地选择网页上的特定元素,我们需要借助一个名为 Selector Gadget 的浏览器插件。它可以帮助我们直观地获取目标元素的 CSS 选择器路径。




以下是安装和使用 Selector Gadget 的步骤:
- 首先,确保你的浏览器书签栏是可见的。在 Chrome 或 Edge 中,通常可以使用快捷键
Cmd/Ctrl + Shift + B来显示。 - 访问
Selector Gadget的官方网站,将其链接拖拽到你的书签栏中完成安装。




安装完成后,你可以通过点击书签来激活它。当你在网页上移动光标时,它会高亮显示不同的元素区域。点击你想要抓取的元素,Selector Gadget 会尝试找出能唯一标识这类元素的 CSS 选择器,并用黄色高亮所有匹配项。如果选择过多,你可以点击不需要的元素(变为红色)来优化选择器。


基础爬虫代码示例



了解了工具的使用后,我们来看看如何用 rvest 编写爬虫代码。核心函数包括读取网页、选择节点和提取内容。



以下是一个从 Reddit 页面抓取评论数量的基础示例:

# 加载必要的库
library(rvest)
library(stringr) # 用于后续处理文本,如正则表达式
# 1. 读取网页
reddit_page <- read_html("https://old.reddit.com")
# 2. 使用从Selector Gadget获取的选择器抓取评论节点
# 假设选择器为 “.comments”
comment_nodes <- html_nodes(reddit_page, ".comments")

# 3. 从节点中提取文本
comment_text <- html_text(comment_nodes)
# 4. 使用正则表达式提取纯数字(例如“10 comments”中的10)
comment_counts <- as.numeric(str_extract(comment_text, "\\d+"))
# 查看结果
head(comment_counts)
同样地,你可以使用类似的方法抓取标题,选择器可能类似于 .title.may-blank。
使用 polite 包进行礼貌爬虫
直接快速地对一个网站发起大量请求可能导致你的 IP 被限制或封禁。因此,在进行大规模爬取时,遵守网络礼仪至关重要。polite 包可以帮助我们管理请求频率。



polite 包通过创建一个“会话”来管理爬虫,它会自动在请求间添加延迟,避免对服务器造成压力。
以下是使用 polite 的基本工作流程:
library(polite)
library(rvest)


# 1. 创建一个礼貌会话,并“拜访”目标网站
session <- bow("https://www.imdb.com/title/tt1490017/") # 乐高大电影页面
# 2. 抓取页面内容
page_content <- scrape(session)

# 3. 使用rvest函数解析内容
# 例如,抓取评分
rating <- page_content %>%
html_nodes(".cxhHyR") %>% # 从Selector Gadget获得的选择器
html_text() %>%
as.numeric()

综合案例:爬取电影演员信息


现在,我们将前面所学的知识结合起来,完成一个综合任务:从一部电影的页面开始,爬取所有主演的名字和链接,然后依次访问每位主演的个人页面,抓取他们正在进行的项目。
这个案例演示了如何自动化地遍历多个页面并结构化存储数据。
library(polite)
library(rvest)
library(stringr)
# 启动会话,从乐高大电影页面开始
session <- bow("https://www.imdb.com/title/tt1490017/")
page_content <- scrape(session)

# 抓取演员名单和对应的个人页面链接
actor_names <- page_content %>%
html_nodes(".gcqK-eh a") %>% # 抓取包含演员名的a标签
html_text()

actor_urls <- page_content %>%
html_nodes(".gcqK-eh a") %>%
html_attr("href") # 提取href属性,即链接
# 为链接向量加上演员名字作为名称,方便后续引用
names(actor_urls) <- actor_names

# 初始化一个空列表,用于存储所有演员的项目信息
all_projects <- list()
# 循环处理前几位演员(例如4位)
for (actor in actor_names[1:4]) {
# 让会话跳转到该演员的个人页面
actor_page <- nod(session, actor_urls[actor]) %>% scrape()
# 从演员页面抓取项目信息(假设选择器为“.ipc-metadata-list-summary-item__t”)
projects <- actor_page %>%
html_nodes(".ipc-metadata-list-summary-item__t") %>%
html_text() %>%
head(10) # 只取前10个项目
# 将结果存入列表
all_projects[[actor]] <- list(
name = rep(actor, length(projects)),
projects = projects
)
# polite会话会自动处理请求间隔
}
# 将列表转换为数据框,便于分析
results_df <- do.call(rbind, lapply(all_projects, as.data.frame))
print(results_df)

其他工具与总结
除了 rvest,还有其他更复杂的网络爬虫工具,例如 R 或 Python 中的 Selenium,以及 Python 中的 Scrapy 和 Beautiful Soup。它们能处理更动态、更复杂的网页。
本节课中我们一起学习了使用 R 语言进行基础网络爬虫的核心技能。我们介绍了 rvest 包的基本用法,学习了如何利用 Selector Gadget 工具定位网页元素,编写了提取文本和属性的代码,并强调了使用 polite 包进行礼貌爬虫以避免被封禁的重要性。最后,通过一个综合案例,我们实践了如何自动化地遍历多个相关页面并收集结构化数据。掌握这些基础将为你在数据收集方面打开一扇新的大门。
18:Tidyverse 与 Tibbles 入门 🧹

在本节课中,我们将学习 R 语言中一个非常重要的工具集——Tidyverse,并重点介绍其核心数据结构 Tibble。Tibble 是数据框(Data Frame)的现代化版本,它优化了显示和操作方式,使数据处理更加高效和直观。

什么是 Tidyverse? 📦
Tidyverse 是一个为数据科学设计的、包含一系列 R 包的“有主见”的集合。这些包共享相同的底层设计理念、语法和数据结构。当你加载 library(tidyverse) 时,会同时加载其核心包,例如 dplyr 和 ggplot2。当然,你也可以单独加载它们。

上一节我们介绍了 Tidyverse 的整体概念,本节中我们来看看它的核心数据结构——Tibble。
Tibble:Tidyverse 的核心数据结构 📊

Tibble 是 Tidyverse 的中央数据结构。它本质上是一种数据框,但进行了一些优化,使其在现代数据科学工作流中更加实用。R 是一门有几十年历史的语言,其基础数据框的一些特性在当时很有用,但现在有时会带来不便。Tibble 的设计就是为了解决这些问题。
Tibble 的创建
创建 Tibble 非常简单,你可以像创建数据框一样创建它。


以下是创建 Tibble 的几种方法:
- 使用
tibble()函数直接创建:library(tibble) my_tibble <- tibble( column_a = c(1, 2, 3), column_b = c("a", "b", "c") )

- 将现有数据框转换为 Tibble:
mtcars_tibble <- as_tibble(mtcars)


- 按行创建 Tibble(便于阅读代码):
tibble_rowwise <- tribble( ~column_a, ~column_b, ~column_c, "A", 1, TRUE, "B", 2, FALSE )
Tibble 的显示特性
Tibble 的打印方法经过了优化,使其在处理大型数据时更友好。



以下是 Tibble 的几个关键显示特性:



- 智能显示:默认只显示前10行,以及能在屏幕上完整显示的列。
- 类型提示:在列名下方会显示该列的数据类型(如
<dbl>,<chr>,<date>),无需调用str()函数。 - 简洁数值:为保持显示紧凑,默认只显示少数几位小数(内部数据精度保持不变)。
- 无行名:Tibble 不鼓励使用行名来存储信息,所有数据都应存储在列中。
Tibble 与数据框的子集选取差异
在子集选取行为上,Tibble 与基础 R 的数据框有一个重要区别,这能提供更一致的行为。


以下是 Tibble 子集选取的规则:
- 单括号
[ ]:始终返回 Tibble,即使只选取一列。# 返回一个只包含一列的 Tibble my_tibble[, “column_a”] - 双括号
[[ ]]或$:用于提取单个列为向量。# 返回一个向量 my_tibble[[“column_a”]] my_tibble$column_a - 强制简化:如果想用单括号获取向量,需使用
drop = TRUE参数。my_tibble[, “column_a”, drop = TRUE]
相比之下,基础数据框在使用单括号选取单列时,默认会简化为向量,这种行为有时会导致意外结果。Tibble 的设计避免了这种不一致性。
控制 Tibble 的打印输出

你可以通过 print() 函数的参数来控制 Tibble 的显示方式。
以下是控制打印输出的常用参数:
n:指定要打印的行数(例如print(my_tibble, n = 20))。width:指定打印的宽度。- 通过
options(pillar.sigfig = 5)可以全局调整显示的有效数字位数。
总结 📝

本节课中我们一起学习了 Tidyverse 及其核心数据结构 Tibble。我们了解到 Tibble 是数据框的增强版,它提供了更清晰的数据显示、更一致的子集选取行为,并鼓励更好的数据组织实践(如将信息存入列而非行名)。这些特性使得 Tibble 成为使用 Tidyverse 进行数据清洗、转换和分析的理想起点。在后续课程中,我们将深入利用 Tibble 来学习 dplyr 等强大的数据处理工具。
19:使用 tidyr 进行数据透视 📊

在本节课中,我们将学习如何使用 tidyr 包中的 pivot_longer 和 pivot_wider 函数来重塑数据。数据透视是数据整理中的核心技能,它可以帮助我们将数据从不整洁的“宽”格式转换为整洁的“长”格式,或者反之,以满足不同的分析需求。


什么是整洁数据?

在开始之前,我们需要理解“整洁数据”的概念。tidyverse 的理念认为,整洁数据遵循以下三个原则:
- 每一列都是一个变量。
- 每一行都是一个观测值。
- 每一个单元格都是一个单一的值。

这听起来简单直接,但我们接收到的数据常常不遵循这些原则。数据透视的目的,就是通过调整数据的结构,使其符合这些整洁数据的原则。

整洁数据示例
以下是一个整洁的数据框示例 storms:
# 示例数据框
storms <- data.frame(
storm = c("Alberto", "Beryl", "Chris"),
wind = c(110, 45, 65),
pressure = c(1007, 1009, 1005),
date = c("2000-08-03", "2000-08-04", "2000-08-05")
)
在这个数据框中,每个变量(风暴名称、风速、气压、日期)都有自己的列,每一行代表一次独立的观测(一场风暴)。这种结构使得向量化操作非常方便。例如,要计算气压与风速的比值,我们可以直接进行列运算:

storms$ratio <- storms$pressure / storms$wind

不整洁数据示例

然而,并非所有数据都以这种整洁的形式呈现。让我们看一个不整洁的例子 cases:



# 不整洁的数据框示例
cases <- data.frame(
country = c("France", "Germany", "US"),
`2011` = c(7000, 5800, 15000),
`2012` = c(6900, 6200, 14000),
`2013` = c(7000, 6000, 13000)
)



在这个数据框中,变量“年份”变成了列名(2011, 2012, 2013),而“病例数”这个变量的值则分散在多个列中。这不符合“每一列是一个变量”的原则,使得数据分析变得困难。

使用 pivot_longer 将数据变长
上一节我们看到了一个“宽”格式的数据。本节中,我们来看看如何将其转换为整洁的“长”格式。我们的目标是将 cases 数据框重塑为包含三列的结构:country(国家)、year(年份)和 cases(病例数)。
以下是使用 pivot_longer 函数实现此转换的步骤:
library(tidyr)
cases_long <- pivot_longer(
data = cases, # 要操作的数据框
cols = `2011`:`2013`, # 需要被“拉长”的列
names_to = "year", # 新列名,用于存放原来的列名(年份)
values_to = "cases" # 新列名,用于存放原来单元格中的值(病例数)
)
执行上述代码后,cases_long 将变成一个包含9行(3个国家 × 3年)和3列的数据框,结构变得整洁。
pivot_longer 函数的关键参数如下:
data:要重塑的数据框。cols:需要被转换为行的列。names_to:一个字符串,指定新列的名称,该列将存放原cols中指定的列名。values_to:一个字符串,指定新列的名称,该列将存放原cols中单元格的值。
注意:names_to 和 values_to 的参数名称是任意的,但应选择有意义的名称。如果只对部分列进行透视,未被选中的列会被自动复制以匹配新的行数。
使用 pivot_wider 将数据变宽
有时,我们需要进行相反的操作,将“长”格式的数据转换为“宽”格式。让我们看另一个例子 pollution:
# 长格式数据示例
pollution <- data.frame(
city = c("New York", "New York", "London", "London", "Beijing", "Beijing"),
size = c("large", "small", "large", "small", "large", "small"),
amount = c(23, 14, 22, 16, 17, 21)
)

在这个数据框中,颗粒物大小(size)是一个变量,但其值(“large”, “small”)占据了行。如果我们希望每个城市独占一行,并分别用“大颗粒物数量”和“小颗粒物数量”两列来表示,就需要使用 pivot_wider。


以下是转换方法:





pollution_wide <- pivot_wider(
data = pollution, # 要操作的数据框
names_from = size, # 哪一列的值将成为新列名
values_from = amount # 哪一列的值将填充到新列下
)
执行后,pollution_wide 将变成一个3行(每个城市一行)3列(城市、large、small)的宽表。
pivot_wider 函数的关键参数如下:
data:要重塑的数据框。names_from:指定哪一列的值将作为新数据框的列名。values_from:指定哪一列的值将填充到新列下的单元格中。
注意:pivot_wider 对大小写和拼写敏感。如果数据中存在不匹配的情况(例如,“New York” 和 “NYC” 被视为两个不同的城市),缺失的组合将用 NA 填充。可以使用 values_fill 参数指定填充值,例如 values_fill = 0。
透视操作是可逆的
一个重要的特性是,pivot_longer 和 pivot_wider 是互逆的操作。这意味着你可以安全地在两种格式之间转换,而不会丢失信息。
- 对
pollution使用pivot_wider得到pollution_wide,再对pollution_wide使用pivot_longer可以恢复原状。 - 对
cases使用pivot_longer得到cases_long,再对cases_long使用pivot_wider也可以恢复原状。
选择使用长格式还是宽格式,取决于你的分析目标。宽格式可能便于计算跨列的差异(如计算年份间的病例数变化),而长格式则更符合大多数 tidyverse 函数(如 ggplot2, dplyr)的处理规范。
总结
本节课中我们一起学习了数据透视的核心技能。我们首先理解了整洁数据的三大原则。然后,我们深入探讨了 tidyr 包中的两个关键函数:
pivot_longer:将数据从宽格式转换为长格式,适用于将列名变为变量值的情况。pivot_wider:将数据从长格式转换为宽格式,适用于将某个变量的值展开为多个列的情况。

掌握这两个函数,能够帮助你灵活应对各种不规则的数据结构,为后续的数据分析和可视化打下坚实的基础。记住,根据分析需求选择合适的数据格式是高效数据分析的关键一步。
20:dplyr - select, filter, mutate 🛠️

在本节课中,我们将学习 tidyverse 生态系统中一个非常重要的包——dplyr。dplyr 提供了强大的数据操作功能,通过一系列简洁的“动词”来帮助我们高效地处理数据。我们将重点介绍 select、filter 和 mutate 这三个核心函数。
概述

dplyr 是 tidyverse 的核心包之一,它将复杂的数据操作任务简化为几个基本的“动词”。这种设计约束了选择,但有助于我们以更清晰、更模块化的方式思考数据处理流程。数据处理通常包括:确定任务、用程序描述任务、执行程序。dplyr 让这些步骤变得快速而简单。

核心动词简介
dplyr 提供了多个用于数据操作的动词,其中最重要的几个是:
select: 用于选择数据框中的列(变量)。filter: 用于根据条件筛选数据框中的行(观测)。mutate: 用于创建新的列(变量)或修改现有列。arrange: 用于改变行的排序顺序。summarize: 用于计算汇总统计量,将多个值聚合成单个值(如计算均值)。
这些动词通常可以与 group_by 结合使用,以便对分组后的数据执行操作。


管道操作符 %>%
在深入学习具体函数前,我们需要了解管道操作符 %>%。它的作用是将左侧表达式的结果作为第一个参数传递给右侧的函数。
代码示例:
# 传统写法
select(starwars, name, homeworld, species, films)
# 使用管道的写法
starwars %>% select(name, homeworld, species, films)
管道语法允许我们将多个操作步骤串联起来,使代码更易读,逻辑更清晰。R 基础版本也引入了原生管道操作符 |>,功能类似。


选择列:select 函数
select 函数用于从数据框中选择特定的列。

基本选择
可以直接指定列名来选择它们。
代码示例:
starwars %>% select(name, homeworld, species, films)

排除列
在列名前加负号 - 可以排除这些列。


代码示例:
starwars %>% select(-name, -eye_color, -birth_year) %>% head(3)

选择列范围
可以使用 : 操作符选择连续的列。

代码示例:
starwars %>% select(name:eye_color)
使用辅助函数
select 提供了一些辅助函数,可以基于列名的模式进行选择。
contains(“string”): 选择包含指定字符串的列。starts_with(“prefix”): 选择以指定前缀开头的列。ends_with(“suffix”): 选择以指定后缀结尾的列。matches(“regex”): 选择匹配正则表达式的列。
代码示例:
# 选择以 “color” 结尾的列和 name 列
starwars %>% select(name, ends_with(“color”))


# 选择列名以 “s” 结尾的列
starwars %>% select(matches(“s$”))
使用变量选择列

有时需要选择的列名存储在一个向量中,这时可以使用 all_of() 或 any_of() 函数。
代码示例:
vars_to_select <- c(“name”, “mass”, “height”)
starwars %>% select(all_of(vars_to_select))

筛选行:filter 函数
filter 函数用于根据逻辑条件筛选数据框的行。
基本筛选

提供一个逻辑向量(条件),filter 会返回条件为 TRUE 的行。


代码示例:
# 筛选出名为 “R2-D2” 的行
starwars %>% filter(name == “R2-D2”)
多条件筛选
可以使用逻辑运算符 & (AND) 和 | (OR) 组合多个条件。

代码示例:
# 筛选物种为 “Human” 或 “Droid” 的行
starwars %>% filter(species %in% c(“Human”, “Droid”))

# 筛选身高小于 175 的行
starwars %>% filter(height < 175)
注意:判断一个值是否在一组值中时,应使用 %in% 运算符,而不是 ==。
结合字符串检测

filter 可以与 stringr::str_detect() 等函数结合,使用正则表达式进行更复杂的字符串匹配筛选。

代码示例:
# 筛选名字以大写 “F” 开头的行
starwars %>% filter(str_detect(name, “^F”))
组合 filter 与 select
可以链式使用 filter 和 select 来先筛选行,再选择列。
代码示例:
starwars %>%
filter(hair_color == “none” | eye_color == “black”) %>%
select(name, species, homeworld, hair_color, eye_color)
创建新列:mutate 函数


mutate 函数用于创建新的列或修改现有的列。
基本创建

新列的计算结果会自动添加到数据框的末尾。
代码示例:
# 将身高从厘米转换为英寸
starwars %>%
mutate(height_inches = height / 2.54) %>%
select(name, height, height_inches) %>%
head(1)
使用向量化函数

mutate 中可以使用各种向量化函数。重要的是,新创建的列必须与数据框的行数相同。
代码示例:
starwars %>%
select(name, mass, birth_year) %>%
mutate(
cummin_mass = cummin(mass), # 累积最小值
mass_ratio = mass / mean(mass, na.rm = TRUE), # 与平均值的比值
pmin_value = pmin(mass, birth_year), # 逐元素最小值
lag_mass = lag(mass, 2) # 将 mass 列的值向下偏移两位
)
注意:像 cummin(累积最小值)、cumsum(累积和)这类函数在处理时间序列数据时更有意义。lead 和 lag 函数则用于获取向前或向后偏移的值。
其他实用函数

排序行:arrange

arrange 函数用于对数据框的行进行排序。
代码示例:
starwars %>%
select(name, birth_year, height, mass) %>%
arrange(desc(birth_year), mass)
按位置选择行:slice
slice 系列函数用于按行的位置进行选择。
slice(5:10): 选择第5到第10行。slice_sample(n = 5): 随机抽取5行。这在检查数据或抽样时非常有用。slice_max(mass, n = 3): 选择mass最大的3行。slice_min(mass, n = 3): 选择mass最小的3行。
代码示例:
# 随机查看5行数据
starwars %>% slice_sample(n = 5)
总结
本节课我们一起学习了 dplyr 包中三个核心的数据操作函数:select、filter 和 mutate。
select用于灵活地选择数据列。filter用于根据条件筛选数据行。mutate用于创建或转换数据列。

我们还介绍了管道操作符 %>%,它可以将这些函数优雅地串联起来,形成清晰的数据处理流程。此外,我们简要了解了 arrange 和 slice 等辅助函数。掌握这些“动词”是进行高效数据整理和分析的基础。建议下载 dplyr 速查表以便随时查阅。
21:dplyr - group_by 与 summarise


在本节课中,我们将继续学习 dplyr 包,重点介绍 group_by 和 summarise 这两个函数。它们能极大地释放数据处理的威力,允许我们按组计算汇总统计量。
概述
summarise 函数用于计算汇总值。汇总函数接收多个值,并将其缩减为一个值。例如,对100个值求均值,会得到一个均值;求中位数,会得到一个中位数。group_by 函数则用于根据分类变量对数据进行分组。当两者结合使用时,我们可以为每个组分别计算汇总统计量。



使用 summarise 函数
summarise 的拼写是 summarise(带“s”),这是 tidyverse 作者 Hadley Wickham(来自新西兰)的拼写习惯。后来也添加了 summarize(带“z”)版本,功能完全相同。我们将使用带“s”的版本。
调用 summarise 时,它会计算汇总值。例如,我们可以对整个 starwars 数据集(共87行)计算多个汇总统计量。
starwars %>%
summarise(
average_height = mean(height, na.rm = TRUE),
variance_height = var(height, na.rm = TRUE),
average_mass = mean(mass, na.rm = TRUE),
min_height = min(height, na.rm = TRUE),
max_mass = max(mass, na.rm = TRUE),
count = n()
)
这段代码会计算身高的平均值、方差,质量的平均值,身高的最小值,质量的最大值,以及总行数。结果被汇总为一行。Tibble 默认只显示几位有效数字,但实际值包含更多小数位。

你可能会问,这和不使用 dplyr 直接计算 mean(starwars$height, na.rm = TRUE) 有什么区别?summarise 的优势在于它能与管道操作符流畅结合,并方便地一次性计算多个统计量。

结合 group_by 使用

summarise 的真正威力在与 group_by 结合时才能完全展现。group_by 根据分类变量(如物种)形成分组。
首先,我们看看仅使用 group_by 的效果:

starwars %>%
group_by(species) %>%
select(name, height, mass, species)



输出看起来和直接使用 select 相似,但会多出一行提示:Groups: species [38]。这表示数据已按38个不同的物种分组,但尚未进行任何计算。
现在,让我们加入 summarise:


starwars %>%
group_by(species) %>%
select(name, height, mass, species) %>%
summarise(
mean_height = mean(height, na.rm = TRUE),
sd_height = sd(height, na.rm = TRUE),
mean_mass = mean(mass, na.rm = TRUE),
sd_mass = sd(mass, na.rm = TRUE),
count = n()
)
因为数据被分成了38个组(每个物种一个组),所以我们会得到38行结果,每行代表一个组的汇总统计。例如,对于“Droid”组(有6个个体),会计算其身高的均值、标准差等。
筛选与排序分组结果
我们可能只关心那些有多个个体的组,以便计算标准差更有意义。我们可以对汇总后的结果进行进一步处理。
starwars %>%
group_by(species) %>%
select(name, height, mass, species) %>%
summarise(
mean_height = mean(height, na.rm = TRUE),
sd_height = sd(height, na.rm = TRUE),
mean_mass = mean(mass, na.rm = TRUE),
sd_mass = sd(mass, na.rm = TRUE),
count = n()
) %>%
filter(count > 1) %>% # 只保留个体数大于1的组
arrange(desc(count)) %>% # 按个体数降序排列
head(6) # 查看前6行
这样,我们就得到了个体数最多的几个物种(如人类、机器人)的汇总信息。



结合 group_by 与 mutate
group_by 也可以与 mutate 结合使用,用于按组计算新变量,例如计算组内的 z 分数。
starwars %>%
filter(species %in% c("Human", "Droid")) %>%
select(name, species, height) %>%
group_by(species) %>% # 按物种分组
mutate(z_score_height = (height - mean(height, na.rm = TRUE)) / sd(height, na.rm = TRUE))

这段代码为人类和机器人分别计算了身高的 z 分数。z 分数的计算公式是:
z = (x - μ) / σ
其中 x 是单个值,μ 是组均值,σ 是组标准差。
因为按组计算,卢克·天行者(人类)的身高低于人类平均身高,所以他的 z 分数为负;而 C-3PO(机器人)的身高高于机器人平均身高,所以他的 z 分数为正。


如果注释掉 group_by(species) 这一行,z 分数将在所有个体间计算,结果会完全不同。
多重分组
group_by 支持按多个变量进行分组。我们用一个简单的示例数据来说明。

假设我们有以下数据:
country: 国家(如阿富汗、巴西、中国)year: 年份(1999, 2000)sex: 性别(男、女)cases: 病例数

首先,我们按国家和年份分组并求和:
toy_data %>%
group_by(country, year) %>%
summarise(total_cases = sum(cases))



这会根据 country 和 year 的唯一组合形成多个组(例如阿富汗-1999、阿富汗-2000),并计算每个组的总病例数。
分组具有层次结构。如果在此基础上再次调用 summarise,它将移除最后一层分组(year),只按 country 进行汇总。
toy_data %>%
group_by(country, year) %>%
summarise(total_cases = sum(cases)) %>%
summarise(total_cases = sum(total_cases)) # 此时只按 country 分组

分组的顺序会影响结果的排序。group_by(year, country) 会先按年份排序,再在国家内部排序。
总结

本节课我们一起学习了 dplyr 包中 group_by 和 summarise 函数的强大功能。
summarise用于将多行数据汇总为单行统计值。group_by用于根据一个或多个分类变量对数据进行分组。- 两者结合,可以轻松计算每个组的汇总统计量(如均值、标准差、计数)。
group_by还可以与mutate联用,进行按组的计算(如 z 分数)。- 理解多重分组的层次结构对于组织复杂的数据分析至关重要。

掌握 group_by 和 summarise 是进行高效数据聚合和分析的关键步骤,在后续的作业和实践中你会经常用到它们。
22:dplyr 连接操作 🧩
在本节课中,我们将要学习如何使用 dplyr 包中的连接操作来合并两个表格或 tibble。连接操作是数据整理中的核心技能,它允许我们基于共同的列将不同数据集的信息组合在一起。

概述

dplyr 提供了多种连接函数,用于将两个表格中的数据行进行组合。这些操作主要分为三类:添加新变量的连接、筛选连接和集合操作。本节课我们将重点介绍最常用的几种添加新变量的连接操作。
连接操作简介
上一节我们介绍了 dplyr 的基本数据操作。本节中我们来看看如何将两个表格连接在一起。连接操作的核心思想是,根据一个或多个关键列(key)的匹配情况,将两个表格的行进行组合。
如果你熟悉 SQL 语言,那么这些操作会非常熟悉。如果不熟悉,本节将是一个很好的入门介绍。
以下是 dplyr 中主要的连接函数:

left_join():保留左侧表格的所有行,并从右侧表格添加匹配的列。right_join():保留右侧表格的所有行,并从左侧表格添加匹配的列。inner_join():只保留两个表格中都能匹配上的行。full_join():保留两个表格中的所有行,无法匹配的位置用NA填充。

连接操作示例
为了清晰地理解这些连接的区别,我们通过一个简单的例子来演示。假设我们有两个表格:people 和 states。
people 表格包含人名及其所在州的缩写:
# 代码示例:创建 people tibble
people <- tibble(
name = c(“Adam”, “Betty”, “Carl”, “Doug”),
state = c(“CA”, “CA”, “NY”, “TX”)
)


states 表格包含州缩写与州全名的对应关系:

# 代码示例:创建 states tibble
states <- tibble(
abbrev = c(“CA”, “NY”, “WA”),
name = c(“California”, “New York”, “Washington”)
)

现在,让我们看看对这同一个 people 和 states 表格应用不同连接函数的结果。
左连接 (left_join)

左连接会保留左侧表格(people)的所有行,然后从右侧表格(states)中寻找匹配的行来添加新列。


# 代码示例:左连接
left_join_result <- left_join(people, states, by = c(“state” = “abbrev”))
结果分析:
- Adam 和 Betty 的
state是 “CA”,在states表中匹配到 “California”。 - Carl 的 “NY” 匹配到 “New York”。
- Doug 的 “TX” 在
states表中没有对应的缩写,因此州全名 (name.y) 显示为NA。 states表中的 “WA” (Washington) 因为没有在people表中出现,所以不出现在结果中。
公式表示:结果 = people 的所有行 + states 的匹配列
右连接 (right_join)
右连接与左连接相反,它会保留右侧表格(states)的所有行,然后从左侧表格(people)中寻找匹配的行来添加新列。
# 代码示例:右连接
right_join_result <- right_join(people, states, by = c(“state” = “abbrev”))


结果分析:
states表中的 “CA” 匹配到了 Adam 和 Betty 两行。- “NY” 匹配到了 Carl。
- “WA” 在
people表中没有对应的人,因此人名 (name.x) 显示为NA。 - Doug (TX) 因为其州缩写不在右侧的
states表中,所以不出现在结果中。
公式表示:结果 = states 的所有行 + people 的匹配列

内连接 (inner_join)
内连接只返回两个表格中都能匹配上的行,相当于取交集。
# 代码示例:内连接
inner_join_result <- inner_join(people, states, by = c(“state” = “abbrev”))
结果分析:
- 只有 “CA” 和 “NY” 这两个缩写同时出现在两个表中。
- 因此,结果只包含 Adam、Betty 和 Carl 这三行,以及他们对应的州全名。
- Doug (TX) 和 Washington (WA) 因为无法在另一张表中找到匹配项,均被排除。


公式表示:结果 = people 的行 ∩ states 的行
全连接 (full_join)

全连接会保留两个表格中的所有行,无论是否匹配。无法匹配的位置用 NA 填充,相当于取并集。
# 代码示例:全连接
full_join_result <- full_join(people, states, by = c(“state” = “abbrev”))
结果分析:
- 包含了
people表的所有四行和states表的所有三行。 - Doug (TX) 的州全名是
NA。 - Washington (WA) 对应的人名是
NA。
公式表示:结果 = people 的所有行 ∪ states 的所有行
指定连接键 (by 参数)
连接操作的关键在于指定用于匹配的列,这是通过 by 参数完成的。
- 当列名相同时:如果两个表格中用于匹配的列名相同(例如都叫
id),可以简写为by = “id”。 - 当列名不同时:如果列名不同(如本例中的
state和abbrev),则需要使用一个命名向量来明确对应关系:by = c(“左表列名” = “右表列名”)。 - 不指定
by参数:如果不指定,dplyr会自动使用两个表格中所有同名的列进行匹配。
连接操作的挑战与建议
连接操作的成功与否,高度依赖于数据的整洁度。如果数据非常规范(例如来自设计良好的数据库),连接会非常顺畅。
然而,在实际工作中,数据往往不够整洁,这会使连接操作变得棘手。常见的挑战包括:
- 同一信息以不同格式存储(如 “姓,名” vs. 单独的 “姓” 列和 “名” 列)。
- 拼写不一致(如 “Jonathan” vs. “John”)。
- 存在多余的空格或标点符号。
处理不整洁的数据通常需要在连接前进行大量的数据清洗和文本处理工作,例如使用 separate()、unite() 或 str_trim() 等函数。
总结
本节课中我们一起学习了 dplyr 包中的四种基本连接操作:
left_join():以左表为基准,合并右表的匹配信息。right_join():以右表为基准,合并左表的匹配信息。inner_join():只保留两个表中完全匹配的行。full_join():保留两个表中的所有行。

理解并熟练运用这些连接操作,是进行复杂数据整合与分析的基础。记住,清晰、整洁的数据是成功进行连接操作的前提。
23:R语言中的字符串处理 🧵

在本节课中,我们将学习如何在R语言中处理字符串(文本)数据。字符串处理是数据分析,特别是文本挖掘和自然语言处理领域的基础。我们将介绍创建字符串、连接字符串、格式化输出以及使用基础R函数和stringr包进行基本操作的方法。
到目前为止,我们的计算和计算方法主要处理数值数据。但越来越重要的是,能够处理文本数据。统计学和机器学习中有专门用于解释基于文本数据的整个领域,例如自然语言处理和文本挖掘。虽然本课程无法深入探讨文本分析的细节,但我们将涵盖一些基本的文本操作,特别是正则表达式,这非常重要。


字符与字符串基础
在R中,我们通过字符和字符串来处理文本数据。字符串是一个包含一个或多个字符的字符变量,我们经常互换使用“字符”和“字符串”这两个术语。
存储为字符的值具有基本类型character。在R中打印它们时,会带有引号。

x <- "Hello world"
x
# 输出: [1] "Hello world"
class(x)
# 输出: [1] "character"
创建字符串
你可以使用引号(单引号或双引号)创建字符串。双引号内可以使用单引号,反之亦然,但不能在单引号内再使用单引号,或在双引号内再使用双引号。
# 双引号内使用单引号
s1 <- "It's a nice day."
# 单引号内使用双引号
s2 <- 'He said, "Hello!"'

如果你想在字符串中包含与外围引号相同的引号,必须使用反斜杠\进行转义。
# 错误示例:双引号内使用双引号(未转义)
# s3 <- "She said, "Wow!"" # 这会导致错误
# 正确示例:使用转义字符
s3 <- "She said, \"Wow!\""

character() 函数
character()函数用于创建指定长度的字符向量,每个元素的默认值是空字符""。
char_vec <- character(5)
char_vec
# 输出: [1] "" "" "" "" ""

空字符""与character(0)(长度为0的字符向量)不同。单个空字符等同于character(1)。
连接字符串:paste() 与 paste0()
paste()函数是最重要的字符串处理函数之一。它接受一个或多个对象,将它们转换为字符,然后连接(粘贴)在一起形成一个或多个字符串。
基本语法是paste(..., sep = " ", collapse = NULL)。sep参数指定连接后字符之间的分隔符,默认为单个空格。collapse参数指定是否以及如何将结果向量折叠成一个字符串。
paste0()是一个相关函数,功能完全相同,只是默认分隔符sep是空字符""。
以下是paste()函数的工作原理示例:
# 示例1:连接字符串和数值,使用默认空格分隔符
paste("I ate some", pi, "and it was delicious")
# 输出: [1] "I ate some 3.14159265358979 and it was delicious"
# 示例2:连接多个字符串,使用自定义分隔符
paste("bears", "beets", "Battlestar Galactica", sep = ", ")
# 输出: [1] "bears, beets, Battlestar Galactica"

# 示例3:将一个字符与向量连接,sep为空
paste("H", c("A", "E", "I"), sep = "")
# 输出: [1] "HA" "HE" "HI"

# 示例4:两个向量按元素连接
paste(letters[1:3], 1:3, sep = "")
# 输出: [1] "a1" "b2" "c3"


# 示例5:使用collapse参数将结果折叠成一个字符串
paste("a", 1:3, sep = "", collapse = ", ")
# 输出: [1] "a1, a2, a3"
字符串输出函数
R提供了几个用于输出字符串的函数:print()、cat()和format()。

print() 函数
print()是通用的打印函数。它有一个可选的逻辑参数quote,用于指定是否用引号打印字符。
x <- "Hello world"
print(x)
# 输出: [1] "Hello world"
print(x, quote = FALSE)
# 输出: [1] Hello world
noquote(x) # 另一种无引号打印方式
# 输出: [1] Hello world




虽然print(x, quote=FALSE)和noquote(x)的输出看起来相同,但noquote()函数输出一个带有noquote类的对象,该对象在后续打印时也会保持无引号。
cat() 函数

cat()函数将多个字符向量连接成一个向量,添加指定的分隔符,并在没有引号的情况下输出结果。它返回一个不可见的NULL值,因此其输出通常无法存储到变量中。


cat(x, "hello universe", sep = ", ")
# 输出: Hello world, hello universe
cat()的一个好处是,可以使用file参数将打印的输出保存到外部文件。append参数决定是追加到现有文件还是覆盖它。
cat(x, "hello universe", sep = ", ", file = "hello.txt")

format() 函数
format()函数用于对对象进行格式化以便美观地打印。一些有用的参数包括:
width:指定生成字符串的最小宽度。trim:决定是否用空格填充。justify:控制字符串的填充方式(左对齐left、右对齐right、居中center或无none)。- 对于数字:
digits(有效数字位数)、nsmall(小数点右侧最小位数)、scientific(是否使用科学计数法)。
# 数字格式化
format(1 / 1:5, digits = 2)
# 输出: [1] "1.00" "0.50" "0.33" "0.25" "0.20"
format(1 / 1:5, scientific = TRUE, digits = 2)
# 输出: [1] "1.0e+00" "5.0e-01" "3.3e-01" "2.5e-01" "2.0e-01"

# 文本格式化
format(c("Hello", "world", "universe"), width = 10, justify = "left")
# 输出: [1] "Hello " "world " "universe "

基础字符串操作函数
R提供了一些用于处理字符串的基本函数。

以下是几个核心函数及其作用:
nchar():计算字符串中的字符数。tolower()/toupper():将字符串转换为全小写或全大写。chartr():进行字符翻译(替换)。substr():提取或替换子字符串。strsplit():根据分隔符拆分字符串。
# nchar: 计算字符数
nchar(c("Hello", "world", "universe"))
# 输出: [1] 5 5 8


# tolower / toupper: 大小写转换
tolower(c("Hello", "WORLD"))
# 输出: [1] "hello" "world"
toupper(c("Hello", "world"))
# 输出: [1] "HELLO" "WORLD"
# chartr: 字符翻译(旧字符->新字符)
chartr("H", "y", c("Hello", "world", "Hello universe"))
# 输出: [1] "yello" "world" "yello universe"
chartr("eli", "31!", "Hello world universe")
# 输出: [1] "H31!o wor!d univ3rs3"
# substr: 提取子字符串
substr("Hello world", start = 2, stop = 9)
# 输出: [1] "ello wor"

# strsplit: 拆分字符串
strsplit("Hello world", split = " ") # 按空格拆分
# 输出: [[1]] [1] "Hello" "world"
strsplit("Hello world", split = "o") # 按字母'o'拆分
# 输出: [[1]] [1] "Hell" " w" "rld"
stringr 包简介

stringr是tidyverse生态系统中的一个包,它使字符串处理(在我看来)更加容易,尤其是在使用我们接下来要讲的正则表达式时。

stringr中的函数旨在使R的字符串函数更加一致、简单和易用。主要改进包括:函数参数名称一致、所有函数都能正确处理NA值和零长度字符、以及各函数的输出数据结构能与其他函数的输入数据结构匹配。
stringr中的许多函数与基础R函数功能对应,但行为更一致。例如:
str_c()类似于paste()str_length()类似于nchar()str_sub()类似于substr()- 此外,还有
str_dup()、str_trim()、str_pad()、str_wrap()等实用函数。

让我们看几个例子,比较stringr和基础R函数的行为差异:
library(stringr)

# 比较 paste 和 str_c 处理零长度向量的方式
paste("x", NULL, character(0), "hello universe")
# 输出: [1] "x hello universe"
str_c("x", NULL, character(0), "hello universe")
# 输出: [1] "xhello universe" # 注意:str_c会忽略零长度输入
# 比较 nchar 和 str_length 处理因子的方式
fac <- factor(c("apple", "banana"))
# nchar(fac) # 这会报错:需要字符向量
str_length(fac) # 这可以正常工作
# 输出: [1] 5 6


本节课中,我们一起学习了R语言中字符串处理的基础知识。我们了解了如何创建和操作字符串,使用paste()和paste0()进行连接,使用print()、cat()和format()进行输出格式化,以及使用nchar()、tolower()等函数进行基本操作。最后,我们简要介绍了更一致、更强大的stringr包。在下一节视频中,我们将开始学习强大的文本模式匹配工具——正则表达式。
24:正则表达式 - 字符集与字符类 🧩




在本节课中,我们将学习正则表达式的基础知识,特别是字符集和字符类的概念。正则表达式是一种强大的工具,用于在文本中查找、匹配和操作特定模式。我们将从最基础的字符匹配开始,逐步介绍如何使用特殊符号(元字符)来构建更灵活的模式。
概述
正则表达式(常称为 RegEx)是一组描述文本模式的符号。它本质上是一种形式语言,其符号遵循一套明确定义的规则来指定所需的模式。正则表达式在数据验证、数据抓取、文本解析等方面非常有用。

基础:字面字符匹配

最基础的正则表达式类型是字面字符匹配,即字符匹配其自身。例如,模式 R 将匹配字符串中的字符 R。这类似于在文档或网页中使用“查找”功能。
英文字母表中的所有字母(大小写共52个)和数字0到9都属于字面字符。
以下是使用 stringr 包中函数进行匹配的示例:
# 加载 stringr 包
library(stringr)

# 示例字符串
text <- "I love stats"
# 使用 string_locate 查找模式 "STAT"
string_locate(text, "STAT")
# 输出:start=8, end=11
# 区分大小写,查找 "stat" 将不匹配
string_locate("I love Stats", "stat")
# 输出:start=NA, end=NA

# 使用 string_locate_all 查找所有匹配项
text2 <- "I love statistics. I am a stats major."
string_locate_all(text2, "stat")
# 输出两个匹配项的位置
元字符与通配符
元字符是那些具有特殊含义的非字面字符。正则表达式的强大之处在于能够使用这些元字符来修改模式匹配的方式。
常见的元字符包括:.、$、*、+、?、{}、[]、()、\、|。

通配符 .
点号 . 是一个通配符,用于匹配除换行符外的任何单个字符(可以是字母、符号或空格)。

# 示例向量
words <- c("note", "knot", "nut", "n t")
# 匹配模式 "n.任何字符.t"
string_detect(words, "n.t")
# 输出:TRUE, TRUE, TRUE, TRUE
# 匹配模式 "n.任何字符.任何字符.t"
string_detect(words, "n..t")
# 输出:TRUE, TRUE, FALSE, FALSE
转义元字符

如果你想匹配元字符本身(例如,匹配一个实际的点号.),你需要使用反斜杠 \ 对其进行转义。在R中,由于反斜杠本身也是字符串中的转义字符,因此需要使用双反斜杠 \\。
# 示例:匹配字面点号
entries <- c("5.00", "5100", "5-00", "5 00")
# 错误:点号被当作通配符
string_detect(entries, "5.00")
# 输出:TRUE, TRUE, TRUE, TRUE

# 正确:转义点号以进行字面匹配
string_detect(entries, "5\\.00")
# 输出:TRUE, FALSE, FALSE, FALSE
字符集 [ ]

上一节我们介绍了通配符,它可以匹配任何字符。但有时我们只想匹配一组特定的字符。这时就需要使用字符集。
字符集使用方括号 [ ] 表示,它会匹配集合内的任意一个字符。重要的是,字符集只匹配一个字符,且集合内字符的顺序无关紧要。
例如,字符集 [aeiou] 将匹配任意一个小写元音字母。
words <- c("pan", "pen", "pin", "p0n", "p.n", "paun", "pwn3d", "happiness")
# 匹配模式:p + (a/e/i/o/u中任意一个) + n
pattern <- "p[aeiou]n"
string_detect(words, pattern)
# 输出:TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, TRUE
# 解释:匹配 pan, pen, pin, happiness中的"pin"

字符范围 -
在字符集中,连字符 - 是一个元字符,用于指定一个字符范围,这比列出所有字符更方便。
[a-z]: 所有小写字母。[A-Z]: 所有大写字母。[0-9]: 所有数字。[a-zA-Z]: 所有字母。[0-7]: 数字0到7。

vec <- c("123", "abc", "ABC", ":-)", "AB12", "A", "A8908AB")
# 匹配三个连续的数字
pattern_digits <- "[0-9][0-9][0-9]"
string_detect(vec, pattern_digits)
# 输出:TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE
# 解释:匹配 "123" 和 "890"(在"A8908AB"中)
# 匹配三个连续的小写字母
pattern_lower <- "[a-z][a-z][a-z]"
string_detect(vec, pattern_lower)
# 输出:FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE

否定字符集 [^ ]
有时我们想匹配“除了某些字符之外”的任意字符。这时可以在字符集的开头使用脱字符 ^ 来创建否定字符集。

例如,[^aeiou] 匹配任意一个非小写元音字母的字符。
chars <- c("A", "b", "?", "1", " ", "^")
# 匹配任何非大写字母 A-Z 的字符
pattern_neg <- "[^A-Z]"
string_detect(chars, pattern_neg)
# 输出:FALSE, TRUE, TRUE, TRUE, TRUE, TRUE
# 解释:只有 "A" 被排除,返回 FALSE

# 注意:如果 ^ 不在开头,则被视为普通字符
pattern_caret <- "[A-Z^]"
string_detect(chars, pattern_caret)
# 输出:TRUE, FALSE, FALSE, FALSE, FALSE, TRUE
# 解释:匹配 "A" 和 "^"
字符集内的元字符
在字符集内部,大多数元字符(如 .、*、$)都会失去特殊含义,被视为字面字符。主要的例外是:]、[、-(用于范围时)、^(在开头时)和 \。
words <- c("pan", "pen", "pin", "p0n", "p.n", "paun", "pwn3d")

# 点号在字符集内被视为字面字符,匹配实际的点号"."
pattern <- "p[aeiou.]n"
string_detect(words, pattern)
# 输出:TRUE, TRUE, TRUE, FALSE, TRUE, FALSE, FALSE
# 解释:匹配 pan, pen, pin, p.n

字符类
字符类是常见字符集的快捷方式。在R中,我们需要使用双反斜杠 \\ 来表示它们。
\\d: 匹配任意数字。等价于[0-9]。\\D: 匹配任意非数字。等价于[^0-9]。\\w: 匹配任意单词字符(字母、数字、下划线_)。等价于[a-zA-Z0-9_]。\\W: 匹配任意非单词字符。\\s: 匹配任意空白字符(空格、制表符、换行符等)。\\S: 匹配任意非空白字符。
words <- c("pan", "pen", "pin", "p0n", "p.n", "paun", "pwn3d")
# 匹配 p 后接一个数字
string_detect(words, "p\\d")
# 输出:FALSE, FALSE, FALSE, TRUE, FALSE, FALSE, FALSE

# 匹配 p 后接一个非数字
string_detect(words, "p\\D")
# 输出:TRUE, TRUE, TRUE, FALSE, TRUE, TRUE, TRUE
# 匹配 p 后接一个非单词字符(此处是点号)
string_detect(words, "p\\W")
# 输出:FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, FALSE
POSIX 字符类
POSIX 字符类提供了另一种指定字符集的快捷方式,在R中需要用双括号 [[: :]] 括起来。
[[:alpha:]]: 字母。[[:digit:]]: 数字。[[:lower:]]: 小写字母。[[:upper:]]: 大写字母。[[:alnum:]]: 字母数字。[[:punct:]]: 标点符号。[[:space:]]: 空白字符。

words <- c("pan", "pen", "pin", "p0n", "p.n", "paun", "pwn3d")
# 匹配包含字母的字符串
string_detect(words, "[[:alpha:]]")
# 输出:全部为 TRUE
# 匹配包含数字的字符串
string_detect(words, "[[:digit:]]")
# 输出:FALSE, FALSE, FALSE, TRUE, FALSE, FALSE, TRUE

总结
本节课我们一起学习了正则表达式中字符集和字符类的核心概念。

- 我们从最基础的字面字符匹配开始。
- 然后介绍了元字符,特别是通配符
.及其转义方法\\。 - 接着深入学习了字符集
[ ],包括如何指定字符范围[a-z]和创建否定字符集[^aeiou]。 - 最后,我们了解了更简洁的字符类(如
\\d,\\w)和 POSIX 字符类(如[[:alpha:]])。

掌握这些基础是构建更复杂正则表达式模式的关键。下一讲,我们将学习如何指定模式的重复次数,例如匹配多个连续数字或可选字符。
25:正则表达式中的锚点与量词 🔍



在本节课中,我们将学习正则表达式中两个核心概念:锚点和量词。锚点用于匹配字符串中的特定位置,而非具体字符;量词则用于指定模式重复的次数。掌握它们能让你更精确地控制文本匹配。
锚点:匹配位置而非字符
上一节我们介绍了字符集,本节中我们来看看锚点。锚点是模式的一部分,它不匹配任何字符,而是匹配字符串中的特定位置,例如字符串的开头、结尾或单词边界。
以下是常见的锚点符号及其含义:
^:匹配字符串的开始位置。$:匹配字符串的结束位置。\b:匹配一个单词边界(例如单词的开头或结尾)。\B:匹配非单词边界的位置。
锚点应用示例
让我们通过一些例子来理解锚点的作用。假设我们有文本:"The quick brown fox jumps over the lazy dog dog"。
- 无锚点匹配:模式
"THE"会匹配所有出现的 “THE”,并将其替换为 “-”。结果:"- quick brown fox jumps over - lazy dog dog"。 - 起始锚点:模式
"^THE"只匹配位于字符串开头的 “THE”。结果:"- quick brown fox jumps over the lazy dog dog"。 - 结束锚点:模式
"dog$"只匹配位于字符串结尾的 “dog”。结果:"The quick brown fox jumps over the lazy dog -"。
单词边界示例
考虑文本:"words jump jumping umpire pump umpinth lumps"。

- 匹配单词边界:模式
"\b."会匹配所有紧跟在单词边界后的单个字符,并将其替换为 “-”。这有效地标记了所有单词的开头。 - 匹配非单词边界:模式
".\B"会匹配所有不在单词边界前的字符,并将其替换为 “-”。这通常会替换单词内部的字母。


锚点与字符集的区别

初学者有时会混淆 ^ 在字符集内外的不同含义。
"^[0-9]":这里的^是锚点,匹配以数字开头的字符串。"[^0-9]":这里的^在字符集[]内部,表示否定,匹配任何非数字的字符。"[0-9^]":这里的^在字符集内部,但不是第一个字符,因此它匹配数字 0-9 或字面字符 ‘^’。
量词:控制模式重复次数
了解了如何匹配位置后,我们来看看如何控制模式的重复次数。量词附加在字面字符、字符类或分组之后,用于指定前面的元素可以出现多少次。
以下是主要的量词:
*:匹配前面的元素 0 次或多次。+:匹配前面的元素 1 次或多次。?:匹配前面的元素 0 次或 1 次(即可选)。{n}:匹配前面的元素 恰好 n 次。{n,}:匹配前面的元素 至少 n 次。{n,m}:匹配前面的元素 至少 n 次,至多 m 次。
量词应用示例
假设我们有文本:"WORDS 123,456 numbers 9,876"。
- 匹配一个或多个非空白字符:模式
"\S+"会找到所有连续的非空白字符组(如 “WORDS”, “123,456”, “numbers”, “9,876”),并将每一组替换为一个 “-”。结果:"- - - -"。 - 匹配一个或多个数字:模式
"\d+"会找到所有连续的数字组(如 “123”, “456”, “9”, “876”),并将每一组替换为一个 “-”。结果:"WORDS -, - numbers -, -"。 - 精确匹配三次:模式
"\d{3}"只匹配恰好连续出现三次的数字。它会匹配 “123” 和 “876”,但不会匹配 “456”(因为后面紧跟逗号,不是三个连续数字)或单个的 “9”。

问号 ? 的两种用途
问号 ? 有两种重要功能:
- 作为量词:表示前面的元素出现 0 次或 1 次(即可选)。
- 例如,模式
"M(OM){1,2}(AY)?"可以匹配 “MOMMY” 和 “MOM”。(AY)?使得 “AY” 成为可选部分,所以 “MOM” 也能匹配。
- 例如,模式

- 使其他量词“非贪婪”:默认情况下,量词如
*和+是“贪婪的”,会尽可能匹配最长的字符串。在它们后面加上?会使其变为“非贪婪”或“懒惰的”,匹配尽可能短的字符串。- 贪婪匹配:在文本
"Peter Piper picked a peck"中,模式"P.*r"会从第一个 ‘P’ 开始,一直匹配到最后一个 ‘r’,得到"Peter Piper picked a peck"。 - 非贪婪匹配:模式
"P.*?r"会进行最短匹配,首先得到"Peter",然后在剩余文本中继续匹配,可能得到"Piper"。
- 贪婪匹配:在文本
总结

本节课中我们一起学习了正则表达式中两个强大的工具:锚点和量词。

- 锚点(如
^,$,\b)让你能够将匹配固定在字符串的特定位置,例如开头、结尾或单词边界,从而实现更精确的定位。 - 量词(如
*,+,?,{n,m})让你能够指定一个模式应该重复多少次,从而灵活地匹配不同长度的内容。 - 特别要注意
?的双重角色:作为独立量词表示“可选”,附加在其他量词后表示“非贪婪匹配”。

结合使用锚点和量词,你可以构建出极其强大和灵活的正则表达式模式,以应对各种复杂的文本匹配和提取任务。
26:正则表达式 - 捕获组与环视

在本节课中,我们将学习正则表达式中两个强大的功能:捕获组和环视。捕获组允许我们提取和引用匹配文本的特定部分,而环视则让我们基于匹配项前后的内容进行条件匹配。掌握这些概念将极大地提升你处理和分析文本数据的能力。

捕获组基础
上一节我们介绍了正则表达式的基本模式匹配。本节中,我们来看看如何使用括号 () 来创建捕获组。

括号 () 有两个主要作用:
- 将模式的一部分组合在一起。
- 创建一个编号的捕获组。匹配括号内模式的内容可以被后续通过组号(如
\1,\2)引用,用于修改或替换。

如果在开括号后立即加上 ?:(例如 (?:...)),则该组变为非捕获组。它仅用于分组,不会被捕获和编号。

以下是一些基本模式示例:
A(BCD):匹配ABCD,并捕获BCD作为组1。A(?:BCD)EF(GHI):匹配ABCDEFGHI,但只捕获GHI作为组1(因为BCD是非捕获组)。
捕获组通常与 str_match() 或 str_match_all() 等函数结合使用,以提取匹配的组。
组内的“或”操作与引用
在捕获组内部,我们可以使用竖线 | 来表示“或”逻辑。正则表达式会从左到右尝试匹配。
例如,模式 (MRS|MS|MR) 会按顺序优先匹配 MRS, 然后是 MS, 最后是 MR。
定义捕获组后,我们可以使用反斜杠加数字(如 \1, \2)来反向引用之前捕获的组。这在 str_replace() 等替换操作中非常有用。
捕获组应用示例

让我们通过几个具体例子来理解捕获组如何工作。
示例1:提取姓名与头衔
假设我们有以下文本:"Mr. Smith, Mrs. Lee, Ms. Garcia, Andy Hope"。
我们可以使用模式 (MRS|MS|MR)\.\s(\w+) 来匹配并提取头衔和姓氏。
(MRS|MS|MR):捕获组1,匹配头衔。\.:匹配字面量句点。\s:匹配一个空白字符(如空格)。(\w+):捕获组2,匹配一个或多个单词字符(即姓氏)。

使用 str_match_all() 会返回一个矩阵,其中包含每个匹配项的全匹配以及对应的两个捕获组(头衔和姓氏)。Andy Hope 因为没有匹配的头衔,所以不会被捕获。
如果我们将模式改为 (?:MRS|MS|MR)\.\s(\w+), 那么 (?:MRS|MS|MR) 就变成了非捕获组。此时,str_match_all() 返回的矩阵将只包含全匹配和一个捕获组(即姓氏)。

示例2:使用反向引用重排姓名
考虑文本:"George Washington, John Adams, Thomas Jefferson"。

模式 (\w+)\s(\w+),? 可以匹配“名 姓”的结构,并分别捕获名和姓。
(\w+):捕获组1,匹配名。\s:匹配空格。(\w+):捕获组2,匹配姓。,?:匹配一个可选的逗号。
现在,我们可以使用 str_replace_all(text, pattern, "\\2, \\1;") 进行替换。这里的 \\2 和 \\1 就是反向引用,分别代表第二个捕获组(姓)和第一个捕获组(名)。
替换结果是:"Washington, George; Adams, John; Jefferson, Thomas;"。我们成功地将“名 姓”的格式转换成了“姓, 名”的格式。
示例3:查找重复单词
模式 \b(\w+)\s+\1\b 可以用来查找文本中连续重复的单词。
\b:单词边界。(\w+):捕获组1,匹配一个单词。\s+:匹配一个或多个空白字符。\1:反向引用,必须匹配与捕获组1完全相同的单词。\b:单词边界。



在句子 "The quick brown fox jumps over the the lazy dog." 中,这个模式会匹配到 "the the" 中的第一个 "the"(注意:匹配的是第一个“the”以及它后面的空格,但反向引用 \1 确保第二个“the”必须存在)。这对于文本校对和清理非常有用。

综合案例:提取和清理电话号码


这是一个更复杂的例子,用于匹配北美常见的电话号码格式。
模式:$?([2-9]\d{2})$?[- .]?([2-9]\d{2})[- .]?(\d{4})
让我们分解一下:
$?:匹配一个可选的左括号(。([2-9]\d{2}):捕获组1,匹配区号。区号首位是2-9,后跟任意两个数字。$?:匹配一个可选的右括号)。[- .]?:匹配一个可选的分隔符(短横线、空格或点)。([2-9]\d{2}):捕获组2,匹配电话号码的前三位。[- .]?:匹配一个可选的第二个分隔符。(\d{4}):捕获组3,匹配电话号码的后四位。

给定文本:"Apple 1-800-613-1032, 209.162.6310, 208.0448, work 825-8430, home 323-242-6111", 使用 str_match() 可以提取出每个有效电话号码的区号和主体部分。
我们还可以利用捕获组进行标准化清理。例如,使用 str_replace_all(phone_text, pattern, "\\1-\\2-\\3") 可以将所有匹配的电话号码统一格式化为 XXX-XXX-XXXX 的形式。

环视
有时我们需要匹配一个模式,但要求它前面或后面必须(或不能)跟着另一个模式。这称为环视。环视本身不消耗字符,也不属于匹配结果的一部分。


以下是四种环视结构:
- 肯定顺序环视
(?=...):匹配后面跟着...的位置。- 示例:
gray(?=hound)在"I put a gray hat on my greyhound."中,只匹配"greyhound"前面的"gray"(即"grey"), 而不匹配第一个"gray"。
- 示例:
- 否定顺序环视
(?!...):匹配后面不跟着...的位置。- 示例:
gray(?!hound)匹配后面不是"hound"的"gray"。在上面的句子中,它会匹配第一个"gray", 而不匹配"greyhound"中的"grey"。
- 示例:
- 肯定逆序环视
(?<=...):匹配前面是...的位置。- 示例:
(?<=\\$)\\d+在"I withdrew $100 in $1 bills, $5 bills, and 5 $20 bills."中,只匹配紧跟在美元符号$后面的数字(1,5,20), 而不匹配100或单独的5。
- 示例:
- 否定逆序环视
(?<!...):匹配前面不是...的位置。- 示例:
(?<!\\$)\\d+匹配前面没有美元符号$的数字。在上面的句子中,它会匹配100和第二个5。
- 示例:
注意:逆序环视中的模式(... 部分)必须是固定长度的,不能使用 * 或 + 这类可变长度的量词。
总结

本节课中我们一起学习了正则表达式的两个高级特性:捕获组和环视。
- 捕获组
()允许我们提取和引用匹配文本的特定部分,通过编号(\1,\2)或使用?:创建非捕获组来灵活控制。 - 反向引用让我们能在同一表达式中或替换操作中复用已捕获的文本。
- 环视(
(?=),(?!),(?<=),(?<!))提供了强大的条件匹配能力,让我们可以基于匹配项周围的上下文进行匹配,而不会将上下文本身包含在结果中。


结合这些工具,你可以构建出极其强大和精确的正则表达式,用于复杂的数据提取、验证和清理任务,例如从非结构化文本中提取电话号码、电子邮件、重排数据格式或执行高级搜索。

浙公网安备 33010602011771号