hav-cs50-merge-06
哈佛 CS50 中文官方笔记(七)
第九讲
-
等等
-
set -
全局变量
-
常量
-
类型提示
-
文档字符串
-
argparse -
解包
-
args和kwargs -
map -
列表推导式
-
filter -
字典推导式
-
enumerate -
生成器和迭代器
-
恭喜!
-
这是 CS50!
等等
-
在过去的许多课程中,我们已经涵盖了与 Python 相关的许多内容!
-
在本课中,我们将关注许多之前未讨论的“等等”项目。“Et cetera”字面意思是“等等”。
-
的确,如果你查看 Python 文档,你会找到许多其他功能。
set
-
在数学中,一个集合会被认为是一个没有重复数字的数字集合。
-
在文本编辑器窗口中,按照以下方式编写代码:
students = [ {"name": "Hermione", "house": "Gryffindor"}, {"name": "Harry", "house": "Gryffindor"}, {"name": "Ron", "house": "Gryffindor"}, {"name": "Draco", "house": "Slytherin"}, {"name": "Padma", "house": "Ravenclaw"}, ] houses = [] for student in students: if student["house"] not in houses: houses.append(student["house"]) for house in sorted(houses): print(house)注意到我们有一个字典列表,每个字典代表一个学生。创建了一个名为
houses的空列表。我们遍历students中的每个student。如果一个学生的house不在houses中,我们就将其添加到我们的houses列表中。 -
结果表明,我们可以使用内置的
set功能来消除重复项。 -
在文本编辑器窗口中,按照以下方式编写代码:
students = [ {"name": "Hermione", "house": "Gryffindor"}, {"name": "Harry", "house": "Gryffindor"}, {"name": "Ron", "house": "Gryffindor"}, {"name": "Draco", "house": "Slytherin"}, {"name": "Padma", "house": "Ravenclaw"}, ] houses = set() for student in students: houses.add(student["house"]) for house in sorted(houses): print(house)注意到我们不需要包含任何检查来确保没有重复项。
set对象会自动为我们处理这个问题。 -
你可以在 Python 的文档中了解更多关于
set的信息:Python 的set文档。
全局变量
-
在其他编程语言中,存在全局变量的概念,这些变量可以被任何函数访问。
-
我们可以利用 Python 中的这一功能。在文本编辑器窗口中,按照以下方式编写代码:
balance = 0 def main(): print("Balance:", balance) if __name__ == "__main__": main()注意到我们如何在任何函数之外创建一个名为
balance的全局变量。 -
由于执行上述代码没有出现错误,你可能会认为一切正常。然而,事实并非如此!在文本编辑器窗口中,按照以下方式编写代码:
balance = 0 def main(): print("Balance:", balance) deposit(100) withdraw(50) print("Balance:", balance) def deposit(n): balance += n def withdraw(n): balance -= n if __name__ == "__main__": main()注意我们现在添加了向
balance添加和提取资金的功能。然而,执行此代码时,我们遇到了一个错误!我们看到一个名为UnboundLocalError的错误。你可能能够猜到,至少在我们当前编写的balance和deposit以及withdraw函数的方式中,我们无法在函数内部重新分配它的新值。 -
要在函数内部与全局变量交互,解决方案是使用
global关键字。在文本编辑器窗口中,按照以下方式编写代码:balance = 0 def main(): print("Balance:", balance) deposit(100) withdraw(50) print("Balance:", balance) def deposit(n): global balance balance += n def withdraw(n): global balance balance -= n if __name__ == "__main__": main()注意到
global关键字告诉每个函数,balance并不指向一个局部变量:相反,它指向我们在代码顶部最初放置的全局变量。现在,我们的代码可以正常工作了! -
利用我们从面向对象编程中获得的经验,我们可以修改我们的代码,使用类而不是全局变量。在文本编辑器窗口中,编写以下代码:
class Account: def __init__(self): self._balance = 0 @property def balance(self): return self._balance def deposit(self, n): self._balance += n def withdraw(self, n): self._balance -= n def main(): account = Account() print("Balance:", account.balance) account.deposit(100) account.withdraw(50) print("Balance:", account.balance) if __name__ == "__main__": main()注意,我们如何使用
account = Account()来创建一个账户。类允许我们更干净地解决需要全局变量的这个问题,因为这些实例变量可以通过self访问本类的所有方法。 -
一般而言,全局变量应该非常谨慎地使用,如果必须使用的话!
常量
-
一些语言允许您创建不可更改的变量,称为“常量”。常量允许程序员进行防御性编程,并减少重要值被更改的机会。
-
在文本编辑器窗口中,编写以下代码:
MEOWS = 3 for _ in range(MEOWS): print("meow")注意,在这个例子中,
MEOWS是我们的常量。常量通常用大写变量名表示,并放置在代码的顶部。尽管这 看起来 像一个常量,但实际上,Python 实际上没有机制来阻止我们在代码中更改该值!相反,您需要遵守诚信原则:如果变量名全部大写,就请不要更改它! -
您可以创建一个名为“常量”的类,现在我们用引号括起来,因为我们知道 Python 并不完全支持“常量”。在文本编辑器窗口中,编写以下代码:
class Cat: MEOWS = 3 def meow(self): for _ in range(Cat.MEOWS): print("meow") cat = Cat() cat.meow()因为
MEOWS是在任何一个特定类方法之外定义的,所以所有这些方法都可以通过Cat.MEOWS访问该值。
类型提示
-
在其他编程语言中,您需要明确表达您想要使用的变量类型。
-
如我们在课程中较早看到的,Python 不需要显式声明类型。
-
尽管如此,确保所有变量都是正确的类型是一个好的实践。
-
mypy是一个程序,可以帮助您测试以确保所有变量都是正确的类型。 -
您可以通过在终端窗口中执行以下命令来安装
mypy:pip install mypy。
在文本编辑器窗口中,编写以下代码:
def meow(n):
for _ in range(n):
print("meow")
number = input("Number: ")
meow(number)
您可能已经看到,number = input("Number: )" 返回了一个 string,而不是 int。但 meow 很可能需要一个 int!
-
可以添加类型提示来给 Python 提示
meow应该期望的变量类型。在文本编辑器窗口中,编写以下代码:def meow(n: int): for _ in range(n): print("meow") number = input("Number: ") meow(number)注意,尽管如此,我们的程序仍然会抛出错误。
-
安装
mypy后,在终端窗口中执行mypy meows.py。mypy将提供一些关于如何修复此错误的指导。 -
您可以对所有变量进行注释。在文本编辑器窗口中,编写以下代码:
def meow(n: int): for _ in range(n): print("meow") number: int = input("Number: ") meow(number)注意,现在
number被提供了一个类型提示。 -
再次强调,在终端窗口中执行
mypy meows.py可以为您提供更具体的反馈。 -
我们可以通过以下方式修复我们的最终错误:
def meow(n: int): for _ in range(n): print("meow") number: int = int(input("Number: ")) meow(number)注意,现在运行
mypy没有错误,因为我们已经将输入转换为整数。 -
让我们通过假设
meow将返回一个字符串,或str,来引入一个新的错误。在文本编辑器窗口中,编写以下代码:def meow(n: int): for _ in range(n): print("meow") number: int = int(input("Number: ")) meows: str = meow(number) print(meows)注意
meow函数只有一个副作用。因为我们只尝试打印meow,而不是返回一个值,所以当我们尝试将meow的返回值存储在meows中时,会抛出一个错误。 -
我们还可以进一步使用类型提示来检查错误,这次注释函数的返回值。在文本编辑器窗口中,代码如下:
def meow(n: int) -> None: for _ in range(n): print("meow") number: int = int(input("Number: ")) meows: str = meow(number) print(meows)注意到
-> None的表示法告诉mypy没有返回值。 -
如果我们希望返回一个字符串,我们可以修改我们的代码:
def meow(n: int) -> str: return "meow\n" * n number: int = int(input("Number: ")) meows: str = meow(number) print(meows, end="")注意我们如何在
meows中存储多个str。运行mypy不会产生错误。 -
你可以在 Python 的Type Hints文档中了解更多信息。
-
你可以通过程序的自身文档了解更多关于
mypy的信息。
Docstrings
-
使用 docstring 来注释函数的目的是一种标准做法。在文本编辑器窗口中,代码如下:
def meow(n): """Meow n times.""" return "meow\n" * n number = int(input("Number: ")) meows = meow(number) print(meows, end="")注意三个双引号指定了函数的功能。
-
你可以使用 docstrings 来标准化你如何记录函数的特性。在文本编辑器窗口中,代码如下:
def meow(n): """ Meow n times. :param n: Number of times to meow :type n: int :raise TypeError: If n is not an int :return: A string of n meows, one per line :rtype: str """ return "meow\n" * n number = int(input("Number: ")) meows = meow(number) print(meows, end="")注意到包含了多个 docstring 参数。例如,它描述了函数接受的参数以及函数返回的内容。
-
建立的标准工具,如Sphinx,可以用来解析 docstrings,并自动以网页和 PDF 文件的形式为我们创建文档,这样你就可以发布和与他人分享。
-
你可以在 Python 的docstrings文档中了解更多信息。
argparse
-
假设我们想在程序中使用命令行参数。在文本编辑器窗口中,代码如下:
import sys if len(sys.argv) == 1: print("meow") elif len(sys.argv) == 3 and sys.argv[1] == "-n": n = int(sys.argv[2]) for _ in range(n): print("meow") else: print("usage: meows.py [-n NUMBER]")注意
sys是如何被导入的,通过它我们可以访问到sys.argv,这是一个数组,包含了运行程序时提供给我们的命令行参数。我们可以使用多个if语句来检查用户是否正确地运行了我们的程序。 -
假设这个程序将会变得更加复杂。我们该如何检查用户可能插入的所有参数呢?如果我们有超过几个命令行参数,我们可能会放弃!
-
幸运的是,
argparse是一个处理复杂命令行参数字符串解析的库。在文本编辑器窗口中,代码如下:import argparse parser = argparse.ArgumentParser() parser.add_argument("-n") args = parser.parse_args() for _ in range(int(args.n)): print("meow")注意我们是如何导入
argparse而不是sys的。从ArgumentParser类创建了一个名为parser的对象。该类的add_argument方法用于告诉argparse,当用户运行我们的程序时,我们应该期望从用户那里得到哪些参数。最后,运行解析器的parse_args方法确保用户已经正确地包括了所有参数。 -
我们还可以编写更干净的代码,这样当用户未能正确使用程序时,他们可以获取一些关于我们代码正确使用方法的信息。在文本编辑器窗口中,代码如下:
import argparse parser = argparse.ArgumentParser(description="Meow like a cat") parser.add_argument("-n", help="number of times to meow") args = parser.parse_args() for _ in range(int(args.n)): print("meow")注意到用户提供了一些文档。具体来说,提供了一个
help参数。现在,如果用户执行python meows.py --help或-h,用户将看到一些关于如何使用此程序的提示。 -
我们可以进一步改进这个程序。在文本编辑器窗口中,代码如下:
import argparse parser = argparse.ArgumentParser(description="Meow like a cat") parser.add_argument("-n", default=1, help="number of times to meow", type=int) args = parser.parse_args() for _ in range(args.n): print("meow")注意到不仅包含了帮助文档,而且当用户没有提供任何参数时,你还可以提供一个
默认值。 -
你可以在 Python 的
argparse文档中了解更多信息。argparse。
解包
-
不想能够将一个变量分割成两个变量不是很好吗?在文本编辑器窗口中,代码如下:
first, _ = input("What's your name? ").split(" ") print(f"hello, {first}")注意到这个程序尝试通过简单地在一个空格上进行分割来获取用户的名字。
-
结果表明,还有其他方法可以解包变量。通过理解如何以看似更高级的方式解包变量,你可以编写更强大、更优雅的代码。在文本编辑器窗口中,代码如下:
def total(galleons, sickles, knuts): return (galleons * 17 + sickles) * 29 + knuts print(total(100, 50, 25), "Knuts")注意到这返回了 Knuts 的总价值。
-
如果我们想要将硬币存储在一个列表中?在文本编辑器窗口中,代码如下:
def total(galleons, sickles, knuts): return (galleons * 17 + sickles) * 29 + knuts coins = [100, 50, 25] print(total(coins[0], coins[1], coins[2]), "Knuts")注意到创建了一个名为
coins的列表。我们可以通过索引使用0、1等来传递每个值。 -
这变得相当冗长。如果我们能够简单地将硬币列表传递给我们的函数,不是很好吗?
-
为了使传递整个列表成为可能,我们可以使用解包。在文本编辑器窗口中,代码如下:
def total(galleons, sickles, knuts): return (galleons * 17 + sickles) * 29 + knuts coins = [100, 50, 25] print(total(*coins), "Knuts")注意到
*如何解包列表的序列,并将每个单独的元素传递给total。 -
假设我们可以以任何顺序传递货币的名称?在文本编辑器窗口中,代码如下:
def total(galleons, sickles, knuts): return (galleons * 17 + sickles) * 29 + knuts print(total(galleons=100, sickles=50, knuts=25), "Knuts")注意到这仍然计算正确。
-
当你开始谈论“名称”和“值”时,字典可能会浮现在你的脑海中!你可以将其实现为一个字典。在文本编辑器窗口中,代码如下:
def total(galleons, sickles, knuts): return (galleons * 17 + sickles) * 29 + knuts coins = {"galleons": 100, "sickles": 50, "knuts": 25} print(total(coins["galleons"], coins["sickles"], coins["knuts"]), "Knuts")注意到提供了一个名为
coins的字典。我们可以使用键,如“galleons”或“sickles”来索引它。 -
由于
total函数期望三个参数,我们不能传递一个字典。我们可以使用解包来帮助解决这个问题。在文本编辑器窗口中,代码如下:def total(galleons, sickles, knuts): return (galleons * 17 + sickles) * 29 + knuts coins = {"galleons": 100, "sickles": 50, "knuts": 25} print(total(**coins), "Knuts")注意到
**允许你解包一个字典。在解包字典时,它提供了键和值。
args和kwargs
-
回想一下我们在这门课程中之前看到的
print文档:print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False) -
args是位置参数,例如我们提供给print的print("Hello", "World")。 -
kwargs是命名参数,或称为“关键字参数”,例如我们提供给print的print(end="")。 -
正如我们在上面
print函数的原型中看到的,我们可以告诉我们的函数期望一个目前未知数量的位置参数。我们也可以告诉它期望一个目前未知数量的关键字参数。在文本编辑器窗口中,代码如下:def f(*args, **kwargs): print("Positional:", args) f(100, 50, 25)注意到执行此代码将打印为位置参数。
-
我们甚至可以传递命名参数。在文本编辑器窗口中,编写如下代码:
def f(*args, **kwargs): print("Named:", kwargs) f(galleons=100, sickles=50, knuts=25)注意命名值是以字典的形式提供的。
-
考虑到上面的
print函数,你可以看到*objects可以接受任意数量的位置参数。 -
你可以在 Python 的文档中了解更多关于
print的信息:print。
map
-
早期,我们开始了过程式编程。
-
我们后来揭示了 Python 是一种面向对象的编程语言。
-
我们看到了函数式编程的暗示,其中函数有副作用但没有返回值。我们可以在文本编辑器窗口中演示,输入
code yell.py并编写如下代码:def main(): yell("This is CS50") def yell(word): print(word.upper()) if __name__ == "__main__": main()注意
yell函数是如何简单地被喊出来的。 -
不想喊一个无限单词的列表吗?修改你的代码如下:
def main(): yell(["This", "is", "CS50"]) def yell(words): uppercased = [] for word in words: uppercased.append(word.upper()) print(*uppercased) if __name__ == "__main__": main()注意我们是如何累积大写单词的,通过迭代每个单词并对它们进行“大写化”。使用
*解包,我们打印出大写列表。 -
移除括号后,我们可以将单词作为参数传递。在文本编辑器窗口中,编写如下代码:
def main(): yell("This", "is", "CS50") def yell(*words): uppercased = [] for word in words: uppercased.append(word.upper()) print(*uppercased) if __name__ == "__main__": main()注意
*words如何允许函数接受多个参数。 -
map允许你将函数映射到一系列值。在实践中,我们可以这样编写代码:def main(): yell("This", "is", "CS50") def yell(*words): uppercased = map(str.upper, words) print(*uppercased) if __name__ == "__main__": main()注意
map接收两个参数。首先,它接收一个我们想要应用到列表中每个元素的函数。其次,它接收那个列表本身,我们将应用上述函数。因此,words中的所有单词都将传递给str.upper函数,并返回到uppercased。 -
你可以在 Python 的文档中了解更多关于
map的信息:map。
列表推导式
-
列表推导式允许你在一行优雅的代码中动态创建一个列表。
-
我们可以在我们的代码中如下实现:
def main(): yell("This", "is", "CS50") def yell(*words): uppercased = [arg.upper() for arg in words] print(*uppercased) if __name__ == "__main__": main()注意我们如何没有使用
map,而是在方括号内编写 Python 表达式。对于每个参数,.upper都会被应用到它上面。 -
将这个概念进一步扩展,让我们转向另一个程序。
-
在文本编辑器窗口中,输入
code gryffindors.py并编写如下代码:students = [ {"name": "Hermione", "house": "Gryffindor"}, {"name": "Harry", "house": "Gryffindor"}, {"name": "Ron", "house": "Gryffindor"}, {"name": "Draco", "house": "Slytherin"}, ] gryffindors = [] for student in students: if student["house"] == "Gryffindor": gryffindors.append(student["name"]) for gryffindor in sorted(gryffindors): print(gryffindor)注意我们在创建列表时有一个条件。如果学生的房子是格兰芬多,我们就将学生添加到名字列表中。最后,我们打印出所有的名字。
-
更优雅地,我们可以用列表推导式简化这段代码,如下所示:
students = [ {"name": "Hermione", "house": "Gryffindor"}, {"name": "Harry", "house": "Gryffindor"}, {"name": "Ron", "house": "Gryffindor"}, {"name": "Draco", "house": "Slytherin"}, ] gryffindors = [ student["name"] for student in students if student["house"] == "Gryffindor" ] for gryffindor in sorted(gryffindors): print(gryffindor)注意列表推导式是如何放在一行上的!
filter
-
使用 Python 的
filter函数允许我们返回一个序列的子集,其中某些条件为真。 -
在文本编辑器窗口中,编写如下代码:
students = [ {"name": "Hermione", "house": "Gryffindor"}, {"name": "Harry", "house": "Gryffindor"}, {"name": "Ron", "house": "Gryffindor"}, {"name": "Draco", "house": "Slytherin"}, ] def is_gryffindor(s): return s["house"] == "Gryffindor" gryffindors = filter(is_gryffindor, students) for gryffindor in sorted(gryffindors, key=lambda s: s["name"]): print(gryffindor["name"])注意如何创建一个名为
is_gryffindor的函数。这是我们用于筛选学生的函数,它将根据学生的学院是否为格兰芬多返回True或False。你可以看到新的filter函数接受两个参数。首先,它接受应用于序列中每个元素的函数——在这个例子中是is_gryffindor。其次,它接受要应用筛选函数的序列——在这个例子中是students。在gryffindors中,我们应该只看到那些在格兰芬多的学生。 -
filter也可以使用 lambda 函数如下:students = [ {"name": "Hermione", "house": "Gryffindor"}, {"name": "Harry", "house": "Gryffindor"}, {"name": "Ron", "house": "Gryffindor"}, {"name": "Draco", "house": "Slytherin"}, ] gryffindors = filter(lambda s: s["house"] == "Gryffindor", students) for gryffindor in sorted(gryffindors, key=lambda s: s["name"]): print(gryffindor["name"])注意提供了相同的学生的列表。
-
你可以在 Python 的
filter函数文档中了解更多信息《filter》。
字典推导式
-
我们可以将列表推导式的相同理念应用到字典中。在文本编辑器窗口中,编写如下代码:
students = ["Hermione", "Harry", "Ron"] gryffindors = [] for student in students: gryffindors.append({"name": student, "house": "Gryffindor"}) print(gryffindors)注意此代码(目前!)没有使用任何推导式。相反,它遵循我们之前看到的相同范例。
-
我们现在可以通过修改我们的代码来应用字典推导式:
students = ["Hermione", "Harry", "Ron"] gryffindors = [{"name": student, "house": "Gryffindor"} for student in students] print(gryffindors)注意所有之前的代码是如何简化成一行,其中为
students中的每个student提供了字典的结构。 -
我们甚至可以进一步简化如下:
students = ["Hermione", "Harry", "Ron"] gryffindors = {student: "Gryffindor" for student in students} print(gryffindors)注意字典将使用键值对构建。
enumerate
-
我们可能希望为每个学生提供一些排名。在文本编辑器窗口中,编写如下代码:
students = ["Hermione", "Harry", "Ron"] for i in range(len(students)): print(i + 1, students[i])注意运行此代码时每个学生是如何被列举的。
-
利用枚举,我们可以做到相同:
students = ["Hermione", "Harry", "Ron"] for i, student in enumerate(students): print(i + 1, student)注意
enumerate如何展示每个student的索引和值。 -
你可以在 Python 的
enumerate函数文档中了解更多信息《enumerate》。
生成器和迭代器
-
在 Python 中,有一种方法可以防止系统资源耗尽,当它们解决的问题变得太大时。
-
在美国,当人们难以入睡时,习惯于在心中“数绵羊”。
-
在文本编辑器窗口中,输入
code sleep.py并编写如下代码:n = int(input("What's n? ")) for i in range(n): print("🐑" * i)注意这个程序将如何计数你要求其数绵羊的数量。
-
我们可以通过添加一个
main函数来使我们的程序更加复杂,如下所示:def main(): n = int(input("What's n? ")) for i in range(n): print("🐑" * i) if __name__ == "__main__": main()注意提供了一个
main函数。 -
我们已经养成了抽象代码部分的习惯。
-
我们可以通过修改我们的代码来调用绵羊函数:
def main(): n = int(input("What's n? ")) for i in range(n): print(sheep(i)) def sheep(n): return "🐑" * n if __name__ == "__main__": main()注意
main函数是如何进行迭代的。 -
我们可以给
sheep函数提供更多功能。在文本编辑器窗口中,编写如下代码:def main(): n = int(input("What's n? ")) for s in sheep(n): print(s) def sheep(n): flock = [] for i in range(n): flock.append("🐑" * i) return flock if __name__ == "__main__": main()注意我们如何创建一群绵羊并返回
flock。 -
执行我们的代码时,你可以尝试不同的绵羊数量,例如
10、1000和10000。如果你要求1000000只绵羊,你的程序可能会完全挂起或崩溃。因为你试图生成一个庞大的绵羊列表,你的电脑可能难以完成计算。 -
yield生成器可以通过一次返回一小部分结果来解决这个问题。在文本编辑器窗口中,编写如下代码:def main(): n = int(input("What's n? ")) for s in sheep(n): print(s) def sheep(n): for i in range(n): yield "🐑" * i if __name__ == "__main__": main()注意到
yield一次只提供单个值,而for循环则持续工作。 -
你可以在 Python 的生成器文档中了解更多信息。
-
你可以在 Python 的迭代器文档中了解更多信息。
恭喜你!
-
当你从这门课程退出时,你拥有更多的心理模型和工具箱来解决编程相关的问题。
-
首先,你学习了函数和变量。
-
第二,你学习了条件语句。
-
第三,你学习了循环。
-
第四,你学习了异常。
-
第五,你学习了库。
-
第六,你学习了单元测试。
-
第七,你学习了文件 I/O。
-
第八,你学习了正则表达式。
-
最近,你学习了面向对象编程。
-
今天,你学习了你可以使用的许多其他工具。
这就是 CS50!
-
一起创建一个最终程序,在你的终端窗口中输入
code say.py,并编写如下代码:import cowsay import pyttsx3 engine = pyttsx3.init() this = input("What's this? ") cowsay.cow(this) engine.say(this) engine.runAndWait()注意到运行这个程序为你提供了一个充满活力的告别。
-
我们伟大的希望是,你将利用在这门课程中学到的知识来解决世界上的实际问题,使我们的地球变得更美好。
-
这就是 CS50!
R
第一讲
-
欢迎!
-
IDE
-
创建您的第一个程序
-
函数
-
错误
-
readline
-
paste
-
文档
-
算术
-
表格
-
向量
-
向量算术
-
外部数据
-
特殊值
-
factor
-
总结
欢迎!
-
欢迎来到 CS50 的 R 编程入门课程!
-
编程 是一种我们可以向计算机传达指令的方式。
-
有许多 编程语言 可以用来编程,包括 C、Python、Java、R 等等!
-
我们可以使用 R 来回答有关数据的问题,例如模拟 COVID-19 在游轮上的传播情况。R 也可以用来可视化这些问题的答案。
IDE
-
集成开发环境(IDE)是一个预配置的工具集,可以用来编程。
-
R 有自己的 IDE,称为 RStudio,用于专门编写 R 代码。
-
在 RStudio 中,注意
>符号。这表示 R 控制台,我们可以在此处发出命令。
创建您的第一个程序
-
您可以通过在 R 控制台中输入
file.create("hello.R")并按键盘上的enter或return键来创建您的第一个程序。 -
注意
hello.R以.R结尾。你可能以前见过其他以.jpg或.gif扩展名结尾的文件。.R是 R 使用的特定文件扩展名。 -
当你发出上述命令时,你应该在 R 控制台中看到
[1] TRUE。关于这一点,稍后会有更多介绍! -
在 R 控制台的右侧,您可以访问 文件资源管理器。注意
hello.R是在我们的工作目录中创建的——所有文件都将默认保存在此位置。 -
我们可以通过双击它来打开我们的
hello.R文件。 -
文件编辑器现在将出现,这是一个我们可以编写多行代码的地方。
-
在文件编辑器中,按照以下方式输入您的第一个程序:
print("hello, world")注意这里出现的所有文本和字符。它们都是必要的。
-
您可以通过点击 保存 图标来保存。
-
你可能习惯于通过双击图标来运行程序。在 R 中,我们必须采取不同的方法来运行我们的程序。
-
R 不仅仅是一种编程语言。它还是一个解释器,可以将我们的 源代码 转换为计算机可以理解和运行的格式。
-
我们可以通过点击 运行 按钮来执行此过程。注意
hello, world现在已经显示出来。做得好!
函数
-
函数 是一种我们可以运行一系列指令的方式。
-
在您的代码中,
print是一个将"hello world"传递给它的函数。我们传递给函数的内容称为参数。 -
这个函数的副作用是在 R 控制台中显示
hello, world。
错误
-
错误 是代码中无意中出现的错误。
-
按照以下方式修改您的代码:
# Demonstrates a bug prin("hello, world")注意
prin中缺少的t。 -
运行你的代码,你会注意到产生了错误。
-
调试是寻找和消除错误的过程。
readline
-
在 R 中,函数
readline可以从用户那里读取输入。 -
按照以下方式修改你的代码:
readline("What's your name? ") print("Hello, Carter")注意如果我们运行此代码,
Carter将始终出现。 -
我们需要创建一种方法,通过这种方法我们可以读取和使用用户提供的名称。
-
函数不仅仅有参数和副作用,它们还有返回值。返回值是由函数提供的。我们可以将返回值存储为变量。在 R 中,变量也可以称为对象,以避免与统计变量混淆——这是一个不同的概念!
-
按照以下方式修改你的代码:
name <- readline("What's your name? ") print("Hello, name")注意名为
name的变量存储了readline的返回值。箭头<-表示返回值是从readline流向name的。这个箭头被称为赋值运算符。 -
运行此代码并打开 IDE 右侧的环境窗口,你可以看到程序中的变量以及它们存储的内容。
paste
-
尽管如此,运行此代码,注意“name”总是出现。这显然是一个错误!
-
我们可以按照以下方式纠正这个错误:
name <- readline("What's your name? ") greeting <- paste("Hello, ", name) print(greeting)注意代码的第一行保持不变。注意我们创建了一个名为
greeting的新变量,并将“Hello, ”和name的字符串连接赋值给greeting。字符串是一组字符。两个单独的字符串通过paste函数合并成一个。使用print函数打印出结果变量greeting。 -
运行此代码,注意环境中出现的新变量。
-
如果你特别留心,仍然有一个错误!在“Hello,”和
name的值之间存储了两个空格。
文档
-
可以通过在 R 控制台中输入
?paste来访问paste的文档。相应地,paste的文档将出现。阅读此文档,可以了解可以使用paste的各种参数。 -
与我们当前工作相关的参数之一是
sep。 -
按照以下方式修改你的代码:
name <- readline("What's your name? ") greeting <- paste("Hello, ", name, sep = "") print(greeting)注意代码中添加了
sep = ""。 -
运行此程序,你会看到输出现在按预期工作。
-
恰好程序员经常需要通过将
sep设置为""来省略这些额外的空格。因此,他们发明了paste0,它可以不使用任何分隔字符连接字符串。paste0可以使用如下:name <- readline("What's your name? ") greeting <- paste0("Hello, ", name) print(greeting)注意
paste变成了paste0。 -
你的程序可以进一步简化如下:
# Ask user for name name <- readline("What's your name? ") # Say hello to user print(paste("Hello,", name))注意
greeting是通过直接将paste的返回值作为print的输入值来消除的。 -
最后,当在函数内部嵌套函数,如上所示时,请考虑你和他人阅读代码时可能遇到的进一步挑战。有时,过多的嵌套可能会导致无法理解代码在做什么。这是一个设计决策。也就是说,你将经常做出关于代码的决定,以使你的用户和程序员都受益。
-
此外,你可能做出的一个风格决策是使用
#符号添加注释,其中描述代码部分的功能。
算术
-
让我们创建一个新的程序,用来统计一些虚构角色的票数。
-
关闭
hello.R文件。 -
在你的控制台中输入
file.create("count.R")。 -
按照以下方式创建你的代码:
mario <- readline("Enter votes for Mario: ") peach <- readline("Enter votes for Peach: ") bowser <- readline("Enter votes for Bowser: ") total <- mario + peach + bowser print(paste("Total votes:", total))注意到
readline的返回值被存储在三个变量中,分别命名为mario、peach和bowser。变量total被分配了mario、peach和bowser的总和值。然后,打印出这个总和。 -
R 有很多算术运算符,包括
+、-、*、/以及其他运算符! -
运行这段代码,并输入票数,会产生一个错误。
-
正好用户输入被当作字符串处理,而不是数字。查看环境,你会注意到
mario和其他值的值被引号包围。这些引号表明这些值被存储为字符字符串,而不是数字。这些值需要是数字才能与+一起相加。 -
在 R 中,变量可以以不同的模式(有时也称为“类型”)存储。其中一些“存储模式”包括字符、双精度和整数。
-
我们可以按照以下方式将这些变量转换为所需的存储模式:
mario <- readline("Enter votes for Mario: ") peach <- readline("Enter votes for Peach: ") bowser <- readline("Enter votes for Bowser: ") mario <- as.integer(mario) peach <- as.integer(peach) bowser <- as.integer(bowser) total <- mario + peach + bowser print(paste("Total votes:", total))注意到如何通过
as.integer使用强制转换将mario和其他值转换为整数。 -
运行这段代码并查看环境,你可以看到这些值现在被存储为没有引号的整数。
-
这个程序可以进一步简化如下:
mario <- as.integer(readline("Enter votes for Mario: ")) peach <- as.integer(readline("Enter votes for Peach: ")) bowser <- as.integer(readline("Enter votes for Bowser: ")) total <- sum(mario, peach, bowser) print(paste("Total votes:", total))注意到
sum函数是如何被用来对三个变量的值进行求和的。 -
有没有一种方法可以利用现有的数据源?
表格
-
表格是我们可以用以组织数据的许多结构之一。
-
表格是一组行和列,其中行通常代表存储的某个实体,列代表这些实体的属性。
-
表格可以存储在多种文件格式中。一种常见的格式是逗号分隔值(CSV)文件。
-
在 CSV 文件中,每一行存储在单独的一行上。列由逗号分隔。
-
在我们开始下一个程序之前,在 R 控制台中输入
ls()以确定环境中所有活动的变量。然后,输入rm(list = ls())从环境中移除所有这些值。再次输入ls(),你会注意到环境中没有剩余的对象。 -
接下来,输入
file.create("tabulate.R")以创建我们的新程序文件。打开你的文件资源管理器,打开tabulate.R文件。此外,你应该从本讲座的源代码下载votes.csv文件并将其拖入你的工作目录。 -
按照以下方式创建你的代码:
votes <- read.table("votes.csv") View(votes)注意代码的第一行是如何从
votes.csv读取表格到votes变量的。然后,View允许你查看votes中存储的内容。 -
运行此代码,你现在可以看到
votes对象中存储的单独标签页。然而,这里有一个错误。注意所有数据都被读入了一个列中。看起来read.table正在从csv文件中读取数据。但是,似乎还需要一些格式化。 -
按照以下方式修改你的代码:
votes <- read.table( "votes.csv", sep = "," ) View(votes)注意
sep是如何用于告诉read.table每个列将根据哪个字符进行分隔的。 -
尽管如此,运行此代码时仍然有错误。我们如何让
read.table识别表格的标题? -
按照以下方式修改你的代码:
votes <- read.table( "votes.csv", sep = ",", header = TRUE ) View(votes)注意
header = TRUE参数允许read.table识别存在标题。 -
运行此文件,表格将按预期显示。
-
程序员创建了一个快捷方式,以便能够更简单地完成此操作。按照以下方式修改你的代码:
votes <- read.csv("votes.csv") View(votes)注意
read.csv如何以前所未有的简单性完成之前代码所做的工作! -
现在我们已经加载数据,我们如何访问它?按照以下方式修改你的代码:
votes <- read.csv("votes.csv") votes[, 1] votes[, 2] votes[, 3]注意如何使用 方括号表示法 以
votes[row, column]格式访问值。因此,votes[, 2]将显示poll列中的数字。
向量
-
向量 是一个具有相同存储模式的值的列表。
-
考虑到我们的候选人投票数据框(或表格),我们可以通过创建一个新向量来访问特定值。
-
我们可以通过调用每个列的精确名称来简化此程序。
votes <- read.csv("votes.csv") colnames(votes) votes$candidate votes$poll votes$mail注意
votes$poll返回一个包含poll列中所有值的向量。现在我们可以通过这个新向量访问poll列的值。 -
运行此代码,注意每个列的值是如何出现的。
-
转到我们关于如何求和这些值的原始问题,按照以下方式修改你的代码:
votes <- read.csv("votes.csv") sum(votes$poll[1], votes$poll[2], votes$poll[3])注意
sum函数是如何用于对poll表格的前三行中的值进行求和的。 -
然而,此代码不是动态的。它相当不灵活。如果有超过三个候选人怎么办?因此,我们可以简化我们的代码如下,使其更具动态性:
votes <- read.csv("votes.csv") sum(votes$poll) sum(votes$mail)注意在向量
votes$poll和votes$mail中找到的值是如何被求和的。 -
如上图所示,使用方括号表示法,我们也可以尝试对
poll和mail列中的每一行的值进行求和。按照以下方式修改你的代码:votes <- read.csv("votes.csv") votes$poll[1] + votes$mail[1] votes$poll[2] + votes$mail[2] votes$poll[3] + votes$mail[3]注意
poll和mail的每一行是如何被加在一起的。 -
这是否是 R 提供的最佳方法?
向量算术
-
有很多时候我们希望能够将一个向量的行与另一个向量的行相加。我们可以通过向量算术来完成此操作。
-
在使我们的代码更加动态的同一种精神下,我们可以进一步修改我们的代码如下:
votes <- read.csv("votes.csv") votes$poll + votes$mail注意向量是如何逐元素相加的。也就是说,第一个向量的第一行加到第二个向量的第一行上,第一个向量的第二行加到第二个向量的第二行上,依此类推。这导致了一个与
poll和mail向量具有相同行数的最终向量。 -
向量算术会产生一个全新的向量。我们可以以各种方式处理这个新向量。
-
自然地,我们可能想要存储我们算术的结果。我们可以通过以下方式修改我们的代码来做到这一点:
votes <- read.csv("votes.csv") votes$total <- votes$poll + votes$mail write.csv(votes, "totals.csv")注意最终的总数被存储在一个名为
votes$total的新向量中,实际上它是votes数据框的新total列。然后我们将结果votes数据框写入一个名为totals.csv的文件。 -
当你查看
csv文件时,会出现一个问题。注意默认情况下,“行名”是包含在内的。这些可以通过修改以下代码来排除:votes <- read.csv("votes.csv") votes$total <- votes$poll + votes$mail write.csv(votes, "totals.csv", row.names = FALSE)注意
row.names被设置为FALSE。
外部数据
-
今天,我们看到了许多关于如何使用 R 的例子。
-
有许多情况下你可能希望使用别人的数据集。
-
你可以如下访问在线数据源:
# Demonstrates reading data from a URL url <- "https://github.com/fivethirtyeight/data/raw/master/non-voters/nonvoters_data.csv" voters <- read.csv(url)注意
read.csv是如何从定义的 URL 中提取数据的。 -
看这个数据框,你可以运行
nrow来获取行数。你可以运行ncol来获取列数。# Demonstrates finding number of rows and columns in a large data set url <- "https://github.com/fivethirtyeight/data/raw/master/non-voters/nonvoters_data.csv" voters <- read.csv(url) nrow(voters) ncol(voters)注意
nrow和ncol是如何用来确定这个数据中有多少行和列的。 -
数据集有时会附带一个代码簿。代码簿是关于这个数据中包含哪些列的指南。例如,列
Q1可能代表在研究中向参与者提出的一个特定问题。通过查看这个数据集的代码簿,我们可以知道有一个名为voter_category的列,它定义了每个参与者的特定投票行为。 -
你可能想了解在这个列中参与者可能选择的各个选项。这可以通过
unique函数来完成。# Demonstrates finding unique values in a vector url <- "https://github.com/fivethirtyeight/data/raw/master/non-voters/nonvoters_data.csv" voters <- read.csv(url) unique(voters$voter_category)注意
unique是如何用来确定参与者可能选择的选项的。
特殊值
-
对于
Q22,我们在代码簿中发现这个问题是关于为什么参与者没有注册投票的原因。查看这个数据,我们看到NA是呈现的值之一。NA在 R 中表示“不可用”的特殊值。 -
R 中的其他特殊值包括
Inf,-Inf,NaN和NULL。分别表示无限大,负无限大,非数字和空(或无)值。 -
要查看
Q22的这些可能值,我们可以运行以下代码:# Demonstrates NA url <- "https://github.com/fivethirtyeight/data/raw/master/non-voters/nonvoters_data.csv" voters <- read.csv(url) voters$Q22 unique(voters$Q22)注意到
unique再次被用来发现Q22的可能值。
factor
-
Q21涉及参与者对未来选举的投票计划。在这个列中,值1,2和3与特定的可能答案相对应。例如,1可能代表“是”。 -
在 R 语言中,我们可以使用
factor函数将数字值转换为特定的文本答案。例如,我们可以使用factor函数将数字1转换为对应的文本“是”。我们可以通过以下方式修改我们的代码来完成这个操作:# Demonstrates converting a vector to a factor url <- "https://github.com/fivethirtyeight/data/raw/master/non-voters/nonvoters_data.csv" voters <- read.csv(url) voters$Q21 factor( voters$Q21 ) factor( voters$Q21, labels = c("?", "Yes", "No", "Unsure/Undecided") )注意
factor(voters$Q21)将会显示Q21的具体级别(类别)数据。在代码中出现的后续factor中,标签被应用于每个级别。例如,1与“是”相关联。 -
在许多情况下,我们可能希望排除某些值。在
Q21中,我们可能希望排除-1,因为这个值代表的含义并不明确。我们可以按照以下方式操作:# Demonstrates excluding values from the levels of a factor url <- "https://github.com/fivethirtyeight/data/raw/master/non-voters/nonvoters_data.csv" voters <- read.csv(url) voters$Q21 <- factor( voters$Q21, labels = c("Yes", "No", "Unsure/Undecided"), exclude = c(-1) )注意
-1是如何被排除的。
总结
在本课中,你学习了如何在 R 语言中表示数据。具体来说,你学习了……
-
函数
-
错误
-
readline -
paste -
文档
-
算术
-
表格
-
向量
-
向量算术
-
外部数据
-
特殊值
-
factor
次次再见,当我们讨论如何转换数据时再见。
第二讲
-
欢迎!
-
异常值
-
逻辑表达式
-
使用逻辑向量的子集
-
数据框的子集
-
菜单
-
转义字符
-
条件语句
-
合并数据源
-
总结
欢迎!
-
欢迎回到 CS50 的 R 语言编程入门课程!
-
我们将学习如何删除数据的一部分,查找特定的数据,以及如何从不同的来源获取不同的数据并将它们合并。
异常值
-
在统计学中,异常值是指超出预期范围的数值。
-
通常,统计学家和数据科学家希望识别异常值以进行特殊处理。有时,可能需要从计算中移除异常值。其他时候,你可能希望包括异常值进行分析。
-
为了说明如何在 R 中处理异常值,你可以在 RStudio 中通过在 R 控制台中输入
file.create("temps.R")来创建一个新文件。进一步,你需要在你的工作目录中下载一个名为temps.RData的文件。 -
要加载数据,我们可以编写如下代码:
# Demonstrates loading data from an .RData file load("temps.RData") mean(temps)注意
load函数如何加载名为temps.RData的数据文件。接下来,mean将计算这些数据的平均值。 -
运行此脚本,你可以看到计算结果。
-
然而,正如之前所述,这些基础数据中存在异常值。让我们来发现这些异常值。
-
从整体上看温度,如图中讲座视频所示,我们希望能够直接访问这些异常温度。
-
回想第 1 周我们如何在向量中索引数据。按照以下方式修改你的代码:
# Demonstrates identifying outliers by index load("temps.RData") temps[2] temps[4] temps[7] temps[c(2, 4, 7)]注意
temps[2]将直接访问一个异常温度。最后一行代码从temps向量中取一个子集,只包括第 2、4 和第 7 个索引的元素。 -
作为下一步,我们可以移除异常值数据:
# Demonstrates removing outliers by index load("temps.RData") no_outliers <- temps[-c(2, 4, 7)] mean(no_outliers) mean(temps)注意数据已加载。然后,
no_outliers是一个只包含非异常温度的新向量。名为temps的向量仍然包含异常值数据。
逻辑表达式
-
逻辑表达式是通过编程回答是和否问题的手段。逻辑表达式利用逻辑运算符,这些运算符用于比较值。
-
在 R 中,你可以使用许多逻辑运算符,包括:
== != > >= < <= -
例如,你可以在 R 控制台中输入
1 == 2来询问 1 是否等于 2。结果应该是FALSE(或“不!”)。然而,1 < 2应该是TRUE(或“是!”)。 -
逻辑值是逻辑表达式提供的响应。逻辑值可以是
TRUE或FALSE。这些值也可以用更简略的形式表示为T或F。 -
在你的代码中使用逻辑运算符,你可以按照以下方式修改你的代码:
# Demonstrates identifying outliers with logical expressions load("temps.RData") temps[1] < 0 temps[2] < 0 temps[3] < 0注意运行此代码将在 R 控制台中产生以
TRUE和FALSE表示的结果。 -
以下代码可以进一步改进如下:
# Demonstrates comparison operators are vectorized load("temps.RData") temps < 0注意到运行此代码将创建一个 逻辑向量(即逻辑值的向量)。逻辑向量中的每个值都回答其对应值是否小于 0。
-
要识别某些逻辑表达式为真的索引,你可以按照以下方式修改你的代码:
# Demonstrates `which` to return indices for which a logical expression is TRUE load("temps.RData") which(temps < 0)注意现在温度向量中小于 0 的 索引 将输出到 R 控制台。函数
which接受一个逻辑向量作为输入,并返回值为TRUE的值的索引。 -
当处理异常值时,一个常见的愿望是显示低于或高于阈值的数值。你可以在代码中按以下方式实现:
# Demonstrates identifying outliers with compound logical expressions load("temps.RData") temps < 0 | temps > 60注意到字符
|符号在表达式中表示 或。这个逻辑表达式对于temps中任何小于0或大于60的值都将返回TRUE。 -
除了我们之前讨论的逻辑运算符之外,我们现在添加了两个新的运算符到我们的词汇表中:
| &注意到表达
or和and的能力是如何被提供的。 -
你可以进一步改进你的代码如下:
# Demonstrates `any` and `all` to test for outliers load("temps.RData") any(temps < 0 | temps > 60) all(temps < 0 | temps > 60)注意到
any和all函数接受逻辑向量作为输入。any回答的问题是,“这些逻辑值中是否有任何一个是真的?”all回答的问题是,“所有这些温度值是否都是真的?”。
逻辑向量的子集
-
如前所述,我们可以创建一个新的向量,如下删除异常值:
# Demonstrates subsetting a vector with a logical vector load("temps.RData") filter <- temps < 0 | temps > 60 temps[filter]注意到如何根据逻辑表达式创建了一个新的子集向量
filter。因此,现在可以将filter提供给temps,以请求temps中那些在逻辑表达式中评估为TRUE的项。 -
同样,代码可以被修改以仅过滤那些不是异常值的项:
# Demonstrates negating a logical expression with ! load("temps.RData") filter <- !(temps < 0 | temps > 60) temps[filter]注意到
!的添加意味着 不等于 或简单地 不是。 -
这种否定可以用来完全从数据中删除异常值:
# Demonstrates removing outliers load("temps.RData") no_outliers <- temps[!(temps < 0 | temps > 60)] save(no_outliers, file = "no_outliers.RData") outliers <- temps[temps < 0 | temps > 60] save(outliers, file = "outliers.RData")注意现在有两个文件被保存。一个排除了异常值,另一个包含了异常值。这些文件保存在工作目录中。
数据框的子集
-
我们如何从一个数据集中找到我们感兴趣的数据子集?
-
想象一个数据表,记录了每只小鸡(一只小鸡宝宝!)、每只小鸡所喂的饲料以及每只小鸡的重量。你可以从讲座源代码中下载
chicks.csv来查看这些数据。 -
在 RStudio 中关闭之前的文件,让我们在 R 控制台中创建一个新的文件,通过输入
file.create("chicks.R")。确保你有chicks.csv在工作目录中,然后选择chicks.R并按照以下方式编写你的代码:# Reads a CSV of data chicks <- read.csv("chicks.csv") View(chicks)注意到
read.csv将 CSV 文件读取到名为chicks的数据框中。然后,查看chicks。 -
查看上述输出的结果,注意其中有很多
NA值,代表不可用数据。考虑这可能会如何影响平均鸡重量的计算。按照以下方式修改你的代码:# Demonstrates `mean` calculation with NA values chicks <- read.csv("chicks.csv") average_weight <- mean(chicks$weight) average_weight注意到运行此代码将导致错误,因为某些值不可用于数学评估。
-
缺失数据在统计学中是一个预期的问题。作为程序员,您需要决定如何处理缺失数据。您可以在移除
NA值的情况下计算平均小鸡体重,如下所示:# Demonstrates na.rm to remove NA values from mean calculation chicks <- read.csv("chicks.csv") average_weight <- mean(chicks$weight, na.rm = TRUE) average_weight注意,
na.rm = TRUE将在计算平均值时移除所有NA值。根据文档,na.rm可以设置为TRUE或FALSE。 -
现在,让我们找出每只小鸡吃的食物如何影响它们的体重:
# Demonstrates computing casein average with explicit indexes chicks <- read.csv("chicks.csv") casein_chicks <- chicks[c(1, 2, 3), ] mean(casein_chicks$weight)注意,通过明确指定适当的索引,创建了一个
chicks数据框的子集。 -
这不是一种高效的编程方式,因为我们不应该期望我们的数据永远不会改变。我们如何修改代码使其更加灵活?我们可以使用逻辑表达式来动态地子集化数据框。
# Demonstrates logical expression to identify rows with casein feed chicks <- read.csv("chicks.csv") chicks$feed == "casein"注意,逻辑表达式识别饲料列中的每个值是否等于“casein”。
-
我们可以在代码中利用这个逻辑表达式如下:
# Demonstrates subsetting data frame with logical vector chicks <- read.csv("chicks.csv") filter <- chicks$feed == "casein" casein_chicks <- chicks[filter, ] mean(casein_chicks$weight)如讲座中先前所示,注意如何创建一个名为
filter的逻辑向量。然后,只有filter中为TRUE的行被带入数据框casein_chicks。 -
现在,我们有了数据框的一个子集。
-
您可以使用
subset函数达到相同的结果:# Demonstrates subsetting with `subset` chicks <- read.csv("chicks.csv") casein_chicks <- subset(chicks, feed == "casein") mean(casein_chicks$weight, na.rm = TRUE)这个数据框,称为
casein_chicks,是通过subset函数创建的。 -
现在,有人可能希望在开始时过滤掉所有
NA值。考虑以下代码:# Demonstrates identifying NA values with `is.na` chicks <- read.csv("chicks.csv") is.na(chicks$weight) !is.na(chicks$weight) chicks$chick[is.na(chicks$weight)]注意,这段代码将使用
is.na来查找NA值。 -
可以通过使用
is.na来完全删除记录,如下所示:# Demonstrates removing NA values and resetting row names chicks <- read.csv("chicks.csv") chicks <- subset(chicks, !is.na(weight)) rownames(chicks) rownames(chicks) <- NULL rownames(chicks)注意,这段代码创建了一个
chicks的子集,其中is.na(weight)等于FALSE。也就是说,chicks只包括weight列中没有NA的行。如果您关心数据框的行名,请注意,当您移除某些行时,您也移除了那些行的rownames。您可以通过运行rownames(chicks) <- NULL来确保您的行名仍然按顺序递增,这将重置所有行的名称。
菜单
-
在 R 中,您可以向用户提供选项。例如,您可以提供用户希望过滤的小鸡的饲料类型。
-
考虑以下代码:
# Demonstrates interactive program to view data by feed type # Read and clean data chicks <- read.csv("chicks.csv") chicks <- subset(chicks, !is.na(weight)) # Determine feed options feed_options <- unique(chicks$feed) # Prompt user with options cat("1.", feed_options[1]) cat("2.", feed_options[2]) cat("3.", feed_options[3]) cat("4.", feed_options[4]) cat("5.", feed_options[5]) cat("6.", feed_options[6]) feed_choice <- as.integer(readline("Feed type: "))注意,这段代码使用
unique来发现独特的饲料选项。然后,使用cat输出每个饲料选项。 -
这段代码在意义上是有效的,因为它显示了各种饲料选项,但它格式不是很好。我们如何在 R 控制台中使不同的选项各自占一行?
转义字符
-
转义字符 是输出方式与输入方式不同的字符。
-
例如,一些常用的转义字符是
\n,它打印一个新行,或者\t,它打印一个制表符。 -
利用转义字符,我们可以修改代码如下:
# Demonstrates \n # Read and clean data chicks <- read.csv("chicks.csv") chicks <- subset(chicks, !is.na(weight)) # Determine feed options feed_options <- unique(chicks$feed) # Prompt user with options cat("1.", feed_options[1], "\n") cat("2.", feed_options[2], "\n") cat("3.", feed_options[3], "\n") cat("4.", feed_options[4], "\n") cat("5.", feed_options[5], "\n") cat("6.", feed_options[6], "\n") feed_choice <- as.integer(readline("Feed type: "))注意,这段代码输出了所有饲料选项,每个选项都在单独的一行上。
-
当我们有正确的菜单显示时,我们仍然可以从设计角度改进我们的代码。例如,为什么我们应该重复所有这些
cat行?如下简化你的代码:# Demonstrates interactive program to view data by feed type # Read and clean data chicks <- read.csv("chicks.csv") chicks <- subset(chicks, !is.na(weight)) # Determine feed options feed_options <- unique(chicks$feed) # Format feed options formatted_options <- paste0(1:length(feed_options), ". ", feed_options) # Prompt user with options cat(formatted_options, sep = "\n") feed_choice <- as.integer(readline("Feed type: "))注意到
formatted_options包括所有单个饲料选项。formatted_options向量的每个元素都通过cat(formatted_options, sep = "\n")打印出来,并且每个元素之间用换行符分隔。 -
现在我们已经指出,我们的意图是创建一个交互式程序。因此,我们现在可以提示用户选择:
# Demonstrates interactive program to view data by feed type # Read and clean data chicks <- read.csv("chicks.csv") chicks <- subset(chicks, !is.na(weight)) # Determine feed options feed_options <- unique(chicks$feed) # Format feed options formatted_options <- paste0(1:length(feed_options), ". ", feed_options) # Prompt user with options cat(formatted_options, sep = "\n") feed_choice <- as.integer(readline("Feed type: ")) # Print selected option selected_feed <- feed_options[feed_choice] print(subset(chicks, feed == selected_feed))注意到用户被提示输入
Feed type:,其中数字可以转换为基于文本的饲料选项表示。然后,用户选择的feed_choice被分配给selected_feed。最后,与selected_feed对应的子集被输出给用户。 -
然而,你可以想象用户可能不会按预期行为。例如,如果用户输入了
0,这不是一个潜在的选择,那么我们程序的输出将会奇怪。我们如何确保用户输入正确的文本?
条件语句
-
条件语句是确定条件是否满足的方法。
-
考虑以下代码:
# Demonstrates interactive program to view data by feed type # Read and clean data chicks <- read.csv("chicks.csv") chicks <- subset(chicks, !is.na(weight)) # Determine feed options feed_options <- unique(chicks$feed) # Format feed options formatted_options <- paste0(1:length(feed_options), ". ", feed_options) # Prompt user with options cat(formatted_options, sep = "\n") feed_choice <- as.integer(readline("Feed type: ")) # Invalid choice? if (feed_choice < 1 || feed_choice > length(feed_options)) { cat("Invalid choice.") } selected_feed <- feed_options[feed_choice] print(subset(chicks, feed == selected_feed))注意到
if (feed_choice < 1 || feed_choice > length(feed_options))是如何确定用户的输入是否超出值范围的。如果是这样,程序将显示“无效选择。”然而,仍然存在问题:即使有无效选择,程序也会继续运行。 -
可以如下利用
if和else来仅当用户输入有效选择时才运行最终计算:# Demonstrates interactive program to view data by feed type # Read and clean data chicks <- read.csv("chicks.csv") chicks <- subset(chicks, !is.na(weight)) # Determine feed options feed_options <- unique(chicks$feed) # Format feed options formatted_options <- paste0(1:length(feed_options), ". ", feed_options) # Prompt user with options cat(formatted_options, sep = "\n") feed_choice <- as.integer(readline("Feed type: ")) # Invalid choice? if (feed_choice < 1 || feed_choice > length(feed_options)) { cat("Invalid choice.") } else { selected_feed <- feed_options[feed_choice] print(subset(chicks, feed == selected_feed)) }注意,被
if包裹的代码仅在存在无效选择时运行。被else包裹的代码仅在if中的先前条件未满足时运行。
结合数据来源
-
作为本讲座的最后一件事,让我们看看如何结合数据来源。
-
想象一个表示销售给客户的表,比如亚马逊可能有的那种。
-
你可以想象数据分布在许多表中的场景。如何将这些数据从多个来源组合起来?
-
考虑以下名为
sales.R的代码:# Reads 4 separate CSVs Q1 <- read.csv("Q1.csv") Q2 <- read.csv("Q2.csv") Q3 <- read.csv("Q3.csv") Q4 <- read.csv("Q4.csv")注意到每个财务数据季度,例如
Q1和Q2,都被读入它们自己的数据框中。 -
现在,让我们将这四个数据框中的数据结合起来:
# Combines data frames with `rbind` Q1 <- read.csv("Q1.csv") Q2 <- read.csv("Q2.csv") Q3 <- read.csv("Q3.csv") Q4 <- read.csv("Q4.csv") sales <- rbind(Q1, Q2, Q3, Q4)注意到
rbind被用来收集来自每个这些数据框的数据。 -
值得注意的是,
rbind在这种情况下是可用的,因为所有四个数据框的结构都是相同的。 -
前一个程序运行的结果是
sales包括了每个数据框中的每一行。而不是为每个客户显示Q1、Q2等,它只是在文件的底部为每行数据创建新的行。因此,随着越来越多的数据被组合到文件中,文件变得越来越长。每个销售值发生的季度完全不清楚。 -
我们的代码可以改进,为每条记录创建一个财务季度的列,如下所示:
# Adds quarter column to data frames Q1 <- read.csv("Q1.csv") Q1$quarter <- "Q1" Q2 <- read.csv("Q2.csv") Q2$quarter <- "Q2" Q3 <- read.csv("Q3.csv") Q3$quarter <- "Q3" Q4 <- read.csv("Q4.csv") Q4$quarter <- "Q4" sales <- rbind(Q1, Q2, Q3, Q4)注意每个季度是如何添加到特定的
季度列中的。因此,当rbind将数据框组合到按季度列组织的sales中时。 -
作为最后的点缀,让我们添加一个
value列,其中记录高回报和常规回报:# Demonstrates flagging sales as high value Q1 <- read.csv("Q1.csv") Q1$quarter <- "Q1" Q2 <- read.csv("Q2.csv") Q2$quarter <- "Q2" Q3 <- read.csv("Q3.csv") Q3$quarter <- "Q3" Q4 <- read.csv("Q4.csv") Q4$quarter <- "Q4" sales <- rbind(Q1, Q2, Q3, Q4) sales$value <- ifelse(sales$sale_amount > 100, "High Value", "Regular")注意最后一行代码在
sale_amount大于100时分配“高价值”。否则,交易被分配为“常规”。
总结
在本课中,你学习了如何在 R 中转换数据。具体来说,你学习了...
-
异常值
-
逻辑表达式
-
子集
-
菜单
-
转义字符
-
条件语句
-
合并数据源
次次见,当我们讨论如何编写我们自己的函数时。
第三讲
-
欢迎!
-
定义函数
-
作用域
-
检查输入
-
循环
-
使用循环
-
使用函数和循环
-
应用函数
-
总结
欢迎!
-
欢迎回到 CS50 的 R 编程入门课程!
-
今天,我们将学习如何应用函数。我们还将学习如何编写自己的函数并应用循环。
-
回想一下我们在上次讲座中创建的名为
count.R的程序。# Demonstrates counting votes for 3 different candidates mario <- as.integer(readline("Mario: ")) peach <- as.integer(readline("Peach: ")) bowser <- as.integer(readline("Bowser: ")) total <- sum(mario, peach, bowser) cat("Total votes:", total)注意到如何重复行以从用户那里获取输入。
-
传统上,在编程中每次重复使用代码都是改进的机会。函数是我们通过定义可以在整个程序中重用的代码块来减少这些冗余的一种方式。
定义函数
-
在 R 中,函数通过语法
function()定义。 -
考虑以下我们程序的改进版本:
# Demonstrates defining a function get_votes <- function() { votes <- as.integer(readline("Enter votes: ")) return(votes) } mario <- get_votes() peach <- get_votes() bowser <- get_votes() total <- sum(mario, peach, bowser) cat("Total votes:", total)注意到创建了一个名为
get_votes的新函数。函数的 主体 由开闭花括号 ({和}) 表示。注意,在主体内部有 2 行代码,每次调用此函数时都会执行。首先,从用户那里收集votes。其次,返回votes。在调用get_votes之后,mario、peach和bowser分别接收返回值。最后,提供值的总和并显示给用户。 -
恭喜你,这是你在 R 中的第一个函数!
-
然而,运行这个函数,我们发现该函数丢失了一些我们之前的功能。我们能否以某种方式向函数提供一个 参数,以便我们可以更准确地提示用户?确实可以!考虑以下:
# Demonstrates defining a parameter get_votes <- function(prompt) { votes <- as.integer(readline(prompt)) } mario <- get_votes("Mario: ") peach <- get_votes("Peach: ") bowser <- get_votes("Bowser: ") total <- sum(mario, peach, bowser) cat("Total votes:", total)注意到向
get_votes函数提供了一个prompt。因此,用户会被提示他们要投票的人的名字。此外,注意已经移除了return(votes)语句。在 R 中,函数会自动返回最后计算出的值。 -
具有参数的函数可能已分配了默认值。考虑以下我们程序的以下更新:
# Demonstrates defining a parameter with a default value get_votes <- function(prompt = "Enter votes: ") { votes <- as.integer(readline(prompt)) } mario <- get_votes() peach <- get_votes() bowser <- get_votes() total <- sum(mario, peach, bowser) cat("Total votes:", total)注意到在代码的第一行提供了一个默认值。
-
我们仍然可以像这样覆盖默认提示:
# Demonstrates exact argument matching get_votes <- function(prompt = "Enter votes: ") { votes <- as.integer(readline(prompt)) } mario <- get_votes(prompt = "Mario: ") peach <- get_votes(prompt = "Peach: ") bowser <- get_votes(prompt = "Bowser: ") total <- sum(mario, peach, bowser) cat("Total votes:", total)注意到对于每次函数调用,给定的参数会覆盖默认参数。
作用域
-
查看我们的 RStudio 环境面板,注意到为
bowser和其他人提供了值。然而,没有为votes提供值。为什么可能会这样? -
结果表明,所有对象都是在某些“环境”中定义的。其中一种环境是“全局”环境。全局环境是您在 R 控制台或函数体外部定义的对象的家园——例如
mario、bowser和peach。默认情况下,RStudio 的环境面板显示您在全局环境中定义的对象。![全局环境的可视化]()
-
get_votes函数也是定义在全局环境中的对象。然而,独特的是,get_votes本身也是一种环境!正如你所看到的,在get_votes的定义中,你可以定义其他对象,如votes和prompt。![环境的可视化]()
-
get_votes的环境不是全局环境。当编写在全局环境中运行的代码时,此环境中的对象不可访问。 -
一个对象可用的环境被称为其“作用域”。
检查输入
-
程序员一直面临的一个挑战是用户的糟糕行为。也就是说,作为程序员,我们应该预期用户不会总是做我们想要的事情。例如,如果用户为
votes提供了文本字符串而不是数字怎么办? -
我们可以将程序改进以捕获输入的错误值:
# Demonstrates anticipating invalid input get_votes <- function(prompt = "Enter votes: ") { votes <- as.integer(readline(prompt)) if (is.na(votes)) { return(0) } else { return(votes) } } mario <- get_votes("Mario: ") peach <- get_votes("Peach: ") bowser <- get_votes("Bowser: ") total <- sum(mario, peach, bowser) cat("Total votes:", total)注意到如果
votes的值是NA,get_votes将返回0。否则,get_votes将返回用户提供的值。 -
虽然这个程序可以工作,但它仍然会提供警告,我们可能不希望用户看到。我们可以如下抑制警告:
# Demonstrates anticipating invalid input get_votes <- function(prompt = "Enter votes: ") { votes <- suppressWarnings(as.integer(readline(prompt))) if (is.na(votes)) { return(0) } else { return(votes) } } mario <- get_votes("Mario: ") peach <- get_votes("Peach: ") bowser <- get_votes("Bowser: ") total <- sum(mario, peach, bowser) cat("Total votes:", total)注意到当运行此代码时,警告现在被抑制了。
-
通过使用
ifelse,我们可以进一步改进这个程序。考虑以下:# Demonstrates ifelse as last evaluated expression get_votes <- function(prompt = "Enter votes: ") { votes <- as.integer(readline(prompt)) ifelse(is.na(votes), 0, votes) } mario <- get_votes("Mario: ") peach <- get_votes("Peach: ") bowser <- get_votes("Bowser: ") total <- sum(mario, peach, bowser) cat("Total votes:", total)注意到
ifelse的第一个值是一个要测试的逻辑表达式。第二个值0是当第一个值is.na(votes)评估为TRUE时将返回的值。最后,第三个值votes是在第一个值评估为FALSE时提供的。 -
我们现在已经发现了检查用户输入的第一种基本方法。
-
如同之前,我们可以抑制警告:
# Demonstrates suppressWarnings get_votes <- function(prompt = "Enter votes: ") { votes <- suppressWarnings(as.integer(readline(prompt))) ifelse(is.na(votes), 0, votes) } mario <- get_votes("Mario: ") peach <- get_votes("Peach: ") bowser <- get_votes("Bowser: ") total <- sum(mario, peach, bowser) cat("Total votes:", total)注意到警告被抑制了。
循环
-
我们可能希望对程序进行的一个显著改进是能够在用户出错时反复提示用户。
-
要了解更多关于循环的信息,让我们请 CS50 Duck Debugger 来帮忙!Quack!
-
考虑以下代码:
# Demonstrates a duck quacking 3 times cat("quack!\n") cat("quack!\n") cat("quack!\n")注意到这段代码将输出“quack”三次。然而,它相当低效!我们重复了相同的代码行三次。
-
我们可以尝试使用以下形式的重复循环来改进此代码:
# Demonstrates duck quacking in an infinite loop repeat { cat("quack!\n") }注意到我们的鸭子“quack”多次,但永远如此。鸭子会非常累的!
-
我们实现循环的一种方法是通过利用
break和next。这样的循环将通过计数器重复一定次数。# Demonstrates quacking 3 times with repeat i <- 3 repeat { cat("quack!\n") i <- i - 1 if (i == 0) { break } else { next } }注意到
i的值被设置为3。然后每次发生quack!时,i的值减少 1。当达到0时,循环将break。否则(或else),这个循环将使用next继续。 -
最后,
next是不必要的。循环将自动继续,无需next语句。我们可以如下移除此语句:# Demonstrates removing extraneous next keyword i <- 3 repeat { cat("quack!\n") i <- i - 1 if (i == 0) { break } }注意当
i等于0时,循环将中断。然而,已经移除了next。循环仍然可以工作。 -
我们可用的另一种循环类型称为while 循环。这种循环将在满足特定条件之前继续。考虑以下代码:
# Demonstrates a while loop, counting down i <- 3 while (i != 0) { cat("quack!\n") i <- i - 1 }注意这个循环将一直运行,直到
i != 0的值为真。 -
另一种类型的循环称为for 循环,它允许我们根据列表或值向量重复操作:
# Demonstrates a for loop for (i in c(1, 2, 3)) { cat("quack!\n") }注意
for循环从i的值为1开始,运行其内部的代码。然后,它将i的值设置为2并运行。最后,它将i设置为3并运行。因此,循环内的代码运行了三次。 -
我们可以通过使用范围
1:3(一至三)来简化我们的代码,以计算1、2和3。# Demonstrates a for loop with syntactic sugar for (i in 1:3) { cat("quack!\n") }注意代码
i in 1:3如何完成与先前示例中相同的任务。
使用循环
-
我们可以在对马里奥和他的朋友们计票时使用我们新学的循环能力。考虑以下使用重复循环的代码:
# Demonstrates reprompting the user for valid input get_votes <- function(prompt = "Enter votes: ") { repeat { votes <- suppressWarnings(as.integer(readline(prompt))) if (!is.na(votes)) { break } } return(votes) } mario <- get_votes("Mario: ") peach <- get_votes("Peach: ") bowser <- get_votes("Bowser: ") total <- sum(mario, peach, bowser) cat("Total votes:", total)注意用户将一直被提示,直到提供的值不是
NA。 -
我们可以进一步改进我们的代码如下:
# Demonstrates tightening return get_votes <- function(prompt = "Enter votes: ") { repeat { votes <- suppressWarnings(as.integer(readline(prompt))) if (!is.na(votes)) { return(votes) } } } mario <- get_votes("Mario: ") peach <- get_votes("Peach: ") bowser <- get_votes("Bowser: ") total <- sum(mario, peach, bowser) cat("Total votes:", total)注意
return(votes)子句是如何替换break的。这个函数的功能保持不变,但代码更简洁。 -
现在,利用我们对
for循环的知识,我们可以改进对马里奥和他的朋友们重复的代码:# Demonstrates prompting for input in a loop get_votes <- function(prompt = "Enter votes: ") { repeat { votes <- suppressWarnings(as.integer(readline(prompt))) if (!is.na(votes)) { return(votes) } } } for (name in c("Mario", "Peach", "Bowser")) { votes <- get_votes(paste0(name, ": ")) }注意,与为每个候选人分别提示选票的三条单独的行不同,
for循环将运行“马里奥”、“桃子”和“霸王龙”的范围以获取选票。paste0语句将冒号字符添加到每个提示中。 -
作为最后的点缀,我们可以使用循环来边走边计票:
# Demonstrates prompting for input, tallying votes in a loop get_votes <- function(prompt = "Enter votes: ") { repeat { votes <- suppressWarnings(as.integer(readline(prompt))) if (!is.na(votes)) { return(votes) } } } total <- 0 for (name in c("Mario", "Peach", "Bowser")) { votes <- get_votes(paste0(name, ": ")) total <- total + votes } cat("Total votes:", total)注意在
for循环的每次迭代中,total选票数是如何更新的。 -
反思上述内容,你可以看到循环为你作为程序员提供的根本编程力量。
使用函数和循环
-
让我们回到之前讨论的一个案例,像下面这样在表中汇总候选人的选票。
![候选人选票表]()
-
现在,让我们使用我们在循环和函数中学习的新能力来创建一个更好的程序。
-
也许我们的第一个目标应该是统计选票。考虑以下代码:
# Demonstrates summing votes for each candidate procedurally votes <- read.csv("votes.csv") total_votes <- c() for (candidate in rownames(votes)) { total_votes[candidate] <- sum(votes[candidate, ]) } total_votes注意这个
for循环将遍历votes数据框中呈现的每个candidate。然后,candidate的votes总和将被存储在total_votes向量中。total_votes <- c()代表一个空向量,稍后将被数据填充。total_votes[candidate]在向量total_votes中创建一个新的元素,每次循环迭代中每个候选人都有一个。 -
第二个目标可能是按每个候选人收到的选票方式汇总。
# Demonstrates summing votes for each voting method procedurally votes <- read.csv("votes.csv") total_votes <- c() for (method in colnames(votes)) { total_votes[method] <- sum(votes[, method]) } total_votes注意这个
for循环如何遍历colnames(或列名)中的每个method。
应用函数
-
上面的程序可以使用一组称为
apply函数的函数进一步优化。 -
apply函数允许你将函数应用于数据结构中的元素(即运行)。例如,apply函数可以在数据表的所有行或列上应用函数。 -
在投票表的例子中,我们可以使用
apply函数如下来获取所有行的sum:# Demonstrates summing votes for each candidate with apply votes <- read.csv("votes.csv") total_votes <- apply(votes, MARGIN = 1, FUN = sum) total_votes注意
sum函数是如何使用MARGIN = 1应用于所有行的。如果我们把MARGIN设置为2,sum函数就会应用于所有列。 -
我们可以这样对每一列求和:
# Demonstrates summing votes for each voting method with apply votes <- read.csv("votes.csv") total_votes <- apply(votes, MARGIN = 2, FUN = sum) total_votes注意
MARGIN = 2。
总结
在本课中,你学习了如何在 R 中应用函数。具体来说,你学习了……
-
定义函数
-
范围
-
检查输入
-
循环
-
使用循环
-
使用函数和循环
-
应用函数
次次再见,当我们讨论如何清理我们的数据时。
第四讲
-
欢迎回来!
-
dplyr
-
select(#select) -
filter(#filter) -
管道操作符
-
arrange(#arrange) -
distinct(#distinct) -
写入数据
-
group_by(#group_by) -
summarize(#summarize) -
ungroup(#ungroup)
-
-
tidyr
-
整洁数据
-
标准化
-
旋转
-
-
stringr
-
总结
欢迎回来!
-
欢迎回到 CS50 的 R 编程入门课程!
-
今天,我们将学习如何整理数据。确实,你可以想象出很多次,表格和数据可能不会是人们希望的样子!
-
包 是开发者创建的代码片段,我们可以将其安装并加载到我们的 R 程序中。这些包可以在 R 中提供一些原生不包含的功能。
-
包存储在 R 的 library 中。因此,你可以使用
library函数来加载包。
dplyr
-
在 dplyr 中,包含了一个名为
storms的数据集,它包含了来自美国国家海洋和大气管理局 NOAA 的风暴数据观测。 -
在加载 dplyr 或 tidyverse 之后,只需在 R 控制台中输入
storms即可加载storms数据集。 -
当你输入
storms时,注意会显示一个 tibble。tibble 是 tidyverse 对 R 的数据框的“重新构想”。注意行、行号和各种列是如何包含并标记的。此外,注意 tibble 中使用的文本颜色。
select
-
让我们定位数据集中最强的风暴。首先,让我们删除我们不需要的列。考虑以下程序:
# Remove selected columns dplyr::select( storms, !c(lat, long, pressure, tropicalstorm_force_diameter, hurricane_force_diameter) )注意到 dplyr 中的
select函数允许你确定哪些列将包含在数据框或 tibble 中。select的第一个参数是要操作的(数据框或 tibble):storms。select的第二个参数是要选择的列的向量。然而,在这种情况下,使用了!:一个!表示后面的列名将被排除。或者,-也有相同的功能。运行此代码将通过删除上述列来简化 tibble。 -
打印出所有这些列有点繁琐!
-
像这样的辅助函数
contains、starts_with或ends_with可以帮助完成这项工作。考虑以下代码:# Introduce ends_with select( storms, !c(lat, long, pressure, ends_with("diameter")) )注意到
ends_with被用来排除所有以 diameter 结尾的列。使用的代码更少,但结果与之前相同。
filter
-
另一个有用的函数是
filter,它可以用来从数据框中筛选行。 -
考虑以下代码:
# Find only rows about hurricanes filter( select( storms, !c(lat, long, pressure, ends_with("diameter")) ), status == "hurricane" )注意,只有包含
status列中hurricane的行被包含在内。 -
注意最新的示例在第一个示例中已经删除了
dplyr::语法。结果是,你不需要命名定义函数的特定包,除非两个或多个包定义了具有相同名称的函数。在这种情况下,你需要通过指定想要使用哪个包的函数来消除歧义。
管道操作符
-
在 R 中,管道操作符 用
|>表示,允许将数据“管道”到特定的函数中。例如,考虑以下代码:# Introduce pipe operator storms |> select(!c(lat, long, pressure, ends_with("diameter"))) |> filter(status == "hurricane")注意
storms是如何被管道到select的,隐式地成为select的第一个参数。然后,注意select的返回值是如何被管道到filter的,隐式地成为filter的第一个参数。当你使用管道操作符时,你可以避免嵌套函数调用,并按顺序编写代码。
arrange
-
现在我们使用
arrange函数来排序我们的行:# Find only rows about hurricanes, and arrange highest wind speed to least storms |> select(!c(lat, long, pressure, ends_with("force_diameter"))) |> filter(status == "hurricane") |> arrange(desc(wind))注意
select函数的返回值是如何被管道到filter的,然后filter的返回值又被管道到arrange。结果数据框中的行按wind列的值降序排列。
distinct
-
你可能会注意到这个 tibble 包含许多相同风暴的行。因为这个数据包含许多相同风暴的观测,所以这并不奇怪。然而,不是很好奇能够找到只有 distinct 飓风吗?
-
distinct函数允许我们在 tibble 中获取独特的项目。 -
Distinct returns distinct rows, finding duplicate rows and returning the first row from the set of duplicates.
-
默认情况下,
distinct只在行的所有值与另一行的所有值完全匹配时才将行视为重复。 -
然而,你可以告诉
distinct在确定行是否重复时考虑哪些值。考虑以下利用这一功能的代码:# Keep only first observation about each hurricane storms |> select(!c(lat, long, pressure, ends_with("force_diameter"))) |> filter(status == "hurricane") |> arrange(desc(wind), name) |> distinct(name, year, .keep_all = TRUE)注意
distinct被告知只查看每个风暴的name和year以确定它是否是独特项目。.keep_all = TRUE告诉distinct仍然返回每行的所有列。
写入数据
-
我们可以将数据保存到 CSV 文件中以便以后使用。
-
考虑以下代码:
# Write subset of columns to a CSV hurricanes <- storms |> select(!c(lat, long, pressure, ends_with("force_diameter"))) |> filter(status == "hurricane") |> arrange(desc(wind), name) |> distinct(name, year, .keep_all = TRUE) hurricanes |> select(c(year, name, wind)) |> write.csv("hurricanes.csv", row.names = FALSE)注意第一个代码块的结果被存储为
hurricanes。要将hurricanes保存为 CSV 文件,select首先选择 3 个特定的列(year、name和wind),并将它们写入名为hurricanes.csv的文件中。
group_by
-
现在我们来找出每年最强大的飓风。
-
考虑以下代码:
# Find most powerful hurricane for each year hurricanes <- read.csv("hurricanes.csv") hurricanes |> group_by(year) |> arrange(desc(wind)) |> slice_head()注意如何将
hurricanes.csv读取到hurricanes中。然后,使用group_by函数将每年所有的飓风分组在一起。对于每个组,使用arrange(desc(wind))按照风速降序排列。最后,使用slice_head输出每个组的顶部行。因此,展示了每年最强的风暴。 -
slice_max在变量中选择最大值。考虑一下我们如何在代码中应用这一点:# Introduce slice_max hurricanes <- read.csv("hurricanes.csv") hurricanes |> group_by(year) |> slice_max(order_by = wind)注意到
hurricanes是按year分组的。然后,使用slice_max展示了wind的最高值。这样做消除了对arrange(desc(wind))的需求。
summarize
-
如果我们想知道每年有多少次飓风?考虑以下代码:
# Find number of hurricanes per year hurricanes <- read.csv("hurricanes.csv") hurricanes |> group_by(year) |> summarize(hurricanes = n())注意到函数
summarize使用n来计算每个组中的行数。
ungroup
-
查看我们的
hurricanes数据框,你会注意到存在分组。实际上,这些分组是根据year进行的。在未来的活动中,你可能会希望取消数据中的分组。因此,考虑以下内容:# Show ungroup hurricanes <- read.csv("hurricanes.csv") hurricanes |> group_by(year) |> slice_max(order_by = wind) |> ungroup()注意到
ungroup命令被用来移除 tibble 的分组。
tidyr
-
当数据已经很好地组织时,dplyr 非常有用。
-
对于数据尚未很好地组织的情况,又该如何处理?
-
对于这一点,tidyr 包可能很有用!
整洁数据
-
根据 tidyverse 的哲学,有三个原则指导我们所说的整洁数据。
1\. Each observation is a row; each row is an observation. 2\. Each variable is a column; each column is a variable. 3\. Each value is a cell; each cell is a single value. -
在评估数据时,最好查看上述三个原则,看看它们是否被观察到。
正常化
-
正常化是将数据转换为满足上述原则的过程。
-
正常化也可以指将数据转换为满足上述指南之外更好的设计原则。
-
从课程文件中下载
students.csv文件并将其放置在你的工作目录中。创建以下新代码:# Read CSV students <- read.csv("students.csv") View(students)注意到这段代码加载了一个名为
students.csv的 CSV 文件,并将这些值存储在students中。 -
检查这些数据,你可能看到它们并没有遵循我们之前提到的原则。哪些原则没有被遵循?
旋转
-
在
students数据集中,你可能会注意到有一些行值本应该是列名:“major”和“GPA”。为了清楚起见,这个数据集违反了整洁数据的第二个原则:学生的任何变化方式都不是一列。 -
我们可以通过
pivot_wider将数据集旋转,将这些变量转换为列。pivot_wider将一个“更长”的数据集(即具有变量作为行值的数据集)转换为“更宽”的数据集(即将这些变量转换为列)。 -
pivot_wider会将students数据集从以下内容转换:![转换前的]()
转换为以下内容:
![转换后的]()
-
但如何操作呢?考虑以下用法:
# Demonstrates pivot_wider students <- read.csv("students.csv") students <- pivot_wider( students, id_cols = student, names_from = attribute, values_from = value )注意到
pivot_wider有几个参数,这里进行解释:-
第一是要操作的数据集,
students。 -
第二个参数,
id_cols,指定了在转换后的数据集中哪一列应该是唯一的。注意,在pivot_wider的转换之前,student列中存在重复值。在pivot_wider的转换之后,student列中存在唯一值。 -
第三个参数,
names_from,指定了包含应作为变量(列)的值的列。注意pivot_wider转换后,attribute列中的值是如何变成列的。 -
最后,第四个参数,
values_from,指定了填充新列值的列。
-
-
由于我们的数据如此整洁,我们可以用数据做更多的事情!
-
考虑以下:
# Demonstrates calculating average GPA by major students <- read.csv("students.csv") students <- pivot_wider( students, id_cols = student, names_from = attribute, values_from = value ) students$GPA <- as.numeric(students$GPA) students |> group_by(major) |> summarize(GPA = mean(GPA))注意这个程序是如何利用
pivot_wider和 tidyr 来发现学生的平均 GPA。students中的GPA被转换为数值。然后,使用管道语法来找到 GPA 的平均值。
stringr
-
我们上面描述的过程在数值本身干净的情况下效果很好。然而,当数值本身不整洁时怎么办呢?
-
stringr为我们提供了一种整理字符串的方法。从课程文件中下载shows.csv并将其放置在你的工作目录中。考虑以下程序:# Tally votes for favorite shows shows <- read.csv("shows.csv") shows |> group_by(show) |> summarize(votes = n()) |> ungroup() |> arrange(desc(votes))注意到节目是如何按
show分组。然后,计算votes的数量。最后,按降序排列votes。 -
观察这个程序的结果,你可以看到有多个版本的《阿凡达:最后的气宗》。我们可能首先应该解决空白问题。
# Clean up inner whitespace shows <- read.csv("shows.csv") shows$show <- shows$show |> str_trim() |> str_squish() shows |> group_by(show) |> summarize(votes = n()) |> ungroup() |> arrange(desc(votes))注意
str_trim是如何用来移除每条记录的前后空白。然后,str_squish用来移除字符之间的额外空白。 -
虽然这一切都非常好,但在大写方面仍然存在一些不一致。我们可以这样解决:
# Clean up capitalization shows <- read.csv("shows.csv") shows$show <- shows$show |> str_trim() |> str_squish() |> str_to_title() shows |> group_by(show) |> summarize(votes = n()) |> ungroup() |> arrange(desc(votes))注意
str_to_title是如何用来强制每个字符串使用标题大小写的。 -
最后,我们可以解决《阿凡达:最后的气宗》的拼写变体问题:
# Clean up spelling shows <- read.csv("shows.csv") shows$show <- shows$show |> str_trim() |> str_squish() |> str_to_title() shows$show[str_detect(shows$show, "Avatar")] <- "Avatar: The Last Airbender" shows |> group_by(show) |> summarize(votes = n()) |> ungroup() |> arrange(desc(votes))注意
str_detect是如何用来定位Avatar实例。每个这些都被转换为Avatar: The Last Airbender。 -
虽然这些工具非常有帮助,但考虑你可能需要谨慎行事,不要覆盖正确的条目。例如,有许多名为《阿凡达》的电影!我们如何知道投票者不是有意为这些电影投票?
总结
在本课中,你学习了如何在 R 中整理数据。具体来说,你学习了三个新的包,它们都是 tidyverse 的一部分:
-
dplyr
-
tidyr
-
stringr
次次见,届时我们将讨论如何可视化我们的数据。
第五讲
-
欢迎!
-
ggplot2
-
规模
-
标签
-
填充
-
主题
-
保存你的图表
-
点
-
随时间可视化
-
总结
欢迎!
-
欢迎回到 CS50 的 R 编程入门课程!
-
今天,我们将学习如何可视化数据。一个好的可视化可以帮助我们以全新的方式解释和理解数据。
ggplot2
-
ggplot中的plot意味着我们将要 绘制 我们的数据。 -
ggplot中的gg指的是一种 图形语法,其中图形的各个组件可以组合在一起来可视化数据。 -
组成图形语法有很多组件,首先是 数据。
-
另一个组件是 几何形状。这些是图表的各种图形表示选项。这包括柱状图、点和线条。
-
最后,美学映射 是数据与我们的图表视觉特征之间的关系。例如,在一个图表中,一个水平的
x轴可能代表每个候选人。然后,一个垂直的y轴可能与每个候选人的投票数相关联。正是通过数据与几何形状之间的这种关系,我们能够可视化和理解图表的美学映射。你可能想象过在某个时候,展示给你的是设计不佳的图表:当映射不正确时,数据更难以解释和理解。 -
下载讲座的源文件,并在 R 控制台中运行
library("tidyverse"),以便将tidyverse载入内存。然后,创建以下可视化:# Create a blank visualization votes <- read.csv("votes.csv") ggplot()注意到
votes.csv被加载到votes中。当运行ggplot时,目前还没有任何可视化。 -
我们可以这样向
ggplot提供输入:# Supply data votes <- read.csv("votes.csv") ggplot(votes)注意到
votes被提供给ggplot,但仍然没有可视化。 -
我们需要告诉
ggplot我们想要什么类型的图表:# Add first geometry votes <- read.csv("votes.csv") ggplot(votes) + geom_col()注意到
geom_col指定数据应该使用柱状几何形状进行可视化。然而,在这个阶段,将会出现错误。错误表明我们需要指定美学映射。 -
注意,
+操作符也有新的含义:使用+操作符在图表的基础层上添加一个图层。 -
要指定美学映射,我们可以如下定义:
# Add x and y aesthetics votes <- read.csv("votes.csv") ggplot(votes, aes(x = candidate, y = votes)) + geom_col()注意到各种由
aes指定的美学映射是如何在括号内定义的。例如,x = candidate和y = votes都是美学映射。现在,ggplot知道哪些数据映射到我们图表的哪些美学特征。 -
运行上述代码,我们的第一个可视化终于出现了!
规模
-
注意到
ggplot决定votes轴的值范围从0到200。如果我们想提供更多的空间,以便我们可以可视化到250,该怎么办?让我们来学习一下 规模。 -
刻度可以是 连续的,范围从一个数字到另一个数字,或者 离散的,这意味着分类。
-
连续刻度有 限制。例如,
votes中提供的数据范围从0到200。因此,我们可以按如下方式修改这些限制:# Adjust y scale votes <- read.csv("votes.csv") ggplot(votes, aes(x = candidate, y = votes)) + geom_col() + scale_y_continuous(limits = c(0, 250))注意
y的刻度是如何通过scale_y_continuous修改为从0到250的。这同样是通过带有+操作符的新层提供的。
标签
-
此外,还可以向图表添加标签。考虑以下示例:
# Add labels votes <- read.csv("votes.csv") ggplot(votes, aes(x = candidate, y = votes)) + geom_col() + scale_y_continuous(limits = c(0, 250)) + labs( x = "Candidate", y = "Votes", title = "Election Results" )注意
x、y和title提供了标签。这些是通过+操作符添加的新层。
填充
-
填充颜色也可以根据
候选人名称进行更改。考虑以下示例:# Add fill aesthetic mapping for geom_col votes <- read.csv("votes.csv") ggplot(votes, aes(x = candidate, y = votes)) + geom_col(aes(fill = candidate)) + scale_y_continuous(limits = c(0, 250)) + labs( x = "Candidate", y = "Votes", title = "Election Results" )注意
fill是通过aes函数依赖于candidate的。 -
我们可能希望调整
fill颜色以适应色盲。我们可以这样做:# Use viridis scale to design for color blindness votes <- read.csv("votes.csv") ggplot(votes, aes(x = candidate, y = votes)) + geom_col(aes(fill = candidate)) + scale_fill_viridis_d("Candidate") + scale_y_continuous(limits = c(0, 250)) + labs( x = "Candidate", y = "Votes", title = "Election Results" )注意
viridis规模是通过scale_fill_viridis_d函数提供的。
主题
-
也可以修改
ggplot使用的主题。你可以这样做:# Adjust ggplot theme votes <- read.csv("votes.csv") ggplot(votes, aes(x = candidate, y = votes)) + geom_col(aes(fill = candidate)) + scale_fill_viridis_d("Candidate") + scale_y_continuous(limits = c(0, 250)) + labs( x = "Candidate", y = "Votes", title = "Election Results" ) + theme_classic()注意
theme_classic被提供。ggplot2 提供几个主题。
保存您的图表
-
最后,可以保存图表。
# Save file votes <- read.csv("votes.csv") p <- ggplot(votes, aes(x = candidate, y = votes)) + geom_col(aes(fill = candidate)) + scale_fill_viridis_d("Candidate") + scale_y_continuous(limits = c(0, 250)) + labs( x = "Candidate", y = "Votes", title = "Election Results" ) + theme_classic() ggsave( "votes.png", plot = p, width = 1200, height = 900, units = "px" )注意整个图表被指定为
p。然后,使用ggsave,指定文件名、图表(在这种情况下,p)、高度、宽度和单位。 -
通过执行此代码,你已经保存了你的第一个图表。恭喜!
点
-
现在,让我们看看一种名为
点的新几何类型。 -
想象一下,糖果的价格百分位数和糖的百分位数是如何表示的。
-
你可以想象
sugar百分位数可以映射到y轴,而price百分位数可以标注在x轴上。 -
这可以通过以下代码形式实现:
# Introduce geom_point load("candy.RData") ggplot( candy, aes(x = price_percentile, y = sugar_percentile) ) + geom_point()注意数据
candy是如何提供给ggplot函数的。然后,使用aes函数设置美学映射。例如,price_percentile被分配给x轴。最后,运行geom_point函数。 -
运行此代码会在图表中表示点。
-
标签可以按如下方式添加:
# Add labels and theme load("candy.RData") ggplot( candy, aes(x = price_percentile, y = sugar_percentile) ) + geom_point() + labs( x = "Price", y = "Sugar", title = "Price and Sugar" ) + theme_classic()注意
x、y和title的labs(标签)是如何提供的。还有一个主题被命名。 -
现在,许多点重叠。可以使用
jitter来帮助可视化重叠的点:# Introduce geom_jitter ggplot( candy, aes(x = price_percentile, y = sugar_percentile) ) + geom_jitter() + labs( x = "Price", y = "Sugar", title = "Price and Sugar" ) + theme_classic()注意
geom_point是如何被geom_jitter替换的。这允许可视化重叠的点。 -
我们可以向我们的点添加颜色美学:
# Introduce size and color aesthetic ggplot( candy, aes(x = price_percentile, y = sugar_percentile) ) + geom_jitter( color = "darkorchid", size = 2 ) + labs( x = "Price", y = "Sugar", title = "Price and Sugar" ) + theme_classic()注意所有点都被改变为一种颜色。
-
此外,我们还可以更改我们点的尺寸和形状:
# Introduce point shape and fill color ggplot( candy, aes(x = price_percentile, y = sugar_percentile) ) + geom_jitter( color = "darkorchid", fill = "orchid", shape = 21, size = 2 ) + labs( x = "Price", y = "Sugar", title = "Price and Sugar" ) + theme_classic()注意
shape和size是如何改变的。你可以参考 文档 来了解哪些数字对应哪些形状。
随时间可视化
-
你可以想象数据是如何随时间表示的。
-
例如,考虑如何表示飓风安妮塔的数据随时间的变化。
-
我们可以像以前一样用点来绘制:
# Visualize with geom_point load("anita.RData") ggplot(anita, aes(x = timestamp, y = wind)) + geom_point()注意
timestamp和风速是如何随时间放置在点上的。 -
虽然这种可视化很有用,但通过显示风速是否增加或减少的线条来展示可能会更有用。每个点都可以通过以下方式用线条连接:
# Introduce geom_line load("anita.RData") ggplot(anita, aes(x = timestamp, y = wind)) + geom_line()注意
geom_line被用作一个新图层。 -
结果是一系列在每次时间戳处改变方向的线条。如果我们能结合
point和line会怎样?嗯,确实可以!# Combine geom_line and geom_point load("anita.RData") ggplot(anita, aes(x = timestamp, y = wind)) + geom_line() + geom_point(color = "deepskyblue4")注意如何通过
geom_line添加带有线条的图层。然后,使用deepskyblue4添加geom_point作为图层。 -
美学可以通过多种方式修改:
# Experiment with geom_line and geom_point aesthetics load("anita.RData") ggplot(anita, aes(x = timestamp, y = wind)) + geom_line( linetype = 1, linewidth = 0.5 ) + geom_point( color = "deepskyblue4", size = 2 )注意如何修改
linetype和linewidth。然后,改变点的size。你可以参考 文档 了解更多关于各种线型的信息。 -
就像我们今天之前的图表一样,我们可以添加标签和主题:
# Add labels and adjust theme load("anita.RData") ggplot(anita, aes(x = timestamp, y = wind)) + geom_line( linetype = 1, linewidth = 0.5 ) + geom_point( color = "deepskyblue4", size = 2 ) + labs( y = "Wind Speed (Knots)", x = "Date", title = "Hurricane Anita" ) + theme_classic()注意
labs如何允许我们为y、x和标题指定标签。然后,启用theme_classic。 -
作为最后的点缀,我们还可以添加一条水平线来划分飓风状态。飓风安妮塔何时成为飓风?
# Add horizontal line to demarcate hurricane status load("anita.RData") ggplot(anita, aes(x = timestamp, y = wind)) + geom_line( linetype = 1, linewidth = 0.5 ) + geom_point( color = "deepskyblue4", size = 2 ) + geom_hline( linetype = 3, yintercept = 64 ) + labs( y = "Wind Speed (Knots)", x = "Date", title = "Hurricane Anita" ) + theme_classic()注意如何添加一个新图层来显示
yintercept = 64处的线条,以指定任何 64 或更高的值都被认为是飓风。linetype被指定为3或点划线。
总结
在本课中,你学习了如何在 R 中可视化数据。具体来说,你学习了以下内容:
-
ggplot2
-
标度
-
标签
-
填充
-
主题
-
点
-
随时间可视化
欢迎下次我们讨论如何测试我们的程序。







浙公网安备 33010602011771号