10  从数据结构到简单分析流程

本章把前面学过的内容放在一个小任务中使用。我们会用“学生信息管理”这个例子,把字典、列表、列表推导式、函数、条件判断和字符串输出组合起来,形成一个简单的数据处理流程。

本章使用基础 Python 数据结构处理数据。这样做的目的,是让你先看清楚数据如何被组织、如何被函数处理、如何一步一步形成新的结果。后面学习 pandas 后,同类任务会有更方便的写法,这里的分析思路仍然适用。

完成本章后,你应该能够:

Note

本章适合作为综合练习。阅读时重点看已有知识如何组织成一个完整流程。

10.1 数据流:如何组织你的代码

如何设计一个分析过程?我们继续用前面榨汁机的例子。

把一个苹果变成苹果汁,假定要3个步骤:

  1. 削皮
  2. 切块
  3. 榨汁

把每个过程看成一个函数,大概是这样的:

削了皮的苹果 = 削皮(苹果)
苹果块 = 切块(削了皮的苹果)
苹果汁 = 榨汁(苹果块)

也可以把它理解为一个数据流:

苹果 -> 削皮 -> 切块 -> 榨汁 -> 苹果汁

其中每一个过程,比如削皮,可能是一个函数,也可能是多行代码组成的模块。我们编写分析流程的整体,以及每一个模块内部,也是类似结构。

处理过程也可以带参数。比如切块的时候切成5块,榨汁的时候用3档速度:

削了皮的苹果 = 削皮(苹果)
苹果块 = 切块(削了皮的苹果, 5)
苹果汁 = 榨汁(苹果块, 3)

因此:

  1. 分析流程一般呈现一种数据流结构:每一个步骤,有上游数据的流入,形成新的数据,成为下游步骤的输入。
  2. 写成函数时,通常可以把数据放在第一个参数,处理数据的条件放在第二位之后。
  3. 函数的作用,是把一个较大的任务拆成若干个有输入、有输出的小步骤。

10.2 从数据开始

例如,我们要处理班级同学的信息。每个同学起码有这几个信息:

  1. 学号
  2. 姓名
  3. 专业
  4. 班级号

阅读数据时,我们通常希望先看到一张表:

student_id name major class_id
2021001 Alex finance 1
2021002 Bob finance 1
2021003 Clare accounting 2
2021004 David marketing 2
2021005 Eva finance 1
2021006 Frank accounting 1

这张表给我们的直觉是:每一行是一位学生,每一列是这位学生的一个属性。

在基础 Python 中,一位同学的信息可以表示成 key-value 结构,所以我们可以用字典 dict 来表示一位同学。

student_1 = {'student_id': 2021001, 'name': 'Alex', 'major': 'finance', 'class_id': 1}
student_2 = {'student_id': 2021002, 'name': 'Bob', 'major': 'finance', 'class_id': 1}
student_3 = {'student_id': 2021003, 'name': 'Clare', 'major': 'accounting', 'class_id': 2}
student_4 = {'student_id': 2021004, 'name': 'David', 'major': 'marketing', 'class_id': 2}
student_5 = {'student_id': 2021005, 'name': 'Eva', 'major': 'finance', 'class_id': 1}
student_6 = {'student_id': 2021006, 'name': 'Frank', 'major': 'accounting', 'class_id': 1}

print(student_1['student_id'])
print(student_2['name'])
print(student_3['major'])
2021001
Bob
accounting

多位同学的信息,可以用列表 list 保存。也就是说,外层列表保存所有学生,内层每个字典保存一位学生的信息。

students_info = [
    student_1,
    student_2,
    student_3,
    student_4,
    student_5,
    student_6
]

print(students_info)
[{'student_id': 2021001, 'name': 'Alex', 'major': 'finance', 'class_id': 1}, {'student_id': 2021002, 'name': 'Bob', 'major': 'finance', 'class_id': 1}, {'student_id': 2021003, 'name': 'Clare', 'major': 'accounting', 'class_id': 2}, {'student_id': 2021004, 'name': 'David', 'major': 'marketing', 'class_id': 2}, {'student_id': 2021005, 'name': 'Eva', 'major': 'finance', 'class_id': 1}, {'student_id': 2021006, 'name': 'Frank', 'major': 'accounting', 'class_id': 1}]

10.3 添加功能

10.3.1 查找

按条件查找是常见功能。例如:在数据中找一位名叫 Alex 的同学。

先用普通循环写:

result = []

for student in students_info:
    if student['name'] == 'Alex':
        result.append(student)

print(result)
[{'student_id': 2021001, 'name': 'Alex', 'major': 'finance', 'class_id': 1}]

同一个筛选,也可以写成列表推导式:

result = [student for student in students_info if student['name'] == 'Alex']

print(result)
[{'student_id': 2021001, 'name': 'Alex', 'major': 'finance', 'class_id': 1}]

注意:列表推导式的结果也是一个列表,即使其中只有1个元素。看打印结果的最外层,是一对中括号。

那么有多少个同学叫 Alex?

len(result)
1

我们也可以获得 Alex 的信息,例如学号和专业:

print(result[0]['student_id'])
print(result[0]['major'])
2021001
finance

按姓名查找是一个常用功能,可以把这个过程写成一个函数:

def find_students_by_name(data, name):
    """
    按姓名查找同学。

    参数:
        data: 保存所有同学信息的数据 List[dict]
        name: 目标姓名
    返回值:
        return: 返回同名同学的列表。如果找不到则返回 []。
    """
    result = [student for student in data if student['name'] == name]
    return result

result = find_students_by_name(students_info, 'Alex')
print(result)
[{'student_id': 2021001, 'name': 'Alex', 'major': 'finance', 'class_id': 1}]

为什么要把一个过程写成函数?

  1. 常用逻辑封装成函数,可以减少重复代码。
  2. 函数名、参数和返回值能说明一段代码的用途。调用函数时,读者可以先关注“这个函数做什么”,再根据需要阅读函数内部的代码。

按学号查找也很常见。学号通常是唯一的,因此可以返回一位学生对应的字典。这个功能会在综合练习中完成。

10.3.2 筛选

查找通常关注某一个目标。筛选通常得到一组符合条件的记录。

例如,我们想找出所有金融专业的同学。

先用普通循环写:

finance_students = []

for student in students_info:
    if student['major'] == 'finance':
        finance_students.append(student)

print(finance_students)
[{'student_id': 2021001, 'name': 'Alex', 'major': 'finance', 'class_id': 1}, {'student_id': 2021002, 'name': 'Bob', 'major': 'finance', 'class_id': 1}, {'student_id': 2021005, 'name': 'Eva', 'major': 'finance', 'class_id': 1}]

同一个筛选,也可以写成列表推导式:

finance_students = [
    student for student in students_info
    if student['major'] == 'finance'
]

print(finance_students)
[{'student_id': 2021001, 'name': 'Alex', 'major': 'finance', 'class_id': 1}, {'student_id': 2021002, 'name': 'Bob', 'major': 'finance', 'class_id': 1}, {'student_id': 2021005, 'name': 'Eva', 'major': 'finance', 'class_id': 1}]

按专业筛选也是一个常用功能,可以写成函数:

def find_students_by_major(data, major):
    '''
    按专业查找同学。
    '''
    result = [
        student for student in data
        if student['major'] == major
    ]
    return result

print(find_students_by_major(students_info, 'accounting'))
[{'student_id': 2021003, 'name': 'Clare', 'major': 'accounting', 'class_id': 2}, {'student_id': 2021006, 'name': 'Frank', 'major': 'accounting', 'class_id': 1}]

10.3.3 简单统计

现在我们想统计金融专业有多少人。

思路是:

  1. 先筛选出金融专业的同学。
  2. 再计算筛选结果的长度。
finance_students = find_students_by_major(students_info, 'finance')
len(finance_students)
3

计数也可以写成函数:

def count_by_major(data, major):
    '''
    计算某个专业的同学人数。
    '''
    class_size = len(find_students_by_major(data, major))
    return class_size

print('金融专业人数:', count_by_major(students_info, 'finance'))
print('会计专业人数:', count_by_major(students_info, 'accounting'))
print('营销专业人数:', count_by_major(students_info, 'marketing'))
金融专业人数: 3
会计专业人数: 2
营销专业人数: 1

10.3.4 添加或修改信息

现在老师拿到了 Alex 的 Python 课成绩,希望把这个分数记录到学生信息中。

先找到 Alex:

alex = find_students_by_name(students_info, 'Alex')[0]
print(alex)
{'student_id': 2021001, 'name': 'Alex', 'major': 'finance', 'class_id': 1}

一位学生的信息保存在字典里。给字典的某个 key 赋值,就可以添加或修改字段:

alex['python_score'] = 86
print(alex)
{'student_id': 2021001, 'name': 'Alex', 'major': 'finance', 'class_id': 1, 'python_score': 86}

python_score 原来不存在,这次赋值会增加一个新字段。如果这个 key 已经存在,同样的写法会修改原来的值。

按学号设置分数也是一个常用功能,可以提炼成函数。这个功能会在综合练习中完成。

小结:

  1. “一位同学”的信息可以用字典 dict 表示:可以用 student_idname 等标签获取信息。
  2. “所有同学”的信息可以用列表 list 保存:列表中的每个元素是一位同学的字典。
  3. 常见操作通常来自具体需求,比如按姓名查找、按专业统计、按学号记录成绩。
  4. 常用操作可以写成函数,既便于重复使用,也能通过函数名说明操作意图。
  5. 添加字段和修改字段都可以通过字典赋值完成。

10.4 综合练习

本章的内容,是针对一组学生信息,构造数据结构,并添加处理数据的功能。

以下练习要求你继续这个过程:在上述内容的基础上,添加新的信息和新的功能。

本次练习还会用到一组 Python 课成绩。它也是已知数据:

python_scores = [
    {'student_id': 2021001, 'python_score': 86},
    {'student_id': 2021002, 'python_score': 59},
    {'student_id': 2021003, 'python_score': 92},
    {'student_id': 2021004, 'python_score': 54},
    {'student_id': 2021005, 'python_score': 88},
    {'student_id': 2021006, 'python_score': 67}
]

10.4.1 按学号查找

问题:按学号查找某一位同学。学号通常是唯一的,因此找到时返回一位学生对应的字典,找不到时返回 None

知识点:循环,条件判断,函数返回值

def find_student_by_id(data, student_id):
    '''
    按学号查找同学。
    '''
    pass

result = find_student_by_id(students_info, 2021002)
print(result)

期望输出:

{'student_id': 2021002, 'name': 'Bob', 'major': 'finance', 'class_id': 1}

提示:可以参考 find_students_by_name(),这里的返回值是一位学生对应的字典。

写好这个函数后,先运行下面的检查代码。确认结果正确,再继续做下一题。后面的练习也应采用同样的习惯:每完成一个函数,就立即检查。

print(find_student_by_id(students_info, 2021002))
print(find_student_by_id(students_info, 2021999))

预期输出:

{'student_id': 2021002, 'name': 'Bob', 'major': 'finance', 'class_id': 1}
None

10.4.2 按学号添加 Python 课分数

先处理其中一位同学:给 Bob 添加 Python 课成绩。Bob 的学号是 2021002,成绩是 59

问题:写一个函数 set_python_score(data, student_id, score),按学号找到学生,并给这个学生添加 python_score

知识点:按学号查找,字典赋值,函数调用

def set_python_score(data, student_id, score):
    '''
    按学号设置某位学生的 Python 课成绩。
    '''
    pass

set_python_score(students_info, 2021002, 59)
print(find_student_by_id(students_info, 2021002))

提示:先调用 find_student_by_id() 找到学生,再给这个学生的字典添加 python_score

检查代码:

set_python_score(students_info, 2021002, 59)
print(find_student_by_id(students_info, 2021002)['python_score'])

预期输出:

59

10.4.3 批量添加 Python 课分数

上一题只处理了 Bob 一位同学。现在把 python_scores 中的所有分数都添加到 students_info 中。

知识点:字典赋值,循环,函数调用

def apply_score_updates(data, scores):
    '''
    批量更新 Python 课成绩。
    '''
    pass

apply_score_updates(students_info, python_scores)
print(find_student_by_id(students_info, 2021001))
print(find_student_by_id(students_info, 2021006))

提示:对 scores 中的每一条记录,调用一次 set_python_score()

检查代码:

apply_score_updates(students_info, python_scores)
print(find_student_by_id(students_info, 2021003)['python_score'])
print(find_student_by_id(students_info, 2021006)['python_score'])

预期输出:

92
67

10.4.4 统计 Python 课平均分

问题:统计 Python 课所有同学的平均分,以及分班级的平均分。

知识点:循环,累加,简单计算,函数调用

def get_avg_python_score(data):
    '''
    统计 Python 课平均分。
    '''
    pass

def find_students_by_class(data, class_id):
    '''
    按班级查找同学。
    '''
    pass

def get_avg_python_score_by_class(data, class_id):
    '''
    按班级统计 Python 课平均分。
    '''
    pass

提示:计算平均分时,先累加所有分数,再除以人数。按班级平均分可以先筛选班级,再调用平均分函数。

检查代码:

print(round(get_avg_python_score(students_info), 2))
print(round(get_avg_python_score_by_class(students_info, 1), 2))
print(round(get_avg_python_score_by_class(students_info, 2), 2))

预期输出:

74.33
75.0
73.0

10.4.5 由分数获得评级

问题:按照分数,给同学添加一个 rank 变量。

知识点:if 条件判断,字典赋值,循环

评级规则:

  1. 90分及以上,"A"
  2. 80分及以上,"B"
  3. 70分及以上,"C"
  4. 60分及以上,"D"
  5. 不足60分,"E"
def get_rank(score):
    '''
    根据分数返回评级。
    '''
    pass

def set_rank(data):
    '''
    根据 python_score 给每位同学添加 rank。
    '''
    pass

提示:可以先写 get_rank(86) 这样的单个分数判断,再把它应用到每位同学。

检查代码:

set_rank(students_info)
print(find_student_by_id(students_info, 2021003)['rank'])
print(find_student_by_id(students_info, 2021002)['rank'])

预期输出:

A
E

10.4.6 判断是否及格

问题:按照分数,给同学添加一个 passed 变量。60分及以上为及格。

知识点:条件判断,字典赋值,布尔值,循环

def set_pass_status(data):
    '''
    根据 python_score 给每位同学添加 passed。
    '''
    pass

def get_pass_rate(data):
    '''
    统计及格率。
    '''
    pass

提示:student['python_score'] >= 60 的结果就是一个布尔值,可以直接赋给 student['passed']

检查代码:

set_pass_status(students_info)
print(find_student_by_id(students_info, 2021002)['passed'])
print(round(get_pass_rate(students_info), 2))

预期输出:

False
0.67

10.4.7 输出某一位同学的信息

问题:以字符串形式输出某一位同学的信息。

知识点:按学号查找,字符串操作,函数返回值

输出结果类似:

学号: 2021001;姓名: Alex;班级: finance 1;Python成绩: 86;评级: B

建议函数返回字符串,由外部决定是否打印。

def format_student_by_id(data, student_id):
    '''
    返回某位同学的信息字符串。
    '''
    pass

提示:先用 find_student_by_id() 找到学生,再把字典中的字段拼接成字符串。

检查代码:

print(format_student_by_id(students_info, 2021001))

预期输出:

学号: 2021001;姓名: Alex;班级: finance 1班;Python成绩: 86;评级: B

10.4.8 列出不及格同学

问题:列出所有不及格同学的信息。这个名单可以用于打印通知、整理补交名单等场景。

知识点:筛选,循环,函数调用,字符串拼接

思路:

  1. 先筛选出 passedFalse 的同学。
  2. 再对每位同学调用 format_student_by_id()
  3. 把每位同学的信息整理成多行字符串。
def list_failed_students(data):
    '''
    返回不及格同学的信息字符串,每位同学一行。
    '''
    pass

提示:可以先得到一个不及格同学列表,再用循环逐个生成字符串。

检查代码:

print(list_failed_students(students_info))

预期输出:

学号: 2021002;姓名: Bob;班级: finance 1班;Python成绩: 59;评级: E
学号: 2021004;姓名: David;班级: marketing 2班;Python成绩: 54;评级: E

10.4.9 找出最高分同学

问题:找出 Python 课最高分同学,并输出他们的信息。最高分可能有多位同学。

知识点:循环,比较,列表,函数调用

思路:

  1. 先找出最高分是多少。
  2. 再筛选出所有等于最高分的同学。
  3. 对每位最高分同学调用 format_student_by_id()

这道题可以直接写过程代码,重点放在组合前面已经写好的函数。

完成后,应该打印出 Clare 的信息:

学号: 2021003;姓名: Clare;班级: accounting 2班;Python成绩: 92;评级: A

10.4.10 作业代码框架

可以按照下面的结构完成本章练习。# %% 可以在 VS Code 中把一个 .py 文件分成多个代码单元,便于逐段运行和检查。

# %% 已知数据
student_1 = {'student_id': 2021001, 'name': 'Alex', 'major': 'finance', 'class_id': 1}
student_2 = {'student_id': 2021002, 'name': 'Bob', 'major': 'finance', 'class_id': 1}
student_3 = {'student_id': 2021003, 'name': 'Clare', 'major': 'accounting', 'class_id': 2}
student_4 = {'student_id': 2021004, 'name': 'David', 'major': 'marketing', 'class_id': 2}
student_5 = {'student_id': 2021005, 'name': 'Eva', 'major': 'finance', 'class_id': 1}
student_6 = {'student_id': 2021006, 'name': 'Frank', 'major': 'accounting', 'class_id': 1}

students_info = [
    student_1,
    student_2,
    student_3,
    student_4,
    student_5,
    student_6
]

def find_students_by_name(data, name):
    result = [student for student in data if student['name'] == name]
    return result

def find_students_by_major(data, major):
    result = [
        student for student in data
        if student['major'] == major
    ]
    return result

def count_by_major(data, major):
    class_size = len(find_students_by_major(data, major))
    return class_size

python_scores = [
    {'student_id': 2021001, 'python_score': 86},
    {'student_id': 2021002, 'python_score': 59},
    {'student_id': 2021003, 'python_score': 92},
    {'student_id': 2021004, 'python_score': 54},
    {'student_id': 2021005, 'python_score': 88},
    {'student_id': 2021006, 'python_score': 67}
]

# %% 1. 按学号查找
# 写 find_student_by_id,然后运行检查代码


# %% 2. 添加 Bob 的 Python 成绩
# 写 set_python_score,然后运行检查代码


# %% 3. 批量添加 Python 成绩
# 写 apply_score_updates,然后运行检查代码


# %% 4. 平均分
# 写 get_avg_python_score、find_students_by_class、get_avg_python_score_by_class


# %% 5. 评级
# 写 get_rank、set_rank


# %% 6. 及格状态
# 写 set_pass_status、get_pass_rate


# %% 7. 输出某一位同学的信息
# 写 format_student_by_id


# %% 8. 列出不及格同学
# 写 list_failed_students


# %% 9. 找出最高分同学
# 直接写过程代码;最高分可能有多位同学